@blocklet/payment-react 1.14.29 → 1.14.31

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/table.js +3 -2
  2. package/es/components/pricing-table.js +2 -1
  3. package/es/components/truncated-text.d.ts +15 -0
  4. package/es/components/truncated-text.js +27 -0
  5. package/es/history/invoice/list.js +14 -4
  6. package/es/index.d.ts +2 -1
  7. package/es/index.js +3 -1
  8. package/es/libs/util.d.ts +2 -0
  9. package/es/libs/util.js +33 -0
  10. package/es/locales/en.js +3 -1
  11. package/es/locales/zh.js +3 -1
  12. package/es/payment/form/index.js +89 -60
  13. package/es/payment/index.js +81 -15
  14. package/es/payment/summary.js +2 -18
  15. package/es/theme/index.js +5 -0
  16. package/lib/checkout/table.js +8 -2
  17. package/lib/components/pricing-table.js +6 -1
  18. package/lib/components/truncated-text.d.ts +15 -0
  19. package/lib/components/truncated-text.js +55 -0
  20. package/lib/history/invoice/list.js +8 -3
  21. package/lib/index.d.ts +2 -1
  22. package/lib/index.js +8 -0
  23. package/lib/libs/util.d.ts +2 -0
  24. package/lib/libs/util.js +35 -0
  25. package/lib/locales/en.js +3 -1
  26. package/lib/locales/zh.js +3 -1
  27. package/lib/payment/form/index.js +37 -11
  28. package/lib/payment/index.js +59 -4
  29. package/lib/payment/summary.js +2 -6
  30. package/lib/theme/index.js +5 -0
  31. package/package.json +8 -8
  32. package/src/checkout/table.tsx +3 -2
  33. package/src/components/pricing-table.tsx +2 -1
  34. package/src/components/truncated-text.tsx +41 -0
  35. package/src/history/invoice/list.tsx +9 -4
  36. package/src/index.ts +2 -0
  37. package/src/libs/util.ts +41 -0
  38. package/src/locales/en.tsx +3 -0
  39. package/src/locales/zh.tsx +2 -0
  40. package/src/payment/form/index.tsx +43 -15
  41. package/src/payment/index.tsx +70 -5
  42. package/src/payment/summary.tsx +2 -8
  43. package/src/theme/index.tsx +5 -0
@@ -0,0 +1,41 @@
1
+ import { Tooltip, TooltipProps, tooltipClasses } from '@mui/material';
2
+ import { styled } from '@mui/system';
3
+ import { truncateText } from '../libs/util';
4
+
5
+ const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
6
+ <Tooltip {...props} classes={{ popper: className }} />
7
+ ))({
8
+ [`& .${tooltipClasses.tooltip}`]: {
9
+ fontSize: 11,
10
+ maxHeight: 120,
11
+ maxWidth: 500,
12
+ overflowY: 'auto',
13
+ },
14
+ });
15
+
16
+ interface TruncatedTextProps {
17
+ text?: string;
18
+ maxLength?: number;
19
+ useWidth?: boolean;
20
+ }
21
+
22
+ export default function TruncatedText({ text = '', maxLength = 100, useWidth = false }: TruncatedTextProps) {
23
+ if (!text) {
24
+ return null;
25
+ }
26
+ const truncatedText = truncateText(text, maxLength, useWidth);
27
+ if (!truncatedText.endsWith('...')) {
28
+ return <span>{truncatedText}</span>;
29
+ }
30
+ return (
31
+ <CustomTooltip title={text} placement="bottom" enterTouchDelay={0}>
32
+ <span title={text}>{truncatedText}</span>
33
+ </CustomTooltip>
34
+ );
35
+ }
36
+
37
+ TruncatedText.defaultProps = {
38
+ useWidth: false,
39
+ text: '',
40
+ maxLength: 100,
41
+ };
@@ -126,7 +126,7 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
126
126
  {
127
127
  label: t('common.amount'),
128
128
  name: 'total',
129
- width: 60,
129
+ width: 80,
130
130
  align: 'right',
131
131
  options: {
132
132
  customBodyRenderLite: (_: string, index: number) => {
@@ -191,9 +191,8 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
191
191
  },
192
192
  {
193
193
  label: t('common.status'),
194
- name: 'created_at',
194
+ name: 'status',
195
195
  options: {
196
- sort: true,
197
196
  customBodyRenderLite: (val: string, index: number) => {
198
197
  const invoice = data?.list[index] as TInvoiceExpanded;
199
198
  const link = getInvoiceLink(invoice, action);
@@ -396,7 +395,12 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
396
395
  <Box flex={1} textAlign="right">
397
396
  {action ? (
398
397
  link.connect ? (
399
- <Button variant="contained" color="primary" size="small" onClick={() => onPay(invoice.id)}>
398
+ <Button
399
+ variant="contained"
400
+ color="primary"
401
+ size="small"
402
+ onClick={() => onPay(invoice.id)}
403
+ sx={{ whiteSpace: 'nowrap' }}>
400
404
  {t('payment.customer.invoice.pay')}
401
405
  </Button>
402
406
  ) : (
@@ -406,6 +410,7 @@ const InvoiceList = React.memo((props: Props & { onPay: (invoiceId: string) => v
406
410
  size="small"
407
411
  href={link.url}
408
412
  target={link.external ? '_blank' : target}
413
+ sx={{ whiteSpace: 'nowrap' }}
409
414
  rel="noreferrer">
410
415
  {t('payment.customer.invoice.pay')}
411
416
  </Button>
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import ProductSkeleton from './payment/product-skeleton';
25
25
  import PaymentSummary from './payment/summary';
26
26
  import PricingItem from './components/pricing-item';
27
27
  import CountrySelect from './components/country-select';
28
+ import TruncatedText from './components/truncated-text';
28
29
 
29
30
  export { PaymentThemeProvider } from './theme';
30
31
 
@@ -65,4 +66,5 @@ export {
65
66
  PricingItem,
66
67
  CountrySelect,
67
68
  Table,
69
+ TruncatedText,
68
70
  };
package/src/libs/util.ts CHANGED
@@ -19,6 +19,8 @@ import { BN, fromUnitToToken } from '@ocap/util';
19
19
  import omit from 'lodash/omit';
20
20
  import trimEnd from 'lodash/trimEnd';
21
21
  import numbro from 'numbro';
22
+ // eslint-disable-next-line import/no-extraneous-dependencies
23
+ import stringWidth from 'string-width';
22
24
  import { defaultCountries } from 'react-international-phone';
23
25
  import { joinURL } from 'ufo';
24
26
 
@@ -1042,3 +1044,42 @@ export function getWordBreakStyle(value: any): 'break-word' | 'break-all' {
1042
1044
  }
1043
1045
  return 'break-all';
1044
1046
  }
1047
+
1048
+ export function isMobileSafari() {
1049
+ const ua = navigator.userAgent.toLowerCase();
1050
+ const isSafari = ua.indexOf('safari') > -1 && ua.indexOf('chrome') === -1;
1051
+ const isMobile = ua.indexOf('mobile') > -1 || /iphone|ipad|ipod/.test(ua);
1052
+ const isIOS = /iphone|ipad|ipod/.test(ua);
1053
+ return isSafari && isMobile && isIOS;
1054
+ }
1055
+
1056
+ export function truncateText(text: string, maxLength: number, useWidth: boolean = false): string {
1057
+ if (!text || !maxLength) {
1058
+ return text;
1059
+ }
1060
+
1061
+ if (!useWidth) {
1062
+ if (text.length <= maxLength) {
1063
+ return text;
1064
+ }
1065
+ return `${text.substring(0, maxLength)}...`;
1066
+ }
1067
+
1068
+ let width = 0;
1069
+ let truncated = '';
1070
+
1071
+ for (let i = 0; i < text.length; i++) {
1072
+ const charWidth = stringWidth(text.charAt(i));
1073
+ if (width + charWidth > maxLength) {
1074
+ break;
1075
+ }
1076
+ truncated += text.charAt(i);
1077
+ width += charWidth;
1078
+ }
1079
+
1080
+ if (truncated === text) {
1081
+ return truncated;
1082
+ }
1083
+
1084
+ return `${truncated}...`;
1085
+ }
@@ -208,6 +208,8 @@ export default flat({
208
208
  comment: 'Any additional feedback?',
209
209
  description:
210
210
  'Your subscription will be canceled, but it is still available until the end of your current billing period on {date}',
211
+ trialDescription:
212
+ 'Free trial subscriptions will be canceled immediately and no longer available, confirm to continue',
211
213
  feedback: {
212
214
  tip: 'We would love your feedback, it will help us improve our service',
213
215
  too_expensive: 'The service is too expensive',
@@ -316,6 +318,7 @@ export default flat({
316
318
  cancel: 'Subscription cancel',
317
319
  manual: 'Manual invoice',
318
320
  upcoming: 'Upcoming invoice',
321
+ slashStake: 'Slash stake',
319
322
  },
320
323
  },
321
324
  },
@@ -204,6 +204,7 @@ export default flat({
204
204
  title: '取消您的订阅',
205
205
  comment: '你还有其他反馈么?',
206
206
  description: '您的订阅将被取消,但仍然可用直到您当前计费周期结束于{date}',
207
+ trialDescription: '免费试用的订阅将被立即取消,不再可用,确认是否继续',
207
208
  feedback: {
208
209
  tip: '我们希望听到您的反馈,这将帮助我们改进我们的服务',
209
210
  too_expensive: '服务费用太高',
@@ -307,6 +308,7 @@ export default flat({
307
308
  cancel: '订阅取消',
308
309
  manual: '人工账单',
309
310
  upcoming: '未来账单',
311
+ slashStake: '罚没质押',
310
312
  },
311
313
  },
312
314
  },
@@ -11,16 +11,17 @@ import type {
11
11
  TPaymentMethodExpanded,
12
12
  } from '@blocklet/payment-types';
13
13
  import { LoadingButton } from '@mui/lab';
14
- import { Divider, Fade, FormLabel, Stack, Typography } from '@mui/material';
14
+ import { Box, Divider, Fade, FormLabel, Stack, Typography } from '@mui/material';
15
15
  import { useMemoizedFn, useSetState } from 'ahooks';
16
16
  import { PhoneNumberUtil } from 'google-libphonenumber';
17
17
  import pWaitFor from 'p-wait-for';
18
- import { useEffect, useMemo, useState } from 'react';
18
+ import { useEffect, useMemo, useRef, useState } from 'react';
19
19
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
20
20
  import { joinURL } from 'ufo';
21
21
  import { dispatch } from 'use-bus';
22
22
  import isEmail from 'validator/es/lib/isEmail';
23
23
 
24
+ import { isEmpty } from 'lodash';
24
25
  import ConfirmDialog from '../../components/confirm';
25
26
  import FormInput from '../../components/input';
26
27
  import { usePaymentContext } from '../../contexts/payment';
@@ -39,6 +40,7 @@ import AddressForm from './address';
39
40
  import CurrencySelector from './currency';
40
41
  import PhoneInput from './phone';
41
42
  import StripeCheckout from './stripe';
43
+ import { useMobile } from '../../hooks/mobile';
42
44
 
43
45
  const phoneUtil = PhoneNumberUtil.getInstance();
44
46
 
@@ -97,9 +99,17 @@ export default function PaymentForm({
97
99
  }: PageData) {
98
100
  // const theme = useTheme();
99
101
  const { t } = useLocaleContext();
102
+ const { isMobile } = useMobile();
100
103
  const { session, connect, payable } = usePaymentContext();
101
104
  const subscription = useSubscription('events');
102
- const { control, getValues, setValue, handleSubmit } = useFormContext();
105
+ const {
106
+ control,
107
+ getValues,
108
+ setValue,
109
+ handleSubmit,
110
+ formState: { errors },
111
+ } = useFormContext();
112
+ const errorRef = useRef<HTMLDivElement | null>(null);
103
113
  const quantityInventoryStatus = useMemo(() => {
104
114
  let status = true;
105
115
  for (const item of checkoutSession.line_items) {
@@ -236,6 +246,12 @@ export default function PaymentForm({
236
246
  }
237
247
  };
238
248
 
249
+ useEffect(() => {
250
+ if (errorRef.current && !isEmpty(errors) && isMobile) {
251
+ errorRef.current.scrollIntoView({ behavior: 'smooth' });
252
+ }
253
+ }, [errors, isMobile]);
254
+
239
255
  const onUserLoggedIn = async () => {
240
256
  const { data: profile } = await api.get('/api/customers/me?fallback=1');
241
257
  if (profile) {
@@ -345,6 +361,9 @@ export default function PaymentForm({
345
361
  };
346
362
 
347
363
  const onAction = () => {
364
+ if (errorRef.current && !isEmpty(errors) && isMobile) {
365
+ errorRef.current.scrollIntoView({ behavior: 'smooth' });
366
+ }
348
367
  if (session?.user) {
349
368
  if (hasDidWallet(session.user)) {
350
369
  handleSubmit(onFormSubmit, onFormError)();
@@ -428,7 +447,13 @@ export default function PaymentForm({
428
447
  {/* <Typography sx={{ color: 'text.secondary', fontWeight: 600 }}>{t('payment.checkout.contact')}</Typography> */}
429
448
  {/* {isColumnLayout || mode !== 'standalone' ? null : <UserButtons />} */}
430
449
  </Stack>
431
- <Stack direction="column" className="cko-payment-form" spacing={0} sx={{ flex: 1, overflow: 'auto' }}>
450
+ <Stack
451
+ direction="column"
452
+ className="cko-payment-form"
453
+ id="cko-payment-form"
454
+ spacing={0}
455
+ ref={!isEmpty(errors) ? (errorRef as any) : undefined}
456
+ sx={{ flex: 1, overflow: 'auto' }}>
432
457
  <FormLabel className="base-label">{t('payment.checkout.customer.name')}</FormLabel>
433
458
  <FormInput
434
459
  name="customer_name"
@@ -481,17 +506,20 @@ export default function PaymentForm({
481
506
  <Divider sx={{ mt: 2.5, mb: 2.5 }} />
482
507
  <Fade in>
483
508
  <Stack className="cko-payment-submit">
484
- <LoadingButton
485
- variant="contained"
486
- color="primary"
487
- size="large"
488
- className="cko-submit-button"
489
- onClick={onAction}
490
- fullWidth
491
- disabled={state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus || !payable}
492
- loading={state.submitting || state.paying}>
493
- {state.submitting || state.paying ? t('payment.checkout.processing') : buttonText}
494
- </LoadingButton>
509
+ <Box className="cko-payment-submit-btn">
510
+ <LoadingButton
511
+ variant="contained"
512
+ color="primary"
513
+ size="large"
514
+ className="cko-submit-button"
515
+ onClick={onAction}
516
+ fullWidth
517
+ disabled={state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus || !payable}
518
+ loading={state.submitting || state.paying}>
519
+ {state.submitting || state.paying ? t('payment.checkout.processing') : buttonText}
520
+ </LoadingButton>
521
+ </Box>
522
+
495
523
  {['subscription', 'setup'].includes(checkoutSession.mode) && (
496
524
  <Typography sx={{ mt: 2.5, color: 'text.lighter', fontSize: '0.9rem', lineHeight: '1.1rem' }}>
497
525
  {t('payment.checkout.confirm', { payee })}
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/indent */
1
2
  /* eslint-disable no-nested-ternary */
2
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
4
  import Toast from '@arcblock/ux/lib/Toast';
@@ -21,7 +22,7 @@ import type { LiteralUnion } from 'type-fest';
21
22
 
22
23
  import { usePaymentContext } from '../contexts/payment';
23
24
  import api from '../libs/api';
24
- import { findCurrency, formatError, getStatementDescriptor, isValidCountry } from '../libs/util';
25
+ import { findCurrency, formatError, getStatementDescriptor, isMobileSafari, isValidCountry } from '../libs/util';
25
26
  import { CheckoutCallbacks, CheckoutContext, CheckoutFormData } from '../types';
26
27
  import PaymentError from './error';
27
28
  import CheckoutFooter from './footer';
@@ -86,6 +87,25 @@ function PaymentInner({
86
87
  },
87
88
  });
88
89
 
90
+ useEffect(() => {
91
+ if (!isMobileSafari()) {
92
+ return () => {};
93
+ }
94
+ let scrollTop = 0;
95
+ const focusinHandler = () => {
96
+ scrollTop = window.scrollY;
97
+ };
98
+ const focusoutHandler = () => {
99
+ window.scrollTo(0, scrollTop);
100
+ };
101
+ document.body.addEventListener('focusin', focusinHandler);
102
+ document.body.addEventListener('focusout', focusoutHandler);
103
+ return () => {
104
+ document.body.removeEventListener('focusin', focusinHandler);
105
+ document.body.removeEventListener('focusout', focusoutHandler);
106
+ };
107
+ }, []);
108
+
89
109
  const currencyId = useWatch({ control: methods.control, name: 'payment_currency', defaultValue: defaultCurrencyId });
90
110
  const currency =
91
111
  (findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) ||
@@ -260,7 +280,9 @@ export default function Payment({
260
280
  const { t } = useLocaleContext();
261
281
  const { refresh, livemode, setLivemode } = usePaymentContext();
262
282
  const [delay, setDelay] = useState(false);
283
+ const { isMobile } = useMobile();
263
284
  const hideSummaryCard = mode.endsWith('-minimal') || !showCheckoutSummary;
285
+ const isMobileSafariEnv = isMobileSafari();
264
286
 
265
287
  useEffect(() => {
266
288
  setTimeout(() => {
@@ -342,11 +364,15 @@ export default function Payment({
342
364
  />
343
365
  );
344
366
  };
367
+
345
368
  return (
346
369
  <Stack
347
370
  display="flex"
348
371
  flexDirection="column"
349
- sx={{ height: mode === 'standalone' ? '100vh' : 'auto', overflow: 'hidden' }}>
372
+ sx={{
373
+ height: mode === 'standalone' ? '100vh' : 'auto',
374
+ overflow: isMobileSafariEnv ? 'visible' : 'hidden',
375
+ }}>
350
376
  {mode === 'standalone' ? (
351
377
  <Header
352
378
  meta={undefined}
@@ -359,7 +385,45 @@ export default function Payment({
359
385
  sx={{ borderBottom: '1px solid var(--stroke-border-base, #EFF1F5)' }}
360
386
  />
361
387
  ) : null}
362
- <Root mode={mode} sx={{ flex: 1 }}>
388
+ <Root
389
+ mode={mode}
390
+ sx={{
391
+ flex: 1,
392
+ overflow: {
393
+ xs: isMobileSafariEnv ? 'visible' : 'auto',
394
+ md: 'hidden',
395
+ },
396
+ ...(isMobile && mode === 'standalone'
397
+ ? {
398
+ '.cko-payment-submit-btn': {
399
+ position: 'fixed',
400
+ bottom: 20,
401
+ left: 0,
402
+ right: 0,
403
+ zIndex: 999,
404
+ background: '#fff',
405
+ padding: '10px',
406
+ textAlign: 'center',
407
+ button: {
408
+ color: '#fff',
409
+ maxWidth: 542,
410
+ },
411
+ },
412
+ '.cko-footer': {
413
+ position: 'fixed',
414
+ bottom: 0,
415
+ left: 0,
416
+ right: 0,
417
+ zIndex: 999,
418
+ background: '#fff',
419
+ marginBottom: 0,
420
+ },
421
+ '.cko-payment': {
422
+ paddingBottom: '100px',
423
+ },
424
+ }
425
+ : {}),
426
+ }}>
363
427
  {goBack && (
364
428
  <ArrowBackOutlined
365
429
  sx={{ mr: 0.5, color: 'text.secondary', alignSelf: 'flex-start', margin: '16px 0', cursor: 'pointer' }}
@@ -517,7 +581,6 @@ export const Root = styled(Box)<{ mode: LiteralUnion<'standalone' | 'inline' | '
517
581
  }
518
582
 
519
583
  @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
520
- background: ${(props) => (props.mode === 'standalone' ? 'var(--backgrounds-bg-subtle, #F9FAFB)' : 'transparent')};
521
584
  padding-top: 0;
522
585
  overflow: auto;
523
586
  &:before {
@@ -526,7 +589,8 @@ export const Root = styled(Box)<{ mode: LiteralUnion<'standalone' | 'inline' | '
526
589
  .cko-container {
527
590
  flex-direction: column;
528
591
  align-items: center;
529
- gap: 32px;
592
+ justify-content: flex-start;
593
+ gap: 0;
530
594
  overflow: visible;
531
595
  min-width: 200px;
532
596
  }
@@ -539,6 +603,7 @@ export const Root = styled(Box)<{ mode: LiteralUnion<'standalone' | 'inline' | '
539
603
  width: 100%;
540
604
  height: fit-content;
541
605
  flex: none;
606
+ border-top: 1px solid var(--stroke-border-base, #eff1f5);
542
607
  &:before {
543
608
  display: none;
544
609
  }
@@ -335,10 +335,7 @@ export default function PaymentSummary({
335
335
  <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
336
336
  <Stack direction="row" alignItems="center" spacing={0.5}>
337
337
  <Typography sx={{ color: 'text.secondary' }}>{t('payment.checkout.paymentRequired')}</Typography>
338
- <Tooltip
339
- title={<Typography>{t('payment.checkout.stakingConfirm')}</Typography>}
340
- placement="top"
341
- sx={{ maxWidth: '150px' }}>
338
+ <Tooltip title={t('payment.checkout.stakingConfirm')} placement="top" sx={{ maxWidth: '150px' }}>
342
339
  <HelpOutline fontSize="small" sx={{ color: 'text.lighter' }} />
343
340
  </Tooltip>
344
341
  </Stack>
@@ -347,10 +344,7 @@ export default function PaymentSummary({
347
344
  <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={1}>
348
345
  <Stack direction="row" alignItems="center" spacing={0.5}>
349
346
  <Typography sx={{ color: 'text.secondary' }}>{t('payment.checkout.staking.title')}</Typography>
350
- <Tooltip
351
- title={<Typography>{t('payment.checkout.staking.tooltip')}</Typography>}
352
- placement="top"
353
- sx={{ maxWidth: '150px' }}>
347
+ <Tooltip title={t('payment.checkout.staking.tooltip')} placement="top" sx={{ maxWidth: '150px' }}>
354
348
  <HelpOutline fontSize="small" sx={{ color: 'text.lighter' }} />
355
349
  </Tooltip>
356
350
  </Stack>
@@ -112,6 +112,11 @@ export function PaymentThemeProvider({
112
112
  enterTouchDelay: 3000,
113
113
  leaveTouchDelay: 100,
114
114
  },
115
+ styleOverrides: {
116
+ tooltip: {
117
+ fontSize: '0.875rem',
118
+ },
119
+ },
115
120
  },
116
121
  MuiPopover: {
117
122
  styleOverrides: {