create-brainerce-store 1.27.5 → 1.27.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,271 +1,271 @@
1
- 'use client';
2
-
3
- import { Suspense, useEffect, useState } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import { Link } from '@/lib/navigation';
6
- import type { WaitForOrderResult, OrderDownloadLink } from 'brainerce';
7
- import { getClient } from '@/lib/brainerce';
8
- import { useCart } from '@/providers/store-provider';
9
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
10
- import { useTranslations } from '@/lib/translations';
11
-
12
- function OrderConfirmationContent() {
13
- const searchParams = useSearchParams();
14
- const checkoutId = searchParams.get('checkout_id');
15
-
16
- const { refreshCart } = useCart();
17
- const t = useTranslations('orderConfirmation');
18
- const tc = useTranslations('common');
19
- const [result, setResult] = useState<WaitForOrderResult | null>(null);
20
- const [loading, setLoading] = useState(true);
21
- const [error, setError] = useState<string | null>(null);
22
-
23
- useEffect(() => {
24
- if (!checkoutId) {
25
- setError(t('missingCheckoutInfo'));
26
- setLoading(false);
27
- return;
28
- }
29
-
30
- async function waitForOrder() {
31
- try {
32
- const client = getClient();
33
-
34
- // Clear cart state after successful payment
35
- client.handlePaymentSuccess(checkoutId!);
36
- await refreshCart();
37
-
38
- // For redirect-based payment providers (e.g. CardCom), the customer
39
- // returns with provider params in the URL (lowprofilecode, etc.).
40
- // Send these to the backend for server-side verification via the
41
- // provider's API (e.g. GetLpResult) — never trust URL params alone.
42
- const lowProfileCode =
43
- searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
44
- if (lowProfileCode) {
45
- try {
46
- await client.confirmSdkPayment(checkoutId!, {
47
- paymentIntentId: lowProfileCode,
48
- });
49
- } catch (err) {
50
- console.warn('Redirect payment confirmation failed:', err);
51
- // Don't block — webhook may still process the payment
52
- }
53
- }
54
-
55
- const orderResult = await client.waitForOrder(checkoutId!, {
56
- maxWaitMs: 30000,
57
- });
58
- setResult(orderResult);
59
- } catch (err) {
60
- const message = err instanceof Error ? err.message : 'Failed to confirm order';
61
- setError(message);
62
- } finally {
63
- setLoading(false);
64
- }
65
- }
66
-
67
- waitForOrder();
68
- }, [checkoutId, refreshCart]);
69
-
70
- if (loading) {
71
- return (
72
- <div className="flex min-h-[60vh] flex-col items-center justify-center">
73
- <LoadingSpinner size="lg" />
74
- <p className="text-muted-foreground mt-4">{t('confirming')}</p>
75
- <p className="text-muted-foreground mt-1 text-xs">{t('confirmingHint')}</p>
76
- </div>
77
- );
78
- }
79
-
80
- if (error) {
81
- return (
82
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
83
- <svg
84
- className="text-destructive mx-auto mb-4 h-16 w-16"
85
- fill="none"
86
- viewBox="0 0 24 24"
87
- stroke="currentColor"
88
- >
89
- <path
90
- strokeLinecap="round"
91
- strokeLinejoin="round"
92
- strokeWidth={1.5}
93
- d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
94
- />
95
- </svg>
96
- <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
97
- <p className="text-muted-foreground mt-2">{error}</p>
98
- <p className="text-muted-foreground mt-1 text-sm">{t('errorChargedHint')}</p>
99
- <Link
100
- href="/"
101
- className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
102
- >
103
- {t('returnHome')}
104
- </Link>
105
- </div>
106
- );
107
- }
108
-
109
- // Order was created successfully
110
- if (result?.success) {
111
- const orderNumber = result.status.orderNumber;
112
-
113
- return (
114
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
115
- <svg
116
- className="text-primary mx-auto mb-4 h-16 w-16"
117
- fill="none"
118
- viewBox="0 0 24 24"
119
- stroke="currentColor"
120
- >
121
- <path
122
- strokeLinecap="round"
123
- strokeLinejoin="round"
124
- strokeWidth={1.5}
125
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
126
- />
127
- </svg>
128
-
129
- <h1 className="text-foreground text-2xl font-bold">{t('thankYou')}</h1>
130
-
131
- {orderNumber && (
132
- <p className="text-foreground mt-3 text-lg">
133
- {t('orderNumber')} <span className="font-semibold">{orderNumber}</span>
134
- </p>
135
- )}
136
-
137
- <p className="text-muted-foreground mt-2">{t('confirmationEmail')}</p>
138
-
139
- {result.status.orderId && (
140
- <ConfirmationDownloads orderId={result.status.orderId} checkoutId={checkoutId!} />
141
- )}
142
-
143
- <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
144
- <Link
145
- href="/products"
146
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
147
- >
148
- {tc('continueShopping')}
149
- </Link>
150
-
151
- <Link
152
- href="/account"
153
- className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
154
- >
155
- {t('viewOrders')}
156
- </Link>
157
- </div>
158
- </div>
159
- );
160
- }
161
-
162
- // Order not yet confirmed (polling timed out) - still show success
163
- return (
164
- <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
165
- <svg
166
- className="text-primary mx-auto mb-4 h-16 w-16"
167
- fill="none"
168
- viewBox="0 0 24 24"
169
- stroke="currentColor"
170
- >
171
- <path
172
- strokeLinecap="round"
173
- strokeLinejoin="round"
174
- strokeWidth={1.5}
175
- d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
176
- />
177
- </svg>
178
-
179
- <h1 className="text-foreground text-2xl font-bold">{t('paymentReceived')}</h1>
180
-
181
- <p className="text-muted-foreground mt-2">{t('orderProcessing')}</p>
182
-
183
- <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
184
- <Link
185
- href="/products"
186
- className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
187
- >
188
- {tc('continueShopping')}
189
- </Link>
190
-
191
- <Link
192
- href="/account"
193
- className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
194
- >
195
- {t('viewOrders')}
196
- </Link>
197
- </div>
198
- </div>
199
- );
200
- }
201
-
202
- function ConfirmationDownloads({ orderId, checkoutId }: { orderId: string; checkoutId: string }) {
203
- const t = useTranslations('orderConfirmation');
204
- const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
205
-
206
- useEffect(() => {
207
- let cancelled = false;
208
- async function fetchDownloads() {
209
- const client = getClient();
210
- // Retry a few times — the worker may still be writing downloadMeta
211
- for (let attempt = 0; attempt < 3; attempt++) {
212
- try {
213
- const links = await client.getOrderDownloads(orderId, { checkoutId });
214
- if (!cancelled && links.length > 0) {
215
- setDownloads(links);
216
- return;
217
- }
218
- } catch {
219
- // Not all orders have downloads
220
- }
221
- if (attempt < 2 && !cancelled) {
222
- await new Promise((r) => setTimeout(r, 1500));
223
- }
224
- }
225
- }
226
- fetchDownloads();
227
- return () => {
228
- cancelled = true;
229
- };
230
- }, [orderId, checkoutId]);
231
-
232
- if (!downloads || downloads.length === 0) return null;
233
-
234
- return (
235
- <div className="border-border bg-muted/30 mx-auto mt-8 max-w-md rounded-lg border p-6 text-start">
236
- <h3 className="text-foreground mb-3 text-sm font-semibold">{t('yourDownloads')}</h3>
237
- <div className="space-y-2">
238
- {downloads.map((link, idx) => (
239
- <div key={idx} className="flex items-center justify-between gap-3">
240
- <div className="min-w-0 flex-1">
241
- <p className="text-foreground truncate text-sm">{link.fileName}</p>
242
- <p className="text-muted-foreground truncate text-xs">{link.productName}</p>
243
- </div>
244
- <a
245
- href={link.downloadUrl}
246
- target="_blank"
247
- rel="noopener noreferrer"
248
- className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1.5 text-xs font-medium hover:opacity-90"
249
- >
250
- {t('download')}
251
- </a>
252
- </div>
253
- ))}
254
- </div>
255
- </div>
256
- );
257
- }
258
-
259
- export default function OrderConfirmationPage() {
260
- return (
261
- <Suspense
262
- fallback={
263
- <div className="flex min-h-[60vh] items-center justify-center">
264
- <LoadingSpinner size="lg" />
265
- </div>
266
- }
267
- >
268
- <OrderConfirmationContent />
269
- </Suspense>
270
- );
271
- }
1
+ 'use client';
2
+
3
+ import { Suspense, useEffect, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { Link } from '@/lib/navigation';
6
+ import type { WaitForOrderResult, OrderDownloadLink } from 'brainerce';
7
+ import { getClient } from '@/lib/brainerce';
8
+ import { useCart } from '@/providers/store-provider';
9
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
10
+ import { useTranslations } from '@/lib/translations';
11
+
12
+ function OrderConfirmationContent() {
13
+ const searchParams = useSearchParams();
14
+ const checkoutId = searchParams.get('checkout_id');
15
+
16
+ const { refreshCart } = useCart();
17
+ const t = useTranslations('orderConfirmation');
18
+ const tc = useTranslations('common');
19
+ const [result, setResult] = useState<WaitForOrderResult | null>(null);
20
+ const [loading, setLoading] = useState(true);
21
+ const [error, setError] = useState<string | null>(null);
22
+
23
+ useEffect(() => {
24
+ if (!checkoutId) {
25
+ setError(t('missingCheckoutInfo'));
26
+ setLoading(false);
27
+ return;
28
+ }
29
+
30
+ async function waitForOrder() {
31
+ try {
32
+ const client = getClient();
33
+
34
+ // Clear cart state after successful payment
35
+ client.handlePaymentSuccess(checkoutId!);
36
+ await refreshCart();
37
+
38
+ // For redirect-based payment providers (e.g. CardCom), the customer
39
+ // returns with provider params in the URL (lowprofilecode, etc.).
40
+ // Send these to the backend for server-side verification via the
41
+ // provider's API (e.g. GetLpResult) — never trust URL params alone.
42
+ const lowProfileCode =
43
+ searchParams.get('lowprofilecode') || searchParams.get('LowProfileCode');
44
+ if (lowProfileCode) {
45
+ try {
46
+ await client.confirmSdkPayment(checkoutId!, {
47
+ paymentIntentId: lowProfileCode,
48
+ });
49
+ } catch (err) {
50
+ console.warn('Redirect payment confirmation failed:', err);
51
+ // Don't block — webhook may still process the payment
52
+ }
53
+ }
54
+
55
+ const orderResult = await client.waitForOrder(checkoutId!, {
56
+ maxWaitMs: 30000,
57
+ });
58
+ setResult(orderResult);
59
+ } catch (err) {
60
+ const message = err instanceof Error ? err.message : 'Failed to confirm order';
61
+ setError(message);
62
+ } finally {
63
+ setLoading(false);
64
+ }
65
+ }
66
+
67
+ waitForOrder();
68
+ }, [checkoutId, refreshCart]);
69
+
70
+ if (loading) {
71
+ return (
72
+ <div className="flex min-h-[60vh] flex-col items-center justify-center">
73
+ <LoadingSpinner size="lg" />
74
+ <p className="text-muted-foreground mt-4">{t('confirming')}</p>
75
+ <p className="text-muted-foreground mt-1 text-xs">{t('confirmingHint')}</p>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ if (error) {
81
+ return (
82
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
83
+ <svg
84
+ className="text-destructive mx-auto mb-4 h-16 w-16"
85
+ fill="none"
86
+ viewBox="0 0 24 24"
87
+ stroke="currentColor"
88
+ >
89
+ <path
90
+ strokeLinecap="round"
91
+ strokeLinejoin="round"
92
+ strokeWidth={1.5}
93
+ d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.834-2.694-.834-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"
94
+ />
95
+ </svg>
96
+ <h1 className="text-foreground text-2xl font-bold">{t('errorTitle')}</h1>
97
+ <p className="text-muted-foreground mt-2">{error}</p>
98
+ <p className="text-muted-foreground mt-1 text-sm">{t('errorChargedHint')}</p>
99
+ <Link
100
+ href="/"
101
+ className="bg-primary text-primary-foreground mt-6 inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
102
+ >
103
+ {t('returnHome')}
104
+ </Link>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ // Order was created successfully
110
+ if (result?.success) {
111
+ const orderNumber = result.status.orderNumber;
112
+
113
+ return (
114
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
115
+ <svg
116
+ className="text-primary mx-auto mb-4 h-16 w-16"
117
+ fill="none"
118
+ viewBox="0 0 24 24"
119
+ stroke="currentColor"
120
+ >
121
+ <path
122
+ strokeLinecap="round"
123
+ strokeLinejoin="round"
124
+ strokeWidth={1.5}
125
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
126
+ />
127
+ </svg>
128
+
129
+ <h1 className="text-foreground text-2xl font-bold">{t('thankYou')}</h1>
130
+
131
+ {orderNumber && (
132
+ <p className="text-foreground mt-3 text-lg">
133
+ {t('orderNumber')} <span className="font-semibold">{orderNumber}</span>
134
+ </p>
135
+ )}
136
+
137
+ <p className="text-muted-foreground mt-2">{t('confirmationEmail')}</p>
138
+
139
+ {result.status.orderId && (
140
+ <ConfirmationDownloads orderId={result.status.orderId} checkoutId={checkoutId!} />
141
+ )}
142
+
143
+ <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
144
+ <Link
145
+ href="/products"
146
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
147
+ >
148
+ {tc('continueShopping')}
149
+ </Link>
150
+
151
+ <Link
152
+ href="/account"
153
+ className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
154
+ >
155
+ {t('viewOrders')}
156
+ </Link>
157
+ </div>
158
+ </div>
159
+ );
160
+ }
161
+
162
+ // Order not yet confirmed (polling timed out) - still show success
163
+ return (
164
+ <div className="mx-auto max-w-2xl px-4 py-16 text-center sm:px-6 lg:px-8">
165
+ <svg
166
+ className="text-primary mx-auto mb-4 h-16 w-16"
167
+ fill="none"
168
+ viewBox="0 0 24 24"
169
+ stroke="currentColor"
170
+ >
171
+ <path
172
+ strokeLinecap="round"
173
+ strokeLinejoin="round"
174
+ strokeWidth={1.5}
175
+ d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
176
+ />
177
+ </svg>
178
+
179
+ <h1 className="text-foreground text-2xl font-bold">{t('paymentReceived')}</h1>
180
+
181
+ <p className="text-muted-foreground mt-2">{t('orderProcessing')}</p>
182
+
183
+ <div className="mt-8 flex flex-col items-center justify-center gap-3 sm:flex-row">
184
+ <Link
185
+ href="/products"
186
+ className="bg-primary text-primary-foreground inline-flex items-center rounded px-6 py-3 font-medium transition-opacity hover:opacity-90"
187
+ >
188
+ {tc('continueShopping')}
189
+ </Link>
190
+
191
+ <Link
192
+ href="/account"
193
+ className="border-border text-foreground hover:bg-muted inline-flex items-center rounded border px-6 py-3 font-medium transition-colors"
194
+ >
195
+ {t('viewOrders')}
196
+ </Link>
197
+ </div>
198
+ </div>
199
+ );
200
+ }
201
+
202
+ function ConfirmationDownloads({ orderId, checkoutId }: { orderId: string; checkoutId: string }) {
203
+ const t = useTranslations('orderConfirmation');
204
+ const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
205
+
206
+ useEffect(() => {
207
+ let cancelled = false;
208
+ async function fetchDownloads() {
209
+ const client = getClient();
210
+ // Retry a few times — the worker may still be writing downloadMeta
211
+ for (let attempt = 0; attempt < 3; attempt++) {
212
+ try {
213
+ const links = await client.getOrderDownloads(orderId, { checkoutId });
214
+ if (!cancelled && links.length > 0) {
215
+ setDownloads(links);
216
+ return;
217
+ }
218
+ } catch {
219
+ // Not all orders have downloads
220
+ }
221
+ if (attempt < 2 && !cancelled) {
222
+ await new Promise((r) => setTimeout(r, 1500));
223
+ }
224
+ }
225
+ }
226
+ fetchDownloads();
227
+ return () => {
228
+ cancelled = true;
229
+ };
230
+ }, [orderId, checkoutId]);
231
+
232
+ if (!downloads || downloads.length === 0) return null;
233
+
234
+ return (
235
+ <div className="border-border bg-muted/30 mx-auto mt-8 max-w-md rounded-lg border p-6 text-start">
236
+ <h3 className="text-foreground mb-3 text-sm font-semibold">{t('yourDownloads')}</h3>
237
+ <div className="space-y-2">
238
+ {downloads.map((link, idx) => (
239
+ <div key={idx} className="flex items-center justify-between gap-3">
240
+ <div className="min-w-0 flex-1">
241
+ <p className="text-foreground truncate text-sm">{link.fileName}</p>
242
+ <p className="text-muted-foreground truncate text-xs">{link.productName}</p>
243
+ </div>
244
+ <a
245
+ href={link.downloadUrl}
246
+ target="_blank"
247
+ rel="noopener noreferrer"
248
+ className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1.5 text-xs font-medium hover:opacity-90"
249
+ >
250
+ {t('download')}
251
+ </a>
252
+ </div>
253
+ ))}
254
+ </div>
255
+ </div>
256
+ );
257
+ }
258
+
259
+ export default function OrderConfirmationPage() {
260
+ return (
261
+ <Suspense
262
+ fallback={
263
+ <div className="flex min-h-[60vh] items-center justify-center">
264
+ <LoadingSpinner size="lg" />
265
+ </div>
266
+ }
267
+ >
268
+ <OrderConfirmationContent />
269
+ </Suspense>
270
+ );
271
+ }
@@ -1,59 +1,59 @@
1
- 'use client';
2
-
3
- import { useEffect } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import { Suspense } from 'react';
6
- import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
-
8
- /**
9
- * Lightweight callback page for iframe-based payment providers (e.g. CardCom).
10
- *
11
- * After the customer pays on the provider's hosted page (rendered inside an
12
- * iframe on the checkout page), the provider redirects *inside the iframe* to
13
- * this page. We extract the relevant query params and send them to the parent
14
- * window via postMessage so the checkout page can verify the payment
15
- * server-side and proceed to order confirmation.
16
- */
17
- function PaymentCompleteContent() {
18
- const searchParams = useSearchParams();
19
-
20
- useEffect(() => {
21
- // Only send postMessage when running inside an iframe
22
- if (window.parent === window) {
23
- // Not in iframe — fallback: redirect to order-confirmation directly
24
- const checkoutId = searchParams.get('checkout_id');
25
- if (checkoutId) {
26
- window.location.href = `/order-confirmation?${searchParams.toString()}`;
27
- }
28
- return;
29
- }
30
-
31
- // Collect all query params from the provider redirect
32
- const data: Record<string, string> = {};
33
- searchParams.forEach((value, key) => {
34
- data[key] = value;
35
- });
36
-
37
- window.parent.postMessage({ type: 'brainerce:payment-complete', data }, window.location.origin);
38
- }, [searchParams]);
39
-
40
- return (
41
- <div className="flex min-h-[200px] items-center justify-center">
42
- <LoadingSpinner size="lg" />
43
- </div>
44
- );
45
- }
46
-
47
- export default function PaymentCompletePage() {
48
- return (
49
- <Suspense
50
- fallback={
51
- <div className="flex min-h-[200px] items-center justify-center">
52
- <LoadingSpinner size="lg" />
53
- </div>
54
- }
55
- >
56
- <PaymentCompleteContent />
57
- </Suspense>
58
- );
59
- }
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import { Suspense } from 'react';
6
+ import { LoadingSpinner } from '@/components/shared/loading-spinner';
7
+
8
+ /**
9
+ * Lightweight callback page for iframe-based payment providers (e.g. CardCom).
10
+ *
11
+ * After the customer pays on the provider's hosted page (rendered inside an
12
+ * iframe on the checkout page), the provider redirects *inside the iframe* to
13
+ * this page. We extract the relevant query params and send them to the parent
14
+ * window via postMessage so the checkout page can verify the payment
15
+ * server-side and proceed to order confirmation.
16
+ */
17
+ function PaymentCompleteContent() {
18
+ const searchParams = useSearchParams();
19
+
20
+ useEffect(() => {
21
+ // Only send postMessage when running inside an iframe
22
+ if (window.parent === window) {
23
+ // Not in iframe — fallback: redirect to order-confirmation directly
24
+ const checkoutId = searchParams.get('checkout_id');
25
+ if (checkoutId) {
26
+ window.location.href = `/order-confirmation?${searchParams.toString()}`;
27
+ }
28
+ return;
29
+ }
30
+
31
+ // Collect all query params from the provider redirect
32
+ const data: Record<string, string> = {};
33
+ searchParams.forEach((value, key) => {
34
+ data[key] = value;
35
+ });
36
+
37
+ window.parent.postMessage({ type: 'brainerce:payment-complete', data }, window.location.origin);
38
+ }, [searchParams]);
39
+
40
+ return (
41
+ <div className="flex min-h-[200px] items-center justify-center">
42
+ <LoadingSpinner size="lg" />
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export default function PaymentCompletePage() {
48
+ return (
49
+ <Suspense
50
+ fallback={
51
+ <div className="flex min-h-[200px] items-center justify-center">
52
+ <LoadingSpinner size="lg" />
53
+ </div>
54
+ }
55
+ >
56
+ <PaymentCompleteContent />
57
+ </Suspense>
58
+ );
59
+ }