@blocklet/payment-react 1.25.10 → 1.26.0

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.
Files changed (160) hide show
  1. package/es/checkout-v2/checkout-v2.d.ts +2 -0
  2. package/es/checkout-v2/checkout-v2.js +121 -0
  3. package/es/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
  4. package/es/checkout-v2/components/dialogs/checkout-dialogs.js +106 -0
  5. package/es/checkout-v2/components/left/billing-toggle.d.ts +6 -0
  6. package/es/checkout-v2/components/left/billing-toggle.js +118 -0
  7. package/es/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
  8. package/es/checkout-v2/components/left/cross-sell-card.js +167 -0
  9. package/es/checkout-v2/components/left/product-item-card.d.ts +26 -0
  10. package/es/checkout-v2/components/left/product-item-card.js +571 -0
  11. package/es/checkout-v2/components/left/promotion-input.d.ts +19 -0
  12. package/es/checkout-v2/components/left/promotion-input.js +178 -0
  13. package/es/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
  14. package/es/checkout-v2/components/left/staking-breakdown.js +48 -0
  15. package/es/checkout-v2/components/left/trial-info.d.ts +13 -0
  16. package/es/checkout-v2/components/left/trial-info.js +48 -0
  17. package/es/checkout-v2/components/right/currency-grid.d.ts +8 -0
  18. package/es/checkout-v2/components/right/currency-grid.js +48 -0
  19. package/es/checkout-v2/components/right/customer-info-card.d.ts +17 -0
  20. package/es/checkout-v2/components/right/customer-info-card.js +156 -0
  21. package/es/checkout-v2/components/right/status-feedback.d.ts +7 -0
  22. package/es/checkout-v2/components/right/status-feedback.js +17 -0
  23. package/es/checkout-v2/components/right/submit-button.d.ts +10 -0
  24. package/es/checkout-v2/components/right/submit-button.js +29 -0
  25. package/es/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
  26. package/es/checkout-v2/components/right/subscription-disclaimer.js +8 -0
  27. package/es/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
  28. package/es/checkout-v2/components/shared/exchange-rate-footer.js +182 -0
  29. package/es/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
  30. package/es/checkout-v2/components/shared/scenario-badge.js +47 -0
  31. package/es/checkout-v2/components/shared/total-display.d.ts +7 -0
  32. package/es/checkout-v2/components/shared/total-display.js +84 -0
  33. package/es/checkout-v2/index.d.ts +2 -0
  34. package/es/checkout-v2/index.js +1 -0
  35. package/es/checkout-v2/layouts/checkout-layout.d.ts +7 -0
  36. package/es/checkout-v2/layouts/checkout-layout.js +226 -0
  37. package/es/checkout-v2/panels/left/composite-panel.d.ts +1 -0
  38. package/es/checkout-v2/panels/left/composite-panel.js +423 -0
  39. package/es/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
  40. package/es/checkout-v2/panels/left/credit-topup-panel.js +615 -0
  41. package/es/checkout-v2/panels/left/scenario-router.d.ts +1 -0
  42. package/es/checkout-v2/panels/left/scenario-router.js +19 -0
  43. package/es/checkout-v2/panels/right/payment-panel.d.ts +1 -0
  44. package/es/checkout-v2/panels/right/payment-panel.js +644 -0
  45. package/es/checkout-v2/types.d.ts +15 -0
  46. package/es/checkout-v2/types.js +0 -0
  47. package/es/checkout-v2/utils/format.d.ts +59 -0
  48. package/es/checkout-v2/utils/format.js +125 -0
  49. package/es/checkout-v2/utils/scenario-detector.d.ts +3 -0
  50. package/es/checkout-v2/utils/scenario-detector.js +17 -0
  51. package/es/checkout-v2/views/error-view.d.ts +7 -0
  52. package/es/checkout-v2/views/error-view.js +269 -0
  53. package/es/checkout-v2/views/loading-view.d.ts +5 -0
  54. package/es/checkout-v2/views/loading-view.js +158 -0
  55. package/es/checkout-v2/views/success-view.d.ts +29 -0
  56. package/es/checkout-v2/views/success-view.js +614 -0
  57. package/es/components/phone-field.d.ts +14 -0
  58. package/es/components/phone-field.js +96 -0
  59. package/es/index.d.ts +3 -1
  60. package/es/index.js +3 -1
  61. package/es/locales/en.js +45 -6
  62. package/es/locales/zh.js +45 -6
  63. package/es/payment/form/index.js +10 -1
  64. package/lib/checkout-v2/checkout-v2.d.ts +2 -0
  65. package/lib/checkout-v2/checkout-v2.js +151 -0
  66. package/lib/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
  67. package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +131 -0
  68. package/lib/checkout-v2/components/left/billing-toggle.d.ts +6 -0
  69. package/lib/checkout-v2/components/left/billing-toggle.js +126 -0
  70. package/lib/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
  71. package/lib/checkout-v2/components/left/cross-sell-card.js +257 -0
  72. package/lib/checkout-v2/components/left/product-item-card.d.ts +26 -0
  73. package/lib/checkout-v2/components/left/product-item-card.js +738 -0
  74. package/lib/checkout-v2/components/left/promotion-input.d.ts +19 -0
  75. package/lib/checkout-v2/components/left/promotion-input.js +220 -0
  76. package/lib/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
  77. package/lib/checkout-v2/components/left/staking-breakdown.js +96 -0
  78. package/lib/checkout-v2/components/left/trial-info.d.ts +13 -0
  79. package/lib/checkout-v2/components/left/trial-info.js +82 -0
  80. package/lib/checkout-v2/components/right/currency-grid.d.ts +8 -0
  81. package/lib/checkout-v2/components/right/currency-grid.js +96 -0
  82. package/lib/checkout-v2/components/right/customer-info-card.d.ts +17 -0
  83. package/lib/checkout-v2/components/right/customer-info-card.js +246 -0
  84. package/lib/checkout-v2/components/right/status-feedback.d.ts +7 -0
  85. package/lib/checkout-v2/components/right/status-feedback.js +30 -0
  86. package/lib/checkout-v2/components/right/submit-button.d.ts +10 -0
  87. package/lib/checkout-v2/components/right/submit-button.js +35 -0
  88. package/lib/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
  89. package/lib/checkout-v2/components/right/subscription-disclaimer.js +33 -0
  90. package/lib/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
  91. package/lib/checkout-v2/components/shared/exchange-rate-footer.js +282 -0
  92. package/lib/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
  93. package/lib/checkout-v2/components/shared/scenario-badge.js +57 -0
  94. package/lib/checkout-v2/components/shared/total-display.d.ts +7 -0
  95. package/lib/checkout-v2/components/shared/total-display.js +154 -0
  96. package/lib/checkout-v2/index.d.ts +2 -0
  97. package/lib/checkout-v2/index.js +13 -0
  98. package/lib/checkout-v2/layouts/checkout-layout.d.ts +7 -0
  99. package/lib/checkout-v2/layouts/checkout-layout.js +308 -0
  100. package/lib/checkout-v2/panels/left/composite-panel.d.ts +1 -0
  101. package/lib/checkout-v2/panels/left/composite-panel.js +515 -0
  102. package/lib/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
  103. package/lib/checkout-v2/panels/left/credit-topup-panel.js +799 -0
  104. package/lib/checkout-v2/panels/left/scenario-router.d.ts +1 -0
  105. package/lib/checkout-v2/panels/left/scenario-router.js +29 -0
  106. package/lib/checkout-v2/panels/right/payment-panel.d.ts +1 -0
  107. package/lib/checkout-v2/panels/right/payment-panel.js +906 -0
  108. package/lib/checkout-v2/types.d.ts +15 -0
  109. package/lib/checkout-v2/types.js +1 -0
  110. package/lib/checkout-v2/utils/format.d.ts +59 -0
  111. package/lib/checkout-v2/utils/format.js +158 -0
  112. package/lib/checkout-v2/utils/scenario-detector.d.ts +3 -0
  113. package/lib/checkout-v2/utils/scenario-detector.js +23 -0
  114. package/lib/checkout-v2/views/error-view.d.ts +7 -0
  115. package/lib/checkout-v2/views/error-view.js +321 -0
  116. package/lib/checkout-v2/views/loading-view.d.ts +5 -0
  117. package/lib/checkout-v2/views/loading-view.js +168 -0
  118. package/lib/checkout-v2/views/success-view.d.ts +29 -0
  119. package/lib/checkout-v2/views/success-view.js +735 -0
  120. package/lib/components/phone-field.d.ts +14 -0
  121. package/lib/components/phone-field.js +130 -0
  122. package/lib/index.d.ts +3 -1
  123. package/lib/index.js +8 -0
  124. package/lib/locales/en.js +45 -6
  125. package/lib/locales/zh.js +45 -6
  126. package/lib/payment/form/index.js +10 -1
  127. package/package.json +4 -3
  128. package/src/checkout-v2/checkout-v2.tsx +155 -0
  129. package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +134 -0
  130. package/src/checkout-v2/components/left/billing-toggle.tsx +122 -0
  131. package/src/checkout-v2/components/left/cross-sell-card.tsx +170 -0
  132. package/src/checkout-v2/components/left/product-item-card.tsx +634 -0
  133. package/src/checkout-v2/components/left/promotion-input.tsx +207 -0
  134. package/src/checkout-v2/components/left/staking-breakdown.tsx +57 -0
  135. package/src/checkout-v2/components/left/trial-info.tsx +63 -0
  136. package/src/checkout-v2/components/right/currency-grid.tsx +59 -0
  137. package/src/checkout-v2/components/right/customer-info-card.tsx +214 -0
  138. package/src/checkout-v2/components/right/status-feedback.tsx +35 -0
  139. package/src/checkout-v2/components/right/submit-button.tsx +37 -0
  140. package/src/checkout-v2/components/right/subscription-disclaimer.tsx +27 -0
  141. package/src/checkout-v2/components/shared/exchange-rate-footer.tsx +221 -0
  142. package/src/checkout-v2/components/shared/scenario-badge.tsx +51 -0
  143. package/src/checkout-v2/components/shared/total-display.tsx +112 -0
  144. package/src/checkout-v2/index.ts +2 -0
  145. package/src/checkout-v2/layouts/checkout-layout.tsx +232 -0
  146. package/src/checkout-v2/panels/left/composite-panel.tsx +465 -0
  147. package/src/checkout-v2/panels/left/credit-topup-panel.tsx +681 -0
  148. package/src/checkout-v2/panels/left/scenario-router.tsx +22 -0
  149. package/src/checkout-v2/panels/right/payment-panel.tsx +703 -0
  150. package/src/checkout-v2/types.ts +18 -0
  151. package/src/checkout-v2/utils/format.ts +204 -0
  152. package/src/checkout-v2/utils/scenario-detector.ts +30 -0
  153. package/src/checkout-v2/views/error-view.tsx +293 -0
  154. package/src/checkout-v2/views/loading-view.tsx +162 -0
  155. package/src/checkout-v2/views/success-view.tsx +770 -0
  156. package/src/components/phone-field.tsx +119 -0
  157. package/src/index.ts +3 -0
  158. package/src/locales/en.tsx +45 -4
  159. package/src/locales/zh.tsx +43 -4
  160. package/src/payment/form/index.tsx +16 -1
@@ -0,0 +1,770 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import {
3
+ Box,
4
+ Typography,
5
+ Stack,
6
+ Alert,
7
+ Button,
8
+ Link,
9
+ Divider,
10
+ LinearProgress,
11
+ Skeleton,
12
+ keyframes,
13
+ } from '@mui/material';
14
+ import CheckIcon from '@mui/icons-material/Check';
15
+ import CheckCircleIcon from '@mui/icons-material/CheckCircle';
16
+ import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
17
+ import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
18
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
19
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
20
+ import { joinURL } from 'ufo';
21
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
22
+ import type { TCheckoutSessionExpanded } from '@blocklet/payment-types';
23
+ import { usePaymentMethodContext } from '@blocklet/payment-react-headless';
24
+
25
+ import { getPrefix } from '../../libs/util';
26
+ import { formatTokenAmount } from '../utils/format';
27
+
28
+ // ── Animations ──
29
+
30
+ const scaleIn = keyframes`
31
+ from { transform: scale(0); opacity: 0; }
32
+ 60% { transform: scale(1.15); }
33
+ to { transform: scale(1); opacity: 1; }
34
+ `;
35
+
36
+ const fadeUp = keyframes`
37
+ from { opacity: 0; transform: translateY(16px); }
38
+ to { opacity: 1; transform: translateY(0); }
39
+ `;
40
+
41
+ // ── Confetti ──
42
+
43
+ function useConfetti(containerRef: React.RefObject<HTMLElement | null>, enabled: boolean) {
44
+ const firedRef = useRef(false);
45
+
46
+ useEffect(() => {
47
+ if (!enabled || firedRef.current || !containerRef.current) return undefined;
48
+ firedRef.current = true;
49
+
50
+ const container = containerRef.current;
51
+ const canvas = document.createElement('canvas');
52
+ canvas.style.cssText = 'position:absolute;inset:0;pointer-events:none;z-index:10;';
53
+ canvas.width = container.offsetWidth;
54
+ canvas.height = container.offsetHeight;
55
+ container.appendChild(canvas);
56
+
57
+ const ctx = canvas.getContext('2d');
58
+ if (!ctx) return undefined;
59
+
60
+ const colors = ['#3b82f6', '#60a5fa', '#34d399', '#fbbf24', '#f472b6', '#a78bfa', '#f97316'];
61
+ const pieces: Array<{
62
+ x: number;
63
+ y: number;
64
+ vx: number;
65
+ vy: number;
66
+ w: number;
67
+ h: number;
68
+ color: string;
69
+ rot: number;
70
+ rv: number;
71
+ opacity: number;
72
+ }> = [];
73
+
74
+ for (let i = 0; i < 80; i++) {
75
+ pieces.push({
76
+ x: canvas.width / 2 + (Math.random() - 0.5) * 60,
77
+ y: canvas.height * 0.35,
78
+ vx: (Math.random() - 0.5) * 12,
79
+ vy: -Math.random() * 14 - 4,
80
+ w: Math.random() * 8 + 4,
81
+ h: Math.random() * 6 + 2,
82
+ color: colors[Math.floor(Math.random() * colors.length)],
83
+ rot: Math.random() * Math.PI * 2,
84
+ rv: (Math.random() - 0.5) * 0.3,
85
+ opacity: 1,
86
+ });
87
+ }
88
+
89
+ let frame: number;
90
+ const gravity = 0.25;
91
+ const friction = 0.99;
92
+
93
+ const animate = () => {
94
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
95
+ let alive = false;
96
+
97
+ for (const p of pieces) {
98
+ p.vy += gravity;
99
+ p.vx *= friction;
100
+ p.x += p.vx;
101
+ p.y += p.vy;
102
+ p.rot += p.rv;
103
+
104
+ if (p.y > canvas.height * 0.6) {
105
+ p.opacity -= 0.02;
106
+ }
107
+ if (p.opacity > 0) {
108
+ alive = true;
109
+ ctx.save();
110
+ ctx.globalAlpha = p.opacity;
111
+ ctx.translate(p.x, p.y);
112
+ ctx.rotate(p.rot);
113
+ ctx.fillStyle = p.color;
114
+ ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
115
+ ctx.restore();
116
+ }
117
+ }
118
+
119
+ if (alive) {
120
+ frame = requestAnimationFrame(animate);
121
+ } else {
122
+ canvas.remove();
123
+ }
124
+ };
125
+
126
+ // Small delay so the icon animation plays first
127
+ const timer = setTimeout(() => {
128
+ frame = requestAnimationFrame(animate);
129
+ }, 400);
130
+
131
+ return () => {
132
+ clearTimeout(timer);
133
+ cancelAnimationFrame(frame);
134
+ canvas.remove();
135
+ };
136
+ }, [enabled, containerRef]);
137
+ }
138
+
139
+ // ── Types ──
140
+
141
+ interface VendorInfo {
142
+ success: boolean;
143
+ status: 'delivered' | 'pending' | 'failed';
144
+ progress: number;
145
+ message: string;
146
+ appUrl?: string;
147
+ title?: string;
148
+ name?: string;
149
+ vendorType: string;
150
+ }
151
+
152
+ interface SuccessViewProps {
153
+ submit: {
154
+ vendorStatus: {
155
+ payment_status: string;
156
+ session_status: string;
157
+ vendors: VendorInfo[];
158
+ error: string | null;
159
+ isAllCompleted: boolean;
160
+ hasFailed: boolean;
161
+ } | null;
162
+ result: {
163
+ checkoutSession: TCheckoutSessionExpanded;
164
+ } | null;
165
+ };
166
+ session: TCheckoutSessionExpanded | null | undefined;
167
+ }
168
+
169
+ // ── Helpers ──
170
+
171
+ function getCustomMessage(session: TCheckoutSessionExpanded | null | undefined): string | undefined {
172
+ return (session as any)?.payment_link?.after_completion?.hosted_confirmation?.custom_message;
173
+ }
174
+
175
+ function getPayee(session: TCheckoutSessionExpanded | null | undefined): string {
176
+ const items = (session as any)?.line_items || [];
177
+ for (const item of items) {
178
+ if (item?.price?.product?.statement_descriptor) {
179
+ return item.price.product.statement_descriptor;
180
+ }
181
+ }
182
+ return (session as any)?.app_name || (session as any)?.payment_link?.app_name || '';
183
+ }
184
+
185
+ function getVendorLabel(vendor: VendorInfo, isFailed: boolean, t: (key: string, params?: any) => string): string {
186
+ const name = vendor.name || vendor.title;
187
+ const isCompleted = vendor.status === 'delivered';
188
+
189
+ if (vendor.vendorType === 'didnames') {
190
+ if (isFailed) return t('payment.checkout.vendor.didnames.failed', { name });
191
+ if (isCompleted) return t('payment.checkout.vendor.didnames.completed', { name });
192
+ return t('payment.checkout.vendor.didnames.processing', { name });
193
+ }
194
+
195
+ if (isFailed) return t('payment.checkout.vendor.launcher.failed', { name });
196
+ if (isCompleted) return t('payment.checkout.vendor.launcher.completed', { name });
197
+ return t('payment.checkout.vendor.launcher.processing', { name });
198
+ }
199
+
200
+ // ── Hero Icon ──
201
+
202
+ function HeroSuccessIcon() {
203
+ return (
204
+ <Box
205
+ sx={{
206
+ position: 'relative',
207
+ mb: 1,
208
+ animation: `${scaleIn} 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both`,
209
+ }}>
210
+ <Box
211
+ sx={{
212
+ position: 'absolute',
213
+ inset: -24,
214
+ background: (theme) =>
215
+ theme.palette.mode === 'dark'
216
+ ? 'radial-gradient(circle, rgba(59,130,246,0.12) 0%, transparent 70%)'
217
+ : 'radial-gradient(circle, rgba(59,130,246,0.08) 0%, transparent 70%)',
218
+ borderRadius: '50%',
219
+ pointerEvents: 'none',
220
+ }}
221
+ />
222
+ <Box
223
+ sx={{
224
+ position: 'relative',
225
+ width: { xs: 100, md: 120 },
226
+ height: { xs: 100, md: 120 },
227
+ borderRadius: { xs: '28px', md: '32px' },
228
+ background: (theme) =>
229
+ theme.palette.mode === 'dark'
230
+ ? 'linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(255,255,255,0.04) 100%)'
231
+ : 'linear-gradient(135deg, #eff6ff 0%, #ffffff 100%)',
232
+ border: '1px solid',
233
+ borderColor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(59,130,246,0.2)' : 'rgba(59,130,246,0.12)'),
234
+ boxShadow: (theme) =>
235
+ theme.palette.mode === 'dark'
236
+ ? '0 10px 30px -5px rgba(0,0,0,0.3)'
237
+ : '0 10px 30px -5px rgba(59,130,246,0.1)',
238
+ display: 'flex',
239
+ alignItems: 'center',
240
+ justifyContent: 'center',
241
+ }}>
242
+ <Box
243
+ sx={{
244
+ position: 'absolute',
245
+ inset: 0,
246
+ borderRadius: 'inherit',
247
+ background: (theme) =>
248
+ theme.palette.mode === 'dark'
249
+ ? 'linear-gradient(to top-right, rgba(59,130,246,0.1), transparent)'
250
+ : 'linear-gradient(to top-right, rgba(59,130,246,0.06), transparent)',
251
+ pointerEvents: 'none',
252
+ }}
253
+ />
254
+ <VerifiedUserIcon
255
+ sx={{
256
+ fontSize: { xs: 52, md: 64 },
257
+ color: 'primary.main',
258
+ filter: (theme) =>
259
+ theme.palette.mode === 'dark'
260
+ ? 'drop-shadow(0 0 12px rgba(59,130,246,0.4))'
261
+ : 'drop-shadow(0 0 12px rgba(59,130,246,0.2))',
262
+ }}
263
+ />
264
+ </Box>
265
+ <Box
266
+ sx={{
267
+ position: 'absolute',
268
+ bottom: { xs: -6, md: -8 },
269
+ right: { xs: -6, md: -8 },
270
+ width: { xs: 32, md: 38 },
271
+ height: { xs: 32, md: 38 },
272
+ borderRadius: { xs: '12px', md: '14px' },
273
+ bgcolor: 'background.paper',
274
+ boxShadow: '0 4px 12px rgba(0,0,0,0.1)',
275
+ border: '1px solid',
276
+ borderColor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'rgba(59,130,246,0.08)'),
277
+ display: 'flex',
278
+ alignItems: 'center',
279
+ justifyContent: 'center',
280
+ }}>
281
+ <CheckCircleIcon sx={{ fontSize: { xs: 20, md: 24 }, color: 'success.main' }} />
282
+ </Box>
283
+ </Box>
284
+ );
285
+ }
286
+
287
+ // ── Payment Receipt (mobile only) ──
288
+
289
+ function PaymentReceipt({
290
+ session,
291
+ t,
292
+ }: {
293
+ session: TCheckoutSessionExpanded | null | undefined;
294
+ t: (key: string, params?: any) => string;
295
+ }) {
296
+ const { currencies } = usePaymentMethodContext();
297
+ const amountTotal = (session as any)?.amount_total;
298
+ const currencyId = (session as any)?.currency_id;
299
+
300
+ // Skip for trials or no amount
301
+ const subData = (session as any)?.subscription_data;
302
+ if (Number(subData?.trial_period_days || 0) > 0) return null;
303
+ if (!amountTotal || amountTotal === '0') return null;
304
+
305
+ // Find currency object from available currencies
306
+ const currency = currencies.find((c) => c.id === currencyId) || null;
307
+ if (!currency) return null;
308
+
309
+ const formatted = formatTokenAmount(amountTotal, currency);
310
+ if (!formatted || formatted === '0') return null;
311
+
312
+ const amountStr = `${formatted} ${currency.symbol || ''}`.trim();
313
+ // Split the translated sentence around the amount to bold it
314
+ const fullText = t('payment.checkout.completed.summary.paid', { amount: amountStr });
315
+ const idx = fullText.indexOf(amountStr);
316
+
317
+ return (
318
+ <Typography
319
+ sx={{
320
+ fontSize: { xs: 15, md: 16 },
321
+ color: 'text.secondary',
322
+ fontWeight: 500,
323
+ lineHeight: 1.6,
324
+ textAlign: 'center',
325
+ maxWidth: 440,
326
+ animation: `${fadeUp} 0.5s ease 0.3s both`,
327
+ }}>
328
+ {idx >= 0 ? (
329
+ <>
330
+ {fullText.slice(0, idx)}
331
+ <Box component="span" sx={{ fontWeight: 700, color: 'text.primary' }}>
332
+ {amountStr}
333
+ </Box>
334
+ {fullText.slice(idx + amountStr.length)}
335
+ </>
336
+ ) : (
337
+ fullText
338
+ )}
339
+ </Typography>
340
+ );
341
+ }
342
+
343
+ // ── Vendor Progress Item ──
344
+
345
+ function VendorProgressItemV2({ vendor, t }: { vendor: VendorInfo; t: (key: string, params?: any) => string }) {
346
+ const [displayProgress, setDisplayProgress] = useState(0);
347
+ const animationRef = useRef<number>();
348
+
349
+ const startAnimation = useCallback(() => {
350
+ const realProgress = vendor.progress || 0;
351
+ let startTime: number;
352
+ let startProgress: number;
353
+
354
+ const animate = (currentTime: number) => {
355
+ if (!startTime) {
356
+ startTime = currentTime;
357
+ startProgress = displayProgress;
358
+ }
359
+
360
+ const elapsed = currentTime - startTime;
361
+ let newProgress: number;
362
+
363
+ if (realProgress === 100) {
364
+ newProgress = 100;
365
+ } else if (realProgress === 0) {
366
+ newProgress = Math.min(startProgress + elapsed / 1000, 99);
367
+ } else if (realProgress > startProgress) {
368
+ const progress = Math.min(elapsed / 1000, 1);
369
+ newProgress = startProgress + (realProgress - startProgress) * progress;
370
+ } else {
371
+ newProgress = Math.min(startProgress + elapsed / 1000, 99);
372
+ }
373
+
374
+ newProgress = Math.round(newProgress);
375
+ setDisplayProgress((pre) => Math.min(pre > newProgress ? pre : newProgress, 100));
376
+
377
+ if (realProgress === 100) return;
378
+ if (newProgress < 99 && realProgress < 100) {
379
+ animationRef.current = requestAnimationFrame(animate);
380
+ }
381
+ };
382
+
383
+ if (animationRef.current) cancelAnimationFrame(animationRef.current);
384
+ animationRef.current = requestAnimationFrame(animate);
385
+ }, [vendor.progress, displayProgress]);
386
+
387
+ useEffect(() => {
388
+ startAnimation();
389
+ return () => {
390
+ if (animationRef.current) cancelAnimationFrame(animationRef.current);
391
+ };
392
+ }, [startAnimation]);
393
+
394
+ const isCompleted = displayProgress >= 100;
395
+ const isFailed = vendor.status === 'failed';
396
+ const nameText = getVendorLabel(vendor, isFailed, t);
397
+
398
+ if (!vendor.name && !vendor.title) {
399
+ return (
400
+ <Box sx={{ mb: 1.5 }}>
401
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.75 }}>
402
+ <Skeleton variant="rounded" height={14} width={150} />
403
+ <Skeleton variant="rounded" height={14} width={50} />
404
+ </Stack>
405
+ <Skeleton variant="rounded" height={6} width="100%" />
406
+ </Box>
407
+ );
408
+ }
409
+
410
+ return (
411
+ <Box sx={{ mb: 1.5 }}>
412
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 0.75 }}>
413
+ <Typography
414
+ sx={{
415
+ fontSize: 13,
416
+ fontWeight: 600,
417
+ color: isFailed ? 'error.main' : 'text.secondary',
418
+ display: 'flex',
419
+ alignItems: 'center',
420
+ }}>
421
+ {nameText}
422
+ {isCompleted && !isFailed && <CheckIcon sx={{ color: 'success.main', ml: 0.5, fontSize: 16 }} />}
423
+ </Typography>
424
+ {!isCompleted && (
425
+ <Typography sx={{ fontSize: 12, fontWeight: 600, color: isFailed ? 'error.main' : 'text.secondary' }}>
426
+ {t('payment.checkout.vendor.progress', { progress: isFailed ? 0 : displayProgress })}
427
+ </Typography>
428
+ )}
429
+ </Stack>
430
+ <LinearProgress
431
+ variant="determinate"
432
+ value={isFailed ? 100 : displayProgress || 0}
433
+ sx={{
434
+ height: 6,
435
+ borderRadius: 3,
436
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'grey.200'),
437
+ '& .MuiLinearProgress-bar': {
438
+ borderRadius: 3,
439
+ // eslint-disable-next-line no-nested-ternary
440
+ bgcolor: isFailed ? 'error.main' : isCompleted ? 'success.main' : 'primary.main',
441
+ transition: 'background-color 0.3s linear',
442
+ },
443
+ }}
444
+ />
445
+ </Box>
446
+ );
447
+ }
448
+
449
+ // ── Vendor Progress Panel ──
450
+
451
+ function VendorProgressPanel({
452
+ vendorStatus,
453
+ pageInfo = undefined,
454
+ locale,
455
+ t,
456
+ }: {
457
+ vendorStatus: NonNullable<SuccessViewProps['submit']['vendorStatus']>;
458
+ pageInfo?: any;
459
+ locale: string;
460
+ t: (key: string, params?: any) => string;
461
+ }) {
462
+ return (
463
+ <Box
464
+ sx={{
465
+ width: '100%',
466
+ maxWidth: 420,
467
+ p: 2.5,
468
+ borderRadius: 3,
469
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'grey.50'),
470
+ border: '1px solid',
471
+ borderColor: 'divider',
472
+ animation: `${fadeUp} 0.5s ease 0.3s both`,
473
+ }}>
474
+ {vendorStatus.vendors.map((vendor, idx) => (
475
+ <VendorProgressItemV2 key={vendor.title || `vendor-${idx}`} vendor={vendor} t={t} />
476
+ ))}
477
+ {vendorStatus.hasFailed && (
478
+ <Typography sx={{ fontSize: 13, fontWeight: 600, color: 'warning.main', mt: 1 }}>
479
+ {t('payment.checkout.vendor.failedMsg')}
480
+ </Typography>
481
+ )}
482
+ {vendorStatus.isAllCompleted && pageInfo?.success_message?.[locale] && (
483
+ <Typography sx={{ fontSize: 14, fontWeight: 600, color: 'text.primary', mt: 1 }}>
484
+ {pageInfo.success_message[locale]}
485
+ </Typography>
486
+ )}
487
+ </Box>
488
+ );
489
+ }
490
+
491
+ // ── Subscription Links ──
492
+
493
+ function SubscriptionLinks({
494
+ mode,
495
+ subscriptions,
496
+ subscriptionId = undefined,
497
+ payee,
498
+ prefix,
499
+ t,
500
+ }: {
501
+ mode: string;
502
+ subscriptions: any[];
503
+ subscriptionId?: string;
504
+ payee: string;
505
+ prefix: string;
506
+ t: (key: string, params?: any) => string;
507
+ }) {
508
+ if (!['subscription', 'setup'].includes(mode)) return null;
509
+
510
+ if (subscriptions.length > 1) {
511
+ return (
512
+ <Box
513
+ sx={{
514
+ width: '100%',
515
+ maxWidth: 420,
516
+ borderRadius: 3,
517
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.04)' : 'grey.50'),
518
+ border: '1px solid',
519
+ borderColor: 'divider',
520
+ overflow: 'hidden',
521
+ animation: `${fadeUp} 0.5s ease 0.35s both`,
522
+ }}>
523
+ {subscriptions.map((sub: any, idx: number) => (
524
+ <Box key={sub.id}>
525
+ {idx > 0 && <Divider />}
526
+ <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ px: 2.5, py: 1.5 }}>
527
+ <Typography sx={{ fontSize: 13, fontWeight: 500, color: 'text.secondary', flex: 1, minWidth: 0 }} noWrap>
528
+ {sub.description || sub.id}
529
+ </Typography>
530
+ <Box
531
+ sx={{
532
+ flex: 1,
533
+ borderBottom: '1px dashed',
534
+ borderColor: 'grey.300',
535
+ mx: 2,
536
+ minWidth: 20,
537
+ }}
538
+ />
539
+ <Link
540
+ href={joinURL(prefix, `/customer/subscription/${sub.id}`)}
541
+ underline="none"
542
+ sx={{
543
+ display: 'flex',
544
+ alignItems: 'center',
545
+ gap: 0.5,
546
+ fontSize: 13,
547
+ fontWeight: 600,
548
+ color: 'primary.main',
549
+ flexShrink: 0,
550
+ }}>
551
+ {t('payment.checkout.next.view')}
552
+ <OpenInNewIcon sx={{ fontSize: 14 }} />
553
+ </Link>
554
+ </Stack>
555
+ </Box>
556
+ ))}
557
+ </Box>
558
+ );
559
+ }
560
+
561
+ if (subscriptionId) {
562
+ return (
563
+ <Button
564
+ variant="contained"
565
+ href={joinURL(prefix, `/customer/subscription/${subscriptionId}`)}
566
+ endIcon={<OpenInNewIcon sx={{ fontSize: 16 }} />}
567
+ sx={{
568
+ width: '100%',
569
+ maxWidth: 320,
570
+ height: 52,
571
+ borderRadius: '16px',
572
+ textTransform: 'none',
573
+ fontWeight: 700,
574
+ fontSize: { xs: 16, md: 17 },
575
+ letterSpacing: '0.02em',
576
+ boxShadow: '0 8px 24px -4px rgba(59,130,246,0.25)',
577
+ '&:hover': {
578
+ boxShadow: '0 12px 28px -4px rgba(59,130,246,0.35)',
579
+ },
580
+ animation: `${fadeUp} 0.5s ease 0.35s both`,
581
+ }}>
582
+ {t('payment.checkout.next.subscription', { payee })}
583
+ </Button>
584
+ );
585
+ }
586
+
587
+ return null;
588
+ }
589
+
590
+ // ── Invoice Link ──
591
+
592
+ function InvoiceLink({
593
+ mode,
594
+ invoiceId = undefined,
595
+ prefix,
596
+ t,
597
+ }: {
598
+ mode: string;
599
+ invoiceId?: string;
600
+ prefix: string;
601
+ t: (key: string, params?: any) => string;
602
+ }) {
603
+ if (mode !== 'payment' || !invoiceId) return null;
604
+
605
+ return (
606
+ <Button
607
+ variant="contained"
608
+ href={joinURL(prefix, `/customer/invoice/${invoiceId}`)}
609
+ endIcon={<OpenInNewIcon sx={{ fontSize: 16 }} />}
610
+ sx={{
611
+ width: '100%',
612
+ maxWidth: 320,
613
+ height: 52,
614
+ borderRadius: '16px',
615
+ textTransform: 'none',
616
+ fontWeight: 700,
617
+ fontSize: { xs: 16, md: 17 },
618
+ letterSpacing: '0.02em',
619
+ boxShadow: '0 8px 24px -4px rgba(59,130,246,0.25)',
620
+ '&:hover': {
621
+ boxShadow: '0 12px 28px -4px rgba(59,130,246,0.35)',
622
+ },
623
+ animation: `${fadeUp} 0.5s ease 0.35s both`,
624
+ }}>
625
+ {t('payment.checkout.next.invoice')}
626
+ </Button>
627
+ );
628
+ }
629
+
630
+ // ── Main Component ──
631
+
632
+ export default function SuccessView({ submit, session }: SuccessViewProps) {
633
+ const { t, locale } = useLocaleContext();
634
+ const { vendorStatus } = submit;
635
+ const payee = getPayee(session);
636
+ const prefix = getPrefix();
637
+
638
+ const mode = session?.mode || 'payment';
639
+ const resultSession = submit.result?.checkoutSession as any;
640
+ const subscriptions = (session as any)?.subscriptions || [];
641
+ const subscriptionId = (session as any)?.subscription_id || resultSession?.subscription_id;
642
+ const invoiceId =
643
+ (session as any)?.invoice_id ||
644
+ (submit.result?.checkoutSession as any)?.invoice_id ||
645
+ (submit.result?.checkoutSession as any)?.payment_intent?.invoice_id;
646
+ const pageInfo = (session as any)?.metadata?.page_info;
647
+
648
+ const customMessage = getCustomMessage(session);
649
+ const submitType = (session as any)?.submit_type;
650
+ const messageKey =
651
+ submitType === 'donate' ? 'payment.checkout.completed.donate' : `payment.checkout.completed.${mode}`;
652
+ const headline = customMessage || t(messageKey);
653
+
654
+ const isVendorProcessing = vendorStatus && !vendorStatus.isAllCompleted && !vendorStatus.hasFailed;
655
+
656
+ // Confetti
657
+ const containerRef = useRef<HTMLDivElement>(null);
658
+ const showConfetti = !vendorStatus?.hasFailed;
659
+ useConfetti(containerRef, showConfetti);
660
+
661
+ return (
662
+ <Box
663
+ ref={containerRef}
664
+ sx={{
665
+ position: 'relative',
666
+ display: 'flex',
667
+ flexDirection: 'column',
668
+ justifyContent: 'center',
669
+ alignItems: 'center',
670
+ flex: 1,
671
+ minHeight: 400,
672
+ p: { xs: 3, md: 4 },
673
+ textAlign: 'center',
674
+ overflow: 'hidden',
675
+ }}>
676
+ <Stack spacing={3} alignItems="center" sx={{ width: '100%', maxWidth: 440 }}>
677
+ {/* Hero icon */}
678
+ {vendorStatus?.hasFailed ? (
679
+ <ErrorOutlineIcon
680
+ sx={{
681
+ fontSize: 64,
682
+ color: 'warning.main',
683
+ animation: `${scaleIn} 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both`,
684
+ }}
685
+ />
686
+ ) : (
687
+ <HeroSuccessIcon />
688
+ )}
689
+
690
+ {/* Title + subtitle */}
691
+ <Stack spacing={1} alignItems="center" sx={{ animation: `${fadeUp} 0.5s ease 0.2s both` }}>
692
+ <Typography
693
+ sx={{
694
+ fontSize: { xs: 28, md: 36 },
695
+ fontWeight: 800,
696
+ color: 'text.primary',
697
+ lineHeight: 1.2,
698
+ letterSpacing: '-0.02em',
699
+ }}>
700
+ {headline}
701
+ </Typography>
702
+ {payee && (
703
+ <Typography
704
+ sx={{
705
+ fontSize: { xs: 14, md: 16 },
706
+ color: 'text.secondary',
707
+ fontWeight: 500,
708
+ lineHeight: 1.6,
709
+ maxWidth: 360,
710
+ }}>
711
+ {t('payment.checkout.completed.tip', { payee })}
712
+ </Typography>
713
+ )}
714
+ </Stack>
715
+
716
+ {/* Payment receipt — trial/subscription summary */}
717
+ <PaymentReceipt session={resultSession || session} t={t} />
718
+
719
+ {/* Vendor progress panel */}
720
+ {vendorStatus && <VendorProgressPanel vendorStatus={vendorStatus} pageInfo={pageInfo} locale={locale} t={t} />}
721
+
722
+ {/* Vendor failed warning */}
723
+ {vendorStatus?.hasFailed && vendorStatus.error && (
724
+ <Alert severity="warning" sx={{ maxWidth: 420, borderRadius: 3, width: '100%' }}>
725
+ {vendorStatus.error}
726
+ </Alert>
727
+ )}
728
+
729
+ {/* Subscription links */}
730
+ {!isVendorProcessing && (
731
+ <SubscriptionLinks
732
+ mode={mode}
733
+ subscriptions={subscriptions}
734
+ subscriptionId={subscriptionId}
735
+ payee={payee}
736
+ prefix={prefix}
737
+ t={t}
738
+ />
739
+ )}
740
+
741
+ {/* Invoice link */}
742
+ {!isVendorProcessing && <InvoiceLink mode={mode} invoiceId={invoiceId} prefix={prefix} t={t} />}
743
+
744
+ {/* Back button */}
745
+ {!isVendorProcessing && (
746
+ <Button
747
+ variant="text"
748
+ startIcon={<ArrowBackIcon sx={{ fontSize: 16 }} />}
749
+ onClick={() => {
750
+ if (window.history.length > 1) {
751
+ window.history.back();
752
+ } else {
753
+ window.location.href = '/';
754
+ }
755
+ }}
756
+ sx={{
757
+ textTransform: 'none',
758
+ fontWeight: 600,
759
+ fontSize: 14,
760
+ color: 'text.secondary',
761
+ '&:hover': { bgcolor: 'action.hover' },
762
+ animation: `${fadeUp} 0.5s ease 0.45s both`,
763
+ }}>
764
+ {t('common.back')}
765
+ </Button>
766
+ )}
767
+ </Stack>
768
+ </Box>
769
+ );
770
+ }