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") }}.
+
+
+
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?
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