@blocklet/payment-react 1.21.16 → 1.22.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 (43) hide show
  1. package/.aigne/doc-smith/translation-cache.yaml +11 -0
  2. package/es/components/over-due-invoice-payment.js +86 -21
  3. package/es/components/payment-beneficiaries.js +3 -3
  4. package/es/components/stripe-payment-action.d.ts +16 -0
  5. package/es/components/stripe-payment-action.js +164 -0
  6. package/es/history/invoice/list.js +58 -2
  7. package/es/index.d.ts +3 -1
  8. package/es/index.js +2 -0
  9. package/es/libs/util.d.ts +2 -1
  10. package/es/libs/util.js +17 -1
  11. package/es/locales/en.js +18 -3
  12. package/es/locales/zh.js +12 -3
  13. package/es/payment/form/stripe/form.d.ts +4 -1
  14. package/es/payment/form/stripe/form.js +9 -5
  15. package/es/payment/product-item.js +1 -1
  16. package/es/payment/summary.js +1 -1
  17. package/lib/components/over-due-invoice-payment.js +99 -31
  18. package/lib/components/payment-beneficiaries.js +3 -2
  19. package/lib/components/stripe-payment-action.d.ts +16 -0
  20. package/lib/components/stripe-payment-action.js +191 -0
  21. package/lib/history/invoice/list.js +58 -10
  22. package/lib/index.d.ts +3 -1
  23. package/lib/index.js +8 -0
  24. package/lib/libs/util.d.ts +2 -1
  25. package/lib/libs/util.js +18 -1
  26. package/lib/locales/en.js +18 -3
  27. package/lib/locales/zh.js +12 -3
  28. package/lib/payment/form/stripe/form.d.ts +4 -1
  29. package/lib/payment/form/stripe/form.js +9 -5
  30. package/lib/payment/product-item.js +1 -1
  31. package/lib/payment/summary.js +1 -1
  32. package/package.json +9 -9
  33. package/src/components/over-due-invoice-payment.tsx +101 -29
  34. package/src/components/payment-beneficiaries.tsx +3 -3
  35. package/src/components/stripe-payment-action.tsx +220 -0
  36. package/src/history/invoice/list.tsx +67 -13
  37. package/src/index.ts +3 -0
  38. package/src/libs/util.ts +18 -1
  39. package/src/locales/en.tsx +16 -0
  40. package/src/locales/zh.tsx +10 -0
  41. package/src/payment/form/stripe/form.tsx +9 -2
  42. package/src/payment/product-item.tsx +1 -1
  43. package/src/payment/summary.tsx +1 -1
@@ -0,0 +1,220 @@
1
+ /* eslint-disable react/require-default-props */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import { Dialog } from '@arcblock/ux';
5
+ import type { Customer, TPaymentMethod, TInvoiceExpanded } from '@blocklet/payment-types';
6
+ import { Button, Typography } from '@mui/material';
7
+ import { useSetState } from 'ahooks';
8
+ import { useEffect, useRef } from 'react';
9
+
10
+ import StripeForm from '../payment/form/stripe';
11
+ import api from '../libs/api';
12
+ import { formatError } from '../libs/util';
13
+
14
+ export interface StripePaymentActionProps {
15
+ invoice?: TInvoiceExpanded;
16
+ invoiceIds?: string[];
17
+ subscriptionId?: string;
18
+ customerId?: string;
19
+ currencyId?: string;
20
+ paymentMethod?: TPaymentMethod;
21
+
22
+ autoTrigger?: boolean;
23
+ onExternalPayment?: (invoiceId?: string) => void;
24
+
25
+ onSuccess?: () => void;
26
+ onError?: (error: Error) => void;
27
+ onClose?: () => void;
28
+
29
+ children?: (onPay: () => void, loading: boolean) => React.ReactNode;
30
+ }
31
+
32
+ export default function StripePaymentAction(props: StripePaymentActionProps) {
33
+ const {
34
+ invoice,
35
+ invoiceIds,
36
+ subscriptionId,
37
+ customerId,
38
+ currencyId,
39
+ paymentMethod,
40
+ autoTrigger = false,
41
+ onExternalPayment,
42
+ onSuccess,
43
+ onError,
44
+ onClose,
45
+ children,
46
+ } = props;
47
+ const { t } = useLocaleContext();
48
+
49
+ const [state, setState] = useSetState<{
50
+ paying: boolean;
51
+ confirmDialog: boolean;
52
+ stripeDialog: boolean;
53
+ clientSecret: string | null;
54
+ publishableKey: string | null;
55
+ customer: Customer | null;
56
+ }>({
57
+ paying: false,
58
+ confirmDialog: false,
59
+ stripeDialog: false,
60
+ clientSecret: null,
61
+ publishableKey: null,
62
+ customer: null,
63
+ });
64
+
65
+ const autoTriggerRef = useRef(false);
66
+
67
+ useEffect(() => {
68
+ if (autoTrigger && !autoTriggerRef.current) {
69
+ autoTriggerRef.current = true;
70
+ handlePay();
71
+ }
72
+ }, [autoTrigger]); // eslint-disable-line react-hooks/exhaustive-deps
73
+
74
+ const handlePay = async () => {
75
+ if (state.paying) {
76
+ return;
77
+ }
78
+
79
+ const hasSubscription = !!(subscriptionId || invoice?.subscription_id);
80
+ const method = paymentMethod || invoice?.paymentMethod;
81
+ const shouldShowConfirm = hasSubscription && method?.type === 'stripe';
82
+
83
+ if (shouldShowConfirm) {
84
+ setState({ confirmDialog: true });
85
+ return;
86
+ }
87
+
88
+ await proceedWithPayment();
89
+ };
90
+
91
+ const proceedWithPayment = async () => {
92
+ setState({ paying: true, confirmDialog: false });
93
+
94
+ const derivedCurrencyId = currencyId || invoice?.currency_id || invoice?.paymentCurrency?.id;
95
+ const derivedPaymentMethod = paymentMethod || invoice?.paymentMethod;
96
+ const isStripePayment = derivedPaymentMethod?.type === 'stripe';
97
+
98
+ if (isStripePayment && derivedCurrencyId) {
99
+ const stripePayload: Record<string, any> = {};
100
+
101
+ if (invoiceIds && invoiceIds.length > 0) {
102
+ stripePayload.invoice_ids = invoiceIds;
103
+ } else if (invoice) {
104
+ stripePayload.invoice_ids = [invoice.id];
105
+ } else if (subscriptionId) {
106
+ stripePayload.subscription_id = subscriptionId;
107
+ } else if (customerId) {
108
+ stripePayload.customer_id = customerId;
109
+ }
110
+
111
+ if (derivedCurrencyId) {
112
+ stripePayload.currency_id = derivedCurrencyId;
113
+ }
114
+
115
+ try {
116
+ const { data: paymentData } = await api.post('/api/invoices/pay-stripe', stripePayload);
117
+ setState({
118
+ paying: false,
119
+ stripeDialog: true,
120
+ clientSecret: paymentData.client_secret,
121
+ publishableKey: paymentData.publishable_key,
122
+ customer: paymentData.customer || null,
123
+ });
124
+ return;
125
+ } catch (err: any) {
126
+ const error = formatError(err);
127
+ Toast.error(error);
128
+ setState({ paying: false });
129
+ onError?.(error);
130
+ return;
131
+ }
132
+ }
133
+
134
+ setState({ paying: false });
135
+ if (onExternalPayment) {
136
+ onExternalPayment(invoice?.id);
137
+ return;
138
+ }
139
+
140
+ Toast.error(t('payment.customer.invoice.payError'));
141
+ onError?.(new Error('EXTERNAL_PAYMENT_HANDLER_NOT_PROVIDED'));
142
+ };
143
+
144
+ const handleConfirmCancel = () => {
145
+ setState({ confirmDialog: false, paying: false });
146
+ onClose?.();
147
+ };
148
+
149
+ const handleStripeConfirm = () => {
150
+ Toast.success(t('payment.customer.invoice.payProcessing'));
151
+ setState({
152
+ paying: false,
153
+ stripeDialog: false,
154
+ clientSecret: null,
155
+ publishableKey: null,
156
+ customer: null,
157
+ });
158
+
159
+ setTimeout(() => {
160
+ onSuccess?.();
161
+ }, 2000);
162
+ };
163
+
164
+ const handleStripeCancel = () => {
165
+ setState({
166
+ paying: false,
167
+ stripeDialog: false,
168
+ clientSecret: null,
169
+ publishableKey: null,
170
+ customer: null,
171
+ });
172
+ onClose?.();
173
+ };
174
+
175
+ return (
176
+ <>
177
+ {children?.(handlePay, state.paying)}
178
+
179
+ {state.confirmDialog && (
180
+ <Dialog
181
+ open={state.confirmDialog}
182
+ title={t('payment.customer.invoice.paymentConfirmTitle')}
183
+ onClose={handleConfirmCancel}
184
+ maxWidth="sm"
185
+ PaperProps={{
186
+ style: {
187
+ minHeight: 0,
188
+ },
189
+ }}
190
+ actions={[
191
+ <Button key="cancel" variant="outlined" onClick={handleConfirmCancel}>
192
+ {t('common.cancel')}
193
+ </Button>,
194
+ <Button key="continue" variant="contained" onClick={proceedWithPayment}>
195
+ {t('payment.customer.invoice.continue')}
196
+ </Button>,
197
+ ]}>
198
+ <Typography variant="body1" sx={{ color: 'text.secondary', mt: -2 }}>
199
+ {t('payment.customer.invoice.paymentConfirmDescription')}
200
+ </Typography>
201
+ </Dialog>
202
+ )}
203
+
204
+ {state.stripeDialog && state.clientSecret && state.publishableKey && state.customer && (
205
+ <StripeForm
206
+ clientSecret={state.clientSecret}
207
+ intentType="setup_intent"
208
+ publicKey={state.publishableKey}
209
+ customer={state.customer}
210
+ mode="setup"
211
+ title={t('payment.customer.invoice.pay')}
212
+ submitButtonText={t('common.submit')}
213
+ onConfirm={handleStripeConfirm}
214
+ onCancel={handleStripeCancel}
215
+ returnUrl={window.location.href}
216
+ />
217
+ )}
218
+ </>
219
+ );
220
+ }
@@ -20,6 +20,7 @@ import Status from '../../components/status';
20
20
  import { usePaymentContext } from '../../contexts/payment';
21
21
  import { useSubscription } from '../../hooks/subscription';
22
22
  import api from '../../libs/api';
23
+ import StripePaymentAction from '../../components/stripe-payment-action';
23
24
  import {
24
25
  formatBNStr,
25
26
  formatError,
@@ -323,11 +324,39 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
323
324
  const isVoid = invoice.status === 'void';
324
325
 
325
326
  if (action && !hidePay) {
326
- return connect ? (
327
- <Button variant="text" size="small" onClick={() => onPay(invoice.id)} sx={{ color: 'text.link' }}>
328
- {t('payment.customer.invoice.pay')}
329
- </Button>
330
- ) : (
327
+ if (connect) {
328
+ if (invoice.paymentMethod?.type === 'stripe') {
329
+ return (
330
+ <StripePaymentAction
331
+ invoice={invoice}
332
+ paymentMethod={invoice.paymentMethod as any}
333
+ onSuccess={() => {
334
+ refresh();
335
+ }}>
336
+ {(handlePay, paying) => (
337
+ <Button
338
+ variant="text"
339
+ size="small"
340
+ sx={{ color: 'text.link' }}
341
+ disabled={paying}
342
+ onClick={(e) => {
343
+ e.preventDefault();
344
+ e.stopPropagation();
345
+ handlePay();
346
+ }}>
347
+ {paying ? t('payment.checkout.processing') : t('payment.customer.invoice.pay')}
348
+ </Button>
349
+ )}
350
+ </StripePaymentAction>
351
+ );
352
+ }
353
+ return (
354
+ <Button variant="text" size="small" onClick={() => onPay(invoice.id)} sx={{ color: 'text.link' }}>
355
+ {t('payment.customer.invoice.pay')}
356
+ </Button>
357
+ );
358
+ }
359
+ return (
331
360
  <Button
332
361
  component="a"
333
362
  variant="text"
@@ -601,14 +630,39 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
601
630
  }}>
602
631
  {action ? (
603
632
  connect ? (
604
- <Button
605
- variant="contained"
606
- color="primary"
607
- size="small"
608
- onClick={() => onPay(invoice.id)}
609
- sx={{ whiteSpace: 'nowrap' }}>
610
- {t('payment.customer.invoice.pay')}
611
- </Button>
633
+ invoice.paymentMethod?.type === 'stripe' ? (
634
+ <StripePaymentAction
635
+ invoice={invoice}
636
+ paymentMethod={invoice.paymentMethod as any}
637
+ onSuccess={async () => {
638
+ await reloadAsync();
639
+ }}>
640
+ {(handlePay, paying) => (
641
+ <Button
642
+ variant="contained"
643
+ color="primary"
644
+ size="small"
645
+ sx={{ whiteSpace: 'nowrap' }}
646
+ disabled={paying}
647
+ onClick={(e) => {
648
+ e.preventDefault();
649
+ e.stopPropagation();
650
+ handlePay();
651
+ }}>
652
+ {paying ? t('payment.checkout.processing') : t('payment.customer.invoice.pay')}
653
+ </Button>
654
+ )}
655
+ </StripePaymentAction>
656
+ ) : (
657
+ <Button
658
+ variant="contained"
659
+ color="primary"
660
+ size="small"
661
+ onClick={() => onPay(invoice.id)}
662
+ sx={{ whiteSpace: 'nowrap' }}>
663
+ {t('payment.customer.invoice.pay')}
664
+ </Button>
665
+ )
612
666
  ) : (
613
667
  <Button
614
668
  component="a"
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ import TruncatedText from './components/truncated-text';
32
32
  import Link from './components/link';
33
33
  import { createLazyComponent } from './components/lazy-loader';
34
34
  import OverdueInvoicePayment from './components/over-due-invoice-payment';
35
+ import StripePaymentAction from './components/stripe-payment-action';
35
36
  import PaymentBeneficiaries from './components/payment-beneficiaries';
36
37
  import LoadingButton from './components/loading-button';
37
38
  import ResumeSubscription from './components/resume-subscription';
@@ -93,6 +94,7 @@ export {
93
94
  TruncatedText,
94
95
  Link,
95
96
  OverdueInvoicePayment,
97
+ StripePaymentAction,
96
98
  PaymentBeneficiaries,
97
99
  LoadingButton,
98
100
  DonateDetails,
@@ -109,3 +111,4 @@ export {
109
111
  };
110
112
 
111
113
  export type { CountrySelectProps } from './components/country-select';
114
+ export type { StripePaymentActionProps } from './components/stripe-payment-action';
package/src/libs/util.ts CHANGED
@@ -1085,8 +1085,8 @@ export function formatTotalPrice({
1085
1085
  };
1086
1086
  }
1087
1087
 
1088
- const unitValue = new BN(price.custom_unit_amount || price.unit_amount);
1089
1088
  const currency: TPaymentCurrency = price?.currency ?? {};
1089
+ const unitValue = new BN(getPriceUintAmountByCurrency(price, currency));
1090
1090
 
1091
1091
  const total = `${fromUnitToToken(unitValue.mul(new BN(quantity)), currency.decimal)} ${currency.symbol} `;
1092
1092
 
@@ -1336,3 +1336,20 @@ export function showStaking(method: TPaymentMethod, currency: TPaymentCurrency,
1336
1336
  }
1337
1337
  return false;
1338
1338
  }
1339
+
1340
+ export function formatLinkWithLocale(url: string, locale?: string) {
1341
+ if (!locale || !url) {
1342
+ return url;
1343
+ }
1344
+ try {
1345
+ const urlObj = new URL(url);
1346
+ urlObj.searchParams.set('locale', locale);
1347
+ return urlObj.toString();
1348
+ } catch (error) {
1349
+ if (/[?&]locale=[^&]*/.test(url)) {
1350
+ return url.replace(/([?&])locale=[^&]*/, `$1locale=${locale}`);
1351
+ }
1352
+ const separator = url.includes('?') ? '&' : '?';
1353
+ return `${url}${separator}locale=${locale}`;
1354
+ }
1355
+ }
@@ -111,6 +111,7 @@ export default flat({
111
111
  },
112
112
  paymentMethod: 'Payment Method',
113
113
  viewInvoice: 'View Invoice',
114
+ submit: 'Submit',
114
115
  },
115
116
  payment: {
116
117
  checkout: {
@@ -472,7 +473,11 @@ export default flat({
472
473
  amountApplied: 'Applied Credit',
473
474
  pay: 'Pay this invoice',
474
475
  paySuccess: 'You have successfully paid the invoice',
476
+ payProcessing: 'Payment is being processed, please refresh in a moment',
475
477
  payError: 'Failed to pay the invoice',
478
+ sync: 'Sync Status',
479
+ syncing: 'Syncing...',
480
+ syncSuccess: 'Synced successfully',
476
481
  renew: 'Renew the subscription',
477
482
  renewSuccess: 'You have successfully renewed the subscription',
478
483
  renewError: 'Failed to renew the subscription',
@@ -482,6 +487,16 @@ export default flat({
482
487
  emptyList: 'No Invoices',
483
488
  noPaymentRequired: 'No Payment Required',
484
489
  payBatch: 'Pay Due Invoices',
490
+ stripePayDescription: 'Complete payment using your saved payment method or add a new one.',
491
+ amount: 'Amount',
492
+ paymentConfirmTitle: 'Payment Confirmation',
493
+ paymentConfirmDescription:
494
+ 'After completing this payment, the payment method you use will be automatically set as the default for this subscription. Additionally, we will retry payment for any other unpaid invoices associated with this subscription.',
495
+ continue: 'Continue',
496
+ },
497
+ overduePayment: {
498
+ setupPaymentDescription: 'Use your saved card or add a new one to complete payment via Stripe.',
499
+ totalAmount: 'Total Amount',
485
500
  },
486
501
  payment: {
487
502
  empty: 'There are no payments',
@@ -554,6 +569,7 @@ export default flat({
554
569
  empty: 'There are no overdue invoices for your subscription {name}.',
555
570
  retry: 'Retry',
556
571
  paid: 'Paid',
572
+ processing: 'Processing',
557
573
  },
558
574
  },
559
575
  },
@@ -111,6 +111,7 @@ export default flat({
111
111
  },
112
112
  paymentMethod: '支付方式',
113
113
  viewInvoice: '查看账单',
114
+ submit: '提交',
114
115
  },
115
116
  payment: {
116
117
  checkout: {
@@ -460,7 +461,11 @@ export default flat({
460
461
  amountApplied: '余额变更',
461
462
  pay: '支付此账单',
462
463
  paySuccess: '支付成功',
464
+ payProcessing: '支付处理中,请稍候刷新查看',
463
465
  payError: '支付失败',
466
+ sync: '同步状态',
467
+ syncing: '同步中...',
468
+ syncSuccess: '同步成功',
464
469
  renew: '恢复订阅',
465
470
  renewSuccess: '订阅恢复成功',
466
471
  renewError: '订阅恢复失败',
@@ -470,6 +475,10 @@ export default flat({
470
475
  emptyList: '没有账单',
471
476
  noPaymentRequired: '无需支付',
472
477
  payBatch: '支付欠款',
478
+ paymentConfirmTitle: '支付确认',
479
+ paymentConfirmDescription:
480
+ '完成本次支付后,您使用的支付方式将自动设置为该订阅的默认支付方式。此外,我们还将对该订阅的其他欠费账单进行重试收费。',
481
+ continue: '继续',
473
482
  },
474
483
  payment: {
475
484
  empty: '没有支付记录',
@@ -541,6 +550,7 @@ export default flat({
541
550
  empty: '您的【{name}】订阅当前没有欠费账单',
542
551
  retry: '重新支付',
543
552
  paid: '已支付',
553
+ processing: '支付中',
544
554
  },
545
555
  },
546
556
  },
@@ -20,6 +20,7 @@ export type StripeCheckoutFormProps = {
20
20
  mode: string;
21
21
  onConfirm: Function;
22
22
  returnUrl?: string;
23
+ submitButtonText?: string;
23
24
  };
24
25
 
25
26
  const PaymentElementContainer = styled('div')`
@@ -39,6 +40,7 @@ function StripeCheckoutForm({
39
40
  mode,
40
41
  onConfirm,
41
42
  returnUrl = '',
43
+ submitButtonText = '',
42
44
  }: StripeCheckoutFormProps) {
43
45
  const stripe = useStripe();
44
46
  const elements = useElements();
@@ -236,7 +238,7 @@ function StripeCheckoutForm({
236
238
  variant="contained"
237
239
  color="primary"
238
240
  size="large">
239
- {t('payment.checkout.continue', { action: t(`payment.checkout.${mode}`) })}
241
+ {submitButtonText || t('payment.checkout.continue', { action: t(`payment.checkout.${mode}`) })}
240
242
  </LoadingButton>
241
243
  )}
242
244
  {state.message && <Typography sx={{ mt: 1, color: 'error.main' }}>{state.message}</Typography>}
@@ -263,6 +265,8 @@ export type StripeCheckoutProps = {
263
265
  onConfirm: Function;
264
266
  onCancel: Function;
265
267
  returnUrl?: string;
268
+ title?: string;
269
+ submitButtonText?: string;
266
270
  };
267
271
  export default function StripeCheckout({
268
272
  clientSecret,
@@ -273,6 +277,8 @@ export default function StripeCheckout({
273
277
  onConfirm,
274
278
  onCancel,
275
279
  returnUrl = '',
280
+ title = '',
281
+ submitButtonText = '',
276
282
  }: StripeCheckoutProps) {
277
283
  const stripePromise = loadStripe(publicKey);
278
284
  const { isMobile } = useMobile();
@@ -294,7 +300,7 @@ export default function StripeCheckout({
294
300
 
295
301
  return (
296
302
  <Dialog
297
- title={t('payment.checkout.cardPay', { action: t(`payment.checkout.${mode}`) })}
303
+ title={title || t('payment.checkout.cardPay', { action: t(`payment.checkout.${mode}`) })}
298
304
  showCloseButton={state.closable}
299
305
  open={state.open}
300
306
  onClose={handleClose}
@@ -344,6 +350,7 @@ export default function StripeCheckout({
344
350
  customer={customer}
345
351
  onConfirm={onConfirm}
346
352
  returnUrl={returnUrl}
353
+ submitButtonText={submitButtonText}
347
354
  />
348
355
  </Elements>
349
356
  </Dialog>
@@ -249,7 +249,7 @@ export default function ProductItem({
249
249
  }
250
250
 
251
251
  const pendingAmountBN = new BN(pendingAmount || '0');
252
- const creditAmountBN = fromTokenToUnit(new BN(creditAmount), creditCurrency?.decimal || 2);
252
+ const creditAmountBN = fromTokenToUnit(creditAmount, creditCurrency?.decimal || 2);
253
253
  const minQuantityNeeded = Math.ceil(pendingAmountBN.mul(new BN(100)).div(creditAmountBN).toNumber() / 100);
254
254
  const currentPurchaseCreditBN = creditAmountBN.mul(new BN(localQuantity || 0));
255
255
  const actualAvailable = currentPurchaseCreditBN.sub(pendingAmountBN).toString();
@@ -475,7 +475,7 @@ export default function PaymentSummary({
475
475
  sx={{
476
476
  justifyContent: 'space-between',
477
477
  alignItems: 'center',
478
- ...(staking > 0 && {
478
+ ...(+staking > 0 && {
479
479
  borderTop: '1px solid',
480
480
  borderColor: 'divider',
481
481
  pt: 1,