@blocklet/payment-react 1.25.10 → 1.26.1

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 +611 -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 +795 -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 +642 -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 +677 -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 +205 -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,703 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
3
+ import CreditCardIcon from '@mui/icons-material/CreditCard';
4
+ import CurrencyBitcoinIcon from '@mui/icons-material/CurrencyBitcoin';
5
+ import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
6
+ import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
7
+ import LocalOfferOutlinedIcon from '@mui/icons-material/LocalOfferOutlined';
8
+ import CloseIcon from '@mui/icons-material/Close';
9
+ import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
10
+ import {
11
+ Avatar,
12
+ Box,
13
+ Button,
14
+ CircularProgress,
15
+ Collapse,
16
+ Divider,
17
+ Drawer,
18
+ MenuItem,
19
+ Select,
20
+ Skeleton,
21
+ Stack,
22
+ ToggleButton,
23
+ ToggleButtonGroup,
24
+ Tooltip,
25
+ Typography,
26
+ } from '@mui/material';
27
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
28
+ import {
29
+ useCheckoutStatus,
30
+ usePaymentMethodFeature,
31
+ useCustomerFormFeature,
32
+ useSubmitFeature,
33
+ usePricingFeature,
34
+ useSessionContext,
35
+ useLineItems,
36
+ usePromotion,
37
+ useExchangeRate,
38
+ } from '@blocklet/payment-react-headless';
39
+ import { joinURL } from 'ufo';
40
+ import { usePaymentContext } from '../../../contexts/payment';
41
+ import { useMobile } from '../../../hooks/mobile';
42
+ import { getPrefix } from '../../../libs/util';
43
+ import OverdueInvoicePayment from '../../../components/over-due-invoice-payment';
44
+
45
+ import { tSafe, whiteTooltipSx } from '../../utils/format';
46
+ import CustomerInfoCard from '../../components/right/customer-info-card';
47
+ import SubscriptionDisclaimer from '../../components/right/subscription-disclaimer';
48
+ import StatusFeedback from '../../components/right/status-feedback';
49
+ import PromotionInput from '../../components/left/promotion-input';
50
+
51
+ export default function PaymentPanel() {
52
+ const { t, locale } = useLocaleContext();
53
+ const { session, sessionData, subscription, refresh } = useSessionContext();
54
+ const { isDonation } = useCheckoutStatus();
55
+ const paymentMethod = usePaymentMethodFeature();
56
+ const form = useCustomerFormFeature();
57
+ const submit = useSubmitFeature();
58
+ const pricing = usePricingFeature();
59
+ const { session: didSession, connect, prefix: paymentKitPrefix } = usePaymentContext();
60
+ const { inventoryOk } = useLineItems();
61
+ const promotion = usePromotion();
62
+ const rate = useExchangeRate();
63
+ const { isMobile } = useMobile();
64
+
65
+ const isAmountLoading = paymentMethod.switching || (rate.hasDynamicPricing && rate.status === 'loading');
66
+
67
+ const { currency, types, isStripe, isCrypto } = paymentMethod;
68
+ const mode = session?.mode || 'payment';
69
+ const discounts = (session as any)?.discounts || [];
70
+ const isLoggedIn = !!didSession?.user;
71
+ const actionLabel = isDonation ? t('payment.checkout.donate') : t(`payment.checkout.${mode}`);
72
+ const buttonLabel = isLoggedIn ? actionLabel : t('payment.checkout.connect', { action: actionLabel });
73
+
74
+ const [customerLimited, setCustomerLimited] = useState(false);
75
+ const [mobileDetailsOpen, setMobileDetailsOpen] = useState(false);
76
+ const [promoDrawerOpen, setPromoDrawerOpen] = useState(false);
77
+
78
+ // Detect CUSTOMER_LIMITED error from submit hook
79
+ useEffect(() => {
80
+ if (submit.status === 'failed' && (submit.context as any)?.code === 'CUSTOMER_LIMITED') {
81
+ setCustomerLimited(true);
82
+ }
83
+ }, [submit.status, submit.context]);
84
+
85
+ const canSubmit = submit.status === 'idle' && session?.status === 'open' && inventoryOk;
86
+ const isProcessing = ['submitting', 'waiting_did'].includes(submit.status);
87
+
88
+ // Pre-login action handler (mirrors V1 form/index.tsx onAction)
89
+ const handleAction = useCallback(() => {
90
+ if (!canSubmit) return;
91
+
92
+ // Lock config to prevent quantity/currency/method changes during submit flow
93
+ submit.lock();
94
+
95
+ if (isLoggedIn || isDonation) {
96
+ // Already logged in or donation mode — submit directly
97
+ submit.execute();
98
+ return;
99
+ }
100
+
101
+ // Not logged in — initiate DID Connect login first
102
+ didSession?.login?.(() => {
103
+ // After login: refresh session data + re-fetch customer info → then submit
104
+ Promise.all([refresh(true), form.refetchCustomer()])
105
+ .then(() => submit.execute())
106
+ .catch((err) => {
107
+ console.error('Post-login refresh failed:', err);
108
+ });
109
+ });
110
+ }, [canSubmit, isLoggedIn, isDonation, didSession, refresh, form, submit]);
111
+
112
+ // When user logs in from top-right (not via handleAction button), refetch customer to fill form
113
+ // Mirrors V1 form/index.tsx useEffect on session?.user
114
+ useEffect(() => {
115
+ if (didSession?.user && !submit.status.startsWith('submitting')) {
116
+ form.refetchCustomer();
117
+ }
118
+ }, [didSession?.user]); // eslint-disable-line react-hooks/exhaustive-deps
119
+
120
+ // Group crypto currencies by network (method)
121
+ const cryptoType = types.find((tp) => tp.type === 'crypto');
122
+ const cryptoMethods = useMemo(
123
+ () => paymentMethod.available.filter((m) => m.type !== 'stripe'),
124
+ [paymentMethod.available]
125
+ );
126
+ const networks = useMemo(() => {
127
+ if (!cryptoMethods.length) return [];
128
+ return cryptoMethods.map((m) => ({
129
+ id: m.id,
130
+ name: m.name,
131
+ logo: m.logo || '',
132
+ currencies: m.payment_currencies || [],
133
+ }));
134
+ }, [cryptoMethods]);
135
+
136
+ // Current network for dropdown
137
+ const currentMethodId = paymentMethod.current?.id || '';
138
+
139
+ // Enter key submit handler
140
+ useEffect(() => {
141
+ const handleKeyDown = (e: KeyboardEvent) => {
142
+ if (e.key === 'Enter' && canSubmit && submit.status === 'idle') {
143
+ const tag = (e.target as HTMLElement)?.tagName?.toLowerCase();
144
+ if (tag === 'textarea') return;
145
+ handleAction();
146
+ }
147
+ };
148
+ document.addEventListener('keydown', handleKeyDown);
149
+ return () => document.removeEventListener('keydown', handleKeyDown);
150
+ }, [canSubmit, submit.status, handleAction]);
151
+
152
+ // DID Connect
153
+ const didConnectOpenedRef = useRef(false);
154
+ useEffect(() => {
155
+ if (submit.status !== 'waiting_did') {
156
+ didConnectOpenedRef.current = false;
157
+ return;
158
+ }
159
+ const ctx = submit.context;
160
+ if (ctx?.type !== 'did_connect' || !connect) return;
161
+ if (didConnectOpenedRef.current) return;
162
+ didConnectOpenedRef.current = true;
163
+
164
+ const didPrefix = `${paymentKitPrefix}/api/did`.replace(/([^:])\/\//g, '$1/');
165
+ connect.open({
166
+ locale: locale as any,
167
+ action: ctx.action,
168
+ prefix: didPrefix,
169
+ saveConnect: false,
170
+ extraParams: ctx.extraParams,
171
+ onSuccess: () => {
172
+ connect.close();
173
+ },
174
+ onClose: () => {
175
+ connect.close();
176
+ submit.reset();
177
+ },
178
+ onError: (err: any) => {
179
+ console.error('DID Connect error:', err);
180
+ submit.reset();
181
+ },
182
+ messages: {
183
+ title: t('payment.checkout.connectModal.title', { action: buttonLabel }),
184
+ scan: t('payment.checkout.connectModal.scan'),
185
+ confirm: t('payment.checkout.connectModal.confirm'),
186
+ } as any,
187
+ });
188
+ }, [submit.status, submit.context, connect, locale, t, buttonLabel]); // eslint-disable-line react-hooks/exhaustive-deps
189
+
190
+ // Active payment type
191
+ const activeType = types.find((tp) => tp.active)?.type || 'crypto';
192
+ const hasMultipleTypes = types.length > 1;
193
+
194
+ return (
195
+ <Box sx={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
196
+ {/* Main content — scrollable when overflows */}
197
+ <Box
198
+ sx={{
199
+ flex: 1,
200
+ display: 'flex',
201
+ flexDirection: 'column',
202
+ overflowY: 'auto',
203
+ minHeight: 0,
204
+ '&::-webkit-scrollbar': { display: 'none' },
205
+ scrollbarWidth: 'none',
206
+ }}>
207
+ {/* PAYMENT METHOD header */}
208
+ <Typography
209
+ sx={{
210
+ fontSize: 13,
211
+ fontWeight: 700,
212
+ letterSpacing: '0.02em',
213
+ color: 'text.primary',
214
+ mb: 2.5,
215
+ }}>
216
+ {t('payment.checkout.paymentDetails')}
217
+ </Typography>
218
+
219
+ {/* Card / Crypto toggle */}
220
+ {hasMultipleTypes && (
221
+ <ToggleButtonGroup
222
+ value={activeType}
223
+ exclusive
224
+ onChange={(_, v) => v && paymentMethod.setType(v)}
225
+ fullWidth
226
+ size="small"
227
+ sx={{
228
+ mb: 2.5,
229
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'grey.100'),
230
+ borderRadius: '10px',
231
+ p: 0.5,
232
+ '& .MuiToggleButton-root': {
233
+ textTransform: 'none',
234
+ borderRadius: '8px !important',
235
+ py: 0.75,
236
+ border: 'none',
237
+ fontSize: 14,
238
+ fontWeight: 500,
239
+ gap: 0.75,
240
+ color: 'text.secondary',
241
+ },
242
+ '& .Mui-selected': {
243
+ bgcolor: 'background.paper !important',
244
+ color: 'text.primary',
245
+ fontWeight: 600,
246
+ boxShadow: (theme) =>
247
+ theme.palette.mode === 'dark' ? '0 1px 3px rgba(0,0,0,0.3)' : '0 1px 3px rgba(0,0,0,0.08)',
248
+ '&:hover': { bgcolor: 'background.paper !important' },
249
+ },
250
+ }}>
251
+ {types.map((tp) => (
252
+ <ToggleButton key={tp.type} value={tp.type}>
253
+ {tp.type === 'stripe' ? (
254
+ <CreditCardIcon sx={{ fontSize: 18 }} />
255
+ ) : (
256
+ <CurrencyBitcoinIcon sx={{ fontSize: 18 }} />
257
+ )}
258
+ {tp.label || (tp.type === 'stripe' ? 'Card' : 'Crypto')}
259
+ </ToggleButton>
260
+ ))}
261
+ </ToggleButtonGroup>
262
+ )}
263
+
264
+ {/* Crypto: Network + Asset selectors (side by side) */}
265
+ {isCrypto && networks.length > 0 && (
266
+ <Stack direction="row" spacing={1.5} sx={{ mb: 2.5 }}>
267
+ {/* Network selector */}
268
+ {networks.length > 1 && (
269
+ <Box sx={{ flex: 1 }}>
270
+ <Typography
271
+ sx={{
272
+ fontSize: 12,
273
+ fontWeight: 600,
274
+ color: 'text.secondary',
275
+ mb: 0.5,
276
+ letterSpacing: '0.02em',
277
+ }}>
278
+ {t('common.network')}
279
+ </Typography>
280
+ <Select
281
+ value={currentMethodId}
282
+ onChange={(e) => {
283
+ const network = networks.find((n) => n.id === e.target.value);
284
+ if (network?.currencies?.[0]) {
285
+ paymentMethod.setCurrency(network.currencies[0].id);
286
+ }
287
+ }}
288
+ fullWidth
289
+ size="small"
290
+ sx={{
291
+ borderRadius: '8px',
292
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'grey.50'),
293
+ '& .MuiOutlinedInput-notchedOutline': { borderColor: 'transparent' },
294
+ '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'divider' },
295
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'primary.main', borderWidth: 1 },
296
+ '& .MuiSelect-select': { display: 'flex', alignItems: 'center', gap: 1, py: 1 },
297
+ }}>
298
+ {networks.map((net) => (
299
+ <MenuItem key={net.id} value={net.id} sx={{ gap: 1 }}>
300
+ {net.logo && <Avatar src={net.logo} sx={{ width: 20, height: 20 }} />}
301
+ <Typography sx={{ fontSize: 14 }}>{net.name}</Typography>
302
+ </MenuItem>
303
+ ))}
304
+ </Select>
305
+ </Box>
306
+ )}
307
+
308
+ {/* Asset selector */}
309
+ <Box sx={{ flex: 1 }}>
310
+ <Typography
311
+ sx={{
312
+ fontSize: 12,
313
+ fontWeight: 600,
314
+ color: 'text.secondary',
315
+ mb: 0.5,
316
+ letterSpacing: '0.02em',
317
+ }}>
318
+ {t('common.currency')}
319
+ </Typography>
320
+ <Select
321
+ value={currency?.id || ''}
322
+ onChange={(e) => paymentMethod.setCurrency(e.target.value as string)}
323
+ fullWidth
324
+ size="small"
325
+ sx={{
326
+ borderRadius: '8px',
327
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'grey.50'),
328
+ '& .MuiOutlinedInput-notchedOutline': { borderColor: 'transparent' },
329
+ '&:hover .MuiOutlinedInput-notchedOutline': { borderColor: 'divider' },
330
+ '&.Mui-focused .MuiOutlinedInput-notchedOutline': { borderColor: 'primary.main', borderWidth: 1 },
331
+ '& .MuiSelect-select': { display: 'flex', alignItems: 'center', gap: 1, py: 1 },
332
+ }}>
333
+ {(isCrypto && networks.length > 1
334
+ ? networks.find((n) => n.id === currentMethodId)?.currencies || []
335
+ : cryptoType?.currencies || []
336
+ ).map((cur) => (
337
+ <MenuItem key={cur.id} value={cur.id} sx={{ gap: 1 }}>
338
+ {cur.logo && <Avatar src={cur.logo} sx={{ width: 20, height: 20 }} />}
339
+ <Typography sx={{ fontSize: 14 }}>{cur.symbol}</Typography>
340
+ <Typography sx={{ fontSize: 12, color: 'text.secondary', ml: 0.5 }}>{cur.name}</Typography>
341
+ </MenuItem>
342
+ ))}
343
+ </Select>
344
+ </Box>
345
+ </Stack>
346
+ )}
347
+
348
+ {/* Card: show currency indicator — same style as crypto asset selector */}
349
+ {isStripe && currency && (
350
+ <Box sx={{ mb: 2.5 }}>
351
+ <Typography
352
+ sx={{
353
+ fontSize: 12,
354
+ fontWeight: 600,
355
+ color: 'text.secondary',
356
+ mb: 0.5,
357
+ letterSpacing: '0.02em',
358
+ }}>
359
+ {t('common.currency')}
360
+ </Typography>
361
+ <Select
362
+ value={currency.id || 'usd'}
363
+ readOnly
364
+ fullWidth
365
+ size="small"
366
+ IconComponent={() => null}
367
+ sx={{
368
+ borderRadius: '8px',
369
+ bgcolor: (theme) => (theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.06)' : 'grey.50'),
370
+ '& .MuiOutlinedInput-notchedOutline': { borderColor: 'transparent !important' },
371
+ '& .MuiSelect-select': { display: 'flex', alignItems: 'center', gap: 1, py: 1 },
372
+ }}>
373
+ <MenuItem value={currency.id || 'usd'} sx={{ gap: 1 }}>
374
+ {currency.logo && <Avatar src={currency.logo} sx={{ width: 20, height: 20 }} />}
375
+ <Typography sx={{ fontSize: 14, fontWeight: 600 }}>{currency.symbol}</Typography>
376
+ <Typography sx={{ fontSize: 12, color: 'text.secondary', ml: 0.5 }}>{currency.name}</Typography>
377
+ </MenuItem>
378
+ </Select>
379
+ </Box>
380
+ )}
381
+
382
+ {/* Customer Info */}
383
+ <CustomerInfoCard form={form} isLoggedIn={isLoggedIn} />
384
+ </Box>
385
+ {/* end main content / scrollable area */}
386
+
387
+ {/* Submit section — fixed on mobile, flows on desktop */}
388
+ <Box className="cko-v2-submit-btn" sx={{ flexShrink: 0 }}>
389
+ {/* Mobile: collapsible details (staking etc.) */}
390
+ {isMobile && pricing.staking && (
391
+ <>
392
+ <Box
393
+ onClick={() => setMobileDetailsOpen(!mobileDetailsOpen)}
394
+ sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', cursor: 'pointer', mb: 1 }}>
395
+ <Typography sx={{ fontSize: 13, fontWeight: 600, color: 'text.secondary' }}>
396
+ {t('payment.checkout.orderSummary')}
397
+ </Typography>
398
+ <ExpandMoreIcon
399
+ sx={{
400
+ fontSize: 18,
401
+ color: 'text.secondary',
402
+ transition: '0.2s',
403
+ transform: mobileDetailsOpen ? 'rotate(180deg)' : 'rotate(0deg)',
404
+ }}
405
+ />
406
+ </Box>
407
+ <Collapse in={mobileDetailsOpen}>
408
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
409
+ <Stack direction="row" spacing={0.5} alignItems="center">
410
+ <Typography sx={{ fontSize: 13, color: 'text.secondary' }}>
411
+ {t('payment.checkout.staking.title')}
412
+ </Typography>
413
+ <HelpOutlineIcon sx={{ fontSize: 14, color: 'text.disabled' }} />
414
+ </Stack>
415
+ {isAmountLoading ? (
416
+ <Skeleton variant="text" width={60} height={18} />
417
+ ) : (
418
+ <Typography sx={{ fontSize: 13, fontWeight: 600 }}>+{pricing.staking}</Typography>
419
+ )}
420
+ </Stack>
421
+ <Divider sx={{ mb: 1 }} />
422
+ </Collapse>
423
+ </>
424
+ )}
425
+
426
+ {/* Desktop: staking + promotion (together with Total Due) */}
427
+ {!isMobile && (
428
+ <>
429
+ <Divider sx={{ mb: 2 }} />
430
+ {pricing.staking && (
431
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 1 }}>
432
+ <Stack direction="row" spacing={0.5} alignItems="center">
433
+ <Typography sx={{ fontSize: 14, color: 'text.secondary' }}>
434
+ {t('payment.checkout.staking.title')}
435
+ </Typography>
436
+ <Tooltip
437
+ title={t('payment.checkout.staking.tooltip')}
438
+ placement="top"
439
+ arrow
440
+ slotProps={{ popper: { sx: whiteTooltipSx } }}>
441
+ <HelpOutlineIcon sx={{ fontSize: 16, color: 'text.disabled' }} />
442
+ </Tooltip>
443
+ </Stack>
444
+ {isAmountLoading ? (
445
+ <Skeleton variant="text" width={80} height={22} />
446
+ ) : (
447
+ <Typography sx={{ fontSize: 14, fontWeight: 600 }}>+{pricing.staking}</Typography>
448
+ )}
449
+ </Stack>
450
+ )}
451
+ <PromotionInput
452
+ promotion={{
453
+ applied: promotion.applied,
454
+ code: promotion.code,
455
+ active: promotion.active,
456
+ inactiveReason: promotion.inactiveReason,
457
+ apply: promotion.apply,
458
+ remove: promotion.remove,
459
+ }}
460
+ discounts={discounts}
461
+ discountAmount={pricing.discount}
462
+ />
463
+ </>
464
+ )}
465
+
466
+ {/* Total amount due */}
467
+ {(() => {
468
+ const totalStr = pricing.total || '0';
469
+ const parts = totalStr.split(/\s+/);
470
+ const num = parts[0] || '0';
471
+ const sym = parts.slice(1).join(' ') || currency?.symbol || '';
472
+ const dotIdx = num.indexOf('.');
473
+ const intPart = dotIdx >= 0 ? num.slice(0, dotIdx) : num;
474
+ const decPart = dotIdx >= 0 ? num.slice(dotIdx) : '';
475
+ return (
476
+ <Box sx={{ mb: isMobile ? 1.5 : 2.5 }}>
477
+ <Stack direction="row" justifyContent="space-between" alignItems="flex-end">
478
+ <Typography sx={{ fontWeight: 700, fontSize: 14, color: 'text.secondary', pb: 0.5 }}>
479
+ {t('common.totalDue')}
480
+ </Typography>
481
+ {isAmountLoading ? (
482
+ <Skeleton variant="text" width={140} height={44} />
483
+ ) : (
484
+ <Box sx={{ textAlign: 'right' }}>
485
+ <Typography
486
+ component="span"
487
+ sx={{
488
+ fontSize: { xs: 28, md: 36 },
489
+ fontWeight: 800,
490
+ color: 'text.primary',
491
+ lineHeight: 1,
492
+ transition: 'opacity 0.3s ease',
493
+ }}>
494
+ {intPart}
495
+ </Typography>
496
+ {decPart && (
497
+ <Typography
498
+ component="span"
499
+ sx={{ fontSize: { xs: 16, md: 20 }, fontWeight: 700, color: 'text.secondary', lineHeight: 1 }}>
500
+ {decPart}
501
+ </Typography>
502
+ )}
503
+ <Typography
504
+ component="span"
505
+ sx={{ fontSize: { xs: 14, md: 16 }, fontWeight: 600, color: 'text.secondary', ml: 0.75 }}>
506
+ {sym}
507
+ </Typography>
508
+ </Box>
509
+ )}
510
+ </Stack>
511
+ {/* Row below Total Due: mobile promo (left) + USD estimate (right) */}
512
+ {(pricing.usdEquivalent || (isMobile && promotion.active)) && !isAmountLoading && (
513
+ <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mt: 0.25 }}>
514
+ {/* Mobile: promo link or applied badge — left-aligned */}
515
+ {(() => {
516
+ if (isMobile && !promotion.applied && promotion.active) {
517
+ return (
518
+ <Box
519
+ onClick={() => setPromoDrawerOpen(true)}
520
+ sx={{ display: 'flex', alignItems: 'center', gap: 0.5, cursor: 'pointer' }}>
521
+ <LocalOfferOutlinedIcon sx={{ fontSize: 14, color: 'primary.main' }} />
522
+ <Typography sx={{ fontSize: 12, fontWeight: 600, color: 'primary.main' }}>
523
+ {tSafe(t, 'payment.checkout.promotion.add', 'Add promo code')}
524
+ </Typography>
525
+ </Box>
526
+ );
527
+ }
528
+ if (isMobile && promotion.applied) {
529
+ return (
530
+ <Stack direction="row" alignItems="center" spacing={0.5}>
531
+ <LocalOfferOutlinedIcon sx={{ fontSize: 14, color: 'success.main' }} />
532
+ <Typography sx={{ fontSize: 12, fontWeight: 600, color: 'success.main' }}>
533
+ {promotion.code}
534
+ </Typography>
535
+ {pricing.discount && (
536
+ <Typography sx={{ fontSize: 11, color: 'text.secondary' }}>
537
+ (-{pricing.discount})
538
+ </Typography>
539
+ )}
540
+ <CloseIcon
541
+ onClick={promotion.remove}
542
+ sx={{ fontSize: 14, color: 'error.main', cursor: 'pointer', ml: 0.25 }}
543
+ />
544
+ </Stack>
545
+ );
546
+ }
547
+ return <Box />;
548
+ })()}
549
+ {/* USD estimate — right-aligned */}
550
+ {pricing.usdEquivalent && (
551
+ <Typography sx={{ fontSize: 13, color: 'text.secondary' }}>≈ {pricing.usdEquivalent}</Typography>
552
+ )}
553
+ </Stack>
554
+ )}
555
+ </Box>
556
+ );
557
+ })()}
558
+
559
+ {/* Submit button */}
560
+ <Button
561
+ variant="contained"
562
+ size="large"
563
+ fullWidth
564
+ disabled={!canSubmit || submit.status === 'waiting_stripe'}
565
+ onClick={handleAction}
566
+ startIcon={isProcessing ? <CircularProgress size={20} color="inherit" /> : null}
567
+ endIcon={!isProcessing ? <ArrowForwardIcon /> : undefined}
568
+ sx={{
569
+ py: 1.5,
570
+ fontSize: '1.1rem',
571
+ fontWeight: 600,
572
+ textTransform: 'none',
573
+ borderRadius: '12px',
574
+ }}>
575
+ {isProcessing ? `${t('payment.checkout.processing')}...` : buttonLabel}
576
+ </Button>
577
+
578
+ {/* Mobile: SSL footer inside fixed bar, below button */}
579
+ {isMobile && (
580
+ <Stack
581
+ direction="row"
582
+ alignItems="center"
583
+ justifyContent="center"
584
+ spacing={0.75}
585
+ sx={{ mt: 1.5, opacity: 0.55 }}>
586
+ <LockOutlinedIcon sx={{ fontSize: 13, color: 'text.secondary' }} />
587
+ <Typography sx={{ fontSize: 11, color: 'text.secondary' }}>
588
+ {tSafe(t, 'payment.checkout.ssl', 'SSL Secure')}
589
+ </Typography>
590
+ <Typography sx={{ fontSize: 11, color: 'text.disabled' }}>·</Typography>
591
+ <Typography sx={{ fontSize: 11, color: 'text.secondary' }}>{tSafe(t, 'common.terms', 'Terms')}</Typography>
592
+ <Typography sx={{ fontSize: 11, color: 'text.disabled' }}>|</Typography>
593
+ <Typography sx={{ fontSize: 11, color: 'text.secondary' }}>
594
+ {tSafe(t, 'common.privacy', 'Privacy')}
595
+ </Typography>
596
+ </Stack>
597
+ )}
598
+ </Box>
599
+ {/* end submit fixed bar */}
600
+
601
+ {/* Mobile: Promo code drawer */}
602
+ {isMobile && (
603
+ <Drawer
604
+ anchor="bottom"
605
+ open={promoDrawerOpen}
606
+ onClose={() => setPromoDrawerOpen(false)}
607
+ PaperProps={{
608
+ sx: {
609
+ borderRadius: '16px 16px 0 0',
610
+ p: 3,
611
+ pb: 4,
612
+ minHeight: '30vh',
613
+ },
614
+ }}>
615
+ <Box sx={{ width: 40, height: 4, bgcolor: 'divider', borderRadius: 2, mx: 'auto', mb: 3 }} />
616
+ <Typography sx={{ fontWeight: 700, fontSize: 16, mb: 2 }}>
617
+ {tSafe(t, 'payment.checkout.promotion.add', 'Add promo code')}
618
+ </Typography>
619
+ <PromotionInput
620
+ initialShowInput
621
+ promotion={{
622
+ applied: promotion.applied,
623
+ code: promotion.code,
624
+ active: promotion.active,
625
+ inactiveReason: promotion.inactiveReason,
626
+ apply: async (...args: Parameters<typeof promotion.apply>) => {
627
+ const result = await promotion.apply(...args);
628
+ if (result.success) setPromoDrawerOpen(false);
629
+ return result;
630
+ },
631
+ remove: promotion.remove,
632
+ }}
633
+ discounts={discounts}
634
+ discountAmount={pricing.discount}
635
+ />
636
+ </Drawer>
637
+ )}
638
+
639
+ {/* Disclaimer + footer — outside fixed bar, desktop only for SSL */}
640
+ <Box sx={{ flexShrink: 0 }}>
641
+ <SubscriptionDisclaimer
642
+ mode={mode}
643
+ subscription={subscription}
644
+ staking={pricing.staking}
645
+ appName={(session?.metadata as any)?.app_name || 'New Payment Kit'}
646
+ />
647
+
648
+ {!isMobile && (
649
+ <Stack
650
+ direction="row"
651
+ alignItems="center"
652
+ justifyContent="center"
653
+ spacing={0.75}
654
+ sx={{ mt: 2.5, opacity: 0.55 }}>
655
+ <LockOutlinedIcon sx={{ fontSize: 13, color: 'text.secondary' }} />
656
+ <Typography sx={{ fontSize: 11, color: 'text.secondary' }}>
657
+ {tSafe(t, 'payment.checkout.ssl', 'SSL Secure')}
658
+ </Typography>
659
+ <Typography sx={{ fontSize: 11, color: 'text.disabled' }}>·</Typography>
660
+ <Typography sx={{ fontSize: 11, color: 'text.secondary' }}>{tSafe(t, 'common.terms', 'Terms')}</Typography>
661
+ <Typography sx={{ fontSize: 11, color: 'text.disabled' }}>|</Typography>
662
+ <Typography sx={{ fontSize: 11, color: 'text.secondary' }}>
663
+ {tSafe(t, 'common.privacy', 'Privacy')}
664
+ </Typography>
665
+ </Stack>
666
+ )}
667
+ </Box>
668
+
669
+ {/* Status Feedback (Toast errors only — dialogs handled by CheckoutDialogs) */}
670
+ <StatusFeedback status={submit.status} context={submit.context} onReset={submit.reset} />
671
+
672
+ {/* Overdue Invoice Payment (CUSTOMER_LIMITED) */}
673
+ {customerLimited && (
674
+ <OverdueInvoicePayment
675
+ customerId={(sessionData as any)?.customer?.id || (session as any)?.user?.did}
676
+ onPaid={() => {
677
+ setCustomerLimited(false);
678
+ submit.retry();
679
+ }}
680
+ alertMessage={t('payment.customer.pastDue.alert.customMessage')}
681
+ detailLinkOptions={{
682
+ enabled: true,
683
+ onClick: () => {
684
+ setCustomerLimited(false);
685
+ window.open(
686
+ joinURL(getPrefix(), `/customer/invoice/past-due?referer=${encodeURIComponent(window.location.href)}`),
687
+ '_self'
688
+ );
689
+ },
690
+ }}
691
+ dialogProps={{
692
+ open: customerLimited,
693
+ onClose: () => {
694
+ setCustomerLimited(false);
695
+ submit.reset();
696
+ },
697
+ title: t('payment.customer.pastDue.alert.title'),
698
+ }}
699
+ />
700
+ )}
701
+ </Box>
702
+ );
703
+ }