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 ; }
1314 color : # e0e0e0 ;
1415 line-height : 1.6 ;
1516 min-height : 100vh ;
16- padding : 20px ;
17+ padding : 40 px 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 : 12 px 24px ;
33- border-radius : 8 px ;
76+ border : 1 px solid # 4af ;
77+ padding : 14 px 24px ;
78+ border-radius : 999 px ;
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 & 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