payment-kit 1.13.39 → 1.13.41
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/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/migrate.ts +17 -0
- package/api/src/store/migrations/20231021-nft.ts +18 -9
- package/api/src/store/migrations/20231023-upsell.ts +5 -2
- package/api/src/store/migrations/20231030-crosssell.ts +22 -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
|
@@ -160,10 +160,6 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
160
160
|
type: DataTypes.JSON,
|
|
161
161
|
defaultValue: [],
|
|
162
162
|
},
|
|
163
|
-
upsell: {
|
|
164
|
-
type: DataTypes.JSON,
|
|
165
|
-
allowNull: true,
|
|
166
|
-
},
|
|
167
163
|
created_at: {
|
|
168
164
|
type: DataTypes.DATE,
|
|
169
165
|
defaultValue: DataTypes.NOW,
|
|
@@ -181,21 +177,30 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
181
177
|
|
|
182
178
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
|
183
179
|
public static initialize(sequelize: any) {
|
|
184
|
-
this.init(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
afterCreate: (model: Price, options) =>
|
|
192
|
-
createEvent('Price', 'price.created', model, options).catch(console.error),
|
|
193
|
-
afterUpdate: (model: Price, options) =>
|
|
194
|
-
createEvent('Price', 'price.updated', model, options).catch(console.error),
|
|
195
|
-
afterDestroy: (model: Price, options) =>
|
|
196
|
-
createEvent('Price', 'price.deleted', model, options).catch(console.error),
|
|
180
|
+
this.init(
|
|
181
|
+
{
|
|
182
|
+
...Price.GENESIS_ATTRIBUTES,
|
|
183
|
+
upsell: {
|
|
184
|
+
type: DataTypes.JSON,
|
|
185
|
+
allowNull: true,
|
|
186
|
+
},
|
|
197
187
|
},
|
|
198
|
-
|
|
188
|
+
{
|
|
189
|
+
sequelize,
|
|
190
|
+
modelName: 'Price',
|
|
191
|
+
tableName: 'prices',
|
|
192
|
+
createdAt: 'created_at',
|
|
193
|
+
updatedAt: 'updated_at',
|
|
194
|
+
hooks: {
|
|
195
|
+
afterCreate: (model: Price, options) =>
|
|
196
|
+
createEvent('Price', 'price.created', model, options).catch(console.error),
|
|
197
|
+
afterUpdate: (model: Price, options) =>
|
|
198
|
+
createEvent('Price', 'price.updated', model, options).catch(console.error),
|
|
199
|
+
afterDestroy: (model: Price, options) =>
|
|
200
|
+
createEvent('Price', 'price.deleted', model, options).catch(console.error),
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
);
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
public static associate(models: any) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/lines-between-class-members */
|
|
2
|
+
import pick from 'lodash/pick';
|
|
2
3
|
import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
|
|
3
4
|
import type { LiteralUnion } from 'type-fest';
|
|
4
5
|
|
|
@@ -46,6 +47,10 @@ export class Product extends Model<InferAttributes<Product>, InferCreationAttrib
|
|
|
46
47
|
// If set, we will mint an nft to consumer on purchase
|
|
47
48
|
declare nft_factory?: string;
|
|
48
49
|
|
|
50
|
+
declare cross_sell?: {
|
|
51
|
+
cross_sells_to_id: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
49
54
|
// TODO: goods related props are not supported
|
|
50
55
|
// declare package_dimensions?: any;
|
|
51
56
|
// declare shippable?: boolean;
|
|
@@ -126,21 +131,30 @@ export class Product extends Model<InferAttributes<Product>, InferCreationAttrib
|
|
|
126
131
|
};
|
|
127
132
|
|
|
128
133
|
public static initialize(sequelize: any) {
|
|
129
|
-
this.init(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
afterCreate: (model: Product, options) =>
|
|
137
|
-
createEvent('Product', 'product.created', model, options).catch(console.error),
|
|
138
|
-
afterUpdate: (model: Product, options) =>
|
|
139
|
-
createEvent('Product', 'product.updated', model, options).catch(console.error),
|
|
140
|
-
afterDestroy: (model: Product, options) =>
|
|
141
|
-
createEvent('Product', 'product.deleted', model, options).catch(console.error),
|
|
134
|
+
this.init(
|
|
135
|
+
{
|
|
136
|
+
...Product.GENESIS_ATTRIBUTES,
|
|
137
|
+
cross_sell: {
|
|
138
|
+
type: DataTypes.JSON,
|
|
139
|
+
allowNull: true,
|
|
140
|
+
},
|
|
142
141
|
},
|
|
143
|
-
|
|
142
|
+
{
|
|
143
|
+
sequelize,
|
|
144
|
+
modelName: 'Product',
|
|
145
|
+
tableName: 'products',
|
|
146
|
+
createdAt: 'created_at',
|
|
147
|
+
updatedAt: 'updated_at',
|
|
148
|
+
hooks: {
|
|
149
|
+
afterCreate: (model: Product, options) =>
|
|
150
|
+
createEvent('Product', 'product.created', model, options).catch(console.error),
|
|
151
|
+
afterUpdate: (model: Product, options) =>
|
|
152
|
+
createEvent('Product', 'product.updated', model, options).catch(console.error),
|
|
153
|
+
afterDestroy: (model: Product, options) =>
|
|
154
|
+
createEvent('Product', 'product.deleted', model, options).catch(console.error),
|
|
155
|
+
},
|
|
156
|
+
}
|
|
157
|
+
);
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
public static associate(models: any) {
|
|
@@ -154,6 +168,51 @@ export class Product extends Model<InferAttributes<Product>, InferCreationAttrib
|
|
|
154
168
|
as: 'default_price',
|
|
155
169
|
});
|
|
156
170
|
}
|
|
171
|
+
|
|
172
|
+
public static async expand(id: string) {
|
|
173
|
+
// @ts-ignore
|
|
174
|
+
const { Price, PaymentCurrency } = this.sequelize.models;
|
|
175
|
+
const product = await Product.findOne({
|
|
176
|
+
where: { id },
|
|
177
|
+
include: [
|
|
178
|
+
{ model: Price, as: 'prices', include: [{ model: PaymentCurrency, as: 'currency' }] },
|
|
179
|
+
{ model: Price, as: 'default_price' },
|
|
180
|
+
],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (product) {
|
|
184
|
+
const doc = product.toJSON();
|
|
185
|
+
// @ts-ignore
|
|
186
|
+
const currencies = await PaymentCurrency.findAll();
|
|
187
|
+
// @ts-ignore
|
|
188
|
+
for (const price of doc.prices) {
|
|
189
|
+
if (Array.isArray(price.currency_options)) {
|
|
190
|
+
price.currency_options.forEach((x: any) => {
|
|
191
|
+
x.currency = currencies.find((c: any) => c.id === x.currency_id);
|
|
192
|
+
});
|
|
193
|
+
const exist = price.currency_options.find((x: any) => x.currency_id === price.currency_id);
|
|
194
|
+
if (!exist) {
|
|
195
|
+
price.currency_options.unshift(
|
|
196
|
+
pick(price, ['currency_id', 'unit_amount', 'tiers', 'custom_unit_amount', 'currency'])
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
price.currency_options = [
|
|
201
|
+
pick(price, ['currency_id', 'unit_amount', 'tiers', 'custom_unit_amount', 'currency']),
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (doc.cross_sell?.cross_sells_to_id) {
|
|
207
|
+
// @ts-ignore
|
|
208
|
+
doc.cross_sell.cross_sells_to = await Product.expand(doc.cross_sell.cross_sells_to_id);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return doc;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
157
216
|
}
|
|
158
217
|
|
|
159
218
|
export type TProduct = InferAttributes<Product>;
|
|
@@ -147,6 +147,7 @@ export type LineItem = {
|
|
|
147
147
|
minimum: number;
|
|
148
148
|
};
|
|
149
149
|
upsell_price_id?: string;
|
|
150
|
+
cross_sell?: boolean;
|
|
150
151
|
// TODO: following are not supported
|
|
151
152
|
// price_data?: any;
|
|
152
153
|
// dynamic_tax_rates?: any;
|
|
@@ -337,6 +338,8 @@ export type PricingTableItem = {
|
|
|
337
338
|
};
|
|
338
339
|
|
|
339
340
|
nft_mint_settings?: NftMintSettings;
|
|
341
|
+
|
|
342
|
+
cross_sell_behavior?: LiteralUnion<'auto' | 'required', string>;
|
|
340
343
|
};
|
|
341
344
|
|
|
342
345
|
export type BrandSettings = {
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.41",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -42,13 +42,13 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@arcblock/did": "^1.18.93",
|
|
44
44
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
45
|
-
"@arcblock/did-connect": "^2.8.
|
|
45
|
+
"@arcblock/did-connect": "^2.8.6",
|
|
46
46
|
"@arcblock/did-util": "^1.18.93",
|
|
47
|
-
"@arcblock/ux": "^2.8.
|
|
48
|
-
"@blocklet/logger": "1.16.17
|
|
49
|
-
"@blocklet/sdk": "1.16.17
|
|
50
|
-
"@blocklet/ui-react": "^2.8.
|
|
51
|
-
"@blocklet/uploader": "^0.0.
|
|
47
|
+
"@arcblock/ux": "^2.8.6",
|
|
48
|
+
"@blocklet/logger": "1.16.17",
|
|
49
|
+
"@blocklet/sdk": "1.16.17",
|
|
50
|
+
"@blocklet/ui-react": "^2.8.6",
|
|
51
|
+
"@blocklet/uploader": "^0.0.33",
|
|
52
52
|
"@mui/icons-material": "^5.14.15",
|
|
53
53
|
"@mui/lab": "^5.0.0-alpha.150",
|
|
54
54
|
"@mui/material": "^5.14.15",
|
|
@@ -100,10 +100,10 @@
|
|
|
100
100
|
"validator": "^13.11.0"
|
|
101
101
|
},
|
|
102
102
|
"devDependencies": {
|
|
103
|
-
"@abtnode/types": "1.16.17
|
|
103
|
+
"@abtnode/types": "1.16.17",
|
|
104
104
|
"@arcblock/eslint-config": "^0.2.4",
|
|
105
105
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
106
|
-
"@did-pay/types": "1.13.
|
|
106
|
+
"@did-pay/types": "1.13.41",
|
|
107
107
|
"@types/cookie-parser": "^1.4.5",
|
|
108
108
|
"@types/cors": "^2.8.15",
|
|
109
109
|
"@types/dotenv-flow": "^3.3.2",
|
|
@@ -140,5 +140,5 @@
|
|
|
140
140
|
"parser": "typescript"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
-
"gitHead": "
|
|
143
|
+
"gitHead": "5f150269dbc497d19cc4d8422b03c67d29793cf2"
|
|
144
144
|
}
|
|
@@ -200,6 +200,26 @@ export function CheckoutPayMain({
|
|
|
200
200
|
}
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
+
const onApplyCrossSell = async (to: string) => {
|
|
204
|
+
try {
|
|
205
|
+
const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`, { to });
|
|
206
|
+
setState({ checkoutSession: data });
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error(err);
|
|
209
|
+
Toast.error(formatError(err));
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const onCancelCrossSell = async () => {
|
|
214
|
+
try {
|
|
215
|
+
const { data } = await api.delete(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`);
|
|
216
|
+
setState({ checkoutSession: data });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
console.error(err);
|
|
219
|
+
Toast.error(formatError(err));
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
203
223
|
return (
|
|
204
224
|
<FormProvider {...methods}>
|
|
205
225
|
<PaymentRoot>
|
|
@@ -212,6 +232,8 @@ export function CheckoutPayMain({
|
|
|
212
232
|
currency={currency}
|
|
213
233
|
onUpsell={onUpsell}
|
|
214
234
|
onDownsell={onDownsell}
|
|
235
|
+
onApplyCrossSell={onApplyCrossSell}
|
|
236
|
+
onCancelCrossSell={onCancelCrossSell}
|
|
215
237
|
/>
|
|
216
238
|
</Stack>
|
|
217
239
|
</Fade>
|
|
@@ -276,6 +298,7 @@ export const PaymentRoot = styled(Box)`
|
|
|
276
298
|
|
|
277
299
|
.cko-overview {
|
|
278
300
|
width: 380px;
|
|
301
|
+
min-height: 540px;
|
|
279
302
|
}
|
|
280
303
|
|
|
281
304
|
.cko-payment {
|
|
@@ -13,35 +13,45 @@ type Props = {
|
|
|
13
13
|
currency: TPaymentCurrency;
|
|
14
14
|
onUpsell: Function;
|
|
15
15
|
onDownsell: Function;
|
|
16
|
+
mode?: 'normal' | 'cross-sell';
|
|
17
|
+
children?: React.ReactNode;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
ProductItem.defaultProps = {
|
|
21
|
+
mode: 'normal',
|
|
22
|
+
children: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export default function ProductItem({ item, session, currency, mode, children, onUpsell, onDownsell }: Props) {
|
|
19
26
|
const { t } = useLocaleContext();
|
|
20
27
|
const pricing = formatLineItemPricing(item, currency, session.subscription_data?.trial_period_days || 0);
|
|
21
28
|
const saving = formatUpsellSaving(session, currency);
|
|
22
29
|
const metered = item.price?.recurring?.usage_type === 'metered' ? ' based on usage' : '';
|
|
23
|
-
const canUpsell = session.line_items.length === 1;
|
|
30
|
+
const canUpsell = mode === 'normal' && session.line_items.length === 1;
|
|
24
31
|
return (
|
|
25
32
|
<Stack direction="column" alignItems="flex-start" spacing={1} sx={{ width: '100%' }}>
|
|
26
|
-
<Stack direction="
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
{
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
33
|
+
<Stack direction="column" alignItems="flex-end" sx={{ width: '100%' }}>
|
|
34
|
+
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
|
|
35
|
+
<ProductCard
|
|
36
|
+
logo={item.price.product?.images[0]}
|
|
37
|
+
name={item.price.product?.name}
|
|
38
|
+
description={item.price.product?.description}
|
|
39
|
+
extra={
|
|
40
|
+
item.price.type === 'recurring' && item.price.recurring
|
|
41
|
+
? [pricing.quantity, `billed ${formatRecurring(item.upsell_price?.recurring || item.price.recurring)} ${metered}`].filter(Boolean).join(', ') // prettier-ignore
|
|
42
|
+
: pricing.quantity
|
|
43
|
+
}
|
|
44
|
+
/>
|
|
45
|
+
<Stack direction="column" alignItems="flex-end" flex={1}>
|
|
46
|
+
<Typography sx={{ color: 'text.primary', fontWeight: 500 }} gutterBottom>
|
|
47
|
+
{pricing.primary}
|
|
48
|
+
</Typography>
|
|
49
|
+
{pricing.secondary && (
|
|
50
|
+
<Typography sx={{ fontSize: '0.85rem', color: 'text.secondary' }}>{pricing.secondary}</Typography>
|
|
51
|
+
)}
|
|
52
|
+
</Stack>
|
|
44
53
|
</Stack>
|
|
54
|
+
{children}
|
|
45
55
|
</Stack>
|
|
46
56
|
{canUpsell && !item.upsell_price_id && item.price.upsell?.upsells_to && (
|
|
47
57
|
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: '100%' }}>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
1
2
|
import type { TCheckoutSessionExpanded, TLineItemExpanded, TPaymentCurrency } from '@did-pay/types';
|
|
3
|
+
import { LoadingButton } from '@mui/lab';
|
|
2
4
|
import { Fade, Stack, Typography } from '@mui/material';
|
|
5
|
+
import { useRequest, useSetState } from 'ahooks';
|
|
6
|
+
import noop from 'lodash/noop';
|
|
3
7
|
|
|
8
|
+
import api from '../../libs/api';
|
|
4
9
|
import { formatCheckoutHeadlines } from '../../libs/util';
|
|
10
|
+
import Status from '../status';
|
|
5
11
|
import PaymentAmount from './amount';
|
|
6
12
|
import ProductItem from './product-item';
|
|
7
13
|
|
|
@@ -10,10 +16,70 @@ type Props = {
|
|
|
10
16
|
currency: TPaymentCurrency;
|
|
11
17
|
onUpsell: Function;
|
|
12
18
|
onDownsell: Function;
|
|
19
|
+
onApplyCrossSell: Function;
|
|
20
|
+
onCancelCrossSell: Function;
|
|
13
21
|
};
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
async function fetchCrossSell(id: string) {
|
|
24
|
+
try {
|
|
25
|
+
const { data } = await api.get(`/api/checkout-sessions/${id}/cross-sell`);
|
|
26
|
+
if (!data.error) {
|
|
27
|
+
return data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function PaymentSummary({
|
|
37
|
+
checkoutSession,
|
|
38
|
+
currency,
|
|
39
|
+
onUpsell,
|
|
40
|
+
onDownsell,
|
|
41
|
+
onApplyCrossSell,
|
|
42
|
+
onCancelCrossSell,
|
|
43
|
+
}: Props) {
|
|
44
|
+
const { t } = useLocaleContext();
|
|
45
|
+
const [state, setState] = useSetState({ loading: false });
|
|
46
|
+
const { data, runAsync } = useRequest(() => fetchCrossSell(checkoutSession.id));
|
|
16
47
|
const headlines = formatCheckoutHeadlines(checkoutSession, currency);
|
|
48
|
+
|
|
49
|
+
const handleUpsell = async (from: string, to: string) => {
|
|
50
|
+
await onUpsell(from, to);
|
|
51
|
+
runAsync();
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleDownsell = async (from: string) => {
|
|
55
|
+
await onDownsell(from);
|
|
56
|
+
runAsync();
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleApplyCrossSell = async () => {
|
|
60
|
+
if (data) {
|
|
61
|
+
try {
|
|
62
|
+
setState({ loading: true });
|
|
63
|
+
await onApplyCrossSell(data.id);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(err);
|
|
66
|
+
} finally {
|
|
67
|
+
setState({ loading: false });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleCancelCrossSell = async () => {
|
|
73
|
+
try {
|
|
74
|
+
setState({ loading: true });
|
|
75
|
+
await onCancelCrossSell();
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(err);
|
|
78
|
+
} finally {
|
|
79
|
+
setState({ loading: false });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
17
83
|
return (
|
|
18
84
|
<Fade in>
|
|
19
85
|
<Stack className="cko-product" direction="column" sx={{ mt: 4 }}>
|
|
@@ -33,11 +99,53 @@ export default function PaymentSummary({ checkoutSession, currency, onUpsell, on
|
|
|
33
99
|
item={x}
|
|
34
100
|
session={checkoutSession}
|
|
35
101
|
currency={currency}
|
|
36
|
-
onUpsell={
|
|
37
|
-
onDownsell={
|
|
38
|
-
|
|
102
|
+
onUpsell={handleUpsell}
|
|
103
|
+
onDownsell={handleDownsell}>
|
|
104
|
+
{x.cross_sell && (
|
|
105
|
+
<LoadingButton
|
|
106
|
+
size="small"
|
|
107
|
+
loadingPosition="end"
|
|
108
|
+
color="error"
|
|
109
|
+
variant="text"
|
|
110
|
+
loading={state.loading}
|
|
111
|
+
onClick={handleCancelCrossSell}>
|
|
112
|
+
{t('checkout.cross_sell.remove')}
|
|
113
|
+
</LoadingButton>
|
|
114
|
+
)}
|
|
115
|
+
</ProductItem>
|
|
39
116
|
))}
|
|
40
117
|
</Stack>
|
|
118
|
+
{data && checkoutSession.line_items.some((x) => x.price_id === data.id) === false && (
|
|
119
|
+
<Stack
|
|
120
|
+
direction="column"
|
|
121
|
+
alignItems="flex-end"
|
|
122
|
+
spacing={0.5}
|
|
123
|
+
sx={{ border: '1px solid #eee', borderRadius: 1, padding: 1, mt: 8, mb: 4 }}>
|
|
124
|
+
<ProductItem
|
|
125
|
+
item={{ quantity: 1, price: data, price_id: data.id, cross_sell: true } as TLineItemExpanded}
|
|
126
|
+
session={checkoutSession}
|
|
127
|
+
currency={currency}
|
|
128
|
+
onUpsell={noop}
|
|
129
|
+
onDownsell={noop}
|
|
130
|
+
/>
|
|
131
|
+
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ width: 1 }}>
|
|
132
|
+
<Typography>
|
|
133
|
+
{checkoutSession.cross_sell_behavior === 'required' && (
|
|
134
|
+
<Status label={t('checkout.required')} color="info" variant="outlined" sx={{ mr: 1 }} />
|
|
135
|
+
)}
|
|
136
|
+
</Typography>
|
|
137
|
+
<LoadingButton
|
|
138
|
+
size="small"
|
|
139
|
+
loadingPosition="end"
|
|
140
|
+
color={checkoutSession.cross_sell_behavior === 'required' ? 'info' : 'info'}
|
|
141
|
+
variant={checkoutSession.cross_sell_behavior === 'required' ? 'text' : 'text'}
|
|
142
|
+
loading={state.loading}
|
|
143
|
+
onClick={handleApplyCrossSell}>
|
|
144
|
+
{t('checkout.cross_sell.add')}
|
|
145
|
+
</LoadingButton>
|
|
146
|
+
</Stack>
|
|
147
|
+
</Stack>
|
|
148
|
+
)}
|
|
41
149
|
</Stack>
|
|
42
150
|
</Fade>
|
|
43
151
|
);
|
|
@@ -156,6 +156,22 @@ export default function BeforePay() {
|
|
|
156
156
|
/>
|
|
157
157
|
)}
|
|
158
158
|
/>
|
|
159
|
+
<Controller
|
|
160
|
+
name="cross_sell_behavior"
|
|
161
|
+
control={control}
|
|
162
|
+
render={({ field }) => (
|
|
163
|
+
<FormControlLabel
|
|
164
|
+
control={
|
|
165
|
+
<Checkbox
|
|
166
|
+
checked={getValues().cross_sell_behavior === 'required'}
|
|
167
|
+
{...field}
|
|
168
|
+
onChange={(_, checked) => setValue(field.name, checked ? 'required' : 'auto')}
|
|
169
|
+
/>
|
|
170
|
+
}
|
|
171
|
+
label={t('admin.paymentLink.requireCrossSell')}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
/>
|
|
159
175
|
<Controller
|
|
160
176
|
name="include_free_trial"
|
|
161
177
|
control={control}
|
|
@@ -34,7 +34,7 @@ export default function UpsellSelect({ price, onSelect, onAdd }: Props) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const onSelectPrice = (e: any) => {
|
|
37
|
-
if (e.target.value) {
|
|
37
|
+
if (e.target.value && e.target.value !== 'empty') {
|
|
38
38
|
if (e.target.value === 'add') {
|
|
39
39
|
setState({ action: 'add' });
|
|
40
40
|
} else {
|
|
@@ -59,7 +59,12 @@ export default function UpsellSelect({ price, onSelect, onAdd }: Props) {
|
|
|
59
59
|
|
|
60
60
|
return (
|
|
61
61
|
<>
|
|
62
|
-
<Select
|
|
62
|
+
<Select
|
|
63
|
+
value="empty"
|
|
64
|
+
sx={{ width: 300 }}
|
|
65
|
+
size="small"
|
|
66
|
+
onChange={onSelectPrice}
|
|
67
|
+
MenuProps={{ style: { maxHeight: 480 } }}>
|
|
63
68
|
<MenuItem value="empty">
|
|
64
69
|
<Stack direction="row" alignItems="center">
|
|
65
70
|
<Typography>{t('admin.price.find')}</Typography>
|
|
@@ -71,6 +71,22 @@ export function PricePaymentSettings({ index }: { index: number }) {
|
|
|
71
71
|
/>
|
|
72
72
|
)}
|
|
73
73
|
/>
|
|
74
|
+
<Controller
|
|
75
|
+
name={getFieldName('cross_sell_behavior')}
|
|
76
|
+
control={control}
|
|
77
|
+
render={({ field }) => (
|
|
78
|
+
<FormControlLabel
|
|
79
|
+
control={
|
|
80
|
+
<Checkbox
|
|
81
|
+
checked={get(values, getFieldName('cross_sell_behavior')) === 'required'}
|
|
82
|
+
{...field}
|
|
83
|
+
onChange={(_, checked) => setValue(field.name, checked ? 'required' : 'auto')}
|
|
84
|
+
/>
|
|
85
|
+
}
|
|
86
|
+
label={t('admin.paymentLink.requireCrossSell')}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
/>
|
|
74
90
|
<Controller
|
|
75
91
|
name={getFieldName('after_completion.type')}
|
|
76
92
|
control={control}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import type { TProductExpanded } from '@did-pay/types';
|
|
3
|
+
import { MenuItem, Select, Stack, Typography } from '@mui/material';
|
|
4
|
+
|
|
5
|
+
import { useProductsContext } from '../../contexts/products';
|
|
6
|
+
import { useSettingsContext } from '../../contexts/settings';
|
|
7
|
+
import { formatProductPrice } from '../../libs/util';
|
|
8
|
+
import InfoCard from '../info-card';
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
data: TProductExpanded;
|
|
12
|
+
onSelect: Function;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function CrossSellSelect({ data, onSelect }: Props) {
|
|
16
|
+
const { t } = useLocaleContext();
|
|
17
|
+
const { products } = useProductsContext();
|
|
18
|
+
const { settings } = useSettingsContext();
|
|
19
|
+
|
|
20
|
+
const onSelectPrice = (e: any) => {
|
|
21
|
+
if (e.target.value && e.target.value !== 'empty') {
|
|
22
|
+
onSelect(e.target.value);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Select
|
|
28
|
+
value="empty"
|
|
29
|
+
sx={{ width: 300 }}
|
|
30
|
+
size="small"
|
|
31
|
+
onChange={onSelectPrice}
|
|
32
|
+
MenuProps={{ style: { maxHeight: 480 } }}>
|
|
33
|
+
<MenuItem value="empty">
|
|
34
|
+
<Stack direction="row" alignItems="center">
|
|
35
|
+
<Typography>{t('admin.product.find')}</Typography>
|
|
36
|
+
</Stack>
|
|
37
|
+
</MenuItem>
|
|
38
|
+
{products
|
|
39
|
+
.filter((x) => x.id !== data.id)
|
|
40
|
+
.map((x) => (
|
|
41
|
+
<MenuItem key={x.id} value={x.id}>
|
|
42
|
+
<InfoCard
|
|
43
|
+
name={x.name}
|
|
44
|
+
description={formatProductPrice(x as any, settings.baseCurrency)}
|
|
45
|
+
logo={x.images[0]}
|
|
46
|
+
/>
|
|
47
|
+
</MenuItem>
|
|
48
|
+
))}
|
|
49
|
+
</Select>
|
|
50
|
+
);
|
|
51
|
+
}
|