@blocklet/payment-react 1.18.30 → 1.18.32

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/es/checkout/donate.js +51 -23
  2. package/es/components/country-select.js +1 -0
  3. package/es/components/over-due-invoice-payment.js +43 -5
  4. package/es/libs/util.d.ts +1 -0
  5. package/es/libs/util.js +15 -2
  6. package/es/locales/en.js +2 -1
  7. package/es/locales/zh.js +2 -1
  8. package/es/payment/form/currency.d.ts +1 -1
  9. package/es/payment/form/currency.js +3 -3
  10. package/es/payment/form/index.d.ts +1 -1
  11. package/es/payment/form/index.js +19 -35
  12. package/es/payment/form/stripe/form.js +4 -2
  13. package/es/payment/index.js +10 -1
  14. package/es/payment/success.d.ts +3 -1
  15. package/es/payment/success.js +78 -6
  16. package/lib/checkout/donate.js +19 -11
  17. package/lib/components/country-select.js +1 -0
  18. package/lib/components/over-due-invoice-payment.js +42 -4
  19. package/lib/libs/util.d.ts +1 -0
  20. package/lib/libs/util.js +16 -2
  21. package/lib/locales/en.js +2 -1
  22. package/lib/locales/zh.js +2 -1
  23. package/lib/payment/form/currency.d.ts +1 -1
  24. package/lib/payment/form/currency.js +3 -3
  25. package/lib/payment/form/index.d.ts +1 -1
  26. package/lib/payment/form/index.js +20 -35
  27. package/lib/payment/form/stripe/form.js +4 -2
  28. package/lib/payment/index.js +10 -1
  29. package/lib/payment/success.d.ts +3 -1
  30. package/lib/payment/success.js +68 -15
  31. package/package.json +8 -8
  32. package/src/checkout/donate.tsx +23 -5
  33. package/src/components/country-select.tsx +1 -0
  34. package/src/components/over-due-invoice-payment.tsx +46 -4
  35. package/src/libs/util.ts +17 -2
  36. package/src/locales/en.tsx +1 -0
  37. package/src/locales/zh.tsx +1 -0
  38. package/src/payment/form/currency.tsx +4 -4
  39. package/src/payment/form/index.tsx +21 -47
  40. package/src/payment/form/stripe/form.tsx +4 -2
  41. package/src/payment/index.tsx +12 -1
  42. package/src/payment/success.tsx +73 -11
  43. package/src/payment/summary.tsx +1 -0
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/indent */
2
- import { useEffect, useMemo, useState } from 'react';
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Button, Typography, Stack, Alert, SxProps } from '@mui/material';
4
4
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
5
  import Toast from '@arcblock/ux/lib/Toast';
@@ -13,6 +13,7 @@ import type {
13
13
  TInvoiceExpanded,
14
14
  } from '@blocklet/payment-types';
15
15
  import { useRequest } from 'ahooks';
16
+ import pWaitFor from 'p-wait-for';
16
17
  import { Dialog } from '@arcblock/ux';
17
18
  import { CheckCircle as CheckCircleIcon } from '@mui/icons-material';
18
19
  import debounce from 'lodash/debounce';
@@ -113,6 +114,7 @@ function OverdueInvoicePayment({
113
114
  const sourceType = subscriptionId ? 'subscription' : 'customer';
114
115
  const effectiveCustomerId = customerId || session?.user?.did;
115
116
  const sourceId = subscriptionId || effectiveCustomerId;
117
+ const customerIdRef = useRef(effectiveCustomerId);
116
118
  const {
117
119
  data = {
118
120
  summary: {},
@@ -123,6 +125,11 @@ function OverdueInvoicePayment({
123
125
  runAsync: refresh,
124
126
  } = useRequest(() => fetchOverdueInvoices({ subscriptionId, customerId: effectiveCustomerId, authToken }), {
125
127
  ready: !!subscriptionId || !!effectiveCustomerId,
128
+ onSuccess: (res) => {
129
+ if (res.customer?.id && res.customer?.id !== customerIdRef.current) {
130
+ customerIdRef.current = res.customer?.id;
131
+ }
132
+ },
126
133
  });
127
134
 
128
135
  const detailUrl = useMemo(() => {
@@ -163,16 +170,50 @@ function OverdueInvoicePayment({
163
170
  }
164
171
  );
165
172
 
173
+ const isCrossOriginRequest = isCrossOrigin();
174
+
166
175
  const subscription = useSubscription('events');
176
+ const waitForInvoiceAllPaid = async () => {
177
+ let isPaid = false;
178
+ await pWaitFor(
179
+ async () => {
180
+ const res = await refresh();
181
+ isPaid = res.invoices?.length === 0;
182
+ return isPaid;
183
+ },
184
+ { interval: 2000, timeout: 3 * 60 * 1000 }
185
+ );
186
+ return isPaid;
187
+ };
188
+
189
+ const handleConnected = async () => {
190
+ if (isCrossOriginRequest) {
191
+ try {
192
+ const paid = await waitForInvoiceAllPaid();
193
+ if (successToast) {
194
+ Toast.close();
195
+ Toast.success(t('payment.customer.invoice.paySuccess'));
196
+ }
197
+ if (paid) {
198
+ setDialogOpen(false);
199
+ onPaid(sourceId as string, selectCurrencyId, sourceType as 'subscription' | 'customer');
200
+ }
201
+ } catch (err) {
202
+ console.error('Check payment status failed:', err);
203
+ }
204
+ }
205
+ };
206
+
167
207
  useEffect(() => {
168
- if (subscription) {
208
+ if (subscription && !isCrossOriginRequest) {
169
209
  subscription.on('invoice.paid', ({ response }: { response: TInvoiceExpanded }) => {
170
210
  const relevantId = subscriptionId || response.customer_id;
171
211
  const uniqueKey = `${relevantId}-${response.currency_id}`;
172
212
 
173
213
  if (
174
214
  (subscriptionId && response.subscription_id === subscriptionId) ||
175
- (effectiveCustomerId && [data.customer?.id, effectiveCustomerId].includes(response.customer_id))
215
+ (effectiveCustomerId && effectiveCustomerId === response.customer_id) ||
216
+ (customerIdRef.current && customerIdRef.current === response.customer_id)
176
217
  ) {
177
218
  if (!processedCurrencies[uniqueKey]) {
178
219
  setProcessedCurrencies((prev) => ({ ...prev, [uniqueKey]: 1 }));
@@ -214,10 +255,11 @@ function OverdueInvoicePayment({
214
255
  saveConnect: false,
215
256
  action: 'collect-batch',
216
257
  prefix: joinURL(getPrefix(), '/api/did'),
217
- useSocket: isCrossOrigin() === false,
258
+ useSocket: !isCrossOriginRequest,
218
259
  extraParams,
219
260
  onSuccess: () => {
220
261
  connect.close();
262
+ handleConnected();
221
263
  setPayLoading(false);
222
264
  setPaymentStatus((prev) => ({
223
265
  ...prev,
package/src/libs/util.ts CHANGED
@@ -597,6 +597,19 @@ export function formatPriceDisplay(
597
597
  return [amount, then].filter(Boolean).join(' ');
598
598
  }
599
599
 
600
+ export function hasMultipleRecurringIntervals(items: TLineItemExpanded[]): boolean {
601
+ const intervals = new Set<string>();
602
+ for (const item of items) {
603
+ if (item.price?.recurring?.interval && item.price?.type === 'recurring') {
604
+ intervals.add(`${item.price.recurring.interval}-${item.price.recurring.interval_count}`);
605
+ if (intervals.size > 1) {
606
+ return true;
607
+ }
608
+ }
609
+ }
610
+ return false;
611
+ }
612
+
600
613
  export function getFreeTrialTime(
601
614
  { trialInDays, trialEnd }: { trialInDays: number; trialEnd: number },
602
615
  locale: string = 'en'
@@ -685,8 +698,10 @@ export function formatCheckoutHeadlines(
685
698
  locale
686
699
  );
687
700
  const hasMetered = items.some((x) => x.price.type === 'recurring' && x.price.recurring?.usage_type === 'metered');
701
+ const differentRecurring = hasMultipleRecurringIntervals(items);
688
702
  // all recurring
689
703
  if (items.every((x) => x.price.type === 'recurring')) {
704
+ // check if there has different recurring price
690
705
  const subscription = [
691
706
  hasMetered ? t('payment.checkout.least', locale) : '',
692
707
  fromUnitToToken(
@@ -749,7 +764,7 @@ export function formatCheckoutHeadlines(
749
764
  action: t('payment.checkout.sub1', locale, { name }),
750
765
  amount,
751
766
  then: hasMetered ? t('payment.checkout.meteredThen', locale, { recurring }) : recurring,
752
- showThen: hasMetered,
767
+ showThen: hasMetered && !differentRecurring,
753
768
  actualAmount,
754
769
  };
755
770
  return {
@@ -780,7 +795,7 @@ export function formatCheckoutHeadlines(
780
795
  hasMetered && Number(subscription) === 0,
781
796
  locale
782
797
  ),
783
- showThen: true,
798
+ showThen: !differentRecurring,
784
799
  actualAmount,
785
800
  };
786
801
 
@@ -118,6 +118,7 @@ export default flat({
118
118
  next: {
119
119
  subscription: 'View subscription',
120
120
  invoice: 'View invoice',
121
+ view: 'View',
121
122
  },
122
123
  paymentRequired: 'Payment Required',
123
124
  staking: {
@@ -118,6 +118,7 @@ export default flat({
118
118
  next: {
119
119
  subscription: '查看订阅',
120
120
  invoice: '查看账单',
121
+ view: '查看',
121
122
  },
122
123
  paymentRequired: '支付金额',
123
124
  staking: {
@@ -3,7 +3,7 @@ import { Avatar, Card, Radio, Stack, Typography } from '@mui/material';
3
3
  import { styled } from '@mui/system';
4
4
 
5
5
  type Props = {
6
- value: number;
6
+ value: string;
7
7
  currencies: TPaymentCurrency[];
8
8
  onChange: Function;
9
9
  };
@@ -18,13 +18,13 @@ export default function CurrencySelector({ value, currencies, onChange }: Props)
18
18
  gap: 12,
19
19
  gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))',
20
20
  }}>
21
- {currencies.map((x, i) => {
22
- const selected = i === value;
21
+ {currencies.map((x) => {
22
+ const selected = x.id === value;
23
23
  return (
24
24
  <Card
25
25
  key={x.id}
26
26
  variant="outlined"
27
- onClick={() => onChange(i)}
27
+ onClick={() => onChange(x.id, (x as any).method?.id)}
28
28
  className={selected ? 'cko-payment-card' : 'cko-payment-card-unselect'}>
29
29
  <Stack direction="row" alignItems="center" sx={{ position: 'relative' }}>
30
30
  <Avatar src={x.logo} alt={x.name} sx={{ width: 40, height: 40, marginRight: '12px' }} />
@@ -4,17 +4,11 @@ import 'react-international-phone/style.css';
4
4
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
5
5
  // import { useTheme } from '@arcblock/ux/lib/Theme';
6
6
  import Toast from '@arcblock/ux/lib/Toast';
7
- import type {
8
- TCheckoutSession,
9
- TCustomer,
10
- TInvoice,
11
- TPaymentIntent,
12
- TPaymentMethodExpanded,
13
- } from '@blocklet/payment-types';
7
+ import type { TCheckoutSession, TCustomer, TPaymentIntent, TPaymentMethodExpanded } from '@blocklet/payment-types';
14
8
  import { Box, Button, CircularProgress, Divider, Fade, FormLabel, Stack, Typography } from '@mui/material';
15
9
  import { useMemoizedFn, useSetState } from 'ahooks';
16
10
  import pWaitFor from 'p-wait-for';
17
- import { useEffect, useMemo, useRef, useState } from 'react';
11
+ import { useEffect, useMemo, useRef } from 'react';
18
12
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
19
13
  import { joinURL } from 'ufo';
20
14
  import { dispatch } from 'use-bus';
@@ -30,7 +24,6 @@ import {
30
24
  formatError,
31
25
  formatQuantityInventory,
32
26
  getPrefix,
33
- getQueryParams,
34
27
  getStatementDescriptor,
35
28
  isCrossOrigin,
36
29
  } from '../../libs/util';
@@ -162,7 +155,6 @@ export default function PaymentForm({
162
155
  onError,
163
156
  // mode,
164
157
  action,
165
- currencyId,
166
158
  onlyShowBtn,
167
159
  isDonation = false,
168
160
  }: PageData) {
@@ -215,37 +207,15 @@ export default function PaymentForm({
215
207
 
216
208
  const currencies = flattenPaymentMethods(paymentMethods);
217
209
 
218
- const [paymentCurrencyIndex, setPaymentCurrencyIndex] = useState(() => {
219
- const query = getQueryParams(window.location.href);
220
- const queryCurrencyId = query.currencyId || currencyId;
221
- const index = currencies.findIndex((x) => x.id === queryCurrencyId);
222
- return index >= 0 ? index : 0;
223
- });
224
-
225
- const handleCurrencyChange = (index: number) => {
226
- setPaymentCurrencyIndex(index);
227
- const selectedCurrencyId = currencies[index]?.id;
228
- if (selectedCurrencyId) {
229
- saveCurrencyPreference(selectedCurrencyId, session?.user?.did);
230
- }
231
- };
232
-
233
210
  const onCheckoutComplete = useMemoizedFn(async ({ response }: { response: TCheckoutSession }) => {
234
211
  if (response.id === checkoutSession.id && state.paid === false) {
235
212
  await handleConnected();
236
213
  }
237
214
  });
238
215
 
239
- const onInvoicePaid = useMemoizedFn(async ({ response }: { response: TInvoice }) => {
240
- if (response.customer_id === customer?.id && state.customerLimited) {
241
- await onAction();
242
- }
243
- });
244
-
245
216
  useEffect(() => {
246
217
  if (subscription) {
247
218
  subscription.on('checkout.session.completed', onCheckoutComplete);
248
- subscription.on('invoice.paid', onInvoicePaid);
249
219
  }
250
220
  }, [subscription]); // eslint-disable-line react-hooks/exhaustive-deps
251
221
 
@@ -317,11 +287,6 @@ export default function PaymentForm({
317
287
  // eslint-disable-next-line react-hooks/exhaustive-deps
318
288
  }, [session?.user, checkoutSession.phone_number_collection?.enabled]);
319
289
 
320
- useEffect(() => {
321
- setValue('payment_method', (currencies[paymentCurrencyIndex] as any)?.method?.id);
322
- setValue('payment_currency', currencies[paymentCurrencyIndex]?.id);
323
- }, [paymentCurrencyIndex, currencies, setValue]);
324
-
325
290
  const paymentMethod = useWatch({ control, name: 'payment_method' });
326
291
 
327
292
  // const domSize = useSize(document.body);
@@ -442,6 +407,11 @@ export default function PaymentForm({
442
407
  setState({ submitting: false, paying: false });
443
408
  onError(err);
444
409
  },
410
+ messages: {
411
+ title: 'DID Connect',
412
+ scan: 'Use following methods to complete this payment',
413
+ confirm: 'Confirm',
414
+ },
445
415
  } as any);
446
416
  }
447
417
  }
@@ -490,14 +460,14 @@ export default function PaymentForm({
490
460
  }
491
461
  if (hasDidWallet(session.user)) {
492
462
  handleSubmit(onFormSubmit, onFormError)();
493
- } else {
494
- session.bindWallet(() => {
495
- // timeout required because https://github.com/ArcBlock/ux/issues/1241
496
- setTimeout(() => {
497
- handleSubmit(onFormSubmit, onFormError)();
498
- }, 2000);
499
- });
463
+ return;
500
464
  }
465
+ session.bindWallet(() => {
466
+ // timeout required because https://github.com/ArcBlock/ux/issues/1241
467
+ setTimeout(() => {
468
+ handleSubmit(onFormSubmit, onFormError)();
469
+ }, 2000);
470
+ });
501
471
  } else {
502
472
  if (isDonationMode) {
503
473
  handleSubmit(onFormSubmit, onFormError)();
@@ -622,11 +592,15 @@ export default function PaymentForm({
622
592
  <Controller
623
593
  name="payment_currency"
624
594
  control={control}
625
- render={() => (
595
+ render={({ field }) => (
626
596
  <CurrencySelector
627
- value={paymentCurrencyIndex}
597
+ value={field.value}
628
598
  currencies={currencies}
629
- onChange={handleCurrencyChange}
599
+ onChange={(id: string, methodId: string) => {
600
+ field.onChange(id);
601
+ setValue('payment_method', methodId);
602
+ saveCurrencyPreference(id, session?.user?.did);
603
+ }}
630
604
  />
631
605
  )}
632
606
  />
@@ -178,10 +178,12 @@ function StripeCheckoutForm({
178
178
 
179
179
  return (
180
180
  <Content onSubmit={handleSubmit}>
181
- {(!state.paymentMethod || state.paymentMethod === 'card') && (
181
+ {(!state.paymentMethod || ['link', 'card'].includes(state.paymentMethod)) && (
182
182
  <LinkAuthenticationElement
183
183
  options={{
184
- defaultEmail: customer.email,
184
+ defaultValues: {
185
+ email: customer.email,
186
+ },
185
187
  }}
186
188
  />
187
189
  )}
@@ -142,6 +142,16 @@ function PaymentInner({
142
142
  },
143
143
  });
144
144
 
145
+ useEffect(() => {
146
+ if (defaultCurrencyId) {
147
+ methods.setValue('payment_currency', defaultCurrencyId);
148
+ }
149
+ if (defaultMethodId) {
150
+ methods.setValue('payment_method', defaultMethodId);
151
+ }
152
+ // eslint-disable-next-line react-hooks/exhaustive-deps
153
+ }, [defaultCurrencyId, defaultMethodId]);
154
+
145
155
  useEffect(() => {
146
156
  if (!isMobileSafari()) {
147
157
  return () => {};
@@ -258,7 +268,7 @@ function PaymentInner({
258
268
  // @ts-ignore
259
269
  state.checkoutSession.subscription_data?.min_stake_amount || 0
260
270
  )}
261
- showStaking={method.type === 'arcblock'}
271
+ showStaking={method.type === 'arcblock' && !state.checkoutSession.subscription_data?.no_stake}
262
272
  currency={currency}
263
273
  onUpsell={onUpsell}
264
274
  onDownsell={onDownsell}
@@ -290,6 +300,7 @@ function PaymentInner({
290
300
  action={state.checkoutSession.mode}
291
301
  invoiceId={state.checkoutSession.invoice_id}
292
302
  subscriptionId={state.checkoutSession.subscription_id}
303
+ subscriptions={state.checkoutSession.subscriptions}
293
304
  message={
294
305
  paymentLink?.after_completion?.hosted_confirmation?.custom_message ||
295
306
  t(
@@ -1,8 +1,9 @@
1
1
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
- import { Grow, Link, Stack, Typography } from '@mui/material';
2
+ import { Grow, Link, Stack, Typography, Box, Paper } from '@mui/material';
3
3
  import { styled } from '@mui/system';
4
4
  import { joinURL } from 'ufo';
5
5
 
6
+ import { Button } from '@arcblock/ux';
6
7
  import { usePaymentContext } from '../contexts/payment';
7
8
 
8
9
  type Props = {
@@ -12,20 +13,80 @@ type Props = {
12
13
  payee: string;
13
14
  invoiceId?: string;
14
15
  subscriptionId?: string;
16
+ subscriptions?: any[];
15
17
  };
16
18
 
17
- export default function PaymentSuccess({ mode, message, action, payee, invoiceId, subscriptionId }: Props) {
19
+ export default function PaymentSuccess({
20
+ mode,
21
+ message,
22
+ action,
23
+ payee,
24
+ invoiceId,
25
+ subscriptionId,
26
+ subscriptions,
27
+ }: Props) {
18
28
  const { t } = useLocaleContext();
19
29
  const { prefix } = usePaymentContext();
20
30
  let next: any = null;
21
- if (['subscription', 'setup'].includes(action) && subscriptionId) {
22
- next = (
23
- <Typography textAlign="center" sx={{ mt: 2 }}>
24
- <Link href={joinURL(prefix, `/customer/subscription/${subscriptionId}`)}>
25
- {t('payment.checkout.next.subscription', { payee })}
26
- </Link>
27
- </Typography>
28
- );
31
+ if (['subscription', 'setup'].includes(action)) {
32
+ if (subscriptions && subscriptions.length > 1) {
33
+ next = (
34
+ <Paper
35
+ elevation={0}
36
+ sx={{
37
+ p: 3,
38
+ backgroundColor: 'grey.50',
39
+ borderRadius: 2,
40
+ width: '100%',
41
+ mt: 2,
42
+ display: 'flex',
43
+ flexDirection: 'column',
44
+ gap: 2,
45
+ }}>
46
+ {subscriptions.map((subscription) => (
47
+ <Box
48
+ key={subscription.id}
49
+ sx={{
50
+ display: 'flex',
51
+ alignItems: 'center',
52
+ justifyContent: 'space-between',
53
+ }}>
54
+ <Typography
55
+ variant="body2"
56
+ sx={{
57
+ color: 'text.secondary',
58
+ fontWeight: 500,
59
+ }}>
60
+ {subscription.description}
61
+ </Typography>
62
+ <Box
63
+ sx={{
64
+ flex: 1,
65
+ borderBottom: '1px dashed',
66
+ borderColor: 'grey.300',
67
+ mx: 2,
68
+ }}
69
+ />
70
+ <Button variant="text" color="primary" size="small">
71
+ <Link
72
+ href={joinURL(prefix, `/customer/subscription/${subscription.id}`)}
73
+ sx={{ color: 'text.secondary' }}>
74
+ {t('payment.checkout.next.view')}
75
+ </Link>
76
+ </Button>
77
+ </Box>
78
+ ))}
79
+ </Paper>
80
+ );
81
+ } else if (subscriptionId) {
82
+ next = (
83
+ <Button variant="outlined" color="primary" sx={{ mt: 2 }}>
84
+ <Link href={joinURL(prefix, `/customer/subscription/${subscriptionId}`)}>
85
+ {t('payment.checkout.next.subscription', { payee })}
86
+ </Link>
87
+ </Button>
88
+ );
89
+ }
29
90
  } else if (invoiceId) {
30
91
  next = (
31
92
  <Typography textAlign="center" sx={{ mt: 2 }}>
@@ -42,7 +103,7 @@ export default function PaymentSuccess({ mode, message, action, payee, invoiceId
42
103
  direction="column"
43
104
  alignItems="center"
44
105
  justifyContent={mode === 'standalone' ? 'center' : 'flex-start'}
45
- sx={{ height: mode === 'standalone' ? 360 : 300 }}>
106
+ sx={{ height: mode === 'standalone' ? 'fit-content' : 300 }}>
46
107
  <Div>
47
108
  <div className="check-icon">
48
109
  <span className="icon-line line-tip" />
@@ -66,6 +127,7 @@ export default function PaymentSuccess({ mode, message, action, payee, invoiceId
66
127
  PaymentSuccess.defaultProps = {
67
128
  invoiceId: '',
68
129
  subscriptionId: '',
130
+ subscriptions: [],
69
131
  };
70
132
 
71
133
  const Div = styled('div')`
@@ -160,6 +160,7 @@ export default function PaymentSummary({
160
160
  );
161
161
  const headlines = formatCheckoutHeadlines(items, currency, { trialEnd, trialInDays }, locale);
162
162
  const staking = showStaking ? getStakingSetup(items, currency, billingThreshold) : '0';
163
+
163
164
  const totalAmount = fromUnitToToken(
164
165
  new BN(fromTokenToUnit(headlines.actualAmount, currency?.decimal)).add(new BN(staking)).toString(),
165
166
  currency?.decimal