Merge branch 'cancel-sub' into staging

This commit is contained in:
Son NK 2020-03-08 23:13:23 +01:00
commit c1c3224a41
7 changed files with 98 additions and 14 deletions

View File

@ -144,6 +144,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")

View File

@ -45,11 +45,35 @@
<div>
Don't want to protect your inbox anymore? <br>
<a class="btn btn-warning" href="{{ sub.cancel_url }}"> Cancel subscription 😔 </a>
<form method="post">
<input type="hidden" name="form-name" value="cancel">
<span class="cancel btn btn-warning">
Cancel subscription <i class="fe fe-alert-triangle text-danger"></i>
</span>
</form>
</div>
{% endif %}
</div>
{% endblock %}
{% block script %}
<script>
$(".cancel").on("click", function (e) {
notie.confirm({
text: `This operation is irreversible, please confirm`,
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -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)

View File

@ -302,6 +302,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():
@ -697,17 +701,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 <ab@cd.com>
# 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")

View File

@ -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"]

View File

@ -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

View File

@ -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)
@ -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 """
<script src="/static/local-storage-polyfill.js"></script>
<script>
// Disable GoatCounter if this script is called