@blocklet/payment-react 1.25.9 → 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 +10 -9
  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,18 @@
1
+ import type { ThemeOptions, SxProps } from '@mui/material';
2
+ import type { LiteralUnion } from 'type-fest';
3
+
4
+ export type CheckoutScenario = 'credit-topup' | 'subscription' | 'composite';
5
+
6
+ export type PaymentThemeOptions = ThemeOptions & {
7
+ sx?: SxProps;
8
+ };
9
+
10
+ export interface CheckoutV2Props {
11
+ id: string;
12
+ onPaid?: (result: any) => void;
13
+ onError?: (err: Error) => void;
14
+ goBack?: () => void;
15
+ theme?: 'default' | 'inherit' | PaymentThemeOptions;
16
+ mode?: LiteralUnion<'standalone' | 'inline', string>;
17
+ extraParams?: Record<string, string>;
18
+ }
@@ -0,0 +1,204 @@
1
+ import { fromUnitToToken } from '@ocap/util';
2
+ import type { TPaymentCurrency } from '@blocklet/payment-types';
3
+
4
+ // Interval key → locale key mapping
5
+ export const INTERVAL_LOCALE_KEY: Record<string, string> = {
6
+ day: 'common.daily',
7
+ week: 'common.weekly',
8
+ month: 'common.monthly',
9
+ year: 'common.yearly',
10
+ };
11
+
12
+ // Convert ISO2 country code to flag emoji (e.g., "us" → "🇺🇸")
13
+ export function countryCodeToFlag(code: string): string {
14
+ if (!code || code.length !== 2) return '';
15
+ return String.fromCodePoint(
16
+ ...code
17
+ .toUpperCase()
18
+ .split('')
19
+ .map((c) => 127397 + c.charCodeAt(0))
20
+ );
21
+ }
22
+
23
+ export function formatTokenAmount(unitAmount: string | number | bigint, currency: TPaymentCurrency | null): string {
24
+ if (!unitAmount || !currency) return '0';
25
+ try {
26
+ const tokenAmount = fromUnitToToken(String(unitAmount), currency.decimal || 0);
27
+ const num = Number(tokenAmount);
28
+ if (!Number.isFinite(num)) return '0';
29
+ const abs = Math.abs(num);
30
+ const precision = abs > 0 && abs < 0.01 ? 6 : 2;
31
+ const formatted = num.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision });
32
+ return formatted.replace(/\.?0+$/, '') || '0';
33
+ } catch {
34
+ return '0';
35
+ }
36
+ }
37
+
38
+ interface CurrencyOption {
39
+ currency_id: string;
40
+ unit_amount: string;
41
+ custom_unit_amount?: { preset?: string; presets?: string[] } | null;
42
+ }
43
+
44
+ interface PriceWithDynamic {
45
+ pricing_type?: string;
46
+ base_amount?: string;
47
+ unit_amount?: string;
48
+ base_currency?: string;
49
+ currency_id?: string;
50
+ currency_options?: CurrencyOption[];
51
+ price_currency_options?: CurrencyOption[];
52
+ }
53
+
54
+ // Get the unit_amount for a specific currency, checking currency_options first
55
+ export function getUnitAmountForCurrency(
56
+ price: PriceWithDynamic | null | undefined,
57
+ currency: TPaymentCurrency | null
58
+ ): string {
59
+ if (!price || !currency) return '0';
60
+ const options = price.price_currency_options || price.currency_options || [];
61
+ const option = options.find((x) => x.currency_id === currency.id);
62
+ if (option) {
63
+ if (option.custom_unit_amount) {
64
+ return option.custom_unit_amount.preset || option.custom_unit_amount.presets?.[0] || option.unit_amount;
65
+ }
66
+ return option.unit_amount;
67
+ }
68
+ if (price.currency_id === currency.id) {
69
+ return price.unit_amount || '0';
70
+ }
71
+ return '0';
72
+ }
73
+
74
+ // Format price with exchange rate for dynamic pricing
75
+ export function formatDynamicUnitPrice(
76
+ price: PriceWithDynamic | null | undefined,
77
+ currency: TPaymentCurrency | null,
78
+ exchangeRate: string | null
79
+ ): string | null {
80
+ if (!price || !currency) return null;
81
+ const isDynamic = price.pricing_type === 'dynamic';
82
+ if (isDynamic && exchangeRate && price.base_amount) {
83
+ const rate = Number(exchangeRate);
84
+ if (rate > 0 && Number.isFinite(rate)) {
85
+ const baseUsd = Number(price.base_amount);
86
+ if (baseUsd > 0 && Number.isFinite(baseUsd)) {
87
+ const tokenAmount = baseUsd / rate;
88
+ const abs = Math.abs(tokenAmount);
89
+ const precision = abs > 0 && abs < 0.01 ? 6 : 2;
90
+ return (
91
+ tokenAmount
92
+ .toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
93
+ .replace(/\.?0+$/, '') || '0'
94
+ );
95
+ }
96
+ }
97
+ }
98
+ // Fiat/Stripe fallback: use base_amount when no exchange rate
99
+ // (unit_amount may be in crypto denomination, not fiat cents)
100
+ if (!exchangeRate && price.base_amount != null) {
101
+ const baseUsd = Number(price.base_amount);
102
+ if (baseUsd >= 0 && Number.isFinite(baseUsd)) {
103
+ return baseUsd.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
104
+ }
105
+ }
106
+ // Fallback: look up unit_amount from currency_options for the selected currency
107
+ return formatTokenAmount(getUnitAmountForCurrency(price, currency), currency);
108
+ }
109
+
110
+ // Safe translation with fallback for missing keys (returns fallback if t() returns the raw key path)
111
+ export function tSafe(t: (key: string, params?: any) => string, key: string, fallback: string): string {
112
+ const v = t(key);
113
+ return v && !v.includes('.') ? v : fallback;
114
+ }
115
+
116
+ // Shared white tooltip style for checkout-v2
117
+ export const whiteTooltipSx = {
118
+ '& .MuiTooltip-tooltip': {
119
+ bgcolor: 'background.paper',
120
+ color: 'text.primary',
121
+ border: '1px solid',
122
+ borderColor: 'divider',
123
+ borderRadius: '10px',
124
+ boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
125
+ p: 1.5,
126
+ maxWidth: 280,
127
+ fontSize: 12,
128
+ },
129
+ '& .MuiTooltip-arrow': {
130
+ color: 'background.paper',
131
+ '&::before': { border: '1px solid', borderColor: 'divider' },
132
+ },
133
+ };
134
+
135
+ // Helper: format trial text
136
+ export function formatTrialText(t: (key: string) => string, days: number, interval: string): string {
137
+ const intervalLabel = t(`common.${interval || 'day'}`);
138
+ return `${days} ${intervalLabel}${days > 1 ? 's' : ''} free`;
139
+ }
140
+
141
+ // ── Item meta: badge / title / subtitle for the header area ──
142
+
143
+ type TFn = (key: string, params?: Record<string, any>) => string;
144
+
145
+ export type ItemTypeBadge = 'subscription' | 'topup' | 'oneTime';
146
+
147
+ interface ItemMeta {
148
+ badge: ItemTypeBadge;
149
+ badgeLabel: string;
150
+ title: string;
151
+ subtitle: string;
152
+ }
153
+
154
+ /**
155
+ * Derive structured badge / title / subtitle for a checkout session header.
156
+ * Works for the "primary product" header above the item list.
157
+ */
158
+ export function getSessionHeaderMeta(t: TFn, session: any, product: any, items: any[]): ItemMeta {
159
+ const mode = session?.mode || 'payment';
160
+ const isSubscription = ['subscription', 'setup'].includes(mode);
161
+ const isCreditTopup =
162
+ items.length === 1 &&
163
+ mode === 'payment' &&
164
+ items[0]?.price?.product?.type === 'credit' &&
165
+ items[0]?.price?.metadata?.credit_config;
166
+
167
+ // Badge
168
+ let badge: ItemTypeBadge;
169
+ if (isCreditTopup) {
170
+ badge = 'topup';
171
+ } else if (isSubscription) {
172
+ badge = 'subscription';
173
+ } else {
174
+ badge = 'oneTime';
175
+ }
176
+ const badgeLabel = t(`payment.checkout.typeBadge.${badge}`);
177
+
178
+ // Title: action + product name based on type
179
+ const productName = product?.name || items[0]?.price?.product?.name || '';
180
+ let title = productName;
181
+ if (productName) {
182
+ if (isSubscription) {
183
+ title = t('payment.checkout.headerTitle.subscribe', { name: productName });
184
+ } else if (!isCreditTopup) {
185
+ title = t('payment.checkout.headerTitle.purchase', { name: productName });
186
+ }
187
+ }
188
+
189
+ // Subtitle: system-generated, not product.description
190
+ let subtitle = '';
191
+ if (isCreditTopup) {
192
+ subtitle = t('payment.checkout.subtitle.creditsTopup');
193
+ } else if (isSubscription) {
194
+ // Use plan name if available; skip generic interval labels like "daily subscription"
195
+ const planName = product?.metadata?.plan_name || product?.metadata?.plan_display_name;
196
+ if (planName) {
197
+ subtitle = planName;
198
+ }
199
+ } else {
200
+ subtitle = t('payment.checkout.subtitle.oneTime');
201
+ }
202
+
203
+ return { badge, badgeLabel, title, subtitle };
204
+ }
@@ -0,0 +1,30 @@
1
+ import type { TCheckoutSessionExpanded, TLineItemExpanded } from '@blocklet/payment-types';
2
+ import type { CheckoutScenario } from '../types';
3
+
4
+ export function detectScenario(
5
+ session: TCheckoutSessionExpanded | null | undefined,
6
+ items: TLineItemExpanded[]
7
+ ): CheckoutScenario {
8
+ if (!session) return 'composite';
9
+
10
+ const isSingleItem = items.length === 1;
11
+ const isPaymentMode = session.mode === 'payment';
12
+ const isSubscriptionMode = ['subscription', 'setup'].includes(session.mode || '');
13
+
14
+ // Credit: single item + payment mode + product.type=credit + metadata.credit_config
15
+ if (isSingleItem && isPaymentMode) {
16
+ const item = items[0];
17
+ const product = (item as any).price?.product;
18
+ if (product?.type === 'credit' && (item as any).price?.metadata?.credit_config) {
19
+ return 'credit-topup';
20
+ }
21
+ }
22
+
23
+ // Subscription: single item + subscription/setup + no cross-sell
24
+ if (isSingleItem && isSubscriptionMode && !items.some((i: any) => i.cross_sell)) {
25
+ return 'subscription';
26
+ }
27
+
28
+ // Default: composite
29
+ return 'composite';
30
+ }
@@ -0,0 +1,293 @@
1
+ import { Box, Button, Stack, Typography } from '@mui/material';
2
+ import { alpha, useTheme } from '@mui/material/styles';
3
+ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
4
+ import Header from '@blocklet/ui-react/lib/Header';
5
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
6
+
7
+ interface ErrorViewProps {
8
+ error: string;
9
+ errorCode?: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
10
+ mode?: string;
11
+ }
12
+
13
+ // Geometric decoration — organic blobs with embedded wireframe mesh
14
+ function GeometricDecoration() {
15
+ const theme = useTheme();
16
+ const gridColor = alpha(theme.palette.primary.main, 0.06);
17
+
18
+ return (
19
+ <Box
20
+ sx={{
21
+ position: 'relative',
22
+ width: { xs: 200, md: 260 },
23
+ height: { xs: 200, md: 260 },
24
+ mb: { xs: 4, md: 6 },
25
+ }}>
26
+ {/* Blob layer 1 */}
27
+ <Box
28
+ sx={{
29
+ position: 'absolute',
30
+ inset: 0,
31
+ borderRadius: '38% 62% 63% 37% / 41% 44% 56% 59%',
32
+ background: (t) =>
33
+ `linear-gradient(45deg, ${alpha(t.palette.primary.main, 0.1)}, ${alpha(t.palette.primary.main, 0.03)})`,
34
+ filter: 'blur(1px)',
35
+ animation: 'spin 20s linear infinite',
36
+ '@keyframes spin': { from: { transform: 'rotate(0deg)' }, to: { transform: 'rotate(360deg)' } },
37
+ }}
38
+ />
39
+ {/* Blob layer 2 */}
40
+ <Box
41
+ sx={{
42
+ position: 'absolute',
43
+ inset: 0,
44
+ borderRadius: '38% 62% 63% 37% / 41% 44% 56% 59%',
45
+ background: (t) =>
46
+ `linear-gradient(135deg, ${alpha(t.palette.primary.main, 0.07)}, ${alpha(t.palette.primary.main, 0.01)})`,
47
+ filter: 'blur(1px)',
48
+ transform: 'scale(0.88)',
49
+ opacity: 0.6,
50
+ animation: 'spinReverse 15s linear infinite',
51
+ '@keyframes spinReverse': {
52
+ from: { transform: 'scale(0.88) rotate(0deg)' },
53
+ to: { transform: 'scale(0.88) rotate(-360deg)' },
54
+ },
55
+ }}
56
+ />
57
+ {/* Wireframe mesh overlay — clipped to blob shape */}
58
+ <Box
59
+ sx={{
60
+ position: 'absolute',
61
+ inset: 0,
62
+ borderRadius: '38% 62% 63% 37% / 41% 44% 56% 59%',
63
+ overflow: 'hidden',
64
+ animation: 'spin 20s linear infinite',
65
+ }}>
66
+ <svg
67
+ width="100%"
68
+ height="100%"
69
+ viewBox="0 0 260 260"
70
+ fill="none"
71
+ xmlns="http://www.w3.org/2000/svg"
72
+ style={{ position: 'absolute', inset: 0 }}>
73
+ {/* Horizontal lines */}
74
+ {[52, 87, 122, 157, 192].map((y) => (
75
+ <line key={`h${y}`} x1="30" y1={y} x2="230" y2={y} stroke={gridColor} strokeWidth="0.5" />
76
+ ))}
77
+ {/* Vertical lines */}
78
+ {[52, 87, 122, 157, 192].map((x) => (
79
+ <line key={`v${x}`} x1={x} y1="30" x2={x} y2="230" stroke={gridColor} strokeWidth="0.5" />
80
+ ))}
81
+ {/* Diagonal accents — sparse, adds depth */}
82
+ <line x1="52" y1="52" x2="192" y2="192" stroke={gridColor} strokeWidth="0.5" />
83
+ <line x1="192" y1="52" x2="52" y2="192" stroke={gridColor} strokeWidth="0.5" />
84
+ {/* Concentric circles — digital radar feel */}
85
+ <circle cx="130" cy="130" r="40" stroke={gridColor} strokeWidth="0.5" />
86
+ <circle cx="130" cy="130" r="75" stroke={gridColor} strokeWidth="0.5" />
87
+ {/* Dot nodes at key intersections */}
88
+ {[
89
+ [122, 87],
90
+ [157, 87],
91
+ [87, 122],
92
+ [122, 122],
93
+ [157, 122],
94
+ [192, 122],
95
+ [87, 157],
96
+ [122, 157],
97
+ [157, 157],
98
+ [122, 192],
99
+ [157, 192],
100
+ ].map(([cx, cy]) => (
101
+ <circle key={`d${cx}-${cy}`} cx={cx} cy={cy} r="1.5" fill={gridColor} />
102
+ ))}
103
+ </svg>
104
+ </Box>
105
+ </Box>
106
+ );
107
+ }
108
+
109
+ function getErrorConfig(
110
+ errorCode: ErrorViewProps['errorCode'],
111
+ error: string,
112
+ t: (key: string) => string
113
+ ): { title: string; description: string; color: string } {
114
+ if (errorCode === 'SESSION_EXPIRED') {
115
+ return {
116
+ title: t('payment.checkout.expired.title'),
117
+ description: t('payment.checkout.expired.description'),
118
+ color: '#f59e0b',
119
+ };
120
+ }
121
+
122
+ if (errorCode === 'EMPTY_LINE_ITEMS') {
123
+ return {
124
+ title: t('payment.checkout.emptyItems.title'),
125
+ description: t('payment.checkout.emptyItems.description'),
126
+ color: '#94a3b8',
127
+ };
128
+ }
129
+
130
+ return {
131
+ title: t('payment.checkout.error.title'),
132
+ description: error,
133
+ color: '#ef4444',
134
+ };
135
+ }
136
+
137
+ function ErrorContent({ error, errorCode = undefined }: { error: string; errorCode?: ErrorViewProps['errorCode'] }) {
138
+ const { t } = useLocaleContext();
139
+ const theme = useTheme();
140
+ const { title, description } = getErrorConfig(errorCode, error, t);
141
+ const primaryColor = theme.palette.primary.main;
142
+
143
+ const appUrl = typeof window !== 'undefined' ? (window as any).blocklet?.appUrl : '/';
144
+
145
+ return (
146
+ <Stack alignItems="center" justifyContent="center" sx={{ flex: 1, textAlign: 'center', px: 3 }}>
147
+ {/* Geometric decoration */}
148
+ <GeometricDecoration />
149
+
150
+ {/* Title */}
151
+ <Typography
152
+ component="h1"
153
+ sx={{
154
+ fontWeight: 800,
155
+ fontSize: { xs: 36, md: 52 },
156
+ lineHeight: 1.05,
157
+ letterSpacing: '-0.03em',
158
+ color: 'text.primary',
159
+ mb: 2,
160
+ }}>
161
+ {title}
162
+ </Typography>
163
+
164
+ {/* Description */}
165
+ <Typography
166
+ sx={{
167
+ color: 'text.secondary',
168
+ fontSize: { xs: 15, md: 17 },
169
+ fontWeight: 300,
170
+ lineHeight: 1.7,
171
+ maxWidth: 420,
172
+ letterSpacing: '0.01em',
173
+ }}>
174
+ {description}
175
+ </Typography>
176
+
177
+ {/* CTA button — large pill with primary bg */}
178
+ {appUrl && (
179
+ <Button
180
+ href={appUrl}
181
+ variant="contained"
182
+ disableElevation
183
+ startIcon={<ArrowBackIcon sx={{ fontSize: '20px !important' }} />}
184
+ sx={{
185
+ mt: 5,
186
+ minWidth: 240,
187
+ height: 56,
188
+ px: 6,
189
+ borderRadius: '9999px',
190
+ textTransform: 'none',
191
+ fontWeight: 600,
192
+ fontSize: 16,
193
+ letterSpacing: '0.02em',
194
+ boxShadow: `0 8px 32px -4px ${alpha(primaryColor, 0.3)}`,
195
+ '&:hover': {
196
+ boxShadow: `0 12px 40px -4px ${alpha(primaryColor, 0.4)}`,
197
+ transform: 'translateY(-1px)',
198
+ },
199
+ transition: 'all 0.2s ease',
200
+ }}>
201
+ {t('common.back')}
202
+ </Button>
203
+ )}
204
+ </Stack>
205
+ );
206
+ }
207
+
208
+ export default function ErrorView({ error, errorCode = undefined, mode = 'inline' }: ErrorViewProps) {
209
+ const theme = useTheme();
210
+ const primaryColor = theme.palette.primary.main;
211
+ const isFullScreen = mode === 'standalone';
212
+
213
+ // Mesh gradient background (matching reference Version 2)
214
+ const meshBg = {
215
+ bgcolor: (t: any) => (t.palette.mode === 'dark' ? 'background.default' : '#f8faff'),
216
+ backgroundImage: (t: any) =>
217
+ t.palette.mode === 'dark'
218
+ ? 'none'
219
+ : `radial-gradient(at 0% 0%, ${alpha(primaryColor, 0.06)} 0px, transparent 50%),
220
+ radial-gradient(at 100% 0%, ${alpha(primaryColor, 0.04)} 0px, transparent 50%),
221
+ radial-gradient(at 100% 100%, ${alpha(primaryColor, 0.06)} 0px, transparent 50%),
222
+ radial-gradient(at 0% 100%, rgba(203,213,225,0.3) 0px, transparent 50%)`,
223
+ };
224
+
225
+ if (!isFullScreen) {
226
+ // Inline (card) mode
227
+ return (
228
+ <Box
229
+ sx={{
230
+ display: 'flex',
231
+ width: '100%',
232
+ minHeight: { xs: 400, md: 520 },
233
+ maxWidth: 1120,
234
+ mx: 'auto',
235
+ borderRadius: '16px',
236
+ overflow: 'hidden',
237
+ boxShadow: 1,
238
+ border: 1,
239
+ borderColor: 'divider',
240
+ ...meshBg,
241
+ }}>
242
+ <Box
243
+ sx={{
244
+ flex: 1,
245
+ p: { xs: 4, md: 6 },
246
+ display: 'flex',
247
+ flexDirection: 'column',
248
+ justifyContent: 'center',
249
+ alignItems: 'center',
250
+ }}>
251
+ <ErrorContent error={error} errorCode={errorCode} />
252
+ </Box>
253
+ </Box>
254
+ );
255
+ }
256
+
257
+ // Standalone (full-screen) mode
258
+ return (
259
+ <Box
260
+ sx={{
261
+ width: '100%',
262
+ height: '100vh',
263
+ minHeight: '100vh',
264
+ display: 'flex',
265
+ flexDirection: 'column',
266
+ position: 'relative',
267
+ overflow: 'hidden',
268
+ ...meshBg,
269
+ }}>
270
+ {/* Header */}
271
+ <Header
272
+ sx={{
273
+ position: 'absolute',
274
+ top: 20,
275
+ left: 0,
276
+ right: 0,
277
+ zIndex: 10,
278
+ background: 'transparent',
279
+ '& .header-container': { height: 'auto' },
280
+ }}
281
+ hideNavMenu
282
+ brand={null}
283
+ description={null}
284
+ addons={(buildIns: any) =>
285
+ buildIns.filter((addon: any) => ['locale-selector', 'theme-mode-toggle', 'session-user'].includes(addon.key))
286
+ }
287
+ />
288
+
289
+ {/* Centered error content */}
290
+ <ErrorContent error={error} errorCode={errorCode} />
291
+ </Box>
292
+ );
293
+ }