Skip to content

Commit 4f269b7

Browse files
pricing+dashboard: wire Razorpay Subscriptions flow
- Buttons now say Subscribe and POST to /billing/create-subscription - Razorpay Checkout.js takes subscription_id instead of order_id - Dashboard banner shows Cancel subscription action when status=active Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f02b30b commit 4f269b7

2 files changed

Lines changed: 40 additions & 34 deletions

File tree

dashboard.html

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,13 @@ <h2 id="apikey-heading">API token for CLI / agent</h2>
338338
function renderBanner(list) {
339339
var label = planLabel(currentUser);
340340
if (label) {
341-
$banner.innerHTML = '<div class="banner paid"><span><strong>' + label.line + '</strong>' + nextRenewal(currentUser) + '</span><a href="/pricing.html">Manage plan</a></div>';
341+
var status = (currentUser && currentUser.subscription_status) || '';
342+
var action = (status === 'active')
343+
? '<a href="#" class="js-cancel-sub">Cancel subscription</a>'
344+
: '<a href="/pricing.html">Manage plan</a>';
345+
$banner.innerHTML = '<div class="banner paid"><span><strong>' + label.line + '</strong>' + nextRenewal(currentUser) + '</span>' + action + '</div>';
346+
var cancelLink = $banner.querySelector('.js-cancel-sub');
347+
if (cancelLink) cancelLink.addEventListener('click', cancelSubscription);
342348
return;
343349
}
344350
var hasPaid = Array.isArray(list) && list.some(function (r) { return (r.tier || '').toLowerCase() === 'paid'; });
@@ -347,6 +353,22 @@ <h2 id="apikey-heading">API token for CLI / agent</h2>
347353
: '<div class="banner free"><span><strong>Free tier</strong> · resources expire in 24h.</span><a href="/pricing.html">Upgrade →</a></div>';
348354
}
349355

356+
function cancelSubscription(e) {
357+
if (e) e.preventDefault();
358+
if (!confirm('Cancel subscription? You\'ll keep paid access until the end of the current billing period.')) return;
359+
fetch(API + '/billing/cancel-subscription', { method: 'POST', credentials: 'include' })
360+
.then(function (res) { return res.json().then(function (b) { return { ok: res.ok, body: b }; }); })
361+
.then(function (r) {
362+
if (r.ok) {
363+
alert('Subscription cancelled. You keep paid access until the current period ends.');
364+
window.location.reload();
365+
} else {
366+
alert((r.body && r.body.message) || 'Could not cancel right now. Please retry in a moment.');
367+
}
368+
})
369+
.catch(function () { alert('Could not reach the billing service. Please retry.'); });
370+
}
371+
350372
function renderResources(list) {
351373
if (!Array.isArray(list) || list.length === 0) {
352374
$subtitle.textContent = 'Provision one with a single curl call.';

pricing.html

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,8 @@ <h1>Upgrade to Developer</h1>
248248
<li>1000 webhook requests stored</li>
249249
<li>Email support</li>
250250
</ul>
251-
<button class="btn secondary js-upgrade" type="button" data-plan="developer" data-label="Upgrade to $12/mo">Upgrade to $12/mo</button>
252-
<div class="msg" role="status" aria-live="polite" data-msg-for="developer"></div>
251+
<button class="btn secondary js-upgrade" type="button" data-plan="monthly" data-label="Subscribe — $12/mo">Subscribe — $12/mo</button>
252+
<div class="msg" role="status" aria-live="polite" data-msg-for="monthly"></div>
253253
</div>
254254

255255
<div class="plan featured" role="region" aria-label="Developer annual plan">
@@ -266,8 +266,8 @@ <h1>Upgrade to Developer</h1>
266266
<li>One annual invoice</li>
267267
<li>Priority support</li>
268268
</ul>
269-
<button class="btn js-upgrade" type="button" data-plan="developer-annual" data-label="Upgrade to $120/yr">Upgrade to $120/yr</button>
270-
<div class="msg" role="status" aria-live="polite" data-msg-for="developer-annual"></div>
269+
<button class="btn js-upgrade" type="button" data-plan="annual" data-label="Subscribe — $120/yr">Subscribe — $120/yr</button>
270+
<div class="msg" role="status" aria-live="polite" data-msg-for="annual"></div>
271271
</div>
272272
</div>
273273

@@ -325,8 +325,8 @@ <h1>Upgrade to Developer</h1>
325325
setTimeout(function () { toastEl.classList.remove('show'); }, 3500);
326326
}
327327

328-
function describePlan(planID) {
329-
if (planID === 'developer-annual') return 'Developer plan – $120/year';
328+
function describePlan(plan) {
329+
if (plan === 'annual') return 'Developer plan – $120/year';
330330
return 'Developer plan – $12/month';
331331
}
332332

@@ -336,16 +336,16 @@ <h1>Upgrade to Developer</h1>
336336
return;
337337
}
338338

339-
var planID = btn.dataset.plan;
339+
var plan = btn.dataset.plan;
340340
var resetLabel = btn.dataset.label;
341341
btn.disabled = true;
342342
btn.textContent = 'Preparing checkout…';
343-
clearMsg(planID);
343+
clearMsg(plan);
344344

345-
var body = { plan_id: planID, currency: 'USD' };
345+
var body = { plan: plan };
346346
if (resourceToken) body.token = resourceToken;
347347

348-
fetch(API_BASE + '/billing/create-order', {
348+
fetch(API_BASE + '/billing/create-subscription', {
349349
method: 'POST',
350350
credentials: 'include',
351351
headers: { 'Content-Type': 'application/json' },
@@ -374,22 +374,14 @@ <h1>Upgrade to Developer</h1>
374374
}
375375
return res.json();
376376
})
377-
.then(function (order) {
378-
if (!order) return; // redirected on 401
377+
.then(function (sub) {
378+
if (!sub) return; // redirected on 401
379379
var options = {
380-
key: order.key_id,
381-
amount: order.amount,
382-
currency: order.currency,
383-
order_id: order.order_id,
380+
key: sub.key_id,
381+
subscription_id: sub.subscription_id,
384382
name: 'InstaNode',
385-
description: describePlan(planID),
386-
prefill: {
387-
name: order.name || '',
388-
email: order.email || '',
389-
contact: order.contact || ''
390-
},
383+
description: describePlan(plan),
391384
theme: { color: '#4af' },
392-
notes: resourceToken ? { token: resourceToken } : undefined,
393385
handler: function () {
394386
window.location.href = '/dashboard?upgraded=1';
395387
},
@@ -405,16 +397,12 @@ <h1>Upgrade to Developer</h1>
405397
rzp.on('payment.failed', function (resp) {
406398
btn.disabled = false;
407399
btn.textContent = resetLabel;
408-
// Razorpay's resp.error.description is gateway-controlled text.
409-
// Only surface it when it's a short, human-ish string; otherwise
410-
// fall back to a generic message so we never paint long SDK
411-
// stack traces or vendor error codes into the UI.
412400
var raw = resp && resp.error && (resp.error.description || resp.error.reason);
413401
var safe = '';
414402
if (typeof raw === 'string' && raw.length > 0 && raw.length <= 140) {
415403
safe = raw;
416404
}
417-
showMsg(planID, 'error', safe
405+
showMsg(plan, 'error', safe
418406
? (safe + ' Please try again.')
419407
: 'Payment failed. Please try a different card or retry in a moment.');
420408
});
@@ -423,14 +411,10 @@ <h1>Upgrade to Developer</h1>
423411
.catch(function (err) {
424412
btn.disabled = false;
425413
btn.textContent = resetLabel;
426-
// err.friendly === true means the message came from our own API
427-
// via a safe `message` field. Anything else (network failure,
428-
// browser block) we render as a generic retry prompt so we
429-
// never paint stack traces or CORS strings at the user.
430414
var text = (err && err.friendly && err.message)
431415
? err.message
432416
: 'Could not reach the checkout service. Please check your connection and retry.';
433-
showMsg(planID, 'error', text);
417+
showMsg(plan, 'error', text);
434418
});
435419
}
436420

0 commit comments

Comments
 (0)