commit
b8ca2d0158
|
@ -52,6 +52,26 @@
|
|||
Yearly <br>
|
||||
$29.99/year
|
||||
</button>
|
||||
|
||||
<hr class="my-6">
|
||||
|
||||
<form method="post">
|
||||
{{ coupon_form.csrf_token }}
|
||||
<input type="hidden" name="form-name" value="create">
|
||||
|
||||
<div class="font-weight-bold mb-2">Coupon</div>
|
||||
<div class="small-text">
|
||||
If you have a lifetime coupon, please paste it here. <br>
|
||||
For information, we offer free premium account for education (student, professor or technical staff working at
|
||||
an educational institute). <br>
|
||||
Drop us an email at hi@simplelogin.io with your student ID or certificate to get the coupon.
|
||||
</div>
|
||||
|
||||
|
||||
{{ coupon_form.code(class="form-control", placeholder="Coupon") }}
|
||||
{{ render_field_errors(coupon_form.code) }}
|
||||
<button class="btn btn-lg btn-success mt-2">Apply</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ from app.dashboard.base import dashboard_bp
|
|||
@login_required
|
||||
def billing():
|
||||
# sanity check: make sure this page is only for user who has paddle subscription
|
||||
if not current_user.is_premium():
|
||||
flash("This page is for paid customer only", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
sub = current_user.get_subscription()
|
||||
|
||||
if not sub:
|
||||
flash("You don't have any active subscription", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
return render_template("dashboard/billing.html", sub=sub)
|
||||
|
|
|
@ -1,13 +1,23 @@
|
|||
from flask import render_template, flash, redirect, url_for
|
||||
from flask_login import login_required, current_user
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, validators
|
||||
|
||||
from app.config import (
|
||||
PADDLE_VENDOR_ID,
|
||||
PADDLE_MONTHLY_PRODUCT_ID,
|
||||
PADDLE_YEARLY_PRODUCT_ID,
|
||||
URL,
|
||||
ADMIN_EMAIL,
|
||||
)
|
||||
from app.dashboard.base import dashboard_bp
|
||||
from app.email_utils import send_email
|
||||
from app.extensions import db
|
||||
from app.models import LifetimeCoupon
|
||||
|
||||
|
||||
class CouponForm(FlaskForm):
|
||||
code = StringField("Coupon Code", validators=[validators.DataRequired()])
|
||||
|
||||
|
||||
@dashboard_bp.route("/pricing", methods=["GET", "POST"])
|
||||
|
@ -18,12 +28,39 @@ def pricing():
|
|||
flash("You are already a premium user", "warning")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
coupon_form = CouponForm()
|
||||
|
||||
if coupon_form.validate_on_submit():
|
||||
code = coupon_form.code.data
|
||||
|
||||
coupon = LifetimeCoupon.get_by(code=code)
|
||||
|
||||
if coupon and coupon.nb_used > 0:
|
||||
coupon.nb_used -= 1
|
||||
current_user.lifetime = True
|
||||
db.session.commit()
|
||||
|
||||
# notify admin
|
||||
send_email(
|
||||
ADMIN_EMAIL,
|
||||
subject=f"User {current_user.id} used lifetime coupon. Coupon nb_used: {coupon.nb_used}",
|
||||
plaintext="",
|
||||
html="",
|
||||
)
|
||||
|
||||
flash("You are upgraded to lifetime premium!", "success")
|
||||
return redirect(url_for("dashboard.index"))
|
||||
|
||||
else:
|
||||
flash(f"Coupon *{code}* expired or invalid", "warning")
|
||||
|
||||
return render_template(
|
||||
"dashboard/pricing.html",
|
||||
PADDLE_VENDOR_ID=PADDLE_VENDOR_ID,
|
||||
PADDLE_MONTHLY_PRODUCT_ID=PADDLE_MONTHLY_PRODUCT_ID,
|
||||
PADDLE_YEARLY_PRODUCT_ID=PADDLE_YEARLY_PRODUCT_ID,
|
||||
success_url=URL + "/dashboard/subscription_success",
|
||||
coupon_form=coupon_form,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -119,6 +119,9 @@ class User(db.Model, ModelMixin, UserMixin):
|
|||
db.Boolean, nullable=False, default=False, server_default="0"
|
||||
)
|
||||
|
||||
# some users could have lifetime premium
|
||||
lifetime = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
|
||||
|
||||
profile_picture = db.relationship(File)
|
||||
|
||||
@classmethod
|
||||
|
@ -143,13 +146,11 @@ class User(db.Model, ModelMixin, UserMixin):
|
|||
|
||||
def is_premium(self):
|
||||
"""user is premium if they have a active subscription"""
|
||||
if self.lifetime:
|
||||
return True
|
||||
|
||||
sub: Subscription = self.get_subscription()
|
||||
if sub:
|
||||
if sub.cancelled:
|
||||
# user is premium until the next billing_date + 1
|
||||
return sub.next_bill_date >= arrow.now().shift(days=-1).date()
|
||||
|
||||
# subscription active, ie not cancelled
|
||||
return True
|
||||
|
||||
return False
|
||||
|
@ -217,8 +218,18 @@ class User(db.Model, ModelMixin, UserMixin):
|
|||
return "Free Plan"
|
||||
|
||||
def get_subscription(self):
|
||||
"""return *active* subscription
|
||||
TODO: support user unsubscribe and re-subscribe
|
||||
"""
|
||||
sub = Subscription.get_by(user_id=self.id)
|
||||
return sub
|
||||
if sub and sub.cancelled:
|
||||
# sub is active until the next billing_date + 1
|
||||
if sub.next_bill_date >= arrow.now().shift(days=-1).date():
|
||||
return sub
|
||||
else: # past subscription, user is considered not having a subscription
|
||||
return None
|
||||
else:
|
||||
return sub
|
||||
|
||||
def verified_custom_domains(self):
|
||||
return CustomDomain.query.filter_by(user_id=self.id, verified=True).all()
|
||||
|
@ -709,3 +720,8 @@ class CustomDomain(db.Model, ModelMixin):
|
|||
|
||||
def __repr__(self):
|
||||
return f"<Custom Domain {self.domain}>"
|
||||
|
||||
|
||||
class LifetimeCoupon(db.Model, ModelMixin):
|
||||
code = db.Column(db.String(128), nullable=False, unique=True)
|
||||
nb_used = db.Column(db.Integer, nullable=False)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
"""empty message
|
||||
|
||||
Revision ID: d29cca963221
|
||||
Revises: 01f808f15b2e
|
||||
Create Date: 2020-01-01 20:01:51.861329
|
||||
|
||||
"""
|
||||
import sqlalchemy_utils
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'd29cca963221'
|
||||
down_revision = '01f808f15b2e'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('lifetime_coupon',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
|
||||
sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
|
||||
sa.Column('code', sa.String(length=128), nullable=False),
|
||||
sa.Column('nb_used', sa.Integer(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('code')
|
||||
)
|
||||
op.add_column('users', sa.Column('lifetime', sa.Boolean(), server_default='0', nullable=False))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('users', 'lifetime')
|
||||
op.drop_table('lifetime_coupon')
|
||||
# ### end Alembic commands ###
|
|
@ -87,7 +87,7 @@ s3transfer==0.2.1 # via boto3
|
|||
sentry-sdk==0.13.5
|
||||
simplejson==3.17.0 # via flask-profiler
|
||||
six==1.12.0 # via bcrypt, cryptography, flask-cors, packaging, pip-tools, prompt-toolkit, pyopenssl, pytest, python-dateutil, sqlalchemy-utils, traitlets
|
||||
sqlalchemy-utils==0.33.11
|
||||
sqlalchemy-utils==0.36.1
|
||||
sqlalchemy==1.3.12 # via alembic, flask-sqlalchemy, sqlalchemy-utils
|
||||
strictyaml==1.0.2 # via yacron
|
||||
traitlets==4.3.2 # via ipython
|
||||
|
|
|
@ -41,6 +41,7 @@ from app.models import (
|
|||
PlanEnum,
|
||||
ApiKey,
|
||||
CustomDomain,
|
||||
LifetimeCoupon,
|
||||
)
|
||||
from app.monitor.base import monitor_bp
|
||||
from app.oauth.base import oauth_bp
|
||||
|
@ -117,6 +118,9 @@ def fake_data():
|
|||
)
|
||||
db.session.commit()
|
||||
|
||||
LifetimeCoupon.create(code="coupon", nb_used=10)
|
||||
db.session.commit()
|
||||
|
||||
# Create a subscription for user
|
||||
Subscription.create(
|
||||
user_id=user.id,
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
</a>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right dropdown-menu-arrow">
|
||||
{% if current_user.is_premium() %}
|
||||
{% if current_user.get_subscription() %}
|
||||
<a class="dropdown-item" href="{{ url_for('dashboard.billing') }}">
|
||||
<i class="dropdown-icon fe fe-dollar-sign"></i> Billing
|
||||
</a>
|
||||
|
|
Loading…
Reference in New Issue