@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.
- package/es/checkout-v2/checkout-v2.d.ts +2 -0
- package/es/checkout-v2/checkout-v2.js +121 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/es/checkout-v2/components/dialogs/checkout-dialogs.js +106 -0
- package/es/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/es/checkout-v2/components/left/billing-toggle.js +118 -0
- package/es/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/es/checkout-v2/components/left/cross-sell-card.js +167 -0
- package/es/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/es/checkout-v2/components/left/product-item-card.js +571 -0
- package/es/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/es/checkout-v2/components/left/promotion-input.js +178 -0
- package/es/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/es/checkout-v2/components/left/staking-breakdown.js +48 -0
- package/es/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/es/checkout-v2/components/left/trial-info.js +48 -0
- package/es/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/es/checkout-v2/components/right/currency-grid.js +48 -0
- package/es/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/es/checkout-v2/components/right/customer-info-card.js +156 -0
- package/es/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/es/checkout-v2/components/right/status-feedback.js +17 -0
- package/es/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/es/checkout-v2/components/right/submit-button.js +29 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/es/checkout-v2/components/right/subscription-disclaimer.js +8 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/es/checkout-v2/components/shared/exchange-rate-footer.js +182 -0
- package/es/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/es/checkout-v2/components/shared/scenario-badge.js +47 -0
- package/es/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/es/checkout-v2/components/shared/total-display.js +84 -0
- package/es/checkout-v2/index.d.ts +2 -0
- package/es/checkout-v2/index.js +1 -0
- package/es/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/es/checkout-v2/layouts/checkout-layout.js +226 -0
- package/es/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/composite-panel.js +423 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/es/checkout-v2/panels/left/credit-topup-panel.js +615 -0
- package/es/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/es/checkout-v2/panels/left/scenario-router.js +19 -0
- package/es/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/es/checkout-v2/panels/right/payment-panel.js +644 -0
- package/es/checkout-v2/types.d.ts +15 -0
- package/es/checkout-v2/types.js +0 -0
- package/es/checkout-v2/utils/format.d.ts +59 -0
- package/es/checkout-v2/utils/format.js +125 -0
- package/es/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/es/checkout-v2/utils/scenario-detector.js +17 -0
- package/es/checkout-v2/views/error-view.d.ts +7 -0
- package/es/checkout-v2/views/error-view.js +269 -0
- package/es/checkout-v2/views/loading-view.d.ts +5 -0
- package/es/checkout-v2/views/loading-view.js +158 -0
- package/es/checkout-v2/views/success-view.d.ts +29 -0
- package/es/checkout-v2/views/success-view.js +614 -0
- package/es/components/phone-field.d.ts +14 -0
- package/es/components/phone-field.js +96 -0
- package/es/index.d.ts +3 -1
- package/es/index.js +3 -1
- package/es/locales/en.js +45 -6
- package/es/locales/zh.js +45 -6
- package/es/payment/form/index.js +10 -1
- package/lib/checkout-v2/checkout-v2.d.ts +2 -0
- package/lib/checkout-v2/checkout-v2.js +151 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.d.ts +1 -0
- package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +131 -0
- package/lib/checkout-v2/components/left/billing-toggle.d.ts +6 -0
- package/lib/checkout-v2/components/left/billing-toggle.js +126 -0
- package/lib/checkout-v2/components/left/cross-sell-card.d.ts +10 -0
- package/lib/checkout-v2/components/left/cross-sell-card.js +257 -0
- package/lib/checkout-v2/components/left/product-item-card.d.ts +26 -0
- package/lib/checkout-v2/components/left/product-item-card.js +738 -0
- package/lib/checkout-v2/components/left/promotion-input.d.ts +19 -0
- package/lib/checkout-v2/components/left/promotion-input.js +220 -0
- package/lib/checkout-v2/components/left/staking-breakdown.d.ts +9 -0
- package/lib/checkout-v2/components/left/staking-breakdown.js +96 -0
- package/lib/checkout-v2/components/left/trial-info.d.ts +13 -0
- package/lib/checkout-v2/components/left/trial-info.js +82 -0
- package/lib/checkout-v2/components/right/currency-grid.d.ts +8 -0
- package/lib/checkout-v2/components/right/currency-grid.js +96 -0
- package/lib/checkout-v2/components/right/customer-info-card.d.ts +17 -0
- package/lib/checkout-v2/components/right/customer-info-card.js +246 -0
- package/lib/checkout-v2/components/right/status-feedback.d.ts +7 -0
- package/lib/checkout-v2/components/right/status-feedback.js +30 -0
- package/lib/checkout-v2/components/right/submit-button.d.ts +10 -0
- package/lib/checkout-v2/components/right/submit-button.js +35 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.d.ts +11 -0
- package/lib/checkout-v2/components/right/subscription-disclaimer.js +33 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.d.ts +23 -0
- package/lib/checkout-v2/components/shared/exchange-rate-footer.js +282 -0
- package/lib/checkout-v2/components/shared/scenario-badge.d.ts +6 -0
- package/lib/checkout-v2/components/shared/scenario-badge.js +57 -0
- package/lib/checkout-v2/components/shared/total-display.d.ts +7 -0
- package/lib/checkout-v2/components/shared/total-display.js +154 -0
- package/lib/checkout-v2/index.d.ts +2 -0
- package/lib/checkout-v2/index.js +13 -0
- package/lib/checkout-v2/layouts/checkout-layout.d.ts +7 -0
- package/lib/checkout-v2/layouts/checkout-layout.js +308 -0
- package/lib/checkout-v2/panels/left/composite-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/composite-panel.js +515 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/left/credit-topup-panel.js +799 -0
- package/lib/checkout-v2/panels/left/scenario-router.d.ts +1 -0
- package/lib/checkout-v2/panels/left/scenario-router.js +29 -0
- package/lib/checkout-v2/panels/right/payment-panel.d.ts +1 -0
- package/lib/checkout-v2/panels/right/payment-panel.js +906 -0
- package/lib/checkout-v2/types.d.ts +15 -0
- package/lib/checkout-v2/types.js +1 -0
- package/lib/checkout-v2/utils/format.d.ts +59 -0
- package/lib/checkout-v2/utils/format.js +158 -0
- package/lib/checkout-v2/utils/scenario-detector.d.ts +3 -0
- package/lib/checkout-v2/utils/scenario-detector.js +23 -0
- package/lib/checkout-v2/views/error-view.d.ts +7 -0
- package/lib/checkout-v2/views/error-view.js +321 -0
- package/lib/checkout-v2/views/loading-view.d.ts +5 -0
- package/lib/checkout-v2/views/loading-view.js +168 -0
- package/lib/checkout-v2/views/success-view.d.ts +29 -0
- package/lib/checkout-v2/views/success-view.js +735 -0
- package/lib/components/phone-field.d.ts +14 -0
- package/lib/components/phone-field.js +130 -0
- package/lib/index.d.ts +3 -1
- package/lib/index.js +8 -0
- package/lib/locales/en.js +45 -6
- package/lib/locales/zh.js +45 -6
- package/lib/payment/form/index.js +10 -1
- package/package.json +10 -9
- package/src/checkout-v2/checkout-v2.tsx +155 -0
- package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +134 -0
- package/src/checkout-v2/components/left/billing-toggle.tsx +122 -0
- package/src/checkout-v2/components/left/cross-sell-card.tsx +170 -0
- package/src/checkout-v2/components/left/product-item-card.tsx +634 -0
- package/src/checkout-v2/components/left/promotion-input.tsx +207 -0
- package/src/checkout-v2/components/left/staking-breakdown.tsx +57 -0
- package/src/checkout-v2/components/left/trial-info.tsx +63 -0
- package/src/checkout-v2/components/right/currency-grid.tsx +59 -0
- package/src/checkout-v2/components/right/customer-info-card.tsx +214 -0
- package/src/checkout-v2/components/right/status-feedback.tsx +35 -0
- package/src/checkout-v2/components/right/submit-button.tsx +37 -0
- package/src/checkout-v2/components/right/subscription-disclaimer.tsx +27 -0
- package/src/checkout-v2/components/shared/exchange-rate-footer.tsx +221 -0
- package/src/checkout-v2/components/shared/scenario-badge.tsx +51 -0
- package/src/checkout-v2/components/shared/total-display.tsx +112 -0
- package/src/checkout-v2/index.ts +2 -0
- package/src/checkout-v2/layouts/checkout-layout.tsx +232 -0
- package/src/checkout-v2/panels/left/composite-panel.tsx +465 -0
- package/src/checkout-v2/panels/left/credit-topup-panel.tsx +681 -0
- package/src/checkout-v2/panels/left/scenario-router.tsx +22 -0
- package/src/checkout-v2/panels/right/payment-panel.tsx +703 -0
- package/src/checkout-v2/types.ts +18 -0
- package/src/checkout-v2/utils/format.ts +204 -0
- package/src/checkout-v2/utils/scenario-detector.ts +30 -0
- package/src/checkout-v2/views/error-view.tsx +293 -0
- package/src/checkout-v2/views/loading-view.tsx +162 -0
- package/src/checkout-v2/views/success-view.tsx +770 -0
- package/src/components/phone-field.tsx +119 -0
- package/src/index.ts +3 -0
- package/src/locales/en.tsx +45 -4
- package/src/locales/zh.tsx +43 -4
- 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
|
+
}
|