Skip to content

Commit 641788b

Browse files
Shawclaude
andcommitted
perf(cloud): paint /bsc credit form on first load, not after user fetch
The amount input / "you pay" / "you receive" panel has zero dependency on auth state, the user-profile fetch, or the crypto-status fetch — but it lived inside the `!isReady || isLoading -> Loading account` gate, so the entire right column blanked for ~5s while `/api/v1/user` round-tripped. Lift the static Cloud-credit card out of the gated block so it paints immediately. Below it: - while user-profile is loading: card-shaped skeleton sized to the final wallet/sign-in card so layout doesn't jank when it lands - signed-out: Sign in CTA (unchanged) - signed-in: Suspense fallback uses the same skeleton until the lazy wallet/attach chunk loads - signed-in + wallet attached: DirectCryptoCreditCard (unchanged) - signed-in + no wallet: AttachWalletCard (unchanged) Also kick off `/api/crypto/status` on mount instead of after user resolves. The endpoint is public; serializing it behind the auth fetch made DirectCryptoCreditCard flash a "Direct wallet payments are not configured yet" state for a beat once the user landed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 78415a8 commit 641788b

1 file changed

Lines changed: 130 additions & 92 deletions

File tree

  • packages/cloud-frontend/src/pages/bsc

packages/cloud-frontend/src/pages/bsc/page.tsx

Lines changed: 130 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import {
99
CardTitle,
1010
CloudVideoBackground,
1111
DashboardErrorState,
12-
DashboardLoadingState,
1312
Input,
1413
} from "@elizaos/ui";
1514
import { Gift } from "lucide-react";
16-
import { lazy, Suspense, useCallback, useEffect, useState } from "react";
15+
import { lazy, Suspense, useEffect, useState } from "react";
1716
import { Helmet } from "react-helmet-async";
1817
import { Link } from "react-router-dom";
1918
import 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

Comments
 (0)