payment-kit 1.13.15

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 (222) hide show
  1. package/.eslintrc.js +15 -0
  2. package/README.md +3 -0
  3. package/api/dev.ts +6 -0
  4. package/api/hooks/pre-start.js +12 -0
  5. package/api/src/hooks/pre-start.ts +21 -0
  6. package/api/src/index.ts +92 -0
  7. package/api/src/jobs/event.ts +72 -0
  8. package/api/src/jobs/invoice.ts +148 -0
  9. package/api/src/jobs/payment.ts +208 -0
  10. package/api/src/jobs/subscription.ts +301 -0
  11. package/api/src/jobs/webhook.ts +113 -0
  12. package/api/src/libs/audit.ts +73 -0
  13. package/api/src/libs/auth.ts +40 -0
  14. package/api/src/libs/chain/arcblock.ts +13 -0
  15. package/api/src/libs/dayjs.ts +17 -0
  16. package/api/src/libs/env.ts +5 -0
  17. package/api/src/libs/hooks.ts +42 -0
  18. package/api/src/libs/logger.ts +27 -0
  19. package/api/src/libs/middleware.ts +12 -0
  20. package/api/src/libs/payment.ts +53 -0
  21. package/api/src/libs/queue/index.ts +263 -0
  22. package/api/src/libs/queue/store.ts +47 -0
  23. package/api/src/libs/security.ts +95 -0
  24. package/api/src/libs/session.ts +164 -0
  25. package/api/src/libs/util.ts +93 -0
  26. package/api/src/locales/en.ts +3 -0
  27. package/api/src/locales/index.ts +37 -0
  28. package/api/src/locales/zh.ts +3 -0
  29. package/api/src/routes/checkout-sessions.ts +536 -0
  30. package/api/src/routes/connect/collect.ts +109 -0
  31. package/api/src/routes/connect/pay.ts +116 -0
  32. package/api/src/routes/connect/setup.ts +121 -0
  33. package/api/src/routes/connect/shared.ts +410 -0
  34. package/api/src/routes/connect/subscribe.ts +128 -0
  35. package/api/src/routes/customers.ts +70 -0
  36. package/api/src/routes/events.ts +76 -0
  37. package/api/src/routes/index.ts +59 -0
  38. package/api/src/routes/invoices.ts +126 -0
  39. package/api/src/routes/payment-currencies.ts +38 -0
  40. package/api/src/routes/payment-intents.ts +122 -0
  41. package/api/src/routes/payment-links.ts +221 -0
  42. package/api/src/routes/payment-methods.ts +39 -0
  43. package/api/src/routes/prices.ts +134 -0
  44. package/api/src/routes/products.ts +191 -0
  45. package/api/src/routes/settings.ts +33 -0
  46. package/api/src/routes/subscription-items.ts +148 -0
  47. package/api/src/routes/subscriptions.ts +254 -0
  48. package/api/src/routes/usage-records.ts +120 -0
  49. package/api/src/routes/webhook-attempts.ts +57 -0
  50. package/api/src/routes/webhook-endpoints.ts +105 -0
  51. package/api/src/store/migrate.ts +16 -0
  52. package/api/src/store/migrations/20230905-genesis.ts +52 -0
  53. package/api/src/store/migrations/20230911-seeding.ts +145 -0
  54. package/api/src/store/models/checkout-session.ts +395 -0
  55. package/api/src/store/models/coupon.ts +137 -0
  56. package/api/src/store/models/customer.ts +199 -0
  57. package/api/src/store/models/discount.ts +116 -0
  58. package/api/src/store/models/event.ts +111 -0
  59. package/api/src/store/models/index.ts +165 -0
  60. package/api/src/store/models/invoice-item.ts +185 -0
  61. package/api/src/store/models/invoice.ts +492 -0
  62. package/api/src/store/models/job.ts +75 -0
  63. package/api/src/store/models/payment-currency.ts +139 -0
  64. package/api/src/store/models/payment-intent.ts +282 -0
  65. package/api/src/store/models/payment-link.ts +219 -0
  66. package/api/src/store/models/payment-method.ts +169 -0
  67. package/api/src/store/models/price.ts +266 -0
  68. package/api/src/store/models/product.ts +162 -0
  69. package/api/src/store/models/promotion-code.ts +112 -0
  70. package/api/src/store/models/setup-intent.ts +206 -0
  71. package/api/src/store/models/subscription-item.ts +103 -0
  72. package/api/src/store/models/subscription-schedule.ts +157 -0
  73. package/api/src/store/models/subscription.ts +307 -0
  74. package/api/src/store/models/types.ts +406 -0
  75. package/api/src/store/models/usage-record.ts +132 -0
  76. package/api/src/store/models/webhook-attempt.ts +96 -0
  77. package/api/src/store/models/webhook-endpoint.ts +96 -0
  78. package/api/src/store/sequelize.ts +15 -0
  79. package/api/third.d.ts +28 -0
  80. package/blocklet.md +3 -0
  81. package/blocklet.yml +89 -0
  82. package/index.html +14 -0
  83. package/logo.png +0 -0
  84. package/package.json +133 -0
  85. package/public/.gitkeep +0 -0
  86. package/screenshots/.gitkeep +0 -0
  87. package/screenshots/1-subscription.png +0 -0
  88. package/screenshots/2-customer-1.png +0 -0
  89. package/screenshots/3-customer-2.png +0 -0
  90. package/screenshots/4-admin-3.png +0 -0
  91. package/screenshots/5-admin-4.png +0 -0
  92. package/scripts/build-clean.js +6 -0
  93. package/scripts/bump-version.mjs +35 -0
  94. package/src/app.tsx +68 -0
  95. package/src/components/actions.tsx +85 -0
  96. package/src/components/blockchain/tx.tsx +29 -0
  97. package/src/components/checkout/amount.tsx +24 -0
  98. package/src/components/checkout/error.tsx +30 -0
  99. package/src/components/checkout/footer.tsx +12 -0
  100. package/src/components/checkout/form/address.tsx +38 -0
  101. package/src/components/checkout/form/index.tsx +295 -0
  102. package/src/components/checkout/header.tsx +23 -0
  103. package/src/components/checkout/pay.tsx +222 -0
  104. package/src/components/checkout/product-card.tsx +56 -0
  105. package/src/components/checkout/product-item.tsx +37 -0
  106. package/src/components/checkout/skeleton/overview.tsx +21 -0
  107. package/src/components/checkout/skeleton/payment.tsx +35 -0
  108. package/src/components/checkout/success.tsx +183 -0
  109. package/src/components/checkout/summary.tsx +34 -0
  110. package/src/components/collapse.tsx +50 -0
  111. package/src/components/confirm.tsx +55 -0
  112. package/src/components/copyable.tsx +38 -0
  113. package/src/components/currency.tsx +15 -0
  114. package/src/components/customer/actions.tsx +73 -0
  115. package/src/components/data.tsx +20 -0
  116. package/src/components/drawer-form.tsx +77 -0
  117. package/src/components/error-fallback.tsx +7 -0
  118. package/src/components/error.tsx +39 -0
  119. package/src/components/event/list.tsx +217 -0
  120. package/src/components/info-card.tsx +40 -0
  121. package/src/components/info-metric.tsx +35 -0
  122. package/src/components/info-row.tsx +28 -0
  123. package/src/components/input.tsx +40 -0
  124. package/src/components/invoice/action.tsx +94 -0
  125. package/src/components/invoice/list.tsx +225 -0
  126. package/src/components/invoice/table.tsx +110 -0
  127. package/src/components/layout.tsx +70 -0
  128. package/src/components/livemode.tsx +23 -0
  129. package/src/components/metadata/editor.tsx +57 -0
  130. package/src/components/metadata/form.tsx +45 -0
  131. package/src/components/payment-intent/actions.tsx +81 -0
  132. package/src/components/payment-intent/list.tsx +204 -0
  133. package/src/components/payment-link/actions.tsx +114 -0
  134. package/src/components/payment-link/after-pay.tsx +87 -0
  135. package/src/components/payment-link/before-pay.tsx +175 -0
  136. package/src/components/payment-link/item.tsx +135 -0
  137. package/src/components/payment-link/product-select.tsx +66 -0
  138. package/src/components/payment-link/rename.tsx +64 -0
  139. package/src/components/portal/invoice/list.tsx +110 -0
  140. package/src/components/portal/subscription/cancel.tsx +83 -0
  141. package/src/components/portal/subscription/list.tsx +232 -0
  142. package/src/components/price/actions.tsx +21 -0
  143. package/src/components/price/form.tsx +292 -0
  144. package/src/components/product/actions.tsx +125 -0
  145. package/src/components/product/add-price.tsx +59 -0
  146. package/src/components/product/create.tsx +97 -0
  147. package/src/components/product/edit-price.tsx +75 -0
  148. package/src/components/product/edit.tsx +67 -0
  149. package/src/components/product/features.tsx +32 -0
  150. package/src/components/product/form.tsx +76 -0
  151. package/src/components/relative-time.tsx +41 -0
  152. package/src/components/section/header.tsx +29 -0
  153. package/src/components/status.tsx +12 -0
  154. package/src/components/subscription/actions/cancel.tsx +66 -0
  155. package/src/components/subscription/actions/index.tsx +172 -0
  156. package/src/components/subscription/actions/pause.tsx +83 -0
  157. package/src/components/subscription/items/actions.tsx +31 -0
  158. package/src/components/subscription/items/index.tsx +107 -0
  159. package/src/components/subscription/list.tsx +200 -0
  160. package/src/components/switch.tsx +48 -0
  161. package/src/components/table.tsx +66 -0
  162. package/src/components/uploader.tsx +81 -0
  163. package/src/components/webhook/attempts.tsx +149 -0
  164. package/src/contexts/products.tsx +42 -0
  165. package/src/contexts/session.ts +10 -0
  166. package/src/contexts/settings.tsx +54 -0
  167. package/src/env.d.ts +17 -0
  168. package/src/global.css +97 -0
  169. package/src/hooks/mobile.ts +15 -0
  170. package/src/index.tsx +6 -0
  171. package/src/libs/api.ts +19 -0
  172. package/src/libs/dayjs.ts +17 -0
  173. package/src/libs/util.ts +474 -0
  174. package/src/locales/en.tsx +395 -0
  175. package/src/locales/index.tsx +8 -0
  176. package/src/locales/zh.tsx +389 -0
  177. package/src/pages/admin/billing/index.tsx +56 -0
  178. package/src/pages/admin/billing/invoices/detail.tsx +215 -0
  179. package/src/pages/admin/billing/invoices/index.tsx +5 -0
  180. package/src/pages/admin/billing/subscriptions/detail.tsx +237 -0
  181. package/src/pages/admin/billing/subscriptions/index.tsx +5 -0
  182. package/src/pages/admin/customers/customers/detail.tsx +209 -0
  183. package/src/pages/admin/customers/customers/index.tsx +109 -0
  184. package/src/pages/admin/customers/index.tsx +47 -0
  185. package/src/pages/admin/developers/events/detail.tsx +77 -0
  186. package/src/pages/admin/developers/events/index.tsx +5 -0
  187. package/src/pages/admin/developers/index.tsx +60 -0
  188. package/src/pages/admin/developers/logs.tsx +3 -0
  189. package/src/pages/admin/developers/overview.tsx +3 -0
  190. package/src/pages/admin/developers/webhooks/detail.tsx +109 -0
  191. package/src/pages/admin/developers/webhooks/index.tsx +102 -0
  192. package/src/pages/admin/index.tsx +120 -0
  193. package/src/pages/admin/overview.tsx +3 -0
  194. package/src/pages/admin/payments/index.tsx +65 -0
  195. package/src/pages/admin/payments/intents/detail.tsx +205 -0
  196. package/src/pages/admin/payments/intents/index.tsx +5 -0
  197. package/src/pages/admin/payments/links/create.tsx +141 -0
  198. package/src/pages/admin/payments/links/detail.tsx +318 -0
  199. package/src/pages/admin/payments/links/index.tsx +167 -0
  200. package/src/pages/admin/products/coupons/index.tsx +3 -0
  201. package/src/pages/admin/products/index.tsx +81 -0
  202. package/src/pages/admin/products/prices/actions.tsx +151 -0
  203. package/src/pages/admin/products/prices/detail.tsx +203 -0
  204. package/src/pages/admin/products/prices/list.tsx +95 -0
  205. package/src/pages/admin/products/pricing-tables.tsx +3 -0
  206. package/src/pages/admin/products/products/create.tsx +105 -0
  207. package/src/pages/admin/products/products/detail.tsx +246 -0
  208. package/src/pages/admin/products/products/index.tsx +154 -0
  209. package/src/pages/admin/settings/branding.tsx +3 -0
  210. package/src/pages/admin/settings/business.tsx +3 -0
  211. package/src/pages/admin/settings/index.tsx +47 -0
  212. package/src/pages/admin/settings/payment-methods.tsx +80 -0
  213. package/src/pages/checkout/index.tsx +38 -0
  214. package/src/pages/checkout/pay.tsx +89 -0
  215. package/src/pages/customer/index.tsx +93 -0
  216. package/src/pages/customer/invoice.tsx +147 -0
  217. package/src/pages/home.tsx +9 -0
  218. package/tsconfig.api.json +9 -0
  219. package/tsconfig.eslint.json +7 -0
  220. package/tsconfig.json +99 -0
  221. package/tsconfig.types.json +11 -0
  222. package/vite.config.ts +19 -0
package/src/app.tsx ADDED
@@ -0,0 +1,68 @@
1
+ import './global.css';
2
+
3
+ import { LocaleProvider } from '@arcblock/ux/lib/Locale/context';
4
+ import { ThemeProvider, createTheme } from '@arcblock/ux/lib/Theme';
5
+ import { ToastProvider } from '@arcblock/ux/lib/Toast';
6
+ import React from 'react';
7
+ import { ErrorBoundary } from 'react-error-boundary';
8
+ import { Navigate, Route, BrowserRouter as Router, Routes } from 'react-router-dom';
9
+ import { joinURL } from 'ufo';
10
+
11
+ import ErrorFallback from './components/error-fallback';
12
+ import { SessionProvider } from './contexts/session';
13
+ import { translations } from './locales';
14
+
15
+ const HomePage = React.lazy(() => import('./pages/home'));
16
+ const CheckoutPage = React.lazy(() => import('./pages/checkout'));
17
+ const AdminPage = React.lazy(() => import('./pages/admin'));
18
+ const CustomerHome = React.lazy(() => import('./pages/customer/index'));
19
+ const CustomerInvoice = React.lazy(() => import('./pages/customer/invoice'));
20
+
21
+ const theme = createTheme({
22
+ typography: {
23
+ fontSize: 14,
24
+ allVariants: {
25
+ textTransform: 'none',
26
+ },
27
+ },
28
+ });
29
+
30
+ function App() {
31
+ return (
32
+ <ThemeProvider theme={theme}>
33
+ <LocaleProvider translations={translations} fallbackLocale="en">
34
+ <ErrorBoundary FallbackComponent={ErrorFallback} onReset={window.location.reload}>
35
+ <Routes>
36
+ <Route path="/" element={<HomePage />} />
37
+ <Route path="/checkout/:action/:id" element={<CheckoutPage />} />
38
+ <Route key="admin-index" path="/admin" element={<AdminPage />} />,
39
+ <Route key="admin-tabs" path="/admin/:group" element={<AdminPage />} />,
40
+ <Route key="admin-sub" path="/admin/:group/:page" element={<AdminPage />} />,
41
+ <Route key="admin-fallback" path="/admin/*" element={<AdminPage />} />,
42
+ <Route key="customer-home" path="/customer" element={<CustomerHome />} />,
43
+ <Route key="customer-invoice" path="/customer/invoice/:id" element={<CustomerInvoice />} />,
44
+ <Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
45
+ <Route path="*" element={<Navigate to="/" />} />
46
+ </Routes>
47
+ </ErrorBoundary>
48
+ </LocaleProvider>
49
+ </ThemeProvider>
50
+ );
51
+ }
52
+
53
+ export default function WrappedApp() {
54
+ // While the blocklet is deploy to a sub path, this will be work properly.
55
+ const prefix = window?.blocklet?.prefix || '/';
56
+
57
+ return (
58
+ <ToastProvider>
59
+ <SessionProvider
60
+ serviceHost={prefix}
61
+ protectedRoutes={['/admin/*', '/customer/*'].map((item) => joinURL(prefix, item))}>
62
+ <Router basename={prefix}>
63
+ <App />
64
+ </Router>
65
+ </SessionProvider>
66
+ </ToastProvider>
67
+ );
68
+ }
@@ -0,0 +1,85 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { ExpandMoreOutlined, MoreHorizOutlined } from '@mui/icons-material';
3
+ import { Button, IconButton, ListItemText, Menu, MenuItem } from '@mui/material';
4
+ import React, { useState } from 'react';
5
+ import type { LiteralUnion } from 'type-fest';
6
+
7
+ type ActionItem = {
8
+ label: string;
9
+ handler: Function;
10
+ color: LiteralUnion<'primary' | 'secondary' | 'error', string>;
11
+ disabled?: boolean;
12
+ divider?: boolean;
13
+ dense?: boolean;
14
+ };
15
+
16
+ export type ActionsProps = {
17
+ actions: ActionItem[];
18
+ variant?: LiteralUnion<'compact' | 'normal', string>;
19
+ sx?: any;
20
+ };
21
+
22
+ Actions.defaultProps = {
23
+ variant: 'compact',
24
+ sx: {},
25
+ };
26
+
27
+ export default function Actions(props: ActionsProps) {
28
+ const { t } = useLocaleContext();
29
+ const [anchorEl, setAnchorEl] = useState(null);
30
+ const open = Boolean(anchorEl);
31
+
32
+ const onOpen = (e: React.SyntheticEvent<any>) => {
33
+ try {
34
+ e.stopPropagation();
35
+ e.preventDefault();
36
+ // eslint-disable-next-line no-empty
37
+ } catch {}
38
+ setAnchorEl(e.currentTarget);
39
+ };
40
+
41
+ const onClose = (e: React.SyntheticEvent<any>, handler?: Function) => {
42
+ try {
43
+ e.stopPropagation();
44
+ e.preventDefault();
45
+ // eslint-disable-next-line no-empty
46
+ } catch {}
47
+ setAnchorEl(null);
48
+
49
+ if (typeof handler === 'function') {
50
+ handler();
51
+ }
52
+ };
53
+
54
+ return (
55
+ <>
56
+ {props.variant === 'compact' ? (
57
+ <IconButton aria-label="actions" sx={props.sx} aria-haspopup="true" onClick={onOpen} size="small">
58
+ <MoreHorizOutlined />
59
+ </IconButton>
60
+ ) : (
61
+ <Button sx={props.sx} onClick={onOpen} size="small" variant="contained" color="primary">
62
+ {t('common.actions')} <ExpandMoreOutlined fontSize="small" />
63
+ </Button>
64
+ )}
65
+ <Menu
66
+ anchorEl={anchorEl}
67
+ open={open}
68
+ // @ts-ignore
69
+ onClose={onClose}
70
+ anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
71
+ transformOrigin={{ vertical: 'top', horizontal: 'right' }}>
72
+ {props.actions.map((action) => (
73
+ <MenuItem
74
+ key={action.label}
75
+ divider={!!action.divider}
76
+ dense={!!action.dense}
77
+ disabled={!!action.disabled}
78
+ onClick={(e) => onClose(e, action.handler)}>
79
+ <ListItemText primary={action.label} primaryTypographyProps={{ color: action.color }} />
80
+ </MenuItem>
81
+ ))}
82
+ </Menu>
83
+ </>
84
+ );
85
+ }
@@ -0,0 +1,29 @@
1
+ import type { TPaymentMethod } from '@did-pay/types';
2
+ import { OpenInNewOutlined } from '@mui/icons-material';
3
+ import { Link, Stack, Typography } from '@mui/material';
4
+ import { joinURL } from 'ufo';
5
+
6
+ const getTxLink = (method: TPaymentMethod, hash: string) => {
7
+ if (method.type === 'arcblock') {
8
+ return joinURL(method.settings.arcblock?.explorer_host as string, '/tx', hash);
9
+ }
10
+ if (method.type === 'bitcoin') {
11
+ return joinURL(method.settings.bitcoin?.explorer_host as string, '/tx', hash);
12
+ }
13
+ if (method.type === 'ethereum') {
14
+ return joinURL(method.settings.ethereum?.explorer_host as string, '/tx', hash);
15
+ }
16
+
17
+ return hash;
18
+ };
19
+
20
+ export default function TxLink(props: { hash: string; method: TPaymentMethod }) {
21
+ return (
22
+ <Link href={getTxLink(props.method, props.hash)} target="_blank" rel="noopener noreferrer">
23
+ <Stack component="span" direction="row" alignItems="center" spacing={1}>
24
+ <Typography component="span">{props.hash}</Typography>
25
+ <OpenInNewOutlined fontSize="small" />
26
+ </Stack>
27
+ </Link>
28
+ );
29
+ }
@@ -0,0 +1,24 @@
1
+ import { Typography } from '@mui/material';
2
+
3
+ type Props = { amount: string; sx?: any };
4
+
5
+ export default function PaymentAmount({ amount, sx }: Props) {
6
+ return (
7
+ <Typography
8
+ sx={{
9
+ my: 0.5,
10
+ fontWeight: 600,
11
+ fontSize: '2.5rem',
12
+ lineHeight: '1.3',
13
+ letterSpacing: '-0.03rem',
14
+ fontVariantNumeric: 'tabular-nums',
15
+ ...sx,
16
+ }}>
17
+ {amount}
18
+ </Typography>
19
+ );
20
+ }
21
+
22
+ PaymentAmount.defaultProps = {
23
+ sx: {},
24
+ };
@@ -0,0 +1,30 @@
1
+ import { Button, Stack, Typography } from '@mui/material';
2
+ import { Link } from 'react-router-dom';
3
+
4
+ type Props = {
5
+ title: string;
6
+ description: string;
7
+ button?: string;
8
+ };
9
+
10
+ export default function PaymentError({ title, description, button }: Props) {
11
+ return (
12
+ <Stack sx={{ height: '100vh' }} alignItems="center" justifyContent="center">
13
+ <Stack sx={{ width: '280px' }} direction="column" alignItems="center" justifyContent="center">
14
+ <Typography variant="h5" sx={{ mb: 2 }}>
15
+ {title}
16
+ </Typography>
17
+ <Typography variant="body1" sx={{ mb: 2, textAlign: 'center' }}>
18
+ {description}
19
+ </Typography>
20
+ <Button variant="text" size="small" component={Link} to={window.blocklet.appUrl}>
21
+ {button}
22
+ </Button>
23
+ </Stack>
24
+ </Stack>
25
+ );
26
+ }
27
+
28
+ PaymentError.defaultProps = {
29
+ button: 'Back',
30
+ };
@@ -0,0 +1,12 @@
1
+ import { Typography } from '@mui/material';
2
+
3
+ export default function CheckoutFooter({ ...props }) {
4
+ return (
5
+ <Typography color="text.secondary" fontSize={12} {...props}>
6
+ Powered by{' '}
7
+ <Typography component="span" sx={{ fontWeight: 'bold', fontSize: 12 }}>
8
+ ArcBlock
9
+ </Typography>
10
+ </Typography>
11
+ );
12
+ }
@@ -0,0 +1,38 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Fade, Stack, Typography } from '@mui/material';
3
+
4
+ import FormInput from '../../input';
5
+
6
+ type Props = {
7
+ mode: string;
8
+ };
9
+
10
+ export default function AddressForm({ mode }: Props) {
11
+ const { t } = useLocaleContext();
12
+ return (
13
+ <Fade in>
14
+ <Stack className="cko-payment-address cko-payment-form">
15
+ <Typography sx={{ mb: 1, color: 'text.primary', fontWeight: 600 }}>{t(`checkout.billing.${mode}`)}</Typography>
16
+ <Stack direction="column" className="cko-payment-form" spacing={0}>
17
+ <FormInput name="billing_address.country" variant="outlined" placeholder={t('checkout.billing.country')} />
18
+ {mode === 'required' && (
19
+ <FormInput name="billing_address.state" variant="outlined" placeholder={t('checkout.billing.state')} />
20
+ )}
21
+ {mode === 'required' && (
22
+ <FormInput name="billing_address.line1" variant="outlined" placeholder={t('checkout.billing.line1')} />
23
+ )}
24
+ <Stack direction="row" spacing={0}>
25
+ {mode === 'required' && (
26
+ <FormInput name="billing_address.city" variant="outlined" placeholder={t('checkout.billing.city')} />
27
+ )}
28
+ <FormInput
29
+ name="billing_address.postal_code"
30
+ variant="outlined"
31
+ placeholder={t('checkout.billing.postal_code')}
32
+ />
33
+ </Stack>
34
+ </Stack>
35
+ </Stack>
36
+ </Fade>
37
+ );
38
+ }
@@ -0,0 +1,295 @@
1
+ import SessionManager from '@arcblock/did-connect/lib/SessionManager';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import LocaleSelector from '@arcblock/ux/lib/Locale/selector';
4
+ import Toast from '@arcblock/ux/lib/Toast';
5
+ import type { TCheckoutSessionExpanded, TCustomer, TPaymentIntent, TPaymentMethodExpanded } from '@did-pay/types';
6
+ import { InfoOutlined } from '@mui/icons-material';
7
+ import { LoadingButton } from '@mui/lab';
8
+ import { Avatar, Fade, InputAdornment, MenuItem, Select, Stack, Tooltip, Typography } from '@mui/material';
9
+ import { useSetState } from 'ahooks';
10
+ import pWaitFor from 'p-wait-for';
11
+ import { useEffect } from 'react';
12
+ import { Controller, FormProvider, useForm, useFormContext } from 'react-hook-form';
13
+
14
+ import { useSessionContext } from '../../../contexts/session';
15
+ import api from '../../../libs/api';
16
+ import { formatError, getStatementDescriptor } from '../../../libs/util';
17
+ import FormInput from '../../input';
18
+ import AddressForm from './address';
19
+
20
+ const waitForCheckoutComplete = (sessionId: string) => {
21
+ return pWaitFor(
22
+ async () => {
23
+ const { data } = await api.get(`/api/checkout-sessions/retrieve/${sessionId}`);
24
+ return (
25
+ data.checkoutSession?.status === 'complete' &&
26
+ ['paid', 'no_payment_required'].includes(data.checkoutSession?.payment_status)
27
+ );
28
+ },
29
+ { interval: 2000, timeout: 3 * 60 * 1000 }
30
+ );
31
+ };
32
+
33
+ type PageData = {
34
+ onPaid: Function;
35
+ onError: Function;
36
+ checkoutSession: TCheckoutSessionExpanded;
37
+ paymentMethods: TPaymentMethodExpanded[];
38
+ paymentIntent?: TPaymentIntent;
39
+ customer?: TCustomer;
40
+ };
41
+
42
+ PaymentFormInner.defaultProps = {
43
+ paymentIntent: null,
44
+ customer: null,
45
+ };
46
+
47
+ // FIXME: https://stripe.com/docs/elements/address-element
48
+ // FIXME: https://github.com/goveo/react-international-phone | https://catamphetamine.gitlab.io/react-phone-number-input/
49
+ // TODO: https://github.com/rocktimsaikia/react-country-dropdown
50
+ // TODO: https://country-regions.github.io/react-country-region-selector/
51
+ // FIXME: add form validation
52
+ export function PaymentFormInner({ checkoutSession, paymentMethods, paymentIntent, onPaid, onError }: PageData) {
53
+ const { t } = useLocaleContext();
54
+ const { session, connectApi } = useSessionContext();
55
+ const { control, getValues, setValue, handleSubmit } = useFormContext();
56
+ const [state, setState] = useSetState<{
57
+ submitting: boolean;
58
+ paying: boolean;
59
+ paid: boolean;
60
+ paymentIntent?: TPaymentIntent;
61
+ }>({
62
+ submitting: false,
63
+ paying: false,
64
+ paid: false,
65
+ paymentIntent,
66
+ });
67
+
68
+ useEffect(() => {
69
+ if (session.user) {
70
+ const values = getValues();
71
+ if (!values.customer_name) {
72
+ setValue('customer_name', session.user.fullName);
73
+ }
74
+ if (!values.customer_email) {
75
+ setValue('customer_email', session.user.email);
76
+ }
77
+ if (!values.customer_phone) {
78
+ setValue('customer_phone', session.user.phone);
79
+ }
80
+ }
81
+ }, [session.user, getValues, setValue]);
82
+
83
+ const currencies = paymentMethods.find((x) => x.id === getValues().payment_method)?.payment_currencies || [];
84
+ const payee = getStatementDescriptor(checkoutSession.line_items);
85
+ const buttonText = session.user
86
+ ? t(`checkout.${checkoutSession.mode}`)
87
+ : t('checkout.connect', { action: t(`checkout.${checkoutSession.mode}`) });
88
+
89
+ const handleConnected = async () => {
90
+ try {
91
+ await waitForCheckoutComplete(checkoutSession.id);
92
+ setState({ paid: true, paying: false });
93
+ onPaid();
94
+ } catch (err) {
95
+ Toast.error(formatError(err));
96
+ } finally {
97
+ setState({ paying: false });
98
+ }
99
+ };
100
+
101
+ const onSubmit = async (data: any) => {
102
+ setState({ submitting: true });
103
+ try {
104
+ const result = await api.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, data);
105
+ setState({ paymentIntent: result.data.paymentIntent, submitting: false, paying: true });
106
+ if (result.data.delegation.sufficient) {
107
+ await handleConnected();
108
+ } else {
109
+ connectApi.open({
110
+ action: checkoutSession.mode,
111
+ timeout: 5 * 60 * 1000,
112
+ extraParams: { checkoutSessionId: checkoutSession.id },
113
+ onSuccess: async () => {
114
+ connectApi.close();
115
+ await handleConnected();
116
+ },
117
+ onClose: () => {
118
+ connectApi.close();
119
+ setState({ submitting: false, paying: false });
120
+ },
121
+ onError: (err: any) => {
122
+ setState({ submitting: false, paying: false });
123
+ onError(err);
124
+ },
125
+ });
126
+ }
127
+ } catch (err) {
128
+ Toast.error(formatError(err));
129
+ } finally {
130
+ setState({ submitting: false });
131
+ }
132
+ };
133
+
134
+ const onAction = async () => {
135
+ if (session.user) {
136
+ await handleSubmit(onSubmit)();
137
+ } else {
138
+ session.login({
139
+ onSuccess: () => {},
140
+ extraParams: {},
141
+ });
142
+ }
143
+ };
144
+
145
+ return (
146
+ <>
147
+ <Fade in>
148
+ <Stack className="cko-payment-contact">
149
+ <Stack direction="row" sx={{ mb: 1 }} alignItems="center" justifyContent="space-between">
150
+ <Typography sx={{ color: 'text.primary', fontWeight: 600 }}>{t('checkout.contact')}</Typography>
151
+ <Stack direction="row" alignItems="center" justifyContent="space-between">
152
+ <LocaleSelector showText={false} />
153
+ <SessionManager session={session} />
154
+ </Stack>
155
+ </Stack>
156
+ <Stack direction="column" className="cko-payment-form" spacing={0}>
157
+ <FormInput
158
+ name="customer_name"
159
+ variant="outlined"
160
+ InputProps={{
161
+ startAdornment: <InputAdornment position="start">{t('checkout.customer.name')}</InputAdornment>,
162
+ }}
163
+ />
164
+ <FormInput
165
+ name="customer_email"
166
+ variant="outlined"
167
+ InputProps={{
168
+ startAdornment: <InputAdornment position="start">{t('checkout.customer.email')}</InputAdornment>,
169
+ }}
170
+ />
171
+ {checkoutSession.phone_number_collection?.enabled && (
172
+ <FormInput
173
+ name="customer_phone"
174
+ variant="outlined"
175
+ InputProps={{
176
+ startAdornment: <InputAdornment position="start">{t('checkout.customer.phone')}</InputAdornment>,
177
+ endAdornment: (
178
+ <InputAdornment position="end">
179
+ <Tooltip title={t('checkout.customer.phoneTip')} arrow>
180
+ <InfoOutlined fontSize="small" sx={{ cursor: 'pointer' }} />
181
+ </Tooltip>
182
+ </InputAdornment>
183
+ ),
184
+ }}
185
+ />
186
+ )}
187
+ </Stack>
188
+ </Stack>
189
+ </Fade>
190
+ <AddressForm mode={checkoutSession.billing_address_collection as string} />
191
+ <Fade in>
192
+ <Stack className="cko-payment-methods">
193
+ <Typography sx={{ mb: 2, color: 'text.primary', fontWeight: 600 }}>{t('checkout.method')}</Typography>
194
+ <Stack direction="row" spacing={1}>
195
+ <Controller
196
+ name="payment_method"
197
+ control={control}
198
+ render={({ field }) => (
199
+ <Select {...field} sx={{ flex: 1 }} size="small">
200
+ {paymentMethods.map((x) => {
201
+ const selected = x.id === getValues().payment_method;
202
+ return (
203
+ <MenuItem key={x.id} value={x.id}>
204
+ <Stack direction="row" spacing={1}>
205
+ <Avatar src={x.logo} alt={x.name} sx={{ width: 20, height: 20 }} />
206
+ <Typography color={selected ? 'text.primary' : 'text.secondary'}>{x.name}</Typography>
207
+ </Stack>
208
+ </MenuItem>
209
+ );
210
+ })}
211
+ </Select>
212
+ )}
213
+ />
214
+ <Controller
215
+ name="payment_currency"
216
+ control={control}
217
+ render={({ field }) => (
218
+ <Select {...field} sx={{ flex: 1 }} size="small">
219
+ {currencies.map((x) => {
220
+ const selected = x.id === getValues().payment_currency;
221
+ return (
222
+ <MenuItem key={x.id} value={x.id}>
223
+ <Stack direction="row" spacing={1}>
224
+ <Avatar src={x.logo} alt={x.name} sx={{ width: 20, height: 20 }} />
225
+ <Typography color={selected ? 'text.primary' : 'text.secondary'}>{x.symbol}</Typography>
226
+ </Stack>
227
+ </MenuItem>
228
+ );
229
+ })}
230
+ </Select>
231
+ )}
232
+ />
233
+ </Stack>
234
+ </Stack>
235
+ </Fade>
236
+ <Fade in>
237
+ <Stack className="cko-payment-submit">
238
+ <LoadingButton
239
+ variant="contained"
240
+ color="primary"
241
+ size="large"
242
+ onClick={onAction}
243
+ fullWidth
244
+ loadingPosition="end"
245
+ loading={state.submitting || state.paying}>
246
+ {state.submitting || state.paying ? t('checkout.processing') : buttonText}
247
+ </LoadingButton>
248
+ {['subscription', 'setup'].includes(checkoutSession.mode) && (
249
+ <Typography
250
+ sx={{ mt: 1, color: 'text.secondary', fontSize: '0.9rem', lineHeight: '1.1rem', textAlign: 'center' }}>
251
+ {t('checkout.confirm', { payee })}
252
+ </Typography>
253
+ )}
254
+ </Stack>
255
+ </Fade>
256
+ </>
257
+ );
258
+ }
259
+
260
+ export default function PaymentForm(props: PageData) {
261
+ const { session } = useSessionContext();
262
+ const { customer, checkoutSession, paymentMethods } = props;
263
+
264
+ const methods = useForm({
265
+ defaultValues: {
266
+ customer_name: customer?.name || session.user?.fullName || '',
267
+ customer_email: customer?.email || session.user?.email || '',
268
+ customer_phone: customer?.phone || session.user?.phone || '',
269
+ payment_method: paymentMethods[0]?.id || '', // FIXME: use default payment method
270
+ payment_currency: checkoutSession.currency_id || '',
271
+ billing_address: Object.assign(
272
+ {
273
+ country: '',
274
+ state: '',
275
+ city: '',
276
+ line1: '',
277
+ line2: '',
278
+ postal_code: '',
279
+ },
280
+ customer?.address || {}
281
+ ),
282
+ },
283
+ });
284
+
285
+ return (
286
+ <FormProvider {...methods}>
287
+ <PaymentFormInner {...props} />
288
+ </FormProvider>
289
+ );
290
+ }
291
+
292
+ PaymentForm.defaultProps = {
293
+ paymentIntent: null,
294
+ customer: null,
295
+ };
@@ -0,0 +1,23 @@
1
+ import type { TCheckoutSessionExpanded } from '@did-pay/types';
2
+ import { Avatar, Stack, Typography } from '@mui/material';
3
+ import { useLocalStorageState } from 'ahooks';
4
+
5
+ import { getStatementDescriptor } from '../../libs/util';
6
+ import Livemode from '../livemode';
7
+
8
+ type Props = {
9
+ checkoutSession: TCheckoutSessionExpanded;
10
+ };
11
+
12
+ export default function PaymentHeader({ checkoutSession }: Props) {
13
+ const [livemode] = useLocalStorageState('livemode', { defaultValue: true });
14
+ const brand = getStatementDescriptor(checkoutSession.line_items);
15
+
16
+ return (
17
+ <Stack className="cko-header" direction="row" spacing={1} alignItems="center">
18
+ <Avatar src={window.blocklet.appLogo} sx={{ width: 32, height: 32 }} />
19
+ <Typography sx={{ color: 'text.primary', fontWeight: 600 }}>{brand}</Typography>
20
+ {!livemode && <Livemode />}
21
+ </Stack>
22
+ );
23
+ }