From 076d9899eade83bb31d14cb1e6f69dd46ffd00f8 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 12 Apr 2020 19:27:14 +0200 Subject: [PATCH 1/7] rename --- app/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models.py b/app/models.py index d40c6d39..1ba7d798 100644 --- a/app/models.py +++ b/app/models.py @@ -202,7 +202,7 @@ class User(db.Model, ModelMixin, UserMixin): return user - def lifetime_or_active_subscription(self) -> bool: + def _lifetime_or_active_subscription(self) -> bool: """True if user has lifetime licence or active subscription""" if self.lifetime: return True @@ -219,7 +219,7 @@ class User(db.Model, ModelMixin, UserMixin): def in_trial(self): """return True if user does not have lifetime licence or an active subscription AND is in trial period""" - if self.lifetime_or_active_subscription(): + if self._lifetime_or_active_subscription(): return False if self.trial_end and arrow.now() < self.trial_end: @@ -228,7 +228,7 @@ class User(db.Model, ModelMixin, UserMixin): return False def should_upgrade(self): - if self.lifetime_or_active_subscription(): + if self._lifetime_or_active_subscription(): # user who has canceled can also re-subscribe sub: Subscription = self.get_subscription() if sub and sub.cancelled: @@ -264,7 +264,7 @@ class User(db.Model, ModelMixin, UserMixin): - in trial period or - active subscription """ - if self.lifetime_or_active_subscription(): + if self._lifetime_or_active_subscription(): return True if self.trial_end and arrow.now() < self.trial_end: From 1bed5252318e83bd764cd63c54aeea30b565753e Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 12 Apr 2020 19:39:31 +0200 Subject: [PATCH 2/7] Prettify alert-primary, alert-danger --- static/style.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/static/style.css b/static/style.css index 1ab73573..1d65d523 100644 --- a/static/style.css +++ b/static/style.css @@ -69,4 +69,17 @@ em { .cursor { cursor: pointer; +} + +/*Left border for alert zone*/ +.alert-primary{ + border-left: 5px #467fcf solid; +} + +.alert-danger{ + border-left: 5px #6b1110 solid; +} + +.alert-danger::before { + content: "⚠️"; } \ No newline at end of file From 51eb550751401a774a5e478a1f91270a6b8ce6c1 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 12 Apr 2020 19:39:47 +0200 Subject: [PATCH 3/7] fix not using lifetime_or_active_subscription --- templates/header.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/header.html b/templates/header.html index 2c05e225..73bec0bf 100644 --- a/templates/header.html +++ b/templates/header.html @@ -27,7 +27,7 @@ {% if current_user.in_trial() %} Trial ends {{ current_user.trial_end|dt }} - {% elif current_user.lifetime_or_active_subscription() %} + {% elif current_user.is_premium() %} Premium {% if current_user.is_cancel() %} until {{ current_user.next_bill_date() }} From 9b91f4a4a467123b5552de22c7c765072fafbaaf Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 12 Apr 2020 19:43:07 +0200 Subject: [PATCH 4/7] support changing plan --- .../templates/dashboard/billing.html | 33 ++++++++++++--- app/dashboard/views/billing.py | 42 +++++++++++++++++-- app/paddle_utils.py | 19 +++++++++ server.py | 33 +++++++++++++++ 4 files changed, 118 insertions(+), 9 deletions(-) diff --git a/app/dashboard/templates/dashboard/billing.html b/app/dashboard/templates/dashboard/billing.html index a18af7e3..d2a74a17 100644 --- a/app/dashboard/templates/dashboard/billing.html +++ b/app/dashboard/templates/dashboard/billing.html @@ -14,8 +14,7 @@ {% if sub.cancelled %}

You are on the {{ sub.plan_name() }} plan.
- You have canceled your subscription and it will end on {{current_user.next_bill_date()}} - ({{ sub.next_bill_date | dt }}). + You have canceled your subscription and it will end on {{ current_user.next_bill_date() }}


@@ -33,23 +32,47 @@ {% else %}

You are on the {{ sub.plan_name() }} plan. Thank you very much for supporting - SimpleLogin. 🙌 + SimpleLogin. 🙌
+ The next billing cycle starts at {{ sub.next_bill_date.strftime("%Y-%m-%d") }}.

Click here to update billing information on Paddle, our payment partner:
- Update billing information + Update billing information +
+
+
+

Change Plan

+ You can change the plan at any moment.
+ Please note that the new billing cycle starts instantly + i.e. you will be charged immediately the annual fee when switching from monthly plan or vice-versa + without pro rata computation .
+ + To change the plan you can also cancel the current one and subscribe a new one by the end of this plan. + + {% if sub.plan == PlanEnum.yearly %} +
+ + +
+ {% else %} +
+ + +
+ {% endif %}

+

Cancel subscription

Don't want to protect your inbox anymore?
- + Cancel subscription diff --git a/app/dashboard/views/billing.py b/app/dashboard/views/billing.py index 9f8bc183..7c44e73d 100644 --- a/app/dashboard/views/billing.py +++ b/app/dashboard/views/billing.py @@ -1,11 +1,12 @@ from flask import render_template, flash, redirect, url_for, request from flask_login import login_required, current_user +from app.config import PADDLE_MONTHLY_PRODUCT_ID, PADDLE_YEARLY_PRODUCT_ID from app.dashboard.base import dashboard_bp from app.log import LOG -from app.models import Subscription +from app.models import Subscription, PlanEnum from app.extensions import db -from app.paddle_utils import cancel_subscription +from app.paddle_utils import cancel_subscription, change_plan @dashboard_bp.route("/billing", methods=["GET", "POST"]) @@ -29,10 +30,43 @@ def billing(): flash("Your subscription has been canceled successfully", "success") else: flash( - "Something went wrong, sorry for the inconvenience. Please retry. We are already notified and will be on it asap", + "Something went wrong, sorry for the inconvenience. Please retry. " + "We are already notified and will be on it asap", + "error", + ) + + return redirect(url_for("dashboard.billing")) + elif request.form.get("form-name") == "change-monthly": + LOG.debug(f"User {current_user} changes to monthly plan") + success = change_plan(sub.subscription_id, PADDLE_MONTHLY_PRODUCT_ID) + + if success: + sub.plan = PlanEnum.monthly + db.session.commit() + flash("Your subscription has been updated", "success") + else: + flash( + "Something went wrong, sorry for the inconvenience. Please retry. " + "We are already notified and will be on it asap", + "error", + ) + + return redirect(url_for("dashboard.billing")) + elif request.form.get("form-name") == "change-yearly": + LOG.debug(f"User {current_user} changes to yearly plan") + success = change_plan(sub.subscription_id, PADDLE_YEARLY_PRODUCT_ID) + + if success: + sub.plan = PlanEnum.yearly + db.session.commit() + flash("Your subscription has been updated", "success") + else: + flash( + "Something went wrong, sorry for the inconvenience. Please retry. " + "We are already notified and will be on it asap", "error", ) return redirect(url_for("dashboard.billing")) - return render_template("dashboard/billing.html", sub=sub) + return render_template("dashboard/billing.html", sub=sub, PlanEnum=PlanEnum) diff --git a/app/paddle_utils.py b/app/paddle_utils.py index 47740e11..cb782e90 100644 --- a/app/paddle_utils.py +++ b/app/paddle_utils.py @@ -76,3 +76,22 @@ def cancel_subscription(subscription_id: int) -> bool: ) return res["success"] + + +def change_plan(subscription_id: int, plan_id) -> bool: + r = requests.post( + "https://vendors.paddle.com/api/2.0/subscription/users/update", + data={ + "vendor_id": PADDLE_VENDOR_ID, + "vendor_auth_code": PADDLE_AUTH_CODE, + "subscription_id": subscription_id, + "plan_id": plan_id, + }, + ) + res = r.json() + if not res["success"]: + LOG.error( + f"cannot change subscription {subscription_id} to {plan_id}, paddle response: {res}" + ) + + return res["success"] diff --git a/server.py b/server.py index bc6cdf99..e60d46b2 100644 --- a/server.py +++ b/server.py @@ -411,7 +411,40 @@ def setup_paddle_callback(app: Flask): db.session.commit() else: return "No such subscription", 400 + elif request.form.get("alert_name") == "subscription_updated": + subscription_id = request.form.get("subscription_id") + sub: Subscription = Subscription.get_by(subscription_id=subscription_id) + if sub: + LOG.debug( + "Update subscription %s %s on %s, next bill date %s", + subscription_id, + sub.user, + request.form.get("cancellation_effective_date"), + sub.next_bill_date, + ) + if ( + int(request.form.get("subscription_plan_id")) + == PADDLE_MONTHLY_PRODUCT_ID + ): + plan = PlanEnum.monthly + else: + plan = PlanEnum.yearly + + sub.cancel_url = request.form.get("cancel_url") + sub.update_url = request.form.get("update_url") + sub.event_time = arrow.now() + sub.next_bill_date = arrow.get( + request.form.get("next_bill_date"), "YYYY-MM-DD" + ).date() + sub.plan = plan + + # make sure to set the new plan as not-cancelled + sub.cancelled = False + + db.session.commit() + else: + return "No such subscription", 400 return "OK" From b845e2a8eb5eda63644581782331acbf7d2dc2d1 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 12 Apr 2020 19:43:35 +0200 Subject: [PATCH 5/7] Handle case where subscription_payment_succeeded arrives BEFORE subscription_created --- server.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/server.py b/server.py index e60d46b2..8df3ad53 100644 --- a/server.py +++ b/server.py @@ -385,12 +385,15 @@ def setup_paddle_callback(app: Flask): LOG.debug("Update subscription %s", subscription_id) sub: Subscription = Subscription.get_by(subscription_id=subscription_id) - sub.event_time = arrow.now() - sub.next_bill_date = arrow.get( - request.form.get("next_bill_date"), "YYYY-MM-DD" - ).date() + # when user subscribes, the "subscription_payment_succeeded" can arrive BEFORE "subscription_created" + # at that time, subscription object does not exist yet + if sub: + sub.event_time = arrow.now() + sub.next_bill_date = arrow.get( + request.form.get("next_bill_date"), "YYYY-MM-DD" + ).date() - db.session.commit() + db.session.commit() elif request.form.get("alert_name") == "subscription_cancelled": subscription_id = request.form.get("subscription_id") From b041591133ecf10b3508bd58aab07945dd65a5b3 Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 12 Apr 2020 19:43:46 +0200 Subject: [PATCH 6/7] Prettify Settings --- app/dashboard/templates/dashboard/setting.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/dashboard/templates/dashboard/setting.html b/app/dashboard/templates/dashboard/setting.html index f586a45e..231d080a 100644 --- a/app/dashboard/templates/dashboard/setting.html +++ b/app/dashboard/templates/dashboard/setting.html @@ -27,7 +27,7 @@
- Change Email Address + Email Address
@@ -60,9 +60,12 @@
- Change Profile + Profile
-
+
+ These informations will be filled up automatically when you use "Sign in with SimpleLogin" button +
+
{{ form.name(class="form-control", value=current_user.name) }} {{ render_field_errors(form.name) }} From 70ce48cd792b85914147c96bc70babbc4083edac Mon Sep 17 00:00:00 2001 From: Son NK <> Date: Sun, 12 Apr 2020 19:43:55 +0200 Subject: [PATCH 7/7] Disable trial on fake data --- server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server.py b/server.py index 8df3ad53..5618cfe7 100644 --- a/server.py +++ b/server.py @@ -133,6 +133,7 @@ def fake_data(): otp_secret="base32secret3232", ) db.session.commit() + user.trial_end = None LifetimeCoupon.create(code="coupon", nb_used=10) db.session.commit()