1
1
'use client' ;
2
- import {
3
- AlertDialog ,
4
- AlertDialogContent ,
5
- AlertDialogDescription ,
6
- AlertDialogFooter ,
7
- AlertDialogHeader ,
8
- AlertDialogTitle
9
- } from '@/src/components/shadcn-ui/alert-dialog' ;
2
+
10
3
import {
11
4
Card ,
12
5
CardContent ,
@@ -15,14 +8,29 @@ import {
15
8
CardHeader ,
16
9
CardTitle
17
10
} from '@/src/components/shadcn-ui/card' ;
11
+ import {
12
+ Dialog ,
13
+ DialogContent ,
14
+ DialogDescription ,
15
+ DialogHeader ,
16
+ DialogTitle
17
+ } from '@/src/components/shadcn-ui/dialog' ;
18
+ import {
19
+ EmbeddedCheckoutProvider ,
20
+ EmbeddedCheckout
21
+ } from '@stripe/react-stripe-js' ;
22
+ import {
23
+ loadStripe ,
24
+ type StripeEmbeddedCheckoutOptions
25
+ } from '@stripe/stripe-js' ;
18
26
import { Tabs , TabsList , TabsTrigger } from '@/src/components/shadcn-ui/tabs' ;
19
27
import { Button } from '@/src/components/shadcn-ui/button' ;
20
- import { Check , SpinnerGap } from '@phosphor-icons/react' ;
21
28
import { useOrgShortcode } from '@/src/hooks/use-params' ;
22
- import { useEffect , useState } from 'react' ;
29
+ import { useCallback , useRef , useState } from 'react' ;
30
+ import { Check } from '@phosphor-icons/react' ;
23
31
import { platform } from '@/src/lib/trpc' ;
24
32
import { cn } from '@/src/lib/utils' ;
25
- import { ms } from '@u22n/utils/ms ' ;
33
+ import { env } from '@/src/env ' ;
26
34
27
35
type PricingSwitchProps = {
28
36
onSwitch : ( value : string ) => void ;
@@ -294,104 +302,60 @@ type StripeModalProps = {
294
302
} ;
295
303
296
304
function StripeModal ( { open, isYearly, plan, setOpen } : StripeModalProps ) {
305
+ if ( ! env . NEXT_PUBLIC_BILLING_STRIPE_PUBLISHABLE_KEY ) {
306
+ throw new Error (
307
+ 'Stripe publishable key not set, cannot render Stripe modal'
308
+ ) ;
309
+ }
297
310
const orgShortcode = useOrgShortcode ( ) ;
298
311
const utils = platform . useUtils ( ) ;
312
+ const stripePromise = useRef (
313
+ loadStripe ( env . NEXT_PUBLIC_BILLING_STRIPE_PUBLISHABLE_KEY )
314
+ ) ;
299
315
300
- const {
301
- data : paymentLink ,
302
- isLoading : paymentLinkLoading ,
303
- error : paymentLinkError
304
- } = platform . org . setup . billing . getOrgSubscriptionPaymentLink . useQuery (
305
- {
316
+ const fetchClientSecret = useCallback (
317
+ ( ) =>
318
+ utils . org . setup . billing . createCheckoutSession
319
+ . fetch ( {
320
+ orgShortcode,
321
+ plan,
322
+ period : isYearly ? 'yearly' : 'monthly'
323
+ } )
324
+ . then ( ( res ) => res . checkoutSessionClientSecret ) ,
325
+ [
326
+ isYearly ,
306
327
orgShortcode ,
307
328
plan ,
308
- period : isYearly ? 'yearly' : 'monthly'
309
- } ,
310
- {
311
- enabled : open
312
- }
329
+ utils . org . setup . billing . createCheckoutSession
330
+ ]
313
331
) ;
332
+ const onComplete = useCallback ( ( ) => {
333
+ setOpen ( false ) ;
334
+ setTimeout ( ( ) => void utils . org . setup . billing . invalidate ( ) , 1000 ) ;
335
+ } , [ setOpen , utils . org . setup . billing ] ) ;
314
336
315
- const { data : overview } =
316
- platform . org . setup . billing . getOrgBillingOverview . useQuery (
317
- { orgShortcode } ,
318
- {
319
- enabled : open && paymentLink && ! paymentLinkLoading ,
320
- refetchOnWindowFocus : true ,
321
- refetchInterval : ms ( '15 seconds' )
322
- }
323
- ) ;
324
-
325
- // Open payment link once payment link is generated
326
- useEffect ( ( ) => {
327
- if ( ! open || paymentLinkLoading || ! paymentLink ) return ;
328
- window . open ( paymentLink . subLink , '_blank' ) ;
329
- } , [ open , paymentLink , paymentLinkLoading ] ) ;
330
-
331
- // handle payment info update
332
- useEffect ( ( ) => {
333
- if ( overview ?. currentPlan === 'pro' ) {
334
- void utils . org . setup . billing . getOrgBillingOverview . invalidate ( {
335
- orgShortcode
336
- } ) ;
337
- setOpen ( false ) ;
338
- }
339
- } , [
340
- orgShortcode ,
341
- overview ,
342
- setOpen ,
343
- utils . org . setup . billing . getOrgBillingOverview
344
- ] ) ;
337
+ const options = {
338
+ fetchClientSecret,
339
+ onComplete
340
+ } satisfies StripeEmbeddedCheckoutOptions ;
345
341
346
342
return (
347
- < AlertDialog open = { open } >
348
- < AlertDialogContent >
349
- < AlertDialogHeader >
350
- < AlertDialogTitle > Upgrade to Pro</ AlertDialogTitle >
351
- < AlertDialogDescription className = "space-y-2 p-2" >
352
- { paymentLinkLoading ? (
353
- < span className = "flex items-center gap-2" >
354
- < SpinnerGap className = "size-4 animate-spin" />
355
- Generating Payment Link
356
- </ span >
357
- ) : paymentLink ? (
358
- 'Waiting for Payment (This may take a few seconds)'
359
- ) : (
360
- < span className = "text-red-9" > { paymentLinkError ?. message } </ span >
361
- ) }
362
- </ AlertDialogDescription >
363
- </ AlertDialogHeader >
364
- < div className = "flex flex-col gap-2 p-2" >
365
- < span >
366
- We are waiting for your payment to be processed. It may take a few
367
- seconds for the payment to reflect in app.
368
- </ span >
369
- { paymentLink && (
370
- < span >
371
- If a new tab was not opened,{ ' ' }
372
- < a
373
- target = "_blank"
374
- href = { paymentLink . subLink }
375
- className = "underline" >
376
- open it manually.
377
- </ a >
378
- </ span >
379
- ) }
380
- < span >
381
- { `If your payment hasn't been detected correctly, please try refreshing
382
- the page.` }
383
- </ span >
384
- < span > If the issue persists, please contact support.</ span >
385
- </ div >
386
-
387
- < AlertDialogFooter >
388
- < Button
389
- onClick = { ( ) => setOpen ( false ) }
390
- className = "w-full" >
391
- Close
392
- </ Button >
393
- </ AlertDialogFooter >
394
- </ AlertDialogContent >
395
- </ AlertDialog >
343
+ < Dialog
344
+ open = { open }
345
+ onOpenChange = { setOpen } >
346
+ < DialogContent className = "w-[90vw] max-w-screen-lg p-0" >
347
+ < DialogHeader className = "sr-only" >
348
+ < DialogTitle > Stripe Checkout</ DialogTitle >
349
+ < DialogDescription > Checkout with Stripe</ DialogDescription >
350
+ </ DialogHeader >
351
+ { open && (
352
+ < EmbeddedCheckoutProvider
353
+ options = { options }
354
+ stripe = { stripePromise . current } >
355
+ < EmbeddedCheckout className = "*:rounded-lg" />
356
+ </ EmbeddedCheckoutProvider >
357
+ ) }
358
+ </ DialogContent >
359
+ </ Dialog >
396
360
) ;
397
361
}
0 commit comments