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") }}.
+ 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/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) }}
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/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:
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..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()
@@ -385,12 +386,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")
@@ -411,7 +415,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"
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
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() }}