add /coinbase to handle Coinbase callback
This commit is contained in:
parent
fbe48b7b3e
commit
b00841f679
115
server.py
115
server.py
|
@ -6,6 +6,8 @@ from datetime import timedelta
|
||||||
import arrow
|
import arrow
|
||||||
import flask_profiler
|
import flask_profiler
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
from coinbase_commerce.error import WebhookInvalidPayload, SignatureVerificationError
|
||||||
|
from coinbase_commerce.webhook import Webhook
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask,
|
Flask,
|
||||||
redirect,
|
redirect,
|
||||||
|
@ -53,6 +55,7 @@ from app.config import (
|
||||||
PADDLE_MONTHLY_PRODUCT_IDS,
|
PADDLE_MONTHLY_PRODUCT_IDS,
|
||||||
PADDLE_YEARLY_PRODUCT_IDS,
|
PADDLE_YEARLY_PRODUCT_IDS,
|
||||||
PGP_SIGNER,
|
PGP_SIGNER,
|
||||||
|
COINBASE_WEBHOOK_SECRET,
|
||||||
)
|
)
|
||||||
from app.dashboard.base import dashboard_bp
|
from app.dashboard.base import dashboard_bp
|
||||||
from app.developer.base import developer_bp
|
from app.developer.base import developer_bp
|
||||||
|
@ -77,6 +80,7 @@ from app.models import (
|
||||||
Referral,
|
Referral,
|
||||||
AliasMailbox,
|
AliasMailbox,
|
||||||
Notification,
|
Notification,
|
||||||
|
CoinbaseSubscription,
|
||||||
)
|
)
|
||||||
from app.monitor.base import monitor_bp
|
from app.monitor.base import monitor_bp
|
||||||
from app.oauth.base import oauth_bp
|
from app.oauth.base import oauth_bp
|
||||||
|
@ -142,6 +146,7 @@ def create_app() -> Flask:
|
||||||
|
|
||||||
init_admin(app)
|
init_admin(app)
|
||||||
setup_paddle_callback(app)
|
setup_paddle_callback(app)
|
||||||
|
setup_coinbase_commerce(app)
|
||||||
setup_do_not_track(app)
|
setup_do_not_track(app)
|
||||||
|
|
||||||
if FLASK_PROFILER_PATH:
|
if FLASK_PROFILER_PATH:
|
||||||
|
@ -198,20 +203,23 @@ def fake_data():
|
||||||
|
|
||||||
user.trial_end = None
|
user.trial_end = None
|
||||||
|
|
||||||
LifetimeCoupon.create(code="coupon", nb_used=10)
|
LifetimeCoupon.create(code="coupon", nb_used=10, commit=True)
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
# Create a subscription for user
|
# Create a subscription for user
|
||||||
Subscription.create(
|
# Subscription.create(
|
||||||
user_id=user.id,
|
# user_id=user.id,
|
||||||
cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234",
|
# cancel_url="https://checkout.paddle.com/subscription/cancel?user=1234",
|
||||||
update_url="https://checkout.paddle.com/subscription/update?user=1234",
|
# update_url="https://checkout.paddle.com/subscription/update?user=1234",
|
||||||
subscription_id="123",
|
# subscription_id="123",
|
||||||
event_time=arrow.now(),
|
# event_time=arrow.now(),
|
||||||
next_bill_date=arrow.now().shift(days=10).date(),
|
# next_bill_date=arrow.now().shift(days=10).date(),
|
||||||
plan=PlanEnum.monthly,
|
# plan=PlanEnum.monthly,
|
||||||
|
# )
|
||||||
|
# db.session.commit()
|
||||||
|
|
||||||
|
CoinbaseSubscription.create(
|
||||||
|
user_id=user.id, end_at=arrow.now().shift(days=10), commit=True
|
||||||
)
|
)
|
||||||
db.session.commit()
|
|
||||||
|
|
||||||
api_key = ApiKey.create(user_id=user.id, name="Chrome")
|
api_key = ApiKey.create(user_id=user.id, name="Chrome")
|
||||||
api_key.code = "code"
|
api_key.code = "code"
|
||||||
|
@ -634,6 +642,91 @@ def setup_paddle_callback(app: Flask):
|
||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_coinbase_commerce(app):
|
||||||
|
@app.route("/coinbase", methods=["POST"])
|
||||||
|
def coinbase_webhook():
|
||||||
|
# event payload
|
||||||
|
request_data = request.data.decode("utf-8")
|
||||||
|
# webhook signature
|
||||||
|
request_sig = request.headers.get("X-CC-Webhook-Signature", None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# signature verification and event object construction
|
||||||
|
event = Webhook.construct_event(
|
||||||
|
request_data, request_sig, COINBASE_WEBHOOK_SECRET
|
||||||
|
)
|
||||||
|
except (WebhookInvalidPayload, SignatureVerificationError) as e:
|
||||||
|
LOG.exception("Invalid Coinbase webhook")
|
||||||
|
return str(e), 400
|
||||||
|
|
||||||
|
LOG.d("Coinbase event %s", event)
|
||||||
|
|
||||||
|
if event["type"] == "charge:confirmed":
|
||||||
|
if handle_coinbase_event(event):
|
||||||
|
return "success", 200
|
||||||
|
else:
|
||||||
|
return "error", 400
|
||||||
|
|
||||||
|
return "success", 200
|
||||||
|
|
||||||
|
|
||||||
|
def handle_coinbase_event(event) -> bool:
|
||||||
|
user_id = int(event["data"]["metadata"]["custom"])
|
||||||
|
code = event["data"]["code"]
|
||||||
|
user = User.get(user_id)
|
||||||
|
if not user:
|
||||||
|
LOG.exception("User not found %s", user_id)
|
||||||
|
return False
|
||||||
|
|
||||||
|
coinbase_subscription: CoinbaseSubscription = CoinbaseSubscription.get_by(
|
||||||
|
user_id=user_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not coinbase_subscription:
|
||||||
|
LOG.d("Create a coinbase subscription for %s", user)
|
||||||
|
coinbase_subscription = CoinbaseSubscription.create(
|
||||||
|
user_id=user_id, end_at=arrow.now().shift(years=1), code=code, commit=True
|
||||||
|
)
|
||||||
|
send_email(
|
||||||
|
user.email,
|
||||||
|
"Your SimpleLogin account has been upgraded",
|
||||||
|
render(
|
||||||
|
"transactional/coinbase/new-subscription.txt",
|
||||||
|
coinbase_subscription=coinbase_subscription,
|
||||||
|
),
|
||||||
|
render(
|
||||||
|
"transactional/coinbase/new-subscription.html",
|
||||||
|
coinbase_subscription=coinbase_subscription,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if coinbase_subscription.code != code:
|
||||||
|
LOG.d("Update code from %s to %s", coinbase_subscription.code, code)
|
||||||
|
coinbase_subscription.code = code
|
||||||
|
|
||||||
|
if coinbase_subscription.is_active():
|
||||||
|
coinbase_subscription.end_at = coinbase_subscription.end_at.shift(years=1)
|
||||||
|
else: # already expired subscription
|
||||||
|
coinbase_subscription.end_at = arrow.now().shift(years=1)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
send_email(
|
||||||
|
user.email,
|
||||||
|
"Your SimpleLogin account has been extended",
|
||||||
|
render(
|
||||||
|
"transactional/coinbase/extend-subscription.txt",
|
||||||
|
coinbase_subscription=coinbase_subscription,
|
||||||
|
),
|
||||||
|
render(
|
||||||
|
"transactional/coinbase/extend-subscription.html",
|
||||||
|
coinbase_subscription=coinbase_subscription,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def init_extensions(app: Flask):
|
def init_extensions(app: Flask):
|
||||||
login_manager.init_app(app)
|
login_manager.init_app(app)
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% call text() %}
|
||||||
|
<h1>
|
||||||
|
Your subscription has been extended!
|
||||||
|
</h1>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
Your payment with cryptocurrency has been successfully processed. <br>
|
||||||
|
Your subscription has been extended to
|
||||||
|
<b>{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}</b>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
Thank you a lot for your support!
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
|
||||||
|
{{ render_text('Best, <br />SimpleLogin Team.') }}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
||||||
|
Your subscription has been extended!
|
||||||
|
|
||||||
|
Your payment with cryptocurrency has been successfully processed.
|
||||||
|
|
||||||
|
Your subscription has been extended to
|
||||||
|
{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}
|
||||||
|
|
||||||
|
Thank you a lot for your support!
|
||||||
|
|
||||||
|
Best,
|
||||||
|
SimpleLogin team.
|
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% call text() %}
|
||||||
|
<h1>
|
||||||
|
Your account has been upgraded!
|
||||||
|
</h1>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
Your payment with cryptocurrency has been successfully processed. <br>
|
||||||
|
Your account has been upgraded to the premium plan until
|
||||||
|
<b>{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}</b>
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
{% call text() %}
|
||||||
|
Thank you a lot for your support!
|
||||||
|
{% endcall %}
|
||||||
|
|
||||||
|
|
||||||
|
{{ render_text('Best, <br />SimpleLogin Team.') }}
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,11 @@
|
||||||
|
Your account has been upgraded!
|
||||||
|
|
||||||
|
Your payment with cryptocurrency has been successfully processed.
|
||||||
|
|
||||||
|
Your account has been upgraded to premium plan until
|
||||||
|
{{ coinbase_subscription.end_at.format("YYYY-MM-DD") }}
|
||||||
|
|
||||||
|
Thank you a lot for your support!
|
||||||
|
|
||||||
|
Best,
|
||||||
|
SimpleLogin team.
|
|
@ -1,6 +1,64 @@
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models import User, CoinbaseSubscription
|
||||||
|
from server import handle_coinbase_event
|
||||||
|
|
||||||
|
|
||||||
def test_redirect_login_page(flask_client):
|
def test_redirect_login_page(flask_client):
|
||||||
"""Start with a blank database."""
|
"""Start with a blank database."""
|
||||||
|
|
||||||
rv = flask_client.get("/")
|
rv = flask_client.get("/")
|
||||||
assert rv.status_code == 302
|
assert rv.status_code == 302
|
||||||
assert rv.location == "http://sl.test/auth/login"
|
assert rv.location == "http://sl.test/auth/login"
|
||||||
|
|
||||||
|
|
||||||
|
def test_coinbase_webhook(flask_client):
|
||||||
|
r = flask_client.post("/coinbase")
|
||||||
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_coinbase_event_new_subscription(flask_client):
|
||||||
|
user = User.create(
|
||||||
|
email="a@b.c",
|
||||||
|
password="password",
|
||||||
|
name="Test User",
|
||||||
|
activated=True,
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
handle_coinbase_event(
|
||||||
|
{"data": {"code": "AAAAAA", "metadata": {"custom": str(user.id)}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user.is_paid()
|
||||||
|
assert user.is_premium()
|
||||||
|
|
||||||
|
assert CoinbaseSubscription.get_by(user_id=user.id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_coinbase_event_extend_subscription(flask_client):
|
||||||
|
user = User.create(
|
||||||
|
email="a@b.c",
|
||||||
|
password="password",
|
||||||
|
name="Test User",
|
||||||
|
activated=True,
|
||||||
|
)
|
||||||
|
user.trial_end = None
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
cb = CoinbaseSubscription.create(
|
||||||
|
user_id=user.id, end_at=arrow.now().shift(days=-400), commit=True
|
||||||
|
)
|
||||||
|
assert not cb.is_active()
|
||||||
|
|
||||||
|
assert not user.is_paid()
|
||||||
|
assert not user.is_premium()
|
||||||
|
|
||||||
|
handle_coinbase_event(
|
||||||
|
{"data": {"code": "AAAAAA", "metadata": {"custom": str(user.id)}}}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert user.is_paid()
|
||||||
|
assert user.is_premium()
|
||||||
|
|
||||||
|
assert CoinbaseSubscription.get_by(user_id=user.id) is not None
|
||||||
|
|
Loading…
Reference in New Issue