Skip to content

Commit 6ed7dc6

Browse files
Add pricing, privacy, terms pages
- pricing.html: Razorpay Checkout integration for Developer tier ($12/mo). No hardcoded keys — reads key_id from POST /billing/create-order. Persists ?token= through OAuth round trip via localStorage. - privacy.html: GDPR-basic privacy policy. - terms.html: Terms of service, 7-day refund window, Indian jurisdiction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b447c82 commit 6ed7dc6

3 files changed

Lines changed: 560 additions & 49 deletions

File tree

pricing.html

Lines changed: 231 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>Pricing - InstaNode</title>
7+
<meta name="description" content="InstaNode Developer plan. $12/month. Real Postgres. No account friction.">
78
<script src="https://checkout.razorpay.com/v1/checkout.js"></script>
89
<style>
910
* { box-sizing: border-box; margin: 0; padding: 0; }
@@ -13,77 +14,258 @@
1314
color: #e0e0e0;
1415
line-height: 1.6;
1516
min-height: 100vh;
16-
padding: 20px;
17+
padding: 40px 20px;
1718
}
18-
.container { max-width: 600px; margin: 0 auto; text-align: center; }
19-
h1 { margin-bottom: 40px; }
19+
.container { max-width: 440px; margin: 0 auto; }
20+
.header { text-align: center; margin-bottom: 32px; }
21+
h1 {
22+
font-size: 1.9rem;
23+
font-weight: 700;
24+
color: #fff;
25+
letter-spacing: -0.02em;
26+
margin-bottom: 8px;
27+
}
28+
.header p { color: #888; font-size: 0.95rem; }
2029
.plan {
2130
background: #111;
2231
border: 1px solid #222;
23-
border-radius: 8px;
24-
padding: 20px;
32+
border-radius: 12px;
33+
padding: 28px 24px;
34+
}
35+
.plan-name {
36+
font-size: 0.8rem;
37+
font-weight: 700;
38+
letter-spacing: 0.12em;
39+
text-transform: uppercase;
40+
color: #4af;
41+
margin-bottom: 12px;
42+
}
43+
.price {
44+
display: flex;
45+
align-items: baseline;
2546
margin-bottom: 20px;
2647
}
27-
.plan h2 { margin-bottom: 10px; }
48+
.price .amount {
49+
font-size: 2.6rem;
50+
font-weight: 700;
51+
color: #fff;
52+
letter-spacing: -0.02em;
53+
}
54+
.price .period { color: #888; margin-left: 6px; font-size: 1rem; }
55+
ul.features {
56+
list-style: none;
57+
margin-bottom: 28px;
58+
}
59+
ul.features li {
60+
padding: 8px 0;
61+
border-top: 1px solid #1a1a1a;
62+
color: #ccc;
63+
font-size: 0.95rem;
64+
}
65+
ul.features li:first-child { border-top: none; }
66+
ul.features li::before {
67+
content: "✓";
68+
color: #4af;
69+
font-weight: 700;
70+
margin-right: 10px;
71+
}
2872
.btn {
73+
width: 100%;
2974
background: #4af;
3075
color: #060a14;
31-
border: none;
32-
padding: 12px 24px;
33-
border-radius: 8px;
76+
border: 1px solid #4af;
77+
padding: 14px 24px;
78+
border-radius: 999px;
3479
font-size: 1rem;
80+
font-weight: 700;
81+
font-family: inherit;
3582
cursor: pointer;
36-
margin-top: 10px;
83+
transition: background 0.15s ease;
84+
letter-spacing: 0.02em;
85+
box-shadow: 0 10px 30px rgba(74, 175, 255, 0.18);
86+
}
87+
.btn:hover:not(:disabled) { background: #66bfff; border-color: #66bfff; }
88+
.btn:disabled { opacity: 0.6; cursor: not-allowed; box-shadow: none; }
89+
.msg {
90+
margin-top: 14px;
91+
padding: 10px 14px;
92+
border-radius: 8px;
93+
font-size: 0.9rem;
94+
display: none;
95+
}
96+
.msg.error { background: #2a1010; color: #f88; border: 1px solid #4a1818; display: block; }
97+
.msg.info { background: #0f1a24; color: #8cf; border: 1px solid #1a3048; display: block; }
98+
.toast {
99+
position: fixed;
100+
bottom: 24px;
101+
left: 50%;
102+
transform: translateX(-50%);
103+
background: #2a1010;
104+
color: #fbb;
105+
border: 1px solid #4a1818;
106+
padding: 10px 18px;
107+
border-radius: 999px;
108+
font-size: 0.9rem;
109+
opacity: 0;
110+
pointer-events: none;
111+
transition: opacity 0.2s ease;
112+
}
113+
.toast.show { opacity: 1; }
114+
.footnote {
115+
text-align: center;
116+
color: #666;
117+
font-size: 0.8rem;
118+
margin-top: 24px;
119+
}
120+
.footnote a { color: #4af; text-decoration: none; }
121+
.footnote a:hover { text-decoration: underline; }
122+
@media (max-width: 480px) {
123+
body { padding: 24px 16px; }
124+
.plan { padding: 24px 20px; }
125+
.price .amount { font-size: 2.2rem; }
37126
}
38-
.btn:hover { background: #66bfff; }
39127
</style>
40128
</head>
41129
<body>
42130
<div class="container">
43-
<h1>Choose Your Plan</h1>
44-
<div class="plan">
45-
<h2>Monthly Plan</h2>
46-
<p>₹500/month</p>
47-
<button class="btn" onclick="pay('monthly')">Subscribe Monthly</button>
131+
<div class="header">
132+
<h1>Upgrade to Developer</h1>
133+
<p>Keep your resources. Lose the limits.</p>
48134
</div>
49-
<div class="plan">
50-
<h2>Annual Plan</h2>
51-
<p>₹5000/year</p>
52-
<button class="btn" onclick="pay('annual')">Subscribe Annual</button>
135+
136+
<div class="plan" role="region" aria-label="Developer plan">
137+
<div class="plan-name">Developer</div>
138+
<div class="price">
139+
<span class="amount">$12</span>
140+
<span class="period">/month</span>
141+
</div>
142+
<ul class="features">
143+
<li>Persistent Postgres &amp; Redis (no 24h expiry)</li>
144+
<li>5 GB storage, 20 concurrent connections</li>
145+
<li>Daily backups</li>
146+
<li>Email support</li>
147+
</ul>
148+
<button id="upgrade-btn" class="btn" type="button">Upgrade to $12/mo Developer</button>
149+
<div id="msg" class="msg" role="status" aria-live="polite"></div>
53150
</div>
151+
152+
<p class="footnote">
153+
Secure checkout by Razorpay. Cancel anytime from the <a href="/dashboard">dashboard</a>.
154+
</p>
54155
</div>
156+
157+
<div id="toast" class="toast" role="alert" aria-live="assertive"></div>
158+
55159
<script>
56-
async function pay(planId) {
57-
try {
58-
const res = await fetch('/billing/create-order', {
160+
(function () {
161+
var API_BASE = 'https://api.instanode.dev';
162+
var btn = document.getElementById('upgrade-btn');
163+
var msgEl = document.getElementById('msg');
164+
var toastEl = document.getElementById('toast');
165+
166+
// Resource token can come in via ?token=<uuid> (from the /start link the
167+
// API emits when limits are hit) OR from localStorage (set on /start.html).
168+
var urlParams = new URLSearchParams(window.location.search);
169+
var tokenParam = urlParams.get('token');
170+
if (tokenParam) {
171+
try { localStorage.setItem('instanode_token', tokenParam); } catch (e) {}
172+
}
173+
var resourceToken = tokenParam || (function () {
174+
try { return localStorage.getItem('instanode_token'); } catch (e) { return null; }
175+
})();
176+
177+
function showMsg(kind, text) {
178+
msgEl.className = 'msg ' + kind;
179+
msgEl.textContent = text;
180+
}
181+
function clearMsg() {
182+
msgEl.className = 'msg';
183+
msgEl.textContent = '';
184+
}
185+
function showToast(text) {
186+
toastEl.textContent = text;
187+
toastEl.classList.add('show');
188+
setTimeout(function () { toastEl.classList.remove('show'); }, 3500);
189+
}
190+
191+
btn.addEventListener('click', function () {
192+
if (typeof Razorpay === 'undefined') {
193+
showMsg('error', 'Payment module failed to load. Check your connection and retry.');
194+
return;
195+
}
196+
197+
btn.disabled = true;
198+
btn.textContent = 'Preparing checkout…';
199+
clearMsg();
200+
201+
var body = { plan_id: 'developer', currency: 'USD' };
202+
if (resourceToken) body.token = resourceToken;
203+
204+
fetch(API_BASE + '/billing/create-order', {
59205
method: 'POST',
206+
credentials: 'include',
60207
headers: { 'Content-Type': 'application/json' },
61-
body: JSON.stringify({ plan_id: planId })
62-
});
63-
const order = await res.json();
64-
65-
const options = {
66-
key: order.key_id,
67-
amount: order.amount,
68-
currency: order.currency,
69-
order_id: order.order_id,
70-
name: order.name,
71-
description: 'InstaNode Subscription',
72-
handler: function (response) {
73-
// After payment, migrate the resource
74-
const token = localStorage.getItem('instanode_token');
75-
if (token) {
76-
fetch('/billing/migrate?token=' + token, { method: 'POST' });
208+
body: JSON.stringify(body)
209+
})
210+
.then(function (res) {
211+
if (res.status === 401) {
212+
// Not signed in — bounce through the claim page so they can auth
213+
// and preserve the token so they come back here after login.
214+
var dest = '/start' + (resourceToken ? ('?token=' + encodeURIComponent(resourceToken)) : '');
215+
window.location.href = dest;
216+
return null;
77217
}
78-
window.location.href = '/dashboard';
79-
}
80-
};
81-
const rzp = new Razorpay(options);
82-
rzp.open();
83-
} catch (e) {
84-
alert('Error: ' + e.message);
85-
}
86-
}
218+
if (!res.ok) {
219+
return res.text().then(function (t) {
220+
throw new Error(t && t.length < 200 ? t : ('HTTP ' + res.status));
221+
});
222+
}
223+
return res.json();
224+
})
225+
.then(function (order) {
226+
if (!order) return; // redirected on 401
227+
var options = {
228+
key: order.key_id,
229+
amount: order.amount,
230+
currency: order.currency,
231+
order_id: order.order_id,
232+
name: 'InstaNode',
233+
description: 'Developer plan – $12/month',
234+
prefill: {
235+
name: order.name || '',
236+
email: order.email || '',
237+
contact: order.contact || ''
238+
},
239+
theme: { color: '#4af' },
240+
notes: resourceToken ? { token: resourceToken } : undefined,
241+
handler: function () {
242+
// Webhook upgrades the tier server-side; land on dashboard.
243+
window.location.href = '/dashboard?upgraded=1';
244+
},
245+
modal: {
246+
ondismiss: function () {
247+
btn.disabled = false;
248+
btn.textContent = 'Upgrade to $12/mo Developer';
249+
showToast('Checkout closed. You can retry anytime.');
250+
}
251+
}
252+
};
253+
var rzp = new Razorpay(options);
254+
rzp.on('payment.failed', function (resp) {
255+
btn.disabled = false;
256+
btn.textContent = 'Upgrade to $12/mo Developer';
257+
var reason = (resp && resp.error && (resp.error.description || resp.error.reason)) || 'Payment failed.';
258+
showMsg('error', reason + ' Please try again.');
259+
});
260+
rzp.open();
261+
})
262+
.catch(function (err) {
263+
btn.disabled = false;
264+
btn.textContent = 'Upgrade to $12/mo Developer';
265+
showMsg('error', 'Could not start checkout: ' + (err && err.message ? err.message : 'unknown error'));
266+
});
267+
});
268+
})();
87269
</script>
88270
</body>
89-
</html>
271+
</html>

0 commit comments

Comments
 (0)