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.
- package/api/src/jobs/payment.ts +17 -4
- package/api/src/libs/error.ts +13 -0
- package/api/src/libs/queue/store.ts +1 -1
- package/api/src/libs/util.ts +0 -14
- package/api/src/routes/checkout-sessions.ts +183 -47
- package/api/src/routes/payment-links.ts +2 -0
- package/api/src/routes/pricing-table.ts +23 -6
- package/api/src/routes/products.ts +4 -40
- package/api/src/routes/redirect.ts +4 -2
- package/api/src/store/migrations/20231030-crosssell.ts +23 -0
- package/api/src/store/models/checkout-session.ts +11 -5
- package/api/src/store/models/index.ts +9 -1
- package/api/src/store/models/payment-link.ts +6 -0
- package/api/src/store/models/price.ts +23 -18
- package/api/src/store/models/product.ts +73 -14
- package/api/src/store/models/types.ts +3 -0
- package/blocklet.yml +1 -1
- package/package.json +10 -10
- package/src/components/checkout/pay.tsx +23 -0
- package/src/components/checkout/product-item.tsx +30 -20
- package/src/components/checkout/summary.tsx +112 -4
- package/src/components/payment-link/before-pay.tsx +16 -0
- package/src/components/price/upsell-select.tsx +7 -2
- package/src/components/pricing-table/payment-settings.tsx +16 -0
- package/src/components/pricing-table/product-settings.tsx +1 -0
- package/src/components/product/cross-sell-select.tsx +51 -0
- package/src/components/product/cross-sell.tsx +83 -0
- package/src/locales/en.tsx +11 -0
- package/src/locales/zh.tsx +11 -0
- package/src/pages/admin/payments/links/create.tsx +1 -0
- package/src/pages/admin/products/products/detail.tsx +7 -0
- package/src/pages/checkout/pay.tsx +3 -5
- 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
|
+
}
|
package/src/locales/en.tsx
CHANGED
|
@@ -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:
|
package/src/locales/zh.tsx
CHANGED
|
@@ -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: '当前订阅',
|
|
@@ -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,
|
|
28
|
-
const { data } = await api.post(`/api/checkout-sessions/start/${id}
|
|
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}
|
|
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
|
})
|