@blocklet/payment-react 1.20.10 → 1.20.12
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/es/components/promotion-code.d.ts +19 -0
- package/es/components/promotion-code.js +153 -0
- package/es/contexts/payment.d.ts +8 -0
- package/es/contexts/payment.js +10 -1
- package/es/index.d.ts +2 -1
- package/es/index.js +3 -1
- package/es/libs/util.d.ts +5 -1
- package/es/libs/util.js +23 -0
- package/es/locales/en.js +40 -15
- package/es/locales/zh.js +29 -0
- package/es/payment/form/index.js +7 -1
- package/es/payment/index.js +19 -0
- package/es/payment/product-item.js +32 -3
- package/es/payment/summary.d.ts +5 -2
- package/es/payment/summary.js +193 -16
- package/lib/components/promotion-code.d.ts +19 -0
- package/lib/components/promotion-code.js +155 -0
- package/lib/contexts/payment.d.ts +8 -0
- package/lib/contexts/payment.js +13 -1
- package/lib/index.d.ts +2 -1
- package/lib/index.js +8 -0
- package/lib/libs/util.d.ts +5 -1
- package/lib/libs/util.js +29 -0
- package/lib/locales/en.js +40 -15
- package/lib/locales/zh.js +29 -0
- package/lib/payment/form/index.js +8 -1
- package/lib/payment/index.js +23 -0
- package/lib/payment/product-item.js +46 -0
- package/lib/payment/summary.d.ts +5 -2
- package/lib/payment/summary.js +153 -11
- package/package.json +9 -9
- package/src/components/promotion-code.tsx +184 -0
- package/src/contexts/payment.tsx +15 -0
- package/src/index.ts +2 -0
- package/src/libs/util.ts +35 -0
- package/src/locales/en.tsx +40 -15
- package/src/locales/zh.tsx +29 -0
- package/src/payment/form/index.tsx +10 -1
- package/src/payment/index.tsx +22 -0
- package/src/payment/product-item.tsx +37 -2
- package/src/payment/summary.tsx +201 -16
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { TextField, Button, Alert, Box, InputAdornment } from '@mui/material';
|
|
3
|
+
import { Add } from '@mui/icons-material';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import LoadingButton from './loading-button';
|
|
6
|
+
import api from '../libs/api';
|
|
7
|
+
import { usePaymentContext } from '../contexts/payment';
|
|
8
|
+
|
|
9
|
+
export interface AppliedPromoCode {
|
|
10
|
+
id: string;
|
|
11
|
+
code: string;
|
|
12
|
+
discount_amount?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface PromotionCodeProps {
|
|
16
|
+
checkoutSessionId: string;
|
|
17
|
+
initialAppliedCodes?: AppliedPromoCode[];
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
className?: string;
|
|
20
|
+
placeholder?: string;
|
|
21
|
+
currencyId: string;
|
|
22
|
+
onUpdate?: (data: { appliedCodes: AppliedPromoCode[]; discountAmount: string }) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function PromotionCode({
|
|
26
|
+
checkoutSessionId,
|
|
27
|
+
initialAppliedCodes = [],
|
|
28
|
+
disabled = false,
|
|
29
|
+
className = '',
|
|
30
|
+
placeholder = '',
|
|
31
|
+
onUpdate = undefined,
|
|
32
|
+
currencyId,
|
|
33
|
+
}: PromotionCodeProps) {
|
|
34
|
+
const { t } = useLocaleContext();
|
|
35
|
+
const [showInput, setShowInput] = useState(false);
|
|
36
|
+
const [code, setCode] = useState('');
|
|
37
|
+
const [error, setError] = useState('');
|
|
38
|
+
const [applying, setApplying] = useState(false);
|
|
39
|
+
const [appliedCodes, setAppliedCodes] = useState<AppliedPromoCode[]>(initialAppliedCodes);
|
|
40
|
+
const { session, paymentState } = usePaymentContext();
|
|
41
|
+
|
|
42
|
+
const handleLoginCheck = () => {
|
|
43
|
+
if (!session.user) {
|
|
44
|
+
session?.login(() => {
|
|
45
|
+
handleApply();
|
|
46
|
+
});
|
|
47
|
+
} else {
|
|
48
|
+
handleApply();
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const handleApply = async () => {
|
|
53
|
+
if (!code.trim()) return;
|
|
54
|
+
|
|
55
|
+
// Prevent applying promotion during payment process
|
|
56
|
+
if (paymentState.paying || paymentState.stripePaying) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
setApplying(true);
|
|
61
|
+
setError('');
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const response = await api.post(`/api/checkout-sessions/${checkoutSessionId}/apply-promotion`, {
|
|
65
|
+
promotion_code: code.trim(),
|
|
66
|
+
currency_id: currencyId,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const discounts = response.data.discounts || [];
|
|
70
|
+
const appliedDiscount = discounts[0];
|
|
71
|
+
|
|
72
|
+
if (appliedDiscount) {
|
|
73
|
+
const newCode: AppliedPromoCode = {
|
|
74
|
+
id: appliedDiscount.promotion_code || appliedDiscount.coupon,
|
|
75
|
+
code: code.trim(),
|
|
76
|
+
discount_amount: appliedDiscount.discount_amount,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
setAppliedCodes([newCode]);
|
|
80
|
+
setCode('');
|
|
81
|
+
setShowInput(false);
|
|
82
|
+
|
|
83
|
+
onUpdate?.({
|
|
84
|
+
appliedCodes: [newCode],
|
|
85
|
+
discountAmount: appliedDiscount.discount_amount,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
} catch (err: any) {
|
|
89
|
+
const errorMessage = err.response?.data?.error || err.message;
|
|
90
|
+
setError(errorMessage);
|
|
91
|
+
} finally {
|
|
92
|
+
setApplying(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const handleKeyPress = (event: React.KeyboardEvent) => {
|
|
97
|
+
if (event.key === 'Enter' && !applying && code.trim()) {
|
|
98
|
+
handleLoginCheck();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const isPaymentInProgress = paymentState.paying || paymentState.stripePaying;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Box className={className}>
|
|
106
|
+
{/* Input field or add button - only show if no codes applied */}
|
|
107
|
+
{appliedCodes.length === 0 &&
|
|
108
|
+
!disabled &&
|
|
109
|
+
!isPaymentInProgress &&
|
|
110
|
+
(showInput ? (
|
|
111
|
+
<Box
|
|
112
|
+
onBlur={() => {
|
|
113
|
+
if (!code.trim()) {
|
|
114
|
+
setShowInput(false);
|
|
115
|
+
}
|
|
116
|
+
}}>
|
|
117
|
+
<TextField
|
|
118
|
+
fullWidth
|
|
119
|
+
value={code}
|
|
120
|
+
onChange={(e) => setCode(e.target.value)}
|
|
121
|
+
onKeyPress={handleKeyPress}
|
|
122
|
+
placeholder={placeholder || t('payment.checkout.promotion.placeholder')}
|
|
123
|
+
variant="outlined"
|
|
124
|
+
size="small"
|
|
125
|
+
disabled={applying}
|
|
126
|
+
autoFocus
|
|
127
|
+
slotProps={{
|
|
128
|
+
input: {
|
|
129
|
+
endAdornment: (
|
|
130
|
+
<InputAdornment position="end">
|
|
131
|
+
<LoadingButton
|
|
132
|
+
size="small"
|
|
133
|
+
onClick={handleLoginCheck}
|
|
134
|
+
loading={applying}
|
|
135
|
+
disabled={!code.trim()}
|
|
136
|
+
variant="text"
|
|
137
|
+
sx={{
|
|
138
|
+
color: 'primary.main',
|
|
139
|
+
fontSize: 'small',
|
|
140
|
+
}}>
|
|
141
|
+
{t('payment.checkout.promotion.apply')}
|
|
142
|
+
</LoadingButton>
|
|
143
|
+
</InputAdornment>
|
|
144
|
+
),
|
|
145
|
+
},
|
|
146
|
+
}}
|
|
147
|
+
sx={{
|
|
148
|
+
'& .MuiOutlinedInput-root': {
|
|
149
|
+
pr: 1,
|
|
150
|
+
},
|
|
151
|
+
}}
|
|
152
|
+
/>
|
|
153
|
+
|
|
154
|
+
{error && (
|
|
155
|
+
<Alert
|
|
156
|
+
severity="error"
|
|
157
|
+
sx={{
|
|
158
|
+
my: 1,
|
|
159
|
+
}}>
|
|
160
|
+
{error}
|
|
161
|
+
</Alert>
|
|
162
|
+
)}
|
|
163
|
+
</Box>
|
|
164
|
+
) : (
|
|
165
|
+
<Button
|
|
166
|
+
onClick={() => setShowInput(true)}
|
|
167
|
+
startIcon={<Add fontSize="small" />}
|
|
168
|
+
variant="text"
|
|
169
|
+
sx={{
|
|
170
|
+
fontWeight: 'normal',
|
|
171
|
+
textTransform: 'none',
|
|
172
|
+
justifyContent: 'flex-start',
|
|
173
|
+
p: 0,
|
|
174
|
+
'&:hover': {
|
|
175
|
+
backgroundColor: 'transparent',
|
|
176
|
+
textDecoration: 'underline',
|
|
177
|
+
},
|
|
178
|
+
}}>
|
|
179
|
+
{t('payment.checkout.promotion.add_code')}
|
|
180
|
+
</Button>
|
|
181
|
+
))}
|
|
182
|
+
</Box>
|
|
183
|
+
);
|
|
184
|
+
}
|
package/src/contexts/payment.tsx
CHANGED
|
@@ -29,6 +29,11 @@ export type PaymentContextType = {
|
|
|
29
29
|
api: Axios;
|
|
30
30
|
payable: boolean;
|
|
31
31
|
setPayable: (status: boolean) => void;
|
|
32
|
+
paymentState: {
|
|
33
|
+
paying: boolean;
|
|
34
|
+
stripePaying: boolean;
|
|
35
|
+
};
|
|
36
|
+
setPaymentState: (state: Partial<{ paying: boolean; stripePaying: boolean }>) => void;
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
export type PaymentContextProps = {
|
|
@@ -188,6 +193,14 @@ function PaymentProvider({
|
|
|
188
193
|
|
|
189
194
|
const prefix = getPrefix();
|
|
190
195
|
const [payable, setPayable] = useState(true);
|
|
196
|
+
const [paymentState, setPaymentState] = useState({
|
|
197
|
+
paying: false,
|
|
198
|
+
stripePaying: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const updatePaymentState = (state: Partial<{ paying: boolean; stripePaying: boolean }>) => {
|
|
202
|
+
setPaymentState((prev) => ({ ...prev, ...state }));
|
|
203
|
+
};
|
|
191
204
|
|
|
192
205
|
if (error) {
|
|
193
206
|
return <Alert severity="error">{error.message}</Alert>;
|
|
@@ -212,6 +225,8 @@ function PaymentProvider({
|
|
|
212
225
|
api,
|
|
213
226
|
payable,
|
|
214
227
|
setPayable,
|
|
228
|
+
paymentState,
|
|
229
|
+
setPaymentState: updatePaymentState,
|
|
215
230
|
}}>
|
|
216
231
|
{children}
|
|
217
232
|
</Provider>
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ import DateRangePicker from './components/date-range-picker';
|
|
|
39
39
|
import AutoTopupModal from './components/auto-topup/modal';
|
|
40
40
|
import AutoTopup from './components/auto-topup';
|
|
41
41
|
import Collapse from './components/collapse';
|
|
42
|
+
import PromotionCode from './components/promotion-code';
|
|
42
43
|
|
|
43
44
|
export { PaymentThemeProvider } from './theme';
|
|
44
45
|
|
|
@@ -102,4 +103,5 @@ export {
|
|
|
102
103
|
AutoTopupModal,
|
|
103
104
|
AutoTopup,
|
|
104
105
|
Collapse,
|
|
106
|
+
PromotionCode,
|
|
105
107
|
};
|
package/src/libs/util.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
PaymentDetails,
|
|
6
6
|
PriceCurrency,
|
|
7
7
|
PriceRecurring,
|
|
8
|
+
TCoupon,
|
|
8
9
|
TInvoiceExpanded,
|
|
9
10
|
TLineItemExpanded,
|
|
10
11
|
TPaymentCurrency,
|
|
@@ -31,6 +32,40 @@ import type { ActionProps, PricingRenderProps } from '../types';
|
|
|
31
32
|
|
|
32
33
|
export const PAYMENT_KIT_DID = 'z2qaCNvKMv5GjouKdcDWexv6WqtHbpNPQDnAk';
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Format coupon discount terms for display
|
|
37
|
+
*/
|
|
38
|
+
export const formatCouponTerms = (coupon: TCoupon, currency: TPaymentCurrency, locale: string = 'en'): string => {
|
|
39
|
+
let couponOff = '';
|
|
40
|
+
|
|
41
|
+
if (coupon.percent_off && coupon.percent_off > 0) {
|
|
42
|
+
couponOff = t('payment.checkout.coupon.percentage', locale, { percent: coupon.percent_off });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (coupon.amount_off && coupon.amount_off !== '0') {
|
|
46
|
+
const { symbol } = currency;
|
|
47
|
+
couponOff =
|
|
48
|
+
coupon.currency_id === currency.id
|
|
49
|
+
? coupon.amount_off || ''
|
|
50
|
+
: coupon.currency_options?.[currency.id]?.amount_off || '';
|
|
51
|
+
if (couponOff) {
|
|
52
|
+
couponOff = t('payment.checkout.coupon.fixedAmount', locale, {
|
|
53
|
+
amount: formatAmount(couponOff, currency.decimal),
|
|
54
|
+
symbol,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!couponOff) {
|
|
60
|
+
return t('payment.checkout.coupon.noDiscount');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return t(`payment.checkout.coupon.terms.${coupon.duration}`, locale, {
|
|
64
|
+
couponOff,
|
|
65
|
+
months: coupon.duration_in_months || 0,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
34
69
|
export const isPaymentKitMounted = () => {
|
|
35
70
|
return (window.blocklet?.componentMountPoints || []).some((x: any) => x.did === PAYMENT_KIT_DID);
|
|
36
71
|
};
|
package/src/locales/en.tsx
CHANGED
|
@@ -104,8 +104,8 @@ export default flat({
|
|
|
104
104
|
know: 'I know',
|
|
105
105
|
relatedSubscription: 'Subscription',
|
|
106
106
|
connect: {
|
|
107
|
-
defaultScan: 'Use following methods to complete this action',
|
|
108
|
-
scan: 'Use following methods to complete this {action}',
|
|
107
|
+
defaultScan: 'Use the following methods to complete this action',
|
|
108
|
+
scan: 'Use the following methods to complete this {action}',
|
|
109
109
|
confirm: 'Confirm',
|
|
110
110
|
cancel: 'Cancel',
|
|
111
111
|
},
|
|
@@ -156,14 +156,14 @@ export default flat({
|
|
|
156
156
|
},
|
|
157
157
|
inactive: 'Donation feature is not enabled',
|
|
158
158
|
enable: 'Enable Donation',
|
|
159
|
-
enableSuccess: '
|
|
160
|
-
configPrompt: '
|
|
159
|
+
enableSuccess: 'Successfully enabled',
|
|
160
|
+
configPrompt: 'The donation feature is enabled. You can configure donation options in Payment Kit',
|
|
161
161
|
configNow: 'Configure Now',
|
|
162
162
|
later: 'Configure Later',
|
|
163
163
|
configTip: 'Configure donation settings in Payment Kit',
|
|
164
164
|
},
|
|
165
165
|
cardPay: '{action} with bank card',
|
|
166
|
-
empty: '
|
|
166
|
+
empty: 'Nothing to pay',
|
|
167
167
|
per: 'per',
|
|
168
168
|
pay: 'Pay {payee}',
|
|
169
169
|
try1: 'Try {name}',
|
|
@@ -177,8 +177,8 @@ export default flat({
|
|
|
177
177
|
least: 'continue with at least',
|
|
178
178
|
completed: {
|
|
179
179
|
payment: 'Thanks for your purchase',
|
|
180
|
-
subscription: 'Thanks for
|
|
181
|
-
setup: 'Thanks for
|
|
180
|
+
subscription: 'Thanks for subscribing',
|
|
181
|
+
setup: 'Thanks for subscribing',
|
|
182
182
|
donate: 'Thanks for your tip',
|
|
183
183
|
tip: 'A payment to {payee} has been completed. You can view the details of this payment in your account.',
|
|
184
184
|
},
|
|
@@ -188,7 +188,7 @@ export default flat({
|
|
|
188
188
|
delivered: 'Installation completed',
|
|
189
189
|
failed: 'Processing failed',
|
|
190
190
|
failedMsg:
|
|
191
|
-
'An exception occurred during installation. We will automatically process a refund for you. We apologize for the inconvenience
|
|
191
|
+
'An exception occurred during installation. We will automatically process a refund for you. We apologize for the inconvenience. Thank you for your understanding!',
|
|
192
192
|
},
|
|
193
193
|
confirm: {
|
|
194
194
|
withStake:
|
|
@@ -241,14 +241,39 @@ export default flat({
|
|
|
241
241
|
},
|
|
242
242
|
emptyItems: {
|
|
243
243
|
title: 'Nothing to show here',
|
|
244
|
-
description: '
|
|
244
|
+
description: 'It seems this checkout session is not configured properly',
|
|
245
245
|
},
|
|
246
246
|
orderSummary: 'Order Summary',
|
|
247
247
|
paymentDetails: 'Payment Details',
|
|
248
248
|
productListTotal: 'Includes {total} items',
|
|
249
|
+
promotion: {
|
|
250
|
+
add_code: 'Add promotion code',
|
|
251
|
+
enter_code: 'Enter promotion code',
|
|
252
|
+
placeholder: 'Enter code',
|
|
253
|
+
apply: 'Apply',
|
|
254
|
+
applied: 'Applied promotion codes',
|
|
255
|
+
dialog: {
|
|
256
|
+
title: 'Add promotion code',
|
|
257
|
+
},
|
|
258
|
+
error: {
|
|
259
|
+
unknown: 'Unknown error',
|
|
260
|
+
network: 'Network error occurred',
|
|
261
|
+
removal: 'Failed to remove code',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
coupon: {
|
|
265
|
+
noDiscount: 'No discount',
|
|
266
|
+
percentage: '{percent}% off',
|
|
267
|
+
fixedAmount: '{amount} {symbol} off',
|
|
268
|
+
terms: {
|
|
269
|
+
forever: '{couponOff} forever',
|
|
270
|
+
once: '{couponOff} once',
|
|
271
|
+
repeating: "{couponOff} for {months} month{months > 1 ? 's' : ''}",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
249
274
|
connectModal: {
|
|
250
275
|
title: '{action}',
|
|
251
|
-
scan: 'Use following methods to complete this payment',
|
|
276
|
+
scan: 'Use the following methods to complete this payment',
|
|
252
277
|
confirm: 'Confirm',
|
|
253
278
|
cancel: 'Cancel',
|
|
254
279
|
},
|
|
@@ -337,7 +362,7 @@ export default flat({
|
|
|
337
362
|
summary: 'Summary',
|
|
338
363
|
products: 'Products',
|
|
339
364
|
update: 'Update Information',
|
|
340
|
-
empty: '
|
|
365
|
+
empty: 'It seems you do not have any subscriptions or payments here',
|
|
341
366
|
cancel: {
|
|
342
367
|
button: 'Unsubscribe',
|
|
343
368
|
title: 'Cancel your subscription',
|
|
@@ -350,7 +375,7 @@ export default flat({
|
|
|
350
375
|
tip: 'We would love your feedback, it will help us improve our service',
|
|
351
376
|
too_expensive: 'The service is too expensive',
|
|
352
377
|
missing_features: 'Features are missing for this service',
|
|
353
|
-
switched_service: 'I have switched to alternative service',
|
|
378
|
+
switched_service: 'I have switched to an alternative service',
|
|
354
379
|
unused: 'I no longer use this service',
|
|
355
380
|
customer_service: 'The customer service is poor',
|
|
356
381
|
too_complex: 'The service is too complex to use',
|
|
@@ -361,7 +386,7 @@ export default flat({
|
|
|
361
386
|
pastDue: {
|
|
362
387
|
button: 'Pay',
|
|
363
388
|
invoices: 'Past Due Invoices',
|
|
364
|
-
warning: 'Past due invoices need to be paid immediately, otherwise you
|
|
389
|
+
warning: 'Past due invoices need to be paid immediately, otherwise you cannot make new purchases anymore.',
|
|
365
390
|
alert: {
|
|
366
391
|
title: 'You have unpaid invoices',
|
|
367
392
|
customMessage: 'Please pay immediately, otherwise new purchases or subscriptions will be prohibited.',
|
|
@@ -419,7 +444,7 @@ export default flat({
|
|
|
419
444
|
amountApplied: 'Applied Credit',
|
|
420
445
|
pay: 'Pay this invoice',
|
|
421
446
|
paySuccess: 'You have successfully paid the invoice',
|
|
422
|
-
payError: 'Failed to
|
|
447
|
+
payError: 'Failed to pay the invoice',
|
|
423
448
|
renew: 'Renew the subscription',
|
|
424
449
|
renewSuccess: 'You have successfully renewed the subscription',
|
|
425
450
|
renewError: 'Failed to renew the subscription',
|
|
@@ -447,7 +472,7 @@ export default flat({
|
|
|
447
472
|
viewAll: 'View all',
|
|
448
473
|
empty: 'There are no subscriptions here',
|
|
449
474
|
changePayment: 'Change payment method',
|
|
450
|
-
trialLeft: '
|
|
475
|
+
trialLeft: 'Trial Left',
|
|
451
476
|
owner: 'Subscription Owner',
|
|
452
477
|
},
|
|
453
478
|
overdue: {
|
package/src/locales/zh.tsx
CHANGED
|
@@ -229,6 +229,35 @@ export default flat({
|
|
|
229
229
|
add: '添加到订单',
|
|
230
230
|
remove: '从订单移除',
|
|
231
231
|
},
|
|
232
|
+
promotion: {
|
|
233
|
+
add_code: '添加促销码',
|
|
234
|
+
enter_code: '输入促销码',
|
|
235
|
+
apply: '应用',
|
|
236
|
+
applied: '已应用的促销码',
|
|
237
|
+
placeholder: '输入促销码',
|
|
238
|
+
duration_once: '1次优惠 {amount} {symbol}',
|
|
239
|
+
duration_repeating: '{months}个月优惠 {amount} {symbol}',
|
|
240
|
+
duration_forever: '永久优惠 {amount} {symbol}',
|
|
241
|
+
dialog: {
|
|
242
|
+
title: '添加促销码',
|
|
243
|
+
},
|
|
244
|
+
error: {
|
|
245
|
+
invalid: '无效的促销码',
|
|
246
|
+
expired: '促销码已过期',
|
|
247
|
+
used: '促销码已被使用',
|
|
248
|
+
not_applicable: '促销码不适用于此订单',
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
coupon: {
|
|
252
|
+
noDiscount: '无优惠',
|
|
253
|
+
percentage: '{percent}%',
|
|
254
|
+
fixedAmount: '{amount} {symbol}',
|
|
255
|
+
terms: {
|
|
256
|
+
forever: '永久享 {couponOff} 折扣',
|
|
257
|
+
once: '单次享 {couponOff} 折扣',
|
|
258
|
+
repeating: '{months} 个月内享 {couponOff} 折扣',
|
|
259
|
+
},
|
|
260
|
+
},
|
|
232
261
|
credit: {
|
|
233
262
|
oneTimeInfo: '付款完成后您将获得 {amount} {symbol} 额度',
|
|
234
263
|
recurringInfo: '您将{period}获得 {amount} {symbol} 额度',
|
|
@@ -176,7 +176,7 @@ export default function PaymentForm({
|
|
|
176
176
|
// const theme = useTheme();
|
|
177
177
|
const { t, locale } = useLocaleContext();
|
|
178
178
|
const { isMobile } = useMobile();
|
|
179
|
-
const { session, connect, payable } = usePaymentContext();
|
|
179
|
+
const { session, connect, payable, setPaymentState } = usePaymentContext();
|
|
180
180
|
const subscription = useSubscription('events');
|
|
181
181
|
const formErrorPosition = 'bottom';
|
|
182
182
|
const {
|
|
@@ -244,6 +244,15 @@ export default function PaymentForm({
|
|
|
244
244
|
}
|
|
245
245
|
}, [subscription]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
246
246
|
|
|
247
|
+
// Sync payment states to PaymentContext
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
setPaymentState({
|
|
250
|
+
paying: state.submitting || state.paying,
|
|
251
|
+
stripePaying: state.stripePaying,
|
|
252
|
+
});
|
|
253
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
254
|
+
}, [state.submitting, state.paying, state.stripePaying]);
|
|
255
|
+
|
|
247
256
|
const mergeUserInfo = (
|
|
248
257
|
customerInfo: UserInfo | (TCustomer & { fullName?: string }),
|
|
249
258
|
userInfo?: UserInfo
|
package/src/payment/index.tsx
CHANGED
|
@@ -177,6 +177,15 @@ function PaymentInner({
|
|
|
177
177
|
if (onChange) {
|
|
178
178
|
onChange(methods.getValues());
|
|
179
179
|
}
|
|
180
|
+
if ((state.checkoutSession as any)?.discounts?.length) {
|
|
181
|
+
api
|
|
182
|
+
.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
|
|
183
|
+
currency_id: currencyId,
|
|
184
|
+
})
|
|
185
|
+
.then(() => {
|
|
186
|
+
onPromotionUpdate();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
180
189
|
}, [currencyId]); // eslint-disable-line
|
|
181
190
|
|
|
182
191
|
const onUpsell = async (from: string, to: string) => {
|
|
@@ -245,6 +254,16 @@ function PaymentInner({
|
|
|
245
254
|
}
|
|
246
255
|
};
|
|
247
256
|
|
|
257
|
+
const onPromotionUpdate = async () => {
|
|
258
|
+
try {
|
|
259
|
+
const { data } = await api.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`);
|
|
260
|
+
setState({ checkoutSession: data.checkoutSession });
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(err);
|
|
263
|
+
Toast.error(formatError(err));
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
248
267
|
const handlePaid = (result: any) => {
|
|
249
268
|
setState({ checkoutSession: result.checkoutSession });
|
|
250
269
|
onPaid(result);
|
|
@@ -292,6 +311,9 @@ function PaymentInner({
|
|
|
292
311
|
donationSettings={paymentLink?.donation_settings}
|
|
293
312
|
action={action}
|
|
294
313
|
completed={completed}
|
|
314
|
+
checkoutSession={state.checkoutSession}
|
|
315
|
+
onPromotionUpdate={onPromotionUpdate}
|
|
316
|
+
paymentMethods={paymentMethods as TPaymentMethodExpanded[]}
|
|
295
317
|
showFeatures={showFeatures}
|
|
296
318
|
/>
|
|
297
319
|
{mode === 'standalone' && !isMobile && (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import type { PriceRecurring, TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
|
|
3
|
-
import { Box, Stack, Typography, IconButton, TextField, Alert } from '@mui/material';
|
|
4
|
-
import { Add, Remove } from '@mui/icons-material';
|
|
3
|
+
import { Box, Stack, Typography, IconButton, TextField, Alert, Chip } from '@mui/material';
|
|
4
|
+
import { Add, Remove, LocalOffer } from '@mui/icons-material';
|
|
5
5
|
|
|
6
6
|
import React, { useMemo, useState } from 'react';
|
|
7
7
|
import Status from '../components/status';
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
formatQuantityInventory,
|
|
15
15
|
formatRecurring,
|
|
16
16
|
formatUpsellSaving,
|
|
17
|
+
formatAmount,
|
|
17
18
|
} from '../libs/util';
|
|
18
19
|
import ProductCard from './product-card';
|
|
19
20
|
import dayjs from '../libs/dayjs';
|
|
@@ -212,6 +213,40 @@ export default function ProductItem({
|
|
|
212
213
|
)}
|
|
213
214
|
</Stack>
|
|
214
215
|
</Stack>
|
|
216
|
+
|
|
217
|
+
{/* Display discount information for this item */}
|
|
218
|
+
{item.discount_amounts && item.discount_amounts.length > 0 && (
|
|
219
|
+
<Stack direction="row" spacing={1} sx={{ mt: 1, alignItems: 'center' }}>
|
|
220
|
+
{item.discount_amounts.map((discountAmount: any) => (
|
|
221
|
+
<Chip
|
|
222
|
+
key={discountAmount.promotion_code}
|
|
223
|
+
icon={<LocalOffer sx={{ fontSize: '0.8rem !important' }} />}
|
|
224
|
+
label={
|
|
225
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
226
|
+
<Typography component="span" sx={{ fontSize: '0.75rem', fontWeight: 'medium' }}>
|
|
227
|
+
{discountAmount.promotion_code?.code || 'DISCOUNT'}
|
|
228
|
+
</Typography>
|
|
229
|
+
<Typography component="span" sx={{ fontSize: '0.75rem' }}>
|
|
230
|
+
(-{formatAmount(discountAmount.amount || '0', currency.decimal)} {currency.symbol})
|
|
231
|
+
</Typography>
|
|
232
|
+
</Box>
|
|
233
|
+
}
|
|
234
|
+
size="small"
|
|
235
|
+
variant="filled"
|
|
236
|
+
sx={{
|
|
237
|
+
height: 20,
|
|
238
|
+
'& .MuiChip-icon': {
|
|
239
|
+
color: 'warning.main',
|
|
240
|
+
},
|
|
241
|
+
'& .MuiChip-label': {
|
|
242
|
+
px: 1,
|
|
243
|
+
},
|
|
244
|
+
}}
|
|
245
|
+
/>
|
|
246
|
+
))}
|
|
247
|
+
</Stack>
|
|
248
|
+
)}
|
|
249
|
+
|
|
215
250
|
{showFeatures && features.length > 0 && (
|
|
216
251
|
<Box
|
|
217
252
|
sx={{
|