@blocklet/payment-react 1.13.113

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 (173) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +29 -0
  3. package/babel.config.es.js +8 -0
  4. package/build.config.ts +29 -0
  5. package/es/api.d.ts +2 -0
  6. package/es/api.js +18 -0
  7. package/es/checkout/index.d.ts +15 -0
  8. package/es/checkout/index.js +61 -0
  9. package/es/components/input.d.ts +23 -0
  10. package/es/components/input.js +44 -0
  11. package/es/components/livemode.d.ts +2 -0
  12. package/es/components/livemode.js +24 -0
  13. package/es/components/pricing-table.d.ts +18 -0
  14. package/es/components/pricing-table.js +175 -0
  15. package/es/components/status.d.ts +3 -0
  16. package/es/components/status.js +20 -0
  17. package/es/components/switch.d.ts +6 -0
  18. package/es/components/switch.js +42 -0
  19. package/es/contexts/payment.d.ts +29 -0
  20. package/es/contexts/payment.js +45 -0
  21. package/es/dayjs.d.ts +2 -0
  22. package/es/dayjs.js +14 -0
  23. package/es/index.d.ts +16 -0
  24. package/es/index.js +29 -0
  25. package/es/locales/en.d.ts +2 -0
  26. package/es/locales/en.js +213 -0
  27. package/es/locales/index.d.ts +10 -0
  28. package/es/locales/index.js +20 -0
  29. package/es/locales/zh.d.ts +2 -0
  30. package/es/locales/zh.js +213 -0
  31. package/es/payment/amount.d.ts +12 -0
  32. package/es/payment/amount.js +22 -0
  33. package/es/payment/error.d.ts +13 -0
  34. package/es/payment/error.js +12 -0
  35. package/es/payment/footer.d.ts +4 -0
  36. package/es/payment/footer.js +9 -0
  37. package/es/payment/form/addon.d.ts +2 -0
  38. package/es/payment/form/addon.js +14 -0
  39. package/es/payment/form/address.d.ts +7 -0
  40. package/es/payment/form/address.js +119 -0
  41. package/es/payment/form/index.d.ts +9 -0
  42. package/es/payment/form/index.js +337 -0
  43. package/es/payment/form/phone.d.ts +4 -0
  44. package/es/payment/form/phone.js +97 -0
  45. package/es/payment/form/stripe.d.ts +13 -0
  46. package/es/payment/form/stripe.js +158 -0
  47. package/es/payment/header.d.ts +7 -0
  48. package/es/payment/header.js +29 -0
  49. package/es/payment/index.d.ts +28 -0
  50. package/es/payment/index.js +327 -0
  51. package/es/payment/product-card.d.ts +21 -0
  52. package/es/payment/product-card.js +34 -0
  53. package/es/payment/product-item.d.ts +19 -0
  54. package/es/payment/product-item.js +107 -0
  55. package/es/payment/product-skeleton.d.ts +4 -0
  56. package/es/payment/product-skeleton.js +34 -0
  57. package/es/payment/skeleton/overview.d.ts +2 -0
  58. package/es/payment/skeleton/overview.js +13 -0
  59. package/es/payment/skeleton/payment.d.ts +2 -0
  60. package/es/payment/skeleton/payment.js +19 -0
  61. package/es/payment/success.d.ts +8 -0
  62. package/es/payment/success.js +164 -0
  63. package/es/payment/summary.d.ts +12 -0
  64. package/es/payment/summary.js +178 -0
  65. package/es/theme.d.ts +1 -0
  66. package/es/theme.js +17 -0
  67. package/es/types/index.d.ts +19 -0
  68. package/es/types/index.js +0 -0
  69. package/es/types/shims.d.ts +18 -0
  70. package/es/util.d.ts +52 -0
  71. package/es/util.js +390 -0
  72. package/lib/api.d.ts +2 -0
  73. package/lib/api.js +26 -0
  74. package/lib/checkout/index.d.ts +15 -0
  75. package/lib/checkout/index.js +83 -0
  76. package/lib/components/input.d.ts +23 -0
  77. package/lib/components/input.js +72 -0
  78. package/lib/components/livemode.d.ts +2 -0
  79. package/lib/components/livemode.js +29 -0
  80. package/lib/components/pricing-table.d.ts +18 -0
  81. package/lib/components/pricing-table.js +232 -0
  82. package/lib/components/status.d.ts +3 -0
  83. package/lib/components/status.js +23 -0
  84. package/lib/components/switch.d.ts +6 -0
  85. package/lib/components/switch.js +51 -0
  86. package/lib/contexts/payment.d.ts +29 -0
  87. package/lib/contexts/payment.js +73 -0
  88. package/lib/dayjs.d.ts +2 -0
  89. package/lib/dayjs.js +21 -0
  90. package/lib/index.d.ts +16 -0
  91. package/lib/index.js +143 -0
  92. package/lib/locales/en.d.ts +2 -0
  93. package/lib/locales/en.js +220 -0
  94. package/lib/locales/index.d.ts +10 -0
  95. package/lib/locales/index.js +33 -0
  96. package/lib/locales/zh.d.ts +2 -0
  97. package/lib/locales/zh.js +220 -0
  98. package/lib/payment/amount.d.ts +12 -0
  99. package/lib/payment/amount.js +28 -0
  100. package/lib/payment/error.d.ts +13 -0
  101. package/lib/payment/error.js +52 -0
  102. package/lib/payment/footer.d.ts +4 -0
  103. package/lib/payment/footer.js +25 -0
  104. package/lib/payment/form/addon.d.ts +2 -0
  105. package/lib/payment/form/addon.js +37 -0
  106. package/lib/payment/form/address.d.ts +7 -0
  107. package/lib/payment/form/address.js +152 -0
  108. package/lib/payment/form/index.d.ts +9 -0
  109. package/lib/payment/form/index.js +464 -0
  110. package/lib/payment/form/phone.d.ts +4 -0
  111. package/lib/payment/form/phone.js +133 -0
  112. package/lib/payment/form/stripe.d.ts +13 -0
  113. package/lib/payment/form/stripe.js +213 -0
  114. package/lib/payment/header.d.ts +7 -0
  115. package/lib/payment/header.js +58 -0
  116. package/lib/payment/index.d.ts +28 -0
  117. package/lib/payment/index.js +382 -0
  118. package/lib/payment/product-card.d.ts +21 -0
  119. package/lib/payment/product-card.js +81 -0
  120. package/lib/payment/product-item.d.ts +19 -0
  121. package/lib/payment/product-item.js +160 -0
  122. package/lib/payment/product-skeleton.d.ts +4 -0
  123. package/lib/payment/product-skeleton.js +71 -0
  124. package/lib/payment/skeleton/overview.d.ts +2 -0
  125. package/lib/payment/skeleton/overview.js +48 -0
  126. package/lib/payment/skeleton/payment.d.ts +2 -0
  127. package/lib/payment/skeleton/payment.js +54 -0
  128. package/lib/payment/success.d.ts +8 -0
  129. package/lib/payment/success.js +215 -0
  130. package/lib/payment/summary.d.ts +12 -0
  131. package/lib/payment/summary.js +225 -0
  132. package/lib/theme.d.ts +1 -0
  133. package/lib/theme.js +19 -0
  134. package/lib/types/index.d.ts +19 -0
  135. package/lib/types/index.js +1 -0
  136. package/lib/types/shims.d.ts +18 -0
  137. package/lib/util.d.ts +52 -0
  138. package/lib/util.js +487 -0
  139. package/package.json +104 -0
  140. package/src/api.ts +24 -0
  141. package/src/checkout/index.tsx +74 -0
  142. package/src/components/input.tsx +58 -0
  143. package/src/components/livemode.tsx +23 -0
  144. package/src/components/pricing-table.tsx +207 -0
  145. package/src/components/status.tsx +19 -0
  146. package/src/components/switch.tsx +48 -0
  147. package/src/contexts/payment.tsx +74 -0
  148. package/src/dayjs.ts +17 -0
  149. package/src/index.ts +32 -0
  150. package/src/locales/en.tsx +218 -0
  151. package/src/locales/index.tsx +30 -0
  152. package/src/locales/zh.tsx +214 -0
  153. package/src/payment/amount.tsx +24 -0
  154. package/src/payment/error.tsx +29 -0
  155. package/src/payment/footer.tsx +12 -0
  156. package/src/payment/form/addon.tsx +24 -0
  157. package/src/payment/form/address.tsx +119 -0
  158. package/src/payment/form/index.tsx +401 -0
  159. package/src/payment/form/phone.tsx +103 -0
  160. package/src/payment/form/stripe.tsx +195 -0
  161. package/src/payment/header.tsx +40 -0
  162. package/src/payment/index.tsx +367 -0
  163. package/src/payment/product-card.tsx +55 -0
  164. package/src/payment/product-item.tsx +121 -0
  165. package/src/payment/product-skeleton.tsx +39 -0
  166. package/src/payment/skeleton/overview.tsx +21 -0
  167. package/src/payment/skeleton/payment.tsx +35 -0
  168. package/src/payment/success.tsx +186 -0
  169. package/src/payment/summary.tsx +198 -0
  170. package/src/theme.ts +18 -0
  171. package/src/types/index.ts +29 -0
  172. package/src/types/shims.d.ts +18 -0
  173. package/src/util.ts +543 -0
@@ -0,0 +1,40 @@
1
+ import { useTheme } from '@arcblock/ux/lib/Theme';
2
+ import type { TCheckoutSessionExpanded } from '@blocklet/payment-types';
3
+ import { Avatar, Stack, Typography } from '@mui/material';
4
+ import { useCreation, useLocalStorageState, useSize } from 'ahooks';
5
+
6
+ import Livemode from '../components/livemode';
7
+ import { getStatementDescriptor } from '../util';
8
+ import UserButtons from './form/addon';
9
+
10
+ type Props = {
11
+ checkoutSession: TCheckoutSessionExpanded;
12
+ };
13
+
14
+ export default function PaymentHeader({ checkoutSession }: Props) {
15
+ const [livemode] = useLocalStorageState('livemode', { defaultValue: true });
16
+ const brand = getStatementDescriptor(checkoutSession.line_items);
17
+ const theme = useTheme();
18
+
19
+ const domSize = useSize(document.body);
20
+
21
+ const isColumnLayout = useCreation(() => {
22
+ if (domSize) {
23
+ if (domSize?.width <= theme.breakpoints.values.md) {
24
+ return true;
25
+ }
26
+ }
27
+ return false;
28
+ }, [domSize, theme]);
29
+
30
+ return (
31
+ <Stack className="cko-header" direction="row" spacing={1} alignItems="center" justifyContent="space-between">
32
+ <Stack direction="row" spacing={1} alignItems="center">
33
+ <Avatar variant="square" src={window.blocklet.appLogo} sx={{ width: 32, height: 32 }} />
34
+ <Typography sx={{ color: 'text.primary', fontWeight: 600 }}>{brand}</Typography>
35
+ {!livemode && <Livemode />}
36
+ </Stack>
37
+ {isColumnLayout ? <UserButtons /> : null}
38
+ </Stack>
39
+ );
40
+ }
@@ -0,0 +1,367 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TCustomer, TPaymentCurrency, TPaymentMethodExpanded } from '@blocklet/payment-types';
4
+ import { Box, Fade, Stack } from '@mui/material';
5
+ import { styled } from '@mui/system';
6
+ import { useSetState } from 'ahooks';
7
+ import { useEffect } from 'react';
8
+ import { FormProvider, useForm } from 'react-hook-form';
9
+
10
+ import api from '../api';
11
+ import { usePaymentContext } from '../contexts/payment';
12
+ import { CheckoutCallbacks, CheckoutContext } from '../types';
13
+ import { findCurrency, formatError, getStatementDescriptor, isValidCountry } from '../util';
14
+ import PaymentError from './error';
15
+ import CheckoutFooter from './footer';
16
+ import PaymentForm from './form';
17
+ import PaymentHeader from './header';
18
+ import OverviewSkeleton from './skeleton/overview';
19
+ import PaymentSkeleton from './skeleton/payment';
20
+ import PaymentSuccess from './success';
21
+ import PaymentSummary from './summary';
22
+
23
+ type Props = CheckoutContext & CheckoutCallbacks & { completed?: boolean; error?: any };
24
+
25
+ Payment.defaultProps = {
26
+ completed: false,
27
+ error: null,
28
+ };
29
+
30
+ export default function Payment({
31
+ checkoutSession,
32
+ paymentMethods,
33
+ paymentIntent,
34
+ paymentLink,
35
+ customer,
36
+ completed,
37
+ error,
38
+ mode,
39
+ onPaid,
40
+ onError,
41
+ }: Props) {
42
+ const { t } = useLocaleContext();
43
+ const { refresh, livemode, setLivemode } = usePaymentContext();
44
+
45
+ useEffect(() => {
46
+ if (checkoutSession) {
47
+ if (livemode !== checkoutSession.livemode) {
48
+ setLivemode(checkoutSession.livemode);
49
+ setTimeout(() => {
50
+ refresh();
51
+ }, 10);
52
+ }
53
+ }
54
+ }, [checkoutSession, livemode, setLivemode, refresh]);
55
+
56
+ if (error) {
57
+ return <PaymentError title="Oops" description={formatError(error)} />;
58
+ }
59
+
60
+ if (!checkoutSession) {
61
+ return (
62
+ <Root mode={mode}>
63
+ <Stack className="cko-container">
64
+ <Stack className="cko-overview">
65
+ <OverviewSkeleton />
66
+ </Stack>
67
+ <Stack className="cko-payment">
68
+ <PaymentSkeleton />
69
+ </Stack>
70
+ <CheckoutFooter className="cko-footer" />
71
+ </Stack>
72
+ </Root>
73
+ );
74
+ }
75
+
76
+ // expired session
77
+ if (checkoutSession.expires_at <= Math.round(Date.now() / 1000)) {
78
+ return (
79
+ <PaymentError
80
+ title={t('payment.checkout.expired.title')}
81
+ description={t('payment.checkout.expired.description')}
82
+ />
83
+ );
84
+ }
85
+
86
+ // completed session
87
+ if (checkoutSession.status === 'complete') {
88
+ return (
89
+ <PaymentError
90
+ title={t('payment.checkout.complete.title')}
91
+ description={t('payment.checkout.complete.description')}
92
+ />
93
+ );
94
+ }
95
+
96
+ return (
97
+ <PaymentInner
98
+ checkoutSession={checkoutSession}
99
+ paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
100
+ paymentLink={paymentLink}
101
+ paymentIntent={paymentIntent}
102
+ completed={completed}
103
+ customer={customer as TCustomer}
104
+ onPaid={onPaid}
105
+ onError={onError}
106
+ mode={mode}
107
+ />
108
+ );
109
+ }
110
+
111
+ type MainProps = CheckoutContext & CheckoutCallbacks & { completed?: boolean };
112
+
113
+ PaymentInner.defaultProps = {
114
+ completed: false,
115
+ };
116
+
117
+ export function PaymentInner({
118
+ checkoutSession,
119
+ paymentMethods,
120
+ paymentLink,
121
+ paymentIntent,
122
+ customer,
123
+ completed,
124
+ mode,
125
+ onPaid,
126
+ onError,
127
+ }: MainProps) {
128
+ const { t } = useLocaleContext();
129
+ const { settings, session } = usePaymentContext();
130
+ const [state, setState] = useSetState({ checkoutSession });
131
+
132
+ const defaultCurrencyId = state.checkoutSession.currency_id || state.checkoutSession.line_items[0]?.price.currency_id;
133
+ const defaultMethodId = paymentMethods.find((m) => m.payment_currencies.some((c) => c.id === defaultCurrencyId))?.id;
134
+
135
+ const methods = useForm({
136
+ defaultValues: {
137
+ customer_name: customer?.name || session.user?.fullName || '',
138
+ customer_email: customer?.email || session.user?.email || '',
139
+ customer_phone: customer?.phone || session.user?.phone || '',
140
+ payment_method: defaultMethodId,
141
+ payment_currency: defaultCurrencyId,
142
+ billing_address: Object.assign(
143
+ {
144
+ country: '',
145
+ state: '',
146
+ city: '',
147
+ line1: '',
148
+ line2: '',
149
+ postal_code: '',
150
+ },
151
+ customer?.address || {},
152
+ { country: isValidCountry(customer?.address?.country || '') ? customer?.address?.country : 'us' }
153
+ ),
154
+ },
155
+ });
156
+
157
+ const currencyId = methods.watch('payment_currency') as string;
158
+ const currency =
159
+ (findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) ||
160
+ settings.baseCurrency;
161
+
162
+ const onUpsell = async (from: string, to: string) => {
163
+ try {
164
+ const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/upsell`, { from, to });
165
+ setState({ checkoutSession: data });
166
+ } catch (err) {
167
+ console.error(err);
168
+ Toast.error(formatError(err));
169
+ }
170
+ };
171
+
172
+ const onDownsell = async (from: string) => {
173
+ try {
174
+ const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/downsell`, { from });
175
+ setState({ checkoutSession: data });
176
+ } catch (err) {
177
+ console.error(err);
178
+ Toast.error(formatError(err));
179
+ }
180
+ };
181
+
182
+ const onApplyCrossSell = async (to: string) => {
183
+ try {
184
+ const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`, { to });
185
+ setState({ checkoutSession: data });
186
+ } catch (err) {
187
+ console.error(err);
188
+ Toast.error(formatError(err));
189
+ }
190
+ };
191
+
192
+ const onCancelCrossSell = async () => {
193
+ try {
194
+ const { data } = await api.delete(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`);
195
+ setState({ checkoutSession: data });
196
+ } catch (err) {
197
+ console.error(err);
198
+ Toast.error(formatError(err));
199
+ }
200
+ };
201
+
202
+ return (
203
+ <FormProvider {...methods}>
204
+ <Root mode={mode}>
205
+ <Stack className="cko-container">
206
+ <Fade in>
207
+ <Stack className="cko-overview" direction="column">
208
+ <PaymentHeader checkoutSession={state.checkoutSession} />
209
+ <PaymentSummary
210
+ checkoutSession={state.checkoutSession}
211
+ currency={currency}
212
+ onUpsell={onUpsell}
213
+ onDownsell={onDownsell}
214
+ onApplyCrossSell={onApplyCrossSell}
215
+ onCancelCrossSell={onCancelCrossSell}
216
+ />
217
+ </Stack>
218
+ </Fade>
219
+ <Stack className="cko-payment" direction="column" spacing={4}>
220
+ {completed && (
221
+ <PaymentSuccess
222
+ payee={getStatementDescriptor(state.checkoutSession.line_items)}
223
+ action={state.checkoutSession.mode}
224
+ message={
225
+ paymentLink?.after_completion?.hosted_confirmation?.custom_message ||
226
+ t(`payment.checkout.completed.${state.checkoutSession.mode}`)
227
+ }
228
+ />
229
+ )}
230
+ {!completed && (
231
+ <PaymentForm
232
+ checkoutSession={state.checkoutSession}
233
+ paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
234
+ paymentIntent={paymentIntent}
235
+ customer={customer}
236
+ onPaid={onPaid}
237
+ onError={onError}
238
+ mode={mode}
239
+ />
240
+ )}
241
+ </Stack>
242
+ <CheckoutFooter className="cko-footer" />
243
+ </Stack>
244
+ </Root>
245
+ </FormProvider>
246
+ );
247
+ }
248
+
249
+ export const Root = styled(Box)<{ mode: string }>`
250
+ box-sizing: border-box;
251
+ display: flex;
252
+ flex-direction: column;
253
+ justify-content: center;
254
+ align-items: center;
255
+ min-height: 100vh;
256
+ position: relative;
257
+
258
+ &:before {
259
+ animation-fill-mode: both;
260
+ background: #ffffff;
261
+ content: '';
262
+ height: 100%;
263
+ position: fixed;
264
+ right: 0;
265
+ top: 0;
266
+ transform-origin: right;
267
+ width: 50%;
268
+ box-shadow: 15px 0 30px 0 rgba(0, 0, 0, 0.18);
269
+ }
270
+
271
+ .cko-container {
272
+ width: 100%;
273
+ max-width: 1000px;
274
+ display: flex;
275
+ flex-direction: row;
276
+ justify-content: space-between;
277
+ position: relative;
278
+ padding: 0 16px;
279
+ }
280
+
281
+ .cko-overview {
282
+ width: 400px;
283
+ min-height: 540px;
284
+ position: relative;
285
+ }
286
+ .cko-header {
287
+ left: 0;
288
+ margin-bottom: 0;
289
+ position: absolute;
290
+ right: 0;
291
+ top: 0;
292
+ transition: background-color 0.15s ease, box-shadow 0.15s ease-out;
293
+ }
294
+ .cko-product-summary {
295
+ }
296
+
297
+ .cko-payment {
298
+ width: 400px;
299
+ .MuiInputBase-root {
300
+ border-radius: 0;
301
+ }
302
+ }
303
+
304
+ .cko-payment-form {
305
+ .MuiInputAdornment-positionStart {
306
+ width: 50px;
307
+ }
308
+
309
+ .MuiBox-root {
310
+ border: 1px solid #ccc;
311
+ margin: -1px 0 0 -1px;
312
+ }
313
+
314
+ .MuiFormHelperText-root {
315
+ margin-left: 14px;
316
+ }
317
+
318
+ .MuiOutlinedInput-notchedOutline {
319
+ border: none;
320
+ }
321
+ }
322
+
323
+ .cko-payment-methods {
324
+ }
325
+
326
+ .cko-payment-submit {
327
+ .MuiButtonBase-root {
328
+ border-radius: 0;
329
+ font-size: 1.3rem;
330
+ }
331
+ }
332
+
333
+ .cko-header {
334
+ }
335
+
336
+ .cko-footer {
337
+ position: absolute;
338
+ bottom: 0;
339
+ left: 12px;
340
+ margin: 12px 0;
341
+ }
342
+
343
+ @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
344
+ &:before {
345
+ display: none;
346
+ }
347
+ .cko-container {
348
+ flex-direction: column;
349
+ align-items: center;
350
+ gap: 24px;
351
+ min-width: 350px;
352
+ max-width: 400px;
353
+ }
354
+ .cko-overview {
355
+ width: 100%;
356
+ min-height: auto;
357
+ }
358
+ .cko-payment {
359
+ width: 100%;
360
+ }
361
+
362
+ .cko-footer {
363
+ position: static;
364
+ margin-top: 0;
365
+ }
366
+ }
367
+ `;
@@ -0,0 +1,55 @@
1
+ import { Avatar, Stack, Typography } from '@mui/material';
2
+ import type { LiteralUnion } from 'type-fest';
3
+
4
+ type Props = {
5
+ name: string;
6
+ description?: string;
7
+ logo?: string;
8
+ size?: number;
9
+ extra?: React.ReactNode;
10
+ variant?: LiteralUnion<'square' | 'rounded' | 'circular', string>;
11
+ };
12
+
13
+ // FIXME: @wangshijun add image filter for logo
14
+ export default function ProductCard({ size, variant, name, logo, description, extra }: Props) {
15
+ const s = { width: size, height: size };
16
+ return (
17
+ <Stack direction="row" alignItems="flex-start" spacing={1} flex={2}>
18
+ {logo ? (
19
+ // @ts-ignore
20
+ <Avatar src={logo} alt={name} variant={variant} sx={s} />
21
+ ) : (
22
+ // @ts-ignore
23
+ <Avatar variant={variant} sx={s}>
24
+ {name.slice(0, 1)}
25
+ </Avatar>
26
+ )}
27
+ <Stack direction="column" alignItems="flex-start" justifyContent="space-around">
28
+ <Typography variant="body1" sx={{ fontWeight: 500, mb: 0.5, lineHeight: 1 }} color="text.primary">
29
+ {name}
30
+ </Typography>
31
+ {description && (
32
+ <Typography
33
+ variant="body1"
34
+ sx={{ fontSize: '0.85rem', mb: 0.5, lineHeight: 1, textAlign: 'left' }}
35
+ color="text.secondary">
36
+ {description}
37
+ </Typography>
38
+ )}
39
+ {extra && (
40
+ <Typography variant="body1" sx={{ fontSize: '0.85rem' }} color="text.secondary">
41
+ {extra}
42
+ </Typography>
43
+ )}
44
+ </Stack>
45
+ </Stack>
46
+ );
47
+ }
48
+
49
+ ProductCard.defaultProps = {
50
+ logo: '',
51
+ size: 48,
52
+ description: '',
53
+ variant: 'rounded',
54
+ extra: undefined,
55
+ };
@@ -0,0 +1,121 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import type {
3
+ PriceRecurring,
4
+ TCheckoutSessionExpanded,
5
+ TLineItemExpanded,
6
+ TPaymentCurrency,
7
+ } from '@blocklet/payment-types';
8
+ import { Stack, Typography } from '@mui/material';
9
+
10
+ import Status from '../components/status';
11
+ import Switch from '../components/switch';
12
+ import { formatLineItemPricing, formatPrice, formatRecurring, formatUpsellSaving } from '../util';
13
+ import ProductCard from './product-card';
14
+
15
+ type Props = {
16
+ item: TLineItemExpanded;
17
+ session: TCheckoutSessionExpanded;
18
+ currency: TPaymentCurrency;
19
+ onUpsell: Function;
20
+ onDownsell: Function;
21
+ mode?: 'normal' | 'cross-sell';
22
+ children?: React.ReactNode;
23
+ };
24
+
25
+ ProductItem.defaultProps = {
26
+ mode: 'normal',
27
+ children: null,
28
+ };
29
+
30
+ export default function ProductItem({ item, session, currency, mode, children, onUpsell, onDownsell }: Props) {
31
+ const { t, locale } = useLocaleContext();
32
+ const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0, locale);
33
+ const saving = formatUpsellSaving(session, currency);
34
+ const metered = item.price?.recurring?.usage_type === 'metered' ? t('common.metered') : '';
35
+ const canUpsell = mode === 'normal' && session.line_items.length === 1;
36
+ return (
37
+ <Stack direction="column" alignItems="flex-start" spacing={1} sx={{ width: '100%' }}>
38
+ <Stack direction="column" alignItems="flex-end" sx={{ width: '100%' }}>
39
+ <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
40
+ <ProductCard
41
+ logo={item.price.product?.images[0]}
42
+ name={item.price.product?.name}
43
+ description={item.price.product?.description}
44
+ extra={
45
+ item.price.type === 'recurring' && item.price.recurring
46
+ ? [pricing.quantity, t('common.billed', { rule: `${formatRecurring(item.upsell_price?.recurring || item.price.recurring, true, 'per', locale)} ${metered}` })].filter(Boolean).join(', ') // prettier-ignore
47
+ : pricing.quantity
48
+ }
49
+ />
50
+ <Stack direction="column" alignItems="flex-end" flex={1}>
51
+ <Typography sx={{ color: 'text.primary', fontWeight: 500 }} gutterBottom>
52
+ {pricing.primary}
53
+ </Typography>
54
+ {pricing.secondary && (
55
+ <Typography sx={{ fontSize: '0.85rem', color: 'text.secondary' }}>{pricing.secondary}</Typography>
56
+ )}
57
+ </Stack>
58
+ </Stack>
59
+ {children}
60
+ </Stack>
61
+ {canUpsell && !item.upsell_price_id && item.price.upsell?.upsells_to && (
62
+ <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
63
+ <Typography
64
+ component="label"
65
+ htmlFor="upsell-switch"
66
+ sx={{
67
+ fontSize: 12,
68
+ cursor: 'pointer',
69
+ color: 'text.primary',
70
+ }}>
71
+ <Switch
72
+ id="upsell-switch"
73
+ sx={{ mr: 1 }}
74
+ variant="success"
75
+ checked={false}
76
+ onChange={() => onUpsell(item.price_id, item.price.upsell?.upsells_to_id)}
77
+ />
78
+ {t('payment.checkout.upsell.save', {
79
+ recurring: formatRecurring(item.price.upsell.upsells_to.recurring as PriceRecurring, true, 'per', locale),
80
+ })}
81
+ <Status
82
+ label={t('payment.checkout.upsell.off', { saving })}
83
+ color="primary"
84
+ variant="outlined"
85
+ sx={{ ml: 1 }}
86
+ />
87
+ </Typography>
88
+ <Typography component="span" sx={{ fontSize: 12 }}>
89
+ {formatPrice(item.price.upsell.upsells_to, currency, item.price.product?.unit_label, 1, true, locale)}
90
+ </Typography>
91
+ </Stack>
92
+ )}
93
+ {canUpsell && item.upsell_price_id && (
94
+ <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
95
+ <Typography
96
+ component="label"
97
+ htmlFor="upsell-switch"
98
+ sx={{
99
+ fontSize: 12,
100
+ cursor: 'pointer',
101
+ color: 'text.secondary',
102
+ }}>
103
+ <Switch
104
+ id="upsell-switch"
105
+ sx={{ mr: 1 }}
106
+ variant="success"
107
+ checked
108
+ onChange={() => onDownsell(item.upsell_price_id)}
109
+ />
110
+ {t('payment.checkout.upsell.revert', {
111
+ recurring: t(`common.${formatRecurring(item.price.recurring as PriceRecurring)}`),
112
+ })}
113
+ </Typography>
114
+ <Typography component="span" sx={{ fontSize: 12 }}>
115
+ {formatPrice(item.price, currency, item.price.product?.unit_label, 1, true, locale)}
116
+ </Typography>
117
+ </Stack>
118
+ )}
119
+ </Stack>
120
+ );
121
+ }
@@ -0,0 +1,39 @@
1
+ import { Fade, Skeleton, Stack, Typography } from '@mui/material';
2
+
3
+ export default function ProductSkeleton({ count }: { count: number }) {
4
+ return (
5
+ <Fade in>
6
+ <Stack
7
+ direction="column"
8
+ alignItems="center"
9
+ padding={4}
10
+ spacing={1}
11
+ sx={{
12
+ width: 320,
13
+ border: '1px solid #eee',
14
+ borderRadius: 1,
15
+ transition: 'border-color 0.3s ease 0s, box-shadow 0.3s ease 0s',
16
+ boxShadow: '0 4px 8px rgba(0, 0, 0, 20%)',
17
+ '&:hover': {
18
+ borderColor: '#ddd',
19
+ boxShadow: '0 8px 16px rgba(0, 0, 0, 20%)',
20
+ },
21
+ }}>
22
+ <Typography component="div" variant="h4" sx={{ width: '50%' }}>
23
+ <Skeleton />
24
+ </Typography>
25
+ <Skeleton variant="text" sx={{ fontSize: '1rem', width: '60%' }} />
26
+ <Typography component="div" variant="h3" sx={{ width: '60%' }}>
27
+ <Skeleton />
28
+ </Typography>
29
+ <Typography component="div" variant="h3" sx={{ width: '100%' }}>
30
+ <Skeleton />
31
+ </Typography>
32
+ {Array.from({ length: count }).map((_, i) => (
33
+ // eslint-disable-next-line react/no-array-index-key
34
+ <Skeleton key={i} variant="text" sx={{ fontSize: '1rem', width: '60%' }} />
35
+ ))}
36
+ </Stack>
37
+ </Fade>
38
+ );
39
+ }
@@ -0,0 +1,21 @@
1
+ import { Fade, Skeleton, Stack, Typography } from '@mui/material';
2
+
3
+ export default function OverviewSkeleton() {
4
+ return (
5
+ <Fade in>
6
+ <Stack direction="column">
7
+ <Stack direction="row" alignItems="center" spacing={2}>
8
+ <Skeleton variant="circular" width={32} height={32} />
9
+ <Skeleton variant="text" sx={{ fontSize: '2rem', width: '40%' }} />
10
+ </Stack>
11
+ <Typography mt={3} component="div" variant="h4">
12
+ <Skeleton />
13
+ </Typography>
14
+ <Typography component="div" variant="h2">
15
+ <Skeleton />
16
+ </Typography>
17
+ <Skeleton sx={{ mt: 3 }} variant="rounded" width={200} height={200} />
18
+ </Stack>
19
+ </Fade>
20
+ );
21
+ }
@@ -0,0 +1,35 @@
1
+ import { Box, Fade, Skeleton, Stack, Typography } from '@mui/material';
2
+
3
+ export default function PaymentSkeleton() {
4
+ return (
5
+ <Fade in>
6
+ <Stack direction="column" spacing={3}>
7
+ <Skeleton variant="text" sx={{ fontSize: '2rem', width: '40%' }} />
8
+ <Box>
9
+ <Typography component="div" variant="h4">
10
+ <Skeleton />
11
+ </Typography>
12
+ <Typography component="div" variant="h1">
13
+ <Skeleton />
14
+ </Typography>
15
+ </Box>
16
+ <Box>
17
+ <Typography component="div" variant="h4">
18
+ <Skeleton />
19
+ </Typography>
20
+ <Typography component="div" variant="h1">
21
+ <Skeleton />
22
+ </Typography>
23
+ </Box>
24
+ <Box>
25
+ <Typography component="div" variant="h4">
26
+ <Skeleton />
27
+ </Typography>
28
+ <Typography component="div" variant="h1">
29
+ <Skeleton />
30
+ </Typography>
31
+ </Box>
32
+ </Stack>
33
+ </Fade>
34
+ );
35
+ }