11"use client" ;
22
33import dynamic from "next/dynamic" ;
4- import { useEffect , useMemo , useState } from "react" ;
4+ import { Suspense , useCallback , useEffect , useMemo , useState } from "react" ;
5+ import { useRouter , useSearchParams } from "next/navigation" ;
56import { LoadingSpinner } from "@/components/LoadingSpinner" ;
67import type { PricingTier , PricingTierInfo } from "@/types" ;
78
@@ -13,55 +14,122 @@ const PricingPage = dynamic(
1314 }
1415) ;
1516
16- export default function PricingRoutePage ( ) {
17+ function PricingRouteContent ( ) {
18+ const router = useRouter ( ) ;
19+ const searchParams = useSearchParams ( ) ;
1720 const [ tiers , setTiers ] = useState < PricingTierInfo [ ] > ( [ ] ) ;
1821 const [ currentTier , setCurrentTier ] = useState < PricingTier > ( "FREE" ) ;
19- const [ message , setMessage ] = useState < string | null > ( null ) ;
22+ const [ message , setMessage ] = useState < {
23+ text : string ;
24+ tone : "success" | "warning" ;
25+ } | null > ( null ) ;
2026
2127 const paidTierSet = useMemo (
2228 ( ) => new Set < PricingTier > ( [ "ADS_FREE" , "LOCAL" , "BYOK" , "HOSTED_AI" ] ) ,
2329 [ ]
2430 ) ;
2531
32+ const loadSubscriptionData = useCallback ( async ( ) => {
33+ try {
34+ const [ tiersRes , currentRes ] = await Promise . all ( [
35+ fetch ( "/api/subscription/tiers" , {
36+ method : "GET" ,
37+ credentials : "include" ,
38+ cache : "no-store" ,
39+ } ) ,
40+ fetch ( "/api/subscription/current" , {
41+ method : "GET" ,
42+ credentials : "include" ,
43+ cache : "no-store" ,
44+ } ) ,
45+ ] ) ;
46+
47+ if ( tiersRes . ok ) {
48+ const tiersData = ( await tiersRes . json ( ) ) as {
49+ data ?: PricingTierInfo [ ] ;
50+ } ;
51+ setTiers ( tiersData . data ?? [ ] ) ;
52+ }
53+ if ( currentRes . ok ) {
54+ const currentData = ( await currentRes . json ( ) ) as {
55+ data ?: { tier ?: PricingTier } ;
56+ } ;
57+ setCurrentTier ( currentData . data ?. tier ?? "FREE" ) ;
58+ }
59+ } catch {
60+ setMessage ( {
61+ text : "Failed to load pricing details. Please refresh." ,
62+ tone : "warning" ,
63+ } ) ;
64+ }
65+ } , [ ] ) ;
66+
2667 useEffect ( ( ) => {
27- const loadData = async ( ) => {
28- try {
29- const [ tiersRes , currentRes ] = await Promise . all ( [
30- fetch ( "/api/subscription/tiers" , {
31- method : "GET" ,
32- credentials : "include" ,
33- cache : "no-store" ,
34- } ) ,
35- fetch ( "/api/subscription/current" , {
36- method : "GET" ,
37- credentials : "include" ,
38- cache : "no-store" ,
39- } ) ,
40- ] ) ;
68+ void loadSubscriptionData ( ) ;
69+ } , [ loadSubscriptionData ] ) ;
70+
71+ const checkoutSessionId = searchParams . get ( "session_id" ) ?. trim ( ) ?? "" ;
72+
73+ useEffect ( ( ) => {
74+ if ( ! checkoutSessionId ) return ;
4175
42- if ( tiersRes . ok ) {
43- const tiersData = ( await tiersRes . json ( ) ) as {
44- data ?: PricingTierInfo [ ] ;
45- } ;
46- setTiers ( tiersData . data ?? [ ] ) ;
76+ let cancelled = false ;
77+
78+ const confirmAndRefresh = async ( ) => {
79+ try {
80+ const res = await fetch ( "/api/stripe/confirm-checkout-session" , {
81+ method : "POST" ,
82+ headers : { "Content-Type" : "application/json" } ,
83+ credentials : "include" ,
84+ body : JSON . stringify ( { sessionId : checkoutSessionId } ) ,
85+ } ) ;
86+ const payload = ( await res . json ( ) ) as {
87+ success ?: boolean ;
88+ error ?: string ;
89+ } ;
90+ if ( cancelled ) return ;
91+ if ( ! res . ok || ! payload . success ) {
92+ setMessage ( {
93+ text :
94+ payload . error ??
95+ "Could not confirm your subscription. If you were charged, contact support." ,
96+ tone : "warning" ,
97+ } ) ;
98+ return ;
4799 }
48- if ( currentRes . ok ) {
49- const currentData = ( await currentRes . json ( ) ) as {
50- data ?: { tier ?: PricingTier } ;
51- } ;
52- setCurrentTier ( currentData . data ?. tier ?? "FREE" ) ;
100+ await loadSubscriptionData ( ) ;
101+ if ( ! cancelled ) {
102+ setMessage ( {
103+ text : "Subscription active. Thank you!" ,
104+ tone : "success" ,
105+ } ) ;
53106 }
54107 } catch {
55- setMessage ( "Failed to load pricing details. Please refresh." ) ;
108+ if ( ! cancelled ) {
109+ setMessage ( {
110+ text : "Could not confirm checkout. Please refresh the page." ,
111+ tone : "warning" ,
112+ } ) ;
113+ }
114+ } finally {
115+ if ( ! cancelled ) {
116+ router . replace ( "/pricing" , { scroll : false } ) ;
117+ }
56118 }
57119 } ;
58120
59- void loadData ( ) ;
60- } , [ ] ) ;
121+ void confirmAndRefresh ( ) ;
122+ return ( ) => {
123+ cancelled = true ;
124+ } ;
125+ } , [ router , checkoutSessionId , loadSubscriptionData ] ) ;
61126
62127 const handleSelectTier = async ( tier : PricingTier ) => {
63128 if ( ! paidTierSet . has ( tier ) ) {
64- setMessage ( "Free tier is already active by default." ) ;
129+ setMessage ( {
130+ text : "Free tier is already active by default." ,
131+ tone : "warning" ,
132+ } ) ;
65133 return ;
66134 }
67135 setMessage ( null ) ;
@@ -78,12 +146,15 @@ export default function PricingRoutePage() {
78146 error ?: string ;
79147 } ;
80148 if ( ! response . ok || ! payload . data ?. url ) {
81- setMessage ( payload . error ?? "Failed to start checkout." ) ;
149+ setMessage ( {
150+ text : payload . error ?? "Failed to start checkout." ,
151+ tone : "warning" ,
152+ } ) ;
82153 return ;
83154 }
84155 window . location . assign ( payload . data . url ) ;
85156 } catch {
86- setMessage ( "Failed to start checkout." ) ;
157+ setMessage ( { text : "Failed to start checkout." , tone : "warning" } ) ;
87158 }
88159 } ;
89160
@@ -95,10 +166,30 @@ export default function PricingRoutePage() {
95166 onSelectTier = { handleSelectTier }
96167 />
97168 { message && (
98- < p className = "mt-4 text-center text-sm text-amber-600 dark:text-amber-400" >
99- { message }
169+ < p
170+ className = { `mt-4 text-center text-sm ${
171+ message . tone === "success"
172+ ? "text-emerald-600 dark:text-emerald-400"
173+ : "text-amber-600 dark:text-amber-400"
174+ } `}
175+ >
176+ { message . text }
100177 </ p >
101178 ) }
102179 </ div >
103180 ) ;
104181}
182+
183+ export default function PricingRoutePage ( ) {
184+ return (
185+ < Suspense
186+ fallback = {
187+ < div className = "mt-6 sm:mt-8 lg:mt-10 flex justify-center py-16" >
188+ < LoadingSpinner size = "md" message = "Loading pricing..." />
189+ </ div >
190+ }
191+ >
192+ < PricingRouteContent />
193+ </ Suspense >
194+ ) ;
195+ }
0 commit comments