11import { useCallback , useState } from 'react'
2+ import { useRouter } from 'next/router'
23import * as Ariakit from '@ariakit/react'
3- import { EmbeddedCheckout , EmbeddedCheckoutProvider } from '@stripe/react-stripe-js'
4+ import {
5+ Elements ,
6+ EmbeddedCheckout ,
7+ EmbeddedCheckoutProvider ,
8+ PaymentElement ,
9+ useElements ,
10+ useStripe
11+ } from '@stripe/react-stripe-js'
412import { loadStripe } from '@stripe/stripe-js'
13+ import { useQueryClient } from '@tanstack/react-query'
514import { Icon } from '~/components/Icon'
615import { AUTH_SERVER , STRIPE_PUBLISHABLE_KEY } from '~/constants'
716import { useAuthContext } from '~/containers/Subscribtion/auth'
@@ -24,19 +33,30 @@ export function StripeCheckoutModal({
2433 billingInterval = 'month'
2534} : StripeCheckoutModalProps ) {
2635 const { authorizedFetch } = useAuthContext ( ) !
36+ const router = useRouter ( )
37+ const queryClient = useQueryClient ( )
2738 const [ error , setError ] = useState < string | null > ( null )
39+ const [ isUpgrade , setIsUpgrade ] = useState ( false )
40+ const [ subscriptionId , setSubscriptionId ] = useState < string | null > ( null )
41+ const [ requiresPayment , setRequiresPayment ] = useState < boolean > ( true )
42+ const [ upgradeClientSecret , setUpgradeClientSecret ] = useState < string | null > ( null )
43+ const [ upgradePricing , setUpgradePricing ] = useState < {
44+ amount : number
45+ currency : string
46+ prorationCredit : number
47+ newSubscriptionPrice : number
48+ } | null > ( null )
2849
2950 const fetchClientSecret = useCallback ( async ( ) => {
3051 try {
3152 setError ( null )
3253
3354 const subscriptionData = {
34- redirectUrl : `${ window . location . origin } /account?session_id={CHECKOUT_SESSION_ID} ` ,
55+ redirectUrl : `${ window . location . origin } /account` ,
3556 cancelUrl : `${ window . location . origin } /subscription` ,
3657 provider : paymentMethod ,
3758 subscriptionType : type || 'api' ,
38- billingInterval,
39- uiMode : 'embedded'
59+ billingInterval
4060 }
4161
4262 const response = await authorizedFetch (
@@ -53,10 +73,46 @@ export function StripeCheckoutModal({
5373
5474 const data = await response . json ( )
5575
76+ console . log ( 'data' , data )
77+
5678 if ( ! response . ok ) {
5779 throw new Error ( data . message || 'Failed to create subscription' )
5880 }
5981
82+ // Check if this is an upgrade
83+ if ( data . isUpgrade ) {
84+ setIsUpgrade ( true )
85+ setSubscriptionId ( data . subscriptionId )
86+ setRequiresPayment ( data . requiresPayment !== false )
87+
88+ // If no payment required, close modal and refresh
89+ if ( ! data . requiresPayment ) {
90+ await queryClient . invalidateQueries ( { queryKey : [ 'subscription' ] } )
91+ onClose ( )
92+ return null
93+ }
94+
95+ // Payment required - set client secret and pricing info
96+ if ( ! data . clientSecret ) {
97+ throw new Error ( 'No client secret returned for upgrade payment' )
98+ }
99+
100+ setUpgradeClientSecret ( data . clientSecret )
101+
102+ // Set pricing information if available
103+ if ( data . amount !== undefined && data . currency ) {
104+ setUpgradePricing ( {
105+ amount : data . amount ,
106+ currency : data . currency ,
107+ prorationCredit : data . prorationCredit || 0 ,
108+ newSubscriptionPrice : data . newSubscriptionPrice || 0
109+ } )
110+ }
111+
112+ return null // Don't use embedded checkout for upgrades
113+ }
114+
115+ // For new subscriptions, client secret is required
60116 if ( ! data . clientSecret ) {
61117 throw new Error ( 'No client secret returned from server' )
62118 }
@@ -67,7 +123,7 @@ export function StripeCheckoutModal({
67123 setError ( errorMessage )
68124 throw err
69125 }
70- } , [ authorizedFetch , paymentMethod , type , billingInterval ] )
126+ } , [ authorizedFetch , paymentMethod , type , billingInterval , onClose , queryClient ] )
71127
72128 const options = { fetchClientSecret }
73129
@@ -90,10 +146,112 @@ export function StripeCheckoutModal({
90146 )
91147 }
92148
149+ // Render upgrade payment form
150+ if ( isUpgrade && upgradeClientSecret && requiresPayment ) {
151+ const formatAmount = ( cents : number , currency : string ) => {
152+ const amount = cents / 100
153+ return new Intl . NumberFormat ( 'en-US' , {
154+ style : 'currency' ,
155+ currency : currency . toUpperCase ( )
156+ } ) . format ( amount )
157+ }
158+
159+ const planName = type === 'api' ? 'API' : type === 'llamafeed' ? 'Pro' : type
160+ const billingPeriod = billingInterval === 'year' ? 'Annual' : 'Monthly'
161+
162+ return (
163+ < Ariakit . DialogProvider open = { isOpen } setOpen = { ( ) => onClose ( ) } >
164+ < Ariakit . Dialog className = "dialog gap-0 md:max-w-[600px]" portal unmountOnHide >
165+ < div className = "top-0 z-10 flex items-center justify-between border-b bg-(--app-bg) p-4" >
166+ < h2 className = "text-xl font-bold" > Complete Your Upgrade</ h2 >
167+ < Ariakit . DialogDismiss className = "rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 dark:hover:text-white" >
168+ < Icon name = "x" className = "h-6 w-6" />
169+ </ Ariakit . DialogDismiss >
170+ </ div >
171+
172+ { error && (
173+ < div className = "border-b border-[#39393E] bg-red-500/10 p-4" >
174+ < div className = "flex items-center gap-2 text-red-400" >
175+ < Icon name = "alert-circle" height = { 20 } width = { 20 } />
176+ < p className = "text-sm" > { error } </ p >
177+ </ div >
178+ </ div >
179+ ) }
180+
181+ < div className = "border-b border-[#39393E] bg-(--app-bg) p-4" >
182+ < div className = "space-y-3" >
183+ < div >
184+ < h3 className = "text-sm font-semibold text-[#8a8c90]" > Upgrading to</ h3 >
185+ < p className = "text-lg font-bold text-black dark:text-white" >
186+ { planName } - { billingPeriod }
187+ </ p >
188+ </ div >
189+
190+ { upgradePricing ? (
191+ < div className = "space-y-2 pt-2" >
192+ < div className = "flex justify-between text-sm" >
193+ < span className = "text-[#8a8c90]" > New subscription price</ span >
194+ < span className = "font-medium" >
195+ { formatAmount ( upgradePricing . newSubscriptionPrice , upgradePricing . currency ) }
196+ < span className = "text-[#8a8c90]" > /{ billingInterval === 'year' ? 'year' : 'month' } </ span >
197+ </ span >
198+ </ div >
199+
200+ { upgradePricing . prorationCredit > 0 && (
201+ < div className = "flex justify-between text-sm" >
202+ < span className = "text-[#8a8c90]" > Proration credit</ span >
203+ < span className = "font-medium text-green-400" >
204+ -{ formatAmount ( upgradePricing . prorationCredit , upgradePricing . currency ) }
205+ </ span >
206+ </ div >
207+ ) }
208+
209+ < div className = "border-t border-[#39393E] pt-2" >
210+ < div className = "flex justify-between" >
211+ < span className = "font-semibold" > Amount due today</ span >
212+ < span className = "text-lg font-bold text-[#5C5CF9]" >
213+ { formatAmount ( upgradePricing . amount , upgradePricing . currency ) }
214+ </ span >
215+ </ div >
216+ </ div >
217+
218+ < p className = "pt-1 text-xs text-[#8a8c90]" >
219+ You'll be charged immediately and your subscription will be updated.
220+ </ p >
221+ </ div >
222+ ) : (
223+ < p className = "text-sm text-[#8a8c90]" > Enter your payment details below to complete the upgrade.</ p >
224+ ) }
225+ </ div >
226+ </ div >
227+
228+ < div className = "p-4" >
229+ < Elements
230+ stripe = { stripeInstance }
231+ options = { {
232+ clientSecret : upgradeClientSecret
233+ } }
234+ >
235+ < UpgradePaymentForm
236+ subscriptionId = { subscriptionId ! }
237+ onSuccess = { ( ) => {
238+ queryClient . invalidateQueries ( { queryKey : [ 'subscription' ] } )
239+ onClose ( )
240+ } }
241+ onError = { setError }
242+ />
243+ </ Elements >
244+ </ div >
245+ </ Ariakit . Dialog >
246+ </ Ariakit . DialogProvider >
247+ )
248+ }
249+
250+ // Render new subscription checkout
93251 return (
94252 < Ariakit . DialogProvider open = { isOpen } setOpen = { ( ) => onClose ( ) } >
95253 < Ariakit . Dialog className = "dialog gap-0 md:max-w-[600px]" portal unmountOnHide >
96- < div className = "sticky top-0 z-10 flex items-center justify-between border-b p-4" >
254+ < div className = "top-0 z-10 flex items-center justify-between border-b bg-(--app-bg) p-4" >
97255 < h2 className = "text-xl font-bold" > Complete Your Purchase</ h2 >
98256 < Ariakit . DialogDismiss className = "rounded-full p-1 text-gray-400 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 dark:hover:text-white" >
99257 < Icon name = "x" className = "h-6 w-6" />
@@ -118,3 +276,90 @@ export function StripeCheckoutModal({
118276 </ Ariakit . DialogProvider >
119277 )
120278}
279+
280+ // Payment form component for upgrades
281+ function UpgradePaymentForm ( {
282+ subscriptionId,
283+ onSuccess,
284+ onError
285+ } : {
286+ subscriptionId : string
287+ onSuccess : ( ) => void
288+ onError : ( error : string ) => void
289+ } ) {
290+ const { authorizedFetch } = useAuthContext ( ) !
291+ const [ isProcessing , setIsProcessing ] = useState ( false )
292+ const stripe = useStripe ( )
293+ const elements = useElements ( )
294+
295+ const handleSubmit = async ( e : React . FormEvent ) => {
296+ e . preventDefault ( )
297+
298+ if ( ! stripe || ! elements ) {
299+ return
300+ }
301+
302+ setIsProcessing ( true )
303+ onError ( '' )
304+
305+ try {
306+ const { error : submitError } = await elements . submit ( )
307+ if ( submitError ) {
308+ throw new Error ( submitError . message )
309+ }
310+
311+ const { error : confirmError , paymentIntent } = await stripe . confirmPayment ( {
312+ elements,
313+ confirmParams : {
314+ return_url : `${ window . location . origin } /account`
315+ } ,
316+ redirect : 'if_required'
317+ } )
318+
319+ if ( confirmError ) {
320+ throw new Error ( confirmError . message )
321+ }
322+
323+ if ( paymentIntent ?. status === 'succeeded' ) {
324+ // Call backend to confirm upgrade
325+ const response = await authorizedFetch (
326+ `${ AUTH_SERVER } /subscription/confirm-upgrade` ,
327+ {
328+ method : 'POST' ,
329+ headers : {
330+ 'Content-Type' : 'application/json'
331+ } ,
332+ body : JSON . stringify ( {
333+ paymentIntentId : paymentIntent . id ,
334+ subscriptionId
335+ } )
336+ } ,
337+ true
338+ )
339+
340+ if ( ! response . ok ) {
341+ throw new Error ( 'Failed to confirm upgrade' )
342+ }
343+
344+ onSuccess ( )
345+ }
346+ } catch ( err ) {
347+ onError ( err instanceof Error ? err . message : 'Payment failed' )
348+ } finally {
349+ setIsProcessing ( false )
350+ }
351+ }
352+
353+ return (
354+ < form onSubmit = { handleSubmit } className = "space-y-6" >
355+ < PaymentElement />
356+ < button
357+ type = "submit"
358+ disabled = { ! stripe || isProcessing }
359+ className = "w-full rounded-lg bg-[#5C5CF9] px-6 py-3 font-medium text-white transition-colors hover:bg-[#4A4AF0] disabled:cursor-not-allowed disabled:opacity-50"
360+ >
361+ { isProcessing ? 'Processing...' : 'Complete Upgrade' }
362+ </ button >
363+ </ form >
364+ )
365+ }
0 commit comments