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)
|
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"])
|
@bp.route("/manage", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
async def manage():
|
async def manage():
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ t.export_title }} - {{ config.APP_NAME }}{% endblock %}
|
{% block title %}{{ t.export_title }} - {{ config.APP_NAME }}{% endblock %}
|
||||||
{% block paddle %}{% include "_paddle.html" %}{% endblock %}
|
{% block paddle %}{% include "_payment_js.html" %}{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
@@ -133,11 +133,7 @@ document.getElementById('export-form').addEventListener('submit', async function
|
|||||||
btn.textContent = '{{ t.export_btn }}';
|
btn.textContent = '{{ t.export_btn }}';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Paddle.Checkout.open({
|
startCheckout(data);
|
||||||
items: data.items,
|
|
||||||
customData: data.customData,
|
|
||||||
settings: data.settings,
|
|
||||||
});
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '{{ t.export_btn }}';
|
btn.textContent = '{{ t.export_btn }}';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ t.sd_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
{% 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 %}
|
{% block head %}
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
<div class="bst-boost__price">€{{ b.price }}/mo</div>
|
<div class="bst-boost__price">€{{ b.price }}/mo</div>
|
||||||
{% if price_ids.get(b.key) %}
|
{% if price_ids.get(b.key) %}
|
||||||
<button type="button" class="bst-buy-btn"
|
<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 }}
|
{{ t.sd_bst_activate }}
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -125,7 +125,7 @@
|
|||||||
<div class="bst-credit-card__price">€{{ cp.price }}</div>
|
<div class="bst-credit-card__price">€{{ cp.price }}</div>
|
||||||
{% if price_ids.get(cp.key) %}
|
{% if price_ids.get(cp.key) %}
|
||||||
<button type="button" class="bst-buy-btn"
|
<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 }}
|
{{ t.sd_bst_buy }}
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -160,3 +160,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 }};
|
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Paddle.Checkout.open({
|
startCheckout(result.data);
|
||||||
items: result.data.items,
|
|
||||||
customData: result.data.customData,
|
|
||||||
settings: result.data.settings
|
|
||||||
});
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
btn.textContent = {{ t.sup_step4_checkout | tojson }};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}{{ t.sup_signup_page_title }} - {{ config.APP_NAME }}{% endblock %}
|
{% 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 %}
|
{% block head %}
|
||||||
<style>
|
<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