feat(billing): A5 — dual-path JS templates for Paddle overlay / Stripe redirect
- New _payment_js.html: conditionally loads Paddle.js or nothing (Stripe uses server-side Checkout Session). Provides startCheckout() helper. - All checkout templates use _payment_js.html instead of _paddle.html - export.html, signup_step_4.html: Paddle.Checkout.open() → startCheckout() - dashboard_boosts.html: inline onclick → buyItem() with server round-trip - New /billing/checkout/item endpoint for single-item purchases (boosts, credits) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,32 @@ async def checkout(plan: str):
|
||||
return jsonify(payload)
|
||||
|
||||
|
||||
@bp.route("/checkout/item", methods=["POST"])
|
||||
@login_required
|
||||
async def checkout_item():
|
||||
"""Return checkout JSON for a single item (boost, credit pack, etc.).
|
||||
|
||||
Used by dashboard boost/credit buttons that need a server round-trip
|
||||
for Stripe (Checkout Session creation) and work with Paddle overlay too.
|
||||
Expects JSON body: {price_key, custom_data, success_url?}
|
||||
"""
|
||||
body = await request.get_json(silent=True) or {}
|
||||
price_key = body.get("price_key", "")
|
||||
custom_data = body.get("custom_data", {})
|
||||
success_url = body.get("success_url", f"{config.BASE_URL}/suppliers/dashboard?tab=boosts")
|
||||
|
||||
price_id = await get_price_id(price_key)
|
||||
if not price_id:
|
||||
return jsonify({"error": "Product not configured."}), 400
|
||||
|
||||
payload = _provider().build_checkout_payload(
|
||||
price_id=price_id,
|
||||
custom_data=custom_data,
|
||||
success_url=success_url,
|
||||
)
|
||||
return jsonify(payload)
|
||||
|
||||
|
||||
@bp.route("/manage", methods=["POST"])
|
||||
@login_required
|
||||
async def manage():
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ t.export_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block paddle %}{% include "_paddle.html" %}{% endblock %}
|
||||
{% block paddle %}{% include "_payment_js.html" %}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
@@ -133,11 +133,7 @@ document.getElementById('export-form').addEventListener('submit', async function
|
||||
btn.textContent = '{{ t.export_btn }}';
|
||||
return;
|
||||
}
|
||||
Paddle.Checkout.open({
|
||||
items: data.items,
|
||||
customData: data.customData,
|
||||
settings: data.settings,
|
||||
});
|
||||
startCheckout(data);
|
||||
btn.disabled = false;
|
||||
btn.textContent = '{{ t.export_btn }}';
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ t.sd_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block paddle %}{% include "_paddle.html" %}{% endblock %}
|
||||
{% block paddle %}{% include "_payment_js.html" %}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<div class="bst-boost__price">€{{ b.price }}/mo</div>
|
||||
{% if price_ids.get(b.key) %}
|
||||
<button type="button" class="bst-buy-btn"
|
||||
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[b.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
||||
onclick="buyItem('{{ b.key }}', {supplier_id:'{{ supplier.id }}'}, this)">
|
||||
{{ t.sd_bst_activate }}
|
||||
</button>
|
||||
{% else %}
|
||||
@@ -125,7 +125,7 @@
|
||||
<div class="bst-credit-card__price">€{{ cp.price }}</div>
|
||||
{% if price_ids.get(cp.key) %}
|
||||
<button type="button" class="bst-buy-btn"
|
||||
onclick="Paddle.Checkout.open({items:[{priceId:'{{ price_ids[cp.key] }}',quantity:1}],customData:{supplier_id:'{{ supplier.id }}'}})">
|
||||
onclick="buyItem('{{ cp.key }}', {supplier_id:'{{ supplier.id }}'}, this)">
|
||||
{{ t.sd_bst_buy }}
|
||||
</button>
|
||||
{% else %}
|
||||
@@ -160,3 +160,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function buyItem(priceKey, customData, btn) {
|
||||
var label = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '...';
|
||||
fetch('/billing/checkout/item', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({price_key: priceKey, custom_data: customData}),
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.error) { alert(data.error); }
|
||||
else { startCheckout(data); }
|
||||
btn.disabled = false;
|
||||
btn.textContent = label;
|
||||
})
|
||||
.catch(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = label;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -124,11 +124,7 @@
|
||||
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||
return;
|
||||
}
|
||||
Paddle.Checkout.open({
|
||||
items: result.data.items,
|
||||
customData: result.data.customData,
|
||||
settings: result.data.settings
|
||||
});
|
||||
startCheckout(result.data);
|
||||
btn.disabled = false;
|
||||
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ t.sup_signup_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||
{% block paddle %}{% include "_paddle.html" %}{% endblock %}
|
||||
{% block paddle %}{% include "_payment_js.html" %}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
|
||||
51
web/src/padelnomics/templates/_payment_js.html
Normal file
51
web/src/padelnomics/templates/_payment_js.html
Normal file
@@ -0,0 +1,51 @@
|
||||
{# Payment JS — conditionally loads provider SDK on checkout pages.
|
||||
Include via {% block paddle %}{% include "_payment_js.html" %}{% endblock %}
|
||||
Paddle: loads Paddle.js SDK + initializes overlay checkout.
|
||||
Stripe: no SDK needed (server-side Checkout Session + redirect). #}
|
||||
|
||||
{% if config.PAYMENT_PROVIDER == "paddle" %}
|
||||
<script defer src="https://cdn.paddle.com/paddle/v2/paddle.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
{% if config.PADDLE_ENVIRONMENT == "sandbox" %}
|
||||
Paddle.Environment.set("sandbox");
|
||||
{% endif %}
|
||||
{% if config.PADDLE_CLIENT_TOKEN %}
|
||||
Paddle.Initialize({
|
||||
token: "{{ config.PADDLE_CLIENT_TOKEN }}",
|
||||
eventCallback: function(ev) {
|
||||
if (ev.name === "checkout.error") console.error("Paddle checkout error:", ev.data);
|
||||
},
|
||||
checkout: {
|
||||
settings: {
|
||||
displayMode: "overlay",
|
||||
theme: "light",
|
||||
locale: "en",
|
||||
}
|
||||
}
|
||||
});
|
||||
{% else %}
|
||||
console.warn("Paddle: PADDLE_CLIENT_TOKEN not configured");
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
/**
|
||||
* startCheckout — dual-path checkout handler.
|
||||
* Paddle: opens overlay with items/customData/settings from response JSON.
|
||||
* Stripe: redirects to checkout_url from response JSON.
|
||||
*/
|
||||
function startCheckout(data) {
|
||||
if (data.checkout_url) {
|
||||
window.location.href = data.checkout_url;
|
||||
} else if (data.items) {
|
||||
Paddle.Checkout.open({
|
||||
items: data.items,
|
||||
customData: data.customData,
|
||||
settings: data.settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user