Merge branch 'cancel-sub' into staging
This commit is contained in:
commit
c1c3224a41
|
@ -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")
|
||||
|
|
|
@ -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 %}
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
19
server.py
19
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)
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue