payment-kit 1.13.38 → 1.13.40

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 (33) hide show
  1. package/api/src/jobs/payment.ts +17 -4
  2. package/api/src/libs/error.ts +13 -0
  3. package/api/src/libs/queue/store.ts +1 -1
  4. package/api/src/libs/util.ts +0 -14
  5. package/api/src/routes/checkout-sessions.ts +183 -47
  6. package/api/src/routes/payment-links.ts +2 -0
  7. package/api/src/routes/pricing-table.ts +23 -6
  8. package/api/src/routes/products.ts +4 -40
  9. package/api/src/routes/redirect.ts +4 -2
  10. package/api/src/store/migrations/20231030-crosssell.ts +23 -0
  11. package/api/src/store/models/checkout-session.ts +11 -5
  12. package/api/src/store/models/index.ts +9 -1
  13. package/api/src/store/models/payment-link.ts +6 -0
  14. package/api/src/store/models/price.ts +23 -18
  15. package/api/src/store/models/product.ts +73 -14
  16. package/api/src/store/models/types.ts +3 -0
  17. package/blocklet.yml +1 -1
  18. package/package.json +10 -10
  19. package/src/components/checkout/pay.tsx +23 -0
  20. package/src/components/checkout/product-item.tsx +30 -20
  21. package/src/components/checkout/summary.tsx +112 -4
  22. package/src/components/payment-link/before-pay.tsx +16 -0
  23. package/src/components/price/upsell-select.tsx +7 -2
  24. package/src/components/pricing-table/payment-settings.tsx +16 -0
  25. package/src/components/pricing-table/product-settings.tsx +1 -0
  26. package/src/components/product/cross-sell-select.tsx +51 -0
  27. package/src/components/product/cross-sell.tsx +83 -0
  28. package/src/locales/en.tsx +11 -0
  29. package/src/locales/zh.tsx +11 -0
  30. package/src/pages/admin/payments/links/create.tsx +1 -0
  31. package/src/pages/admin/products/products/detail.tsx +7 -0
  32. package/src/pages/checkout/pay.tsx +3 -5
  33. package/src/pages/checkout/pricing-table.tsx +1 -1
@@ -0,0 +1,83 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import type { TProductExpanded } from '@did-pay/types';
4
+ import { DeleteOutlineOutlined } from '@mui/icons-material';
5
+ import { CircularProgress, Grid, IconButton, Stack } from '@mui/material';
6
+ import { useSetState } from 'ahooks';
7
+
8
+ import { ProductsProvider } from '../../contexts/products';
9
+ import { useSettingsContext } from '../../contexts/settings';
10
+ import api from '../../libs/api';
11
+ import { formatError, formatProductPrice } from '../../libs/util';
12
+ import InfoCard from '../info-card';
13
+ import InfoRow from '../info-row';
14
+ import CrossSellSelect from './cross-sell-select';
15
+
16
+ export function CrossSellForm({ data, onChange }: { data: TProductExpanded; onChange: Function }) {
17
+ const { settings } = useSettingsContext();
18
+ const [state, setState] = useSetState({
19
+ loading: false,
20
+ });
21
+
22
+ const onRemoveUpsell = async () => {
23
+ try {
24
+ setState({ loading: true });
25
+ await api.put(`/api/products/${data.id}`, { cross_sell: { cross_sells_to_id: '' } }).then((res) => res.data);
26
+ setState({ loading: false });
27
+ onChange();
28
+ } catch (err) {
29
+ console.error(err);
30
+ Toast.error(formatError(err));
31
+ setState({ loading: false });
32
+ }
33
+ };
34
+
35
+ const onSelectUpsell = async (id: string) => {
36
+ try {
37
+ setState({ loading: true });
38
+ await api.put(`/api/products/${data.id}`, { cross_sell: { cross_sells_to_id: id } }).then((res) => res.data);
39
+ setState({ loading: false });
40
+ onChange();
41
+ } catch (err) {
42
+ console.error(err);
43
+ Toast.error(formatError(err));
44
+ setState({ loading: false });
45
+ }
46
+ };
47
+
48
+ if (state.loading) {
49
+ return <CircularProgress />;
50
+ }
51
+
52
+ if (data.cross_sell?.cross_sells_to_id) {
53
+ const to = data.cross_sell.cross_sells_to;
54
+ return (
55
+ <Stack spacing={1} direction="row" alignItems="center">
56
+ <InfoCard
57
+ name={to.name}
58
+ description={formatProductPrice(to as any, settings.baseCurrency)}
59
+ logo={to.images[0]}
60
+ />
61
+ <IconButton size="small" sx={{ ml: 1 }} onClick={onRemoveUpsell}>
62
+ <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
63
+ </IconButton>
64
+ </Stack>
65
+ );
66
+ }
67
+
68
+ return <CrossSellSelect data={data} onSelect={onSelectUpsell} />;
69
+ }
70
+
71
+ export default function ProductCrossSell({ data, onChange }: { data: TProductExpanded; onChange: Function }) {
72
+ const { t } = useLocaleContext();
73
+
74
+ return (
75
+ <ProductsProvider>
76
+ <Grid container>
77
+ <Grid item xs={12} md={6}>
78
+ <InfoRow label={t('admin.product.cross_sell.to')} value={<CrossSellForm data={data} onChange={onChange} />} />
79
+ </Grid>
80
+ </Grid>
81
+ </ProductsProvider>
82
+ );
83
+ }
@@ -97,6 +97,7 @@ export default flat({
97
97
  additional: 'Additional options',
98
98
  edit: 'Edit product',
99
99
  pricing: 'Pricing',
100
+ find: 'Find a product',
100
101
  archive: 'Archive product',
101
102
  archiveTip: 'Archiving will hide this product from new purchases. Are you sure you want to archive this product?',
102
103
  unarchive: 'Unarchive product',
@@ -134,6 +135,11 @@ export default flat({
134
135
  label: 'Unit label',
135
136
  placeholder: 'Seat',
136
137
  },
138
+ cross_sell: {
139
+ title: 'Cross-sells',
140
+ to: 'Cross-sells to',
141
+ tip: '',
142
+ },
137
143
  },
138
144
  price: {
139
145
  name: 'Price',
@@ -221,6 +227,7 @@ export default flat({
221
227
  requireBillingAddress: 'Collect customers billing addresses',
222
228
  requirePhoneNumber: 'Collect customers phone numbers',
223
229
  allowPromotionCodes: 'Allow promotion codes',
230
+ requireCrossSell: 'Require cross sell products selected if eligible',
224
231
  includeFreeTrail: 'Include a free trial',
225
232
  includeCustomFields: 'Add custom fields',
226
233
  confirmPage: 'Confirmation Page',
@@ -521,6 +528,10 @@ export default flat({
521
528
  revert: 'Switch to {recurring} billing',
522
529
  off: '{saving}% off',
523
530
  },
531
+ cross_sell: {
532
+ add: 'Add to order',
533
+ remove: 'Remove from order',
534
+ },
524
535
  expired: {
525
536
  title: 'Expired Link',
526
537
  description:
@@ -97,6 +97,7 @@ export default flat({
97
97
  additional: '附加选项',
98
98
  edit: '编辑产品',
99
99
  pricing: '定价',
100
+ find: '选择产品',
100
101
  archive: '存档产品',
101
102
  archiveTip: '存档将隐藏此产品不再允许新购买。确定要存档此产品吗?',
102
103
  unarchive: '取消存档产品',
@@ -133,6 +134,11 @@ export default flat({
133
134
  label: '单位标签',
134
135
  placeholder: '座位',
135
136
  },
137
+ cross_sell: {
138
+ title: '交叉销售',
139
+ to: '交叉销售',
140
+ tip: '',
141
+ },
136
142
  },
137
143
  price: {
138
144
  name: '价格',
@@ -221,6 +227,7 @@ export default flat({
221
227
  allowPromotionCodes: '允许促销代码',
222
228
  includeFreeTrail: '包含免费试用',
223
229
  includeCustomFields: '添加自定义字段',
230
+ requireCrossSell: '用户必须选择交叉销售的商品(如果有的话)',
224
231
  confirmPage: '确认页面',
225
232
  showConfirmPage: '显示确认页面',
226
233
  customMessage: '用自定义消息替换默认消息',
@@ -520,6 +527,10 @@ export default flat({
520
527
  title: '结账已完成',
521
528
  description: '此结账会话已完成。这意味着您的付款已成功处理完成。',
522
529
  },
530
+ cross_sell: {
531
+ add: '添加到订单',
532
+ remove: '从订单移除',
533
+ },
523
534
  },
524
535
  customer: {
525
536
  subscriptions: '当前订阅',
@@ -69,6 +69,7 @@ export default function CreatePaymentLink() {
69
69
  enabled: false,
70
70
  factory: '',
71
71
  },
72
+ cross_sell_behavior: 'auto',
72
73
  metadata: [], // FIXME:
73
74
  custom_fields: [], // FIXME:
74
75
  submit_type: 'auto', // FIXME:
@@ -16,6 +16,7 @@ import InfoRow from '../../../../components/info-row';
16
16
  import MetadataEditor from '../../../../components/metadata/editor';
17
17
  import ProductActions from '../../../../components/product/actions';
18
18
  import AddPrice from '../../../../components/product/add-price';
19
+ import ProductCrossSell from '../../../../components/product/cross-sell';
19
20
  import EditProduct from '../../../../components/product/edit';
20
21
  import SectionHeader from '../../../../components/section/header';
21
22
  import { useSettingsContext } from '../../../../contexts/settings';
@@ -204,6 +205,12 @@ export default function ProductDetail(props: { id: string }) {
204
205
  )}
205
206
  </Box>
206
207
  </Box>
208
+ <Box className="section">
209
+ <SectionHeader title={t('admin.product.cross_sell.title')} />
210
+ <Box className="section-body">
211
+ <ProductCrossSell data={data} onChange={runAsync} />
212
+ </Box>
213
+ </Box>
207
214
  <Box className="section">
208
215
  <SectionHeader title={t('common.metadata.label')}>
209
216
  <Button
@@ -24,8 +24,8 @@ type PageData = {
24
24
  customer?: TCustomer;
25
25
  };
26
26
 
27
- const startFromPaymentLink = async (id: string, preview: string, redirect: string = ''): Promise<PageData> => {
28
- const { data } = await api.post(`/api/checkout-sessions/start/${id}?preview=${preview}&redirect=${redirect}`);
27
+ const startFromPaymentLink = async (id: string, params: string): Promise<PageData> => {
28
+ const { data } = await api.post(`/api/checkout-sessions/start/${id}?${params}`);
29
29
  return data;
30
30
  };
31
31
 
@@ -42,9 +42,7 @@ export default function Payment({ id }: Props) {
42
42
  const [state, setState] = useSetState({ completed: false, appError: null });
43
43
 
44
44
  const { error: apiError, data } = useRequest(() =>
45
- type === 'paymentLink'
46
- ? startFromPaymentLink(id, params.get('preview') || '', params.get('redirect') || '')
47
- : fetchCheckoutSession(id)
45
+ type === 'paymentLink' ? startFromPaymentLink(id, params.toString()) : fetchCheckoutSession(id)
48
46
  );
49
47
 
50
48
  useEffect(() => {
@@ -125,7 +125,7 @@ export default function PricingTable({ id }: Props) {
125
125
  const onStartCheckoutSession = (priceId: string) => {
126
126
  setState({ loading: priceId });
127
127
  api
128
- .post(`/api/pricing-tables/${data.id}/checkout/${priceId}?redirect=${params.get('redirect') || ''}`)
128
+ .post(`/api/pricing-tables/${data.id}/checkout/${priceId}?${params.toString()}`)
129
129
  .then((res) => {
130
130
  window.location.href = res.data.url;
131
131
  })