Skip to content

Commit 2c13622

Browse files
authored
Merge pull request #389 from hardcordev/fix/wallet-modal-scrollspy-session-docs-nav
2 parents ef26a0d + aebf88e commit 2c13622

19 files changed

Lines changed: 157 additions & 35 deletions

app/pay/[linkId]/page.tsx

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, Suspense } from "react";
3+
import { useState, useMemo, Suspense } from "react";
44
import { useRouter } from "next/navigation";
55
import dynamic from "next/dynamic";
66
import { motion, AnimatePresence } from "framer-motion";
@@ -28,18 +28,30 @@ import { signWithFreighter } from "@/lib/stellar/freighter";
2828
import { apiClient } from "@/lib/api/axios";
2929
import { MULTI_CURRENCY_ASSETS, MOCK_RATES } from "@/lib/utils/constants";
3030
import { WalletModalFallback } from "@/components/wallet/WalletModalFallback";
31+
import { WalletModalErrorBoundary } from "@/components/wallet/WalletModalErrorBoundary";
3132
import { QRCodeModal } from "@/components/payments/QRCode";
32-
const WalletModal = dynamic(
33-
() => import("@/components/wallet/WalletModal").then((m) => m.WalletModal),
34-
{ ssr: false },
35-
);
3633

3734
export default function PaymentLinkPage() {
3835
const router = useRouter();
3936
const { isConnected, connect, address } = useWalletStore();
4037
const { error: notifyError } = useNotify();
4138
const [walletModalOpen, setWalletModalOpen] = useState(false);
4239
const [qrModalOpen, setQrModalOpen] = useState(false);
40+
// Bumping this recreates the `dynamic()` import below with a fresh promise,
41+
// so retrying after a chunk-load failure re-fetches the chunk instead of
42+
// replaying the same cached rejection.
43+
const [walletModalRetryKey, setWalletModalRetryKey] = useState(0);
44+
const WalletModal = useMemo(
45+
() =>
46+
dynamic(
47+
() => import("@/components/wallet/WalletModal").then((m) => m.WalletModal),
48+
{ ssr: false },
49+
),
50+
// walletModalRetryKey isn't read inside the factory — it's only a cache
51+
// key so bumping it forces a new dynamic() call (and a fresh import()).
52+
// eslint-disable-next-line react-hooks/exhaustive-deps
53+
[walletModalRetryKey],
54+
);
4355

4456
// Mock data for this link
4557
const linkData = {
@@ -158,19 +170,25 @@ export default function PaymentLinkPage() {
158170
return (
159171
<div className="min-h-screen bg-background flex flex-col items-center justify-center p-4">
160172
<div className="w-full max-w-md">
161-
<Suspense
162-
fallback={
163-
<WalletModalFallback
173+
<WalletModalErrorBoundary
174+
open={walletModalOpen}
175+
onOpenChange={setWalletModalOpen}
176+
onRetry={() => setWalletModalRetryKey((key) => key + 1)}
177+
>
178+
<Suspense
179+
fallback={
180+
<WalletModalFallback
181+
open={walletModalOpen}
182+
onOpenChange={setWalletModalOpen}
183+
/>
184+
}
185+
>
186+
<WalletModal
164187
open={walletModalOpen}
165188
onOpenChange={setWalletModalOpen}
166189
/>
167-
}
168-
>
169-
<WalletModal
170-
open={walletModalOpen}
171-
onOpenChange={setWalletModalOpen}
172-
/>
173-
</Suspense>
190+
</Suspense>
191+
</WalletModalErrorBoundary>
174192

175193
{/* Merchant Branding Header */}
176194
<div className="flex flex-col items-center mb-8">

components/SessionTimeoutModal.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client';
22

3+
import { useEffect } from 'react';
34
import {
45
Dialog,
56
DialogContent,
@@ -9,7 +10,7 @@ import {
910
DialogTitle,
1011
} from '@/components/ui/dialog';
1112
import { Button } from '@/components/ui';
12-
import { AlertTriangle, Clock, LogOut, RefreshCcw } from 'lucide-react';
13+
import { announce } from '@/lib/utils/announce';
1314

1415
interface SessionTimeoutModalProps {
1516
open: boolean;
@@ -24,12 +25,31 @@ function formatCountdown(seconds: number): string {
2425
return `${m}:${s.toString().padStart(2, '0')}`;
2526
}
2627

28+
/**
29+
* Purely presentational — all countdown/timer logic lives in useSessionTimeout;
30+
* this component only renders whatever `secondsRemaining` it's given. Focus
31+
* trapping and Escape-to-dismiss come for free from the shared Dialog
32+
* primitive, matching every other modal in the app.
33+
*/
2734
export function SessionTimeoutModal({
2835
open,
2936
secondsRemaining,
3037
onExtend,
3138
onLogout,
3239
}: SessionTimeoutModalProps) {
40+
// Explicitly announce the warning once when it appears — Dialog already
41+
// moves focus and exposes the title/description via aria-labelledby/
42+
// aria-describedby, but this mirrors the codebase's announce() convention
43+
// (see useNotify) so the warning is reliably read out even if focus
44+
// handling alone doesn't trigger it in a given screen reader.
45+
useEffect(() => {
46+
if (open) {
47+
announce(`Session expiring in ${secondsRemaining} seconds. Stay logged in to continue.`);
48+
}
49+
// Only announce on the open transition, not on every countdown tick.
50+
// eslint-disable-next-line react-hooks/exhaustive-deps
51+
}, [open]);
52+
3353
return (
3454
<Dialog open={open} onOpenChange={() => {}}>
3555
<DialogContent className="sm:max-w-md" showCloseButton={false}>

components/docs/content/Authentication.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ export function Authentication() {
7777
return (
7878
<SectionShell
7979
id="authentication"
80-
title="Authentication"
8180
lead="BettaPay issues a signed JWT that you attach as a Bearer token on every authenticated request. There are two ways to obtain one: Google OAuth and Stellar wallet auth."
8281
>
8382
{/* ── Google OAuth ── */}

components/docs/content/ErrorCodes.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export function ErrorCodes() {
2323
return (
2424
<SectionShell
2525
id="error-codes"
26-
title="Error Codes"
2726
lead="Every failure returns the same envelope with a stable, machine-readable code. Branch on error.code — messages may be reworded, codes will not."
2827
>
2928
<Snippet code={errorShape} lang="json" filename="Error envelope" />

components/docs/content/FxRates.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export function FxRates() {
77
return (
88
<SectionShell
99
id="fx-rates"
10-
title="FX & Rates"
1110
lead="The gateway proxies the FX engine so you can read live USDC/fiat rates and price a conversion. These endpoints are public — no JWT required."
1211
>
1312
<P>

components/docs/content/HttpStatus.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export function HttpStatus() {
1919
return (
2020
<SectionShell
2121
id="http-status"
22-
title="HTTP Status"
2322
lead="How the gateway maps outcomes onto HTTP status codes, plus the rate limits and timeouts that govern every route."
2423
>
2524
<div className="overflow-x-auto rounded-xl border border-border">

components/docs/content/IdempotencyKeys.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export function IdempotencyKeys() {
3838
return (
3939
<SectionShell
4040
id="idempotency-keys"
41-
title="Idempotency Keys"
4241
lead="An idempotency key makes a create request safe to retry. Without one, a dropped response can turn a single checkout into two charges."
4342
>
4443
<SubSection id="idempotency-how" title="How they work">

components/docs/content/Merchants.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export function Merchants() {
77
return (
88
<SectionShell
99
id="merchants"
10-
title="Merchants"
1110
lead="A merchant is the account that owns payments and settlements. The merchant your JWT acts as is fixed by the merchantId claim in the token."
1211
>
1312
<P>

components/docs/content/Overview.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export function Overview() {
2525
return (
2626
<SectionShell
2727
id="overview"
28-
title="Overview"
2928
lead="The BettaPay API lets you accept USDC payments on Stellar, settle to fiat, register merchants and read live FX rates — all over a small, predictable REST surface."
3029
>
3130
<SubSection id="overview-base-url" title="Base URL">

components/docs/content/Payments.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export function Payments() {
77
return (
88
<SectionShell
99
id="payments"
10-
title="Payments"
1110
lead="Create payments (and hosted payment links), read their status as they settle on Stellar, and advance them through their lifecycle."
1211
>
1312
<SubSection id="payment-lifecycle" title="Status lifecycle">

0 commit comments

Comments
 (0)