@@ -9,11 +9,10 @@ import {
99 CardTitle ,
1010 CloudVideoBackground ,
1111 DashboardErrorState ,
12- DashboardLoadingState ,
1312 Input ,
1413} from "@elizaos/ui" ;
1514import { Gift } from "lucide-react" ;
16- import { lazy , Suspense , useCallback , useEffect , useState } from "react" ;
15+ import { lazy , Suspense , useEffect , useState } from "react" ;
1716import { Helmet } from "react-helmet-async" ;
1817import { Link } from "react-router-dom" ;
1918import type { CryptoStatusResponse } from "@/lib/types/crypto-status" ;
@@ -42,18 +41,27 @@ export default function BscPromoPage() {
4241 const [ amount , setAmount ] = useState ( "10" ) ;
4342 const [ status , setStatus ] = useState < CryptoStatusResponse | null > ( null ) ;
4443
45- const fetchCryptoStatus = useCallback ( async ( ) => {
46- const response = await fetch ( "/api/crypto/status" ) ;
47- if ( ! response . ok ) return ;
48- // Guard against the SPA fallback serving index.html for unknown /api/* paths.
49- const contentType = response . headers . get ( "content-type" ) ?? "" ;
50- if ( ! contentType . includes ( "application/json" ) ) return ;
51- setStatus ( await response . json ( ) ) ;
52- } , [ ] ) ;
53-
44+ // `/api/crypto/status` is a public endpoint (no auth required). Firing it on
45+ // mount instead of after `useUserProfile` resolves cuts a serial roundtrip:
46+ // by the time the user-profile fetch finishes, status is usually already in
47+ // hand, so DirectCryptoCreditCard doesn't flash an empty/"not configured"
48+ // state.
5449 useEffect ( ( ) => {
55- if ( isAuthenticated && user ) fetchCryptoStatus ( ) ;
56- } , [ fetchCryptoStatus , isAuthenticated , user ] ) ;
50+ let cancelled = false ;
51+ void ( async ( ) => {
52+ const response = await fetch ( "/api/crypto/status" ) ;
53+ if ( cancelled || ! response . ok ) return ;
54+ // Guard against the SPA fallback serving index.html for unknown /api/*
55+ // paths.
56+ const contentType = response . headers . get ( "content-type" ) ?? "" ;
57+ if ( ! contentType . includes ( "application/json" ) ) return ;
58+ const data = ( await response . json ( ) ) as CryptoStatusResponse ;
59+ if ( ! cancelled ) setStatus ( data ) ;
60+ } ) ( ) ;
61+ return ( ) => {
62+ cancelled = true ;
63+ } ;
64+ } , [ ] ) ;
5765
5866 const parsed = Number . parseFloat ( amount ) ;
5967 const amountValue = Number . isFinite ( parsed ) ? parsed : null ;
@@ -113,8 +121,79 @@ export default function BscPromoPage() {
113121 </ div >
114122
115123 < div className = "space-y-4" >
124+ { /*
125+ Cloud credit summary is static (amount input + derived "you
126+ pay / you receive") — nothing in it depends on auth state, the
127+ user profile, or the crypto-status fetch. Render it on first
128+ paint so the form is interactable immediately, instead of
129+ blanking the right column for ~5s while user-profile +
130+ crypto-status round-trip. The auth/wallet panel below it
131+ streams in as data arrives.
132+ */ }
133+ < Card className = "rounded-xs border-black/12 bg-white/88 text-black shadow-xl backdrop-blur-md" >
134+ < CardHeader className = "p-5 pb-4" >
135+ < CardTitle className = "text-lg text-black" >
136+ Cloud credit
137+ </ CardTitle >
138+ < p className = "text-sm text-black/62" >
139+ The $5 BSC bonus applies at $10 or more.
140+ </ p >
141+ </ CardHeader >
142+ < CardContent className = "space-y-4 border-t border-black/10 p-5" >
143+ < label
144+ className = "block space-y-2"
145+ htmlFor = "bsc-credit-amount"
146+ >
147+ < span className = "text-xs font-medium text-black/62" >
148+ Purchase amount
149+ </ span >
150+ < div className = "flex items-center rounded-xs border border-black/14 bg-white" >
151+ < span className = "px-3 text-black/56" > $</ span >
152+ < Input
153+ id = "bsc-credit-amount"
154+ type = "number"
155+ min = { 10 }
156+ max = { 10000 }
157+ value = { amount }
158+ onChange = { ( event ) => setAmount ( event . target . value ) }
159+ variant = "config"
160+ density = "relaxed"
161+ className = "border-0 bg-transparent px-0 text-base text-black focus:border-0 focus:ring-0"
162+ />
163+ </ div >
164+ </ label >
165+ < div className = "grid grid-cols-2 gap-3 text-sm" >
166+ < div className = "rounded-xs border border-black/10 bg-black/[0.03] p-3" >
167+ < p className = "text-xs text-black/58" > You pay</ p >
168+ < p className = "mt-1 text-lg font-semibold text-black" >
169+ ${ amountValue ?. toFixed ( 2 ) ?? "0.00" }
170+ </ p >
171+ </ div >
172+ < div className = "rounded-xs border border-black/10 bg-black/[0.03] p-3" >
173+ < p className = "text-xs text-black/58" > You receive</ p >
174+ < p className = "mt-1 text-lg font-semibold text-black" >
175+ { totalCredits . toFixed ( 2 ) } credits
176+ </ p >
177+ </ div >
178+ </ div >
179+ </ CardContent >
180+ </ Card >
181+
182+ { /* Auth-gated panel below the static form. While the user
183+ profile resolves we show a card-shaped skeleton, sized to
184+ roughly match the final wallet/sign-in card so layout
185+ doesn't jank in. */ }
116186 { ! isReady || ( isAuthenticated && isLoading ) ? (
117- < DashboardLoadingState label = "Loading account" />
187+ < Card
188+ aria-busy = "true"
189+ className = "rounded-xs border-black/12 bg-white/88 text-black shadow-xl backdrop-blur-md"
190+ >
191+ < CardContent className = "space-y-3 p-5" >
192+ < div className = "h-4 w-32 animate-pulse rounded-xs bg-black/10" />
193+ < div className = "h-3 w-64 max-w-full animate-pulse rounded-xs bg-black/8" />
194+ < div className = "mt-2 h-10 w-full animate-pulse rounded-xs bg-black/10" />
195+ </ CardContent >
196+ </ Card >
118197 ) : isError ? (
119198 < DashboardErrorState
120199 message = {
@@ -134,84 +213,43 @@ export default function BscPromoPage() {
134213 </ CardContent >
135214 </ Card >
136215 ) : (
137- < >
138- < Card className = "rounded-xs border-black/12 bg-white/88 text-black shadow-xl backdrop-blur-md" >
139- < CardHeader className = "p-5 pb-4" >
140- < CardTitle className = "text-lg text-black" >
141- Cloud credit
142- </ CardTitle >
143- < p className = "text-sm text-black/62" >
144- The $5 BSC bonus applies at $10 or more.
145- </ p >
146- </ CardHeader >
147- < CardContent className = "space-y-4 border-t border-black/10 p-5" >
148- < label
149- className = "block space-y-2"
150- htmlFor = "bsc-credit-amount"
151- >
152- < span className = "text-xs font-medium text-black/62" >
153- Purchase amount
154- </ span >
155- < div className = "flex items-center rounded-xs border border-black/14 bg-white" >
156- < span className = "px-3 text-black/56" > $</ span >
157- < Input
158- id = "bsc-credit-amount"
159- type = "number"
160- min = { 10 }
161- max = { 10000 }
162- value = { amount }
163- onChange = { ( event ) => setAmount ( event . target . value ) }
164- variant = "config"
165- density = "relaxed"
166- className = "border-0 bg-transparent px-0 text-base text-black focus:border-0 focus:ring-0"
167- />
168- </ div >
169- </ label >
170- < div className = "grid grid-cols-2 gap-3 text-sm" >
171- < div className = "rounded-xs border border-black/10 bg-black/[0.03] p-3" >
172- < p className = "text-xs text-black/58" > You pay</ p >
173- < p className = "mt-1 text-lg font-semibold text-black" >
174- ${ amountValue ?. toFixed ( 2 ) ?? "0.00" }
175- </ p >
176- </ div >
177- < div className = "rounded-xs border border-black/10 bg-black/[0.03] p-3" >
178- < p className = "text-xs text-black/58" > You receive</ p >
179- < p className = "mt-1 text-lg font-semibold text-black" >
180- { totalCredits . toFixed ( 2 ) } credits
181- </ p >
182- </ div >
183- </ div >
184- </ CardContent >
185- </ Card >
186- < Suspense
187- fallback = {
188- < DashboardLoadingState label = "Loading wallet checkout" />
189- }
190- >
191- < LazyStewardWalletProviders >
192- { user . wallet_address ? (
193- < LazyDirectCryptoCreditCard
194- amount = { amountValue }
195- promoCode = "bsc"
196- status = { status }
197- accountWalletAddress = { user . wallet_address }
198- surface = "cloud"
199- lockedNetwork = "bsc"
200- onSuccess = { ( ) => undefined }
201- />
202- ) : (
203- // OAuth signups (Google / Discord / GitHub / Magic
204- // Link / Passkey) land here. The direct-crypto-payments
205- // endpoint requires `user.wallet_address`, so until
206- // they verify a wallet against their account the pay
207- // button is a dead-end. AttachWalletCard drives the
208- // SIWE flow that fixes that, then `useUserProfile`
209- // refetches and this branch swaps to the purchase UI.
210- < LazyAttachWalletCard chainId = { 56 } />
211- ) }
212- </ LazyStewardWalletProviders >
213- </ Suspense >
214- </ >
216+ < Suspense
217+ fallback = {
218+ < Card
219+ aria-busy = "true"
220+ className = "rounded-xs border-black/12 bg-white/88 text-black shadow-xl backdrop-blur-md"
221+ >
222+ < CardContent className = "space-y-3 p-5" >
223+ < div className = "h-4 w-40 animate-pulse rounded-xs bg-black/10" />
224+ < div className = "h-3 w-56 max-w-full animate-pulse rounded-xs bg-black/8" />
225+ < div className = "mt-2 h-10 w-full animate-pulse rounded-xs bg-black/10" />
226+ </ CardContent >
227+ </ Card >
228+ }
229+ >
230+ < LazyStewardWalletProviders >
231+ { user . wallet_address ? (
232+ < LazyDirectCryptoCreditCard
233+ amount = { amountValue }
234+ promoCode = "bsc"
235+ status = { status }
236+ accountWalletAddress = { user . wallet_address }
237+ surface = "cloud"
238+ lockedNetwork = "bsc"
239+ onSuccess = { ( ) => undefined }
240+ />
241+ ) : (
242+ // OAuth signups (Google / Discord / GitHub / Magic
243+ // Link / Passkey) land here. The direct-crypto-payments
244+ // endpoint requires `user.wallet_address`, so until
245+ // they verify a wallet against their account the pay
246+ // button is a dead-end. AttachWalletCard drives the
247+ // SIWE flow that fixes that, then `useUserProfile`
248+ // refetches and this branch swaps to the purchase UI.
249+ < LazyAttachWalletCard chainId = { 56 } />
250+ ) }
251+ </ LazyStewardWalletProviders >
252+ </ Suspense >
215253 ) }
216254 </ div >
217255 </ section >
0 commit comments