From 8aacd5e6da7cffbc0e3ca206fc7ceea9d70302aa Mon Sep 17 00:00:00 2001 From: Son NK Date: Sun, 8 Mar 2020 10:27:26 +0100 Subject: [PATCH 1/8] Add PADDLE_AUTH_CODE config --- app/config.py | 2 ++ example.env | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/config.py b/app/config.py index a015df45..41ac6c3c 100644 --- a/app/config.py +++ b/app/config.py @@ -141,6 +141,8 @@ PADDLE_PUBLIC_KEY_PATH = get_abs_path( os.environ.get("PADDLE_PUBLIC_KEY_PATH", "local_data/paddle.key.pub") ) +PADDLE_AUTH_CODE = os.environ.get("PADDLE_AUTH_CODE") + # OpenID keys, used to sign id_token OPENID_PRIVATE_KEY_PATH = get_abs_path( os.environ.get("OPENID_PRIVATE_KEY_PATH", "local_data/jwtRS256.key") diff --git a/example.env b/example.env index ce0b5eb9..5fc0d855 100644 --- a/example.env +++ b/example.env @@ -82,10 +82,11 @@ AWS_SECRET_ACCESS_KEY=to_fill # <<< END AWS >>> # Paddle -PADDLE_VENDOR_ID = 123 -PADDLE_MONTHLY_PRODUCT_ID = 123 -PADDLE_YEARLY_PRODUCT_ID = 123 +PADDLE_VENDOR_ID=123 +PADDLE_MONTHLY_PRODUCT_ID=123 +PADDLE_YEARLY_PRODUCT_ID=123 PADDLE_PUBLIC_KEY_PATH=local_data/paddle.key.pub +PADDLE_AUTH_CODE=123 # OpenId key OPENID_PRIVATE_KEY_PATH=local_data/jwtRS256.key From aea717eafc920a0895ac269d467bb0a424c8c618 Mon Sep 17 00:00:00 2001 From: Son NK Date: Sun, 8 Mar 2020 10:27:50 +0100 Subject: [PATCH 2/8] add paddle_utils.cancel_subscription --- app/paddle_utils.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/paddle_utils.py b/app/paddle_utils.py index 49904e15..47740e11 100644 --- a/app/paddle_utils.py +++ b/app/paddle_utils.py @@ -8,15 +8,18 @@ import collections # PHPSerialize can be found at https://pypi.python.org/pypi/phpserialize import phpserialize +import requests from Crypto.Hash import SHA1 # Crypto can be found at https://pypi.org/project/pycryptodome/ from Crypto.PublicKey import RSA from Crypto.Signature import PKCS1_v1_5 -from app.config import PADDLE_PUBLIC_KEY_PATH +from app.config import PADDLE_PUBLIC_KEY_PATH, PADDLE_VENDOR_ID, PADDLE_AUTH_CODE # Your Paddle public key. +from app.log import LOG + with open(PADDLE_PUBLIC_KEY_PATH) as f: public_key = f.read() @@ -55,3 +58,21 @@ def verify_incoming_request(form_data: dict) -> bool: if verifier.verify(digest, signature): return True return False + + +def cancel_subscription(subscription_id: int) -> bool: + r = requests.post( + "https://vendors.paddle.com/api/2.0/subscription/users_cancel", + data={ + "vendor_id": PADDLE_VENDOR_ID, + "vendor_auth_code": PADDLE_AUTH_CODE, + "subscription_id": subscription_id, + }, + ) + res = r.json() + if not res["success"]: + LOG.error( + f"cannot cancel subscription {subscription_id}, paddle response: {res}" + ) + + return res["success"] From 1acbf173ea4598c2efd73cd2e41376a912435c25 Mon Sep 17 00:00:00 2001 From: Son NK Date: Sun, 8 Mar 2020 10:28:13 +0100 Subject: [PATCH 3/8] Handle subscription cancel directly --- .../templates/dashboard/billing.html | 26 ++++++++++++++++++- app/dashboard/views/billing.py | 25 ++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/dashboard/templates/dashboard/billing.html b/app/dashboard/templates/dashboard/billing.html index 0dfc3a72..a18af7e3 100644 --- a/app/dashboard/templates/dashboard/billing.html +++ b/app/dashboard/templates/dashboard/billing.html @@ -45,11 +45,35 @@
Don't want to protect your inbox anymore?
- Cancel subscription 😔 + +
+ + + + Cancel subscription + +
+
{% endif %} +{% endblock %} +{% block script %} + {% endblock %} \ No newline at end of file diff --git a/app/dashboard/views/billing.py b/app/dashboard/views/billing.py index 0648be24..2e0d5272 100644 --- a/app/dashboard/views/billing.py +++ b/app/dashboard/views/billing.py @@ -1,17 +1,38 @@ -from flask import render_template, flash, redirect, url_for +from flask import render_template, flash, redirect, url_for, request from flask_login import login_required, current_user from app.dashboard.base import dashboard_bp +from app.log import LOG +from app.models import Subscription +from app.extensions import db +from app.paddle_utils import cancel_subscription @dashboard_bp.route("/billing", methods=["GET", "POST"]) @login_required def billing(): # sanity check: make sure this page is only for user who has paddle subscription - sub = current_user.get_subscription() + sub: Subscription = current_user.get_subscription() if not sub: flash("You don't have any active subscription", "warning") return redirect(url_for("dashboard.index")) + if request.method == "POST": + if request.form.get("form-name") == "cancel": + LOG.error(f"User {current_user} cancels their subscription") + success = cancel_subscription(sub.subscription_id) + + if success: + sub.cancelled = True + db.session.commit() + 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", + "error", + ) + + return redirect(url_for("dashboard.billing")) + return render_template("dashboard/billing.html", sub=sub) From 08e6f89585750ca9df4fdf3541dfdfca2c88adc4 Mon Sep 17 00:00:00 2001 From: Son NK Date: Sun, 8 Mar 2020 11:33:54 +0100 Subject: [PATCH 4/8] increase size on ForwardEmail columns --- app/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models.py b/app/models.py index 849ee6a3..fe3cb7eb 100644 --- a/app/models.py +++ b/app/models.py @@ -692,17 +692,17 @@ class ForwardEmail(db.Model, ModelMixin): ) # used to be envelope header, should be mail header from instead - website_email = db.Column(db.String(256), nullable=False) + website_email = db.Column(db.String(512), nullable=False) # the email from header, e.g. AB CD # nullable as this field is added after website_email - website_from = db.Column(db.String(256), nullable=True) + website_from = db.Column(db.String(1024), nullable=True) # when user clicks on "reply", they will reply to this address. # This address allows to hide user personal email # this reply email is created every time a website sends an email to user # it has the prefix "reply+" to distinguish with other email - reply_email = db.Column(db.String(256), nullable=False) + reply_email = db.Column(db.String(512), nullable=False) gen_email = db.relationship(GenEmail, backref="forward_emails") From b86937c5c7be33ca1650bdea0a0a42abbbdc2ce2 Mon Sep 17 00:00:00 2001 From: Son NK Date: Sun, 8 Mar 2020 11:34:39 +0100 Subject: [PATCH 5/8] Fix next_bill_update update: the event is subscription_payment_succeeded and not subscription_updated --- server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 663fb8f9..268ac2b4 100644 --- a/server.py +++ b/server.py @@ -323,7 +323,7 @@ def jinja2_filter(app): def setup_paddle_callback(app: Flask): @app.route("/paddle", methods=["GET", "POST"]) def paddle(): - LOG.debug(f"paddle callback{request.form.get('alert_name')} {request.form}") + LOG.debug(f"paddle callback {request.form.get('alert_name')} {request.form}") # make sure the request comes from Paddle if not paddle_utils.verify_incoming_request(dict(request.form)): @@ -380,7 +380,7 @@ def setup_paddle_callback(app: Flask): db.session.commit() - elif request.form.get("alert_name") == "subscription_updated": + elif request.form.get("alert_name") == "subscription_payment_succeeded": subscription_id = request.form.get("subscription_id") LOG.debug("Update subscription %s", subscription_id) From 84f3d7c278a7cbdfbfd0191ac1c3244bb024a15d Mon Sep 17 00:00:00 2001 From: Son NK Date: Sun, 8 Mar 2020 11:36:09 +0100 Subject: [PATCH 6/8] TODO: add next_bill_date check on active subscription next April --- app/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models.py b/app/models.py index fe3cb7eb..10cffbb3 100644 --- a/app/models.py +++ b/app/models.py @@ -297,6 +297,10 @@ class User(db.Model, ModelMixin, UserMixin): TODO: support user unsubscribe and re-subscribe """ sub = Subscription.get_by(user_id=self.id) + # TODO: sub is active only if sub.next_bill_date > now + # due to a bug on next_bill_date, wait until next month (April 8) + # when all next_bill_date are correctly updated to add this check + 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(): From bada1869621fbe33ea81504ead11e44ca47da843 Mon Sep 17 00:00:00 2001 From: Son NK Date: Sun, 8 Mar 2020 11:38:45 +0100 Subject: [PATCH 7/8] Log more info on cancel event --- server.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index 268ac2b4..598a3467 100644 --- a/server.py +++ b/server.py @@ -394,12 +394,23 @@ def setup_paddle_callback(app: Flask): elif request.form.get("alert_name") == "subscription_cancelled": subscription_id = request.form.get("subscription_id") - LOG.warning("Cancel subscription %s", subscription_id) sub: Subscription = Subscription.get_by(subscription_id=subscription_id) if sub: + # cancellation_effective_date should be the same as next_bill_date + LOG.error( + "Cancel subscription %s %s on %s, next bill date %s", + subscription_id, + sub.user, + request.form.get("cancellation_effective_date"), + sub.next_bill_date + ) + sub.event_time = arrow.now() + sub.cancelled = True db.session.commit() + else: + return "No such subscription", 400 return "OK" @@ -425,7 +436,7 @@ def setup_do_not_track(app): def do_not_track(): return """ - +