@blocklet/payment-react 1.20.11 → 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 +25 -0
- 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 +25 -0
- 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 +25 -0
- 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
|
@@ -122,7 +122,8 @@ function PaymentForm({
|
|
|
122
122
|
const {
|
|
123
123
|
session,
|
|
124
124
|
connect,
|
|
125
|
-
payable
|
|
125
|
+
payable,
|
|
126
|
+
setPaymentState
|
|
126
127
|
} = (0, _payment.usePaymentContext)();
|
|
127
128
|
const subscription = (0, _subscription.useSubscription)("events");
|
|
128
129
|
const formErrorPosition = "bottom";
|
|
@@ -174,6 +175,12 @@ function PaymentForm({
|
|
|
174
175
|
subscription.on("checkout.session.completed", onCheckoutComplete);
|
|
175
176
|
}
|
|
176
177
|
}, [subscription]);
|
|
178
|
+
(0, _react.useEffect)(() => {
|
|
179
|
+
setPaymentState({
|
|
180
|
+
paying: state.submitting || state.paying,
|
|
181
|
+
stripePaying: state.stripePaying
|
|
182
|
+
});
|
|
183
|
+
}, [state.submitting, state.paying, state.stripePaying]);
|
|
177
184
|
const mergeUserInfo = (customerInfo, userInfo) => {
|
|
178
185
|
return {
|
|
179
186
|
...(userInfo || {}),
|
package/lib/payment/index.js
CHANGED
|
@@ -149,6 +149,13 @@ function PaymentInner({
|
|
|
149
149
|
if (onChange) {
|
|
150
150
|
onChange(methods.getValues());
|
|
151
151
|
}
|
|
152
|
+
if (state.checkoutSession?.discounts?.length) {
|
|
153
|
+
_api.default.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, {
|
|
154
|
+
currency_id: currencyId
|
|
155
|
+
}).then(() => {
|
|
156
|
+
onPromotionUpdate();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
152
159
|
}, [currencyId]);
|
|
153
160
|
const onUpsell = async (from, to) => {
|
|
154
161
|
try {
|
|
@@ -244,6 +251,19 @@ function PaymentInner({
|
|
|
244
251
|
_Toast.default.error((0, _util2.formatError)(err));
|
|
245
252
|
}
|
|
246
253
|
};
|
|
254
|
+
const onPromotionUpdate = async () => {
|
|
255
|
+
try {
|
|
256
|
+
const {
|
|
257
|
+
data
|
|
258
|
+
} = await _api.default.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`);
|
|
259
|
+
setState({
|
|
260
|
+
checkoutSession: data.checkoutSession
|
|
261
|
+
});
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.error(err);
|
|
264
|
+
_Toast.default.error((0, _util2.formatError)(err));
|
|
265
|
+
}
|
|
266
|
+
};
|
|
247
267
|
const handlePaid = result => {
|
|
248
268
|
setState({
|
|
249
269
|
checkoutSession: result.checkoutSession
|
|
@@ -292,6 +312,9 @@ function PaymentInner({
|
|
|
292
312
|
donationSettings: paymentLink?.donation_settings,
|
|
293
313
|
action,
|
|
294
314
|
completed,
|
|
315
|
+
checkoutSession: state.checkoutSession,
|
|
316
|
+
onPromotionUpdate,
|
|
317
|
+
paymentMethods,
|
|
295
318
|
showFeatures
|
|
296
319
|
}), mode === "standalone" && !isMobile && /* @__PURE__ */(0, _jsxRuntime.jsx)(_footer.default, {
|
|
297
320
|
className: "cko-footer",
|
|
@@ -183,6 +183,52 @@ function ProductItem({
|
|
|
183
183
|
children: pricing.secondary
|
|
184
184
|
})]
|
|
185
185
|
})]
|
|
186
|
+
}), item.discount_amounts && item.discount_amounts.length > 0 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
|
|
187
|
+
direction: "row",
|
|
188
|
+
spacing: 1,
|
|
189
|
+
sx: {
|
|
190
|
+
mt: 1,
|
|
191
|
+
alignItems: "center"
|
|
192
|
+
},
|
|
193
|
+
children: item.discount_amounts.map(discountAmount => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Chip, {
|
|
194
|
+
icon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.LocalOffer, {
|
|
195
|
+
sx: {
|
|
196
|
+
fontSize: "0.8rem !important"
|
|
197
|
+
}
|
|
198
|
+
}),
|
|
199
|
+
label: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
|
|
200
|
+
sx: {
|
|
201
|
+
display: "flex",
|
|
202
|
+
alignItems: "center",
|
|
203
|
+
gap: 0.5
|
|
204
|
+
},
|
|
205
|
+
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
206
|
+
component: "span",
|
|
207
|
+
sx: {
|
|
208
|
+
fontSize: "0.75rem",
|
|
209
|
+
fontWeight: "medium"
|
|
210
|
+
},
|
|
211
|
+
children: discountAmount.promotion_code?.code || "DISCOUNT"
|
|
212
|
+
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
|
|
213
|
+
component: "span",
|
|
214
|
+
sx: {
|
|
215
|
+
fontSize: "0.75rem"
|
|
216
|
+
},
|
|
217
|
+
children: ["(-", (0, _util.formatAmount)(discountAmount.amount || "0", currency.decimal), " ", currency.symbol, ")"]
|
|
218
|
+
})]
|
|
219
|
+
}),
|
|
220
|
+
size: "small",
|
|
221
|
+
variant: "filled",
|
|
222
|
+
sx: {
|
|
223
|
+
height: 20,
|
|
224
|
+
"& .MuiChip-icon": {
|
|
225
|
+
color: "warning.main"
|
|
226
|
+
},
|
|
227
|
+
"& .MuiChip-label": {
|
|
228
|
+
px: 1
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}, discountAmount.promotion_code))
|
|
186
232
|
}), showFeatures && features.length > 0 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
187
233
|
sx: {
|
|
188
234
|
display: "flex",
|
package/lib/payment/summary.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DonationSettings, TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
|
|
1
|
+
import type { DonationSettings, TLineItemExpanded, TPaymentCurrency, TCheckoutSession, TPaymentMethodExpanded } from '@blocklet/payment-types';
|
|
2
2
|
type Props = {
|
|
3
3
|
items: TLineItemExpanded[];
|
|
4
4
|
currency: TPaymentCurrency;
|
|
@@ -17,7 +17,10 @@ type Props = {
|
|
|
17
17
|
donationSettings?: DonationSettings;
|
|
18
18
|
action?: string;
|
|
19
19
|
completed?: boolean;
|
|
20
|
+
checkoutSession?: TCheckoutSession;
|
|
21
|
+
onPromotionUpdate?: () => void;
|
|
22
|
+
paymentMethods?: TPaymentMethodExpanded[];
|
|
20
23
|
showFeatures?: boolean;
|
|
21
24
|
};
|
|
22
|
-
export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onQuantityChange, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, completed, showFeatures, ...rest }: Props): import("react").JSX.Element;
|
|
25
|
+
export default function PaymentSummary({ items, currency, trialInDays, billingThreshold, onUpsell, onDownsell, onQuantityChange, onApplyCrossSell, onCancelCrossSell, onChangeAmount, checkoutSessionId, crossSellBehavior, showStaking, donationSettings, action, trialEnd, completed, checkoutSession, paymentMethods, onPromotionUpdate, showFeatures, ...rest }: Props): import("react").JSX.Element;
|
|
23
26
|
export {};
|
package/lib/payment/summary.js
CHANGED
|
@@ -24,6 +24,7 @@ var _livemode = _interopRequireDefault(require("../components/livemode"));
|
|
|
24
24
|
var _payment = require("../contexts/payment");
|
|
25
25
|
var _mobile = require("../hooks/mobile");
|
|
26
26
|
var _loadingButton = _interopRequireDefault(require("../components/loading-button"));
|
|
27
|
+
var _promotionCode = _interopRequireDefault(require("../components/promotion-code"));
|
|
27
28
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
28
29
|
const ExpandMore = (0, _styles.styled)(props => {
|
|
29
30
|
const {
|
|
@@ -101,6 +102,9 @@ function PaymentSummary({
|
|
|
101
102
|
action = "",
|
|
102
103
|
trialEnd = 0,
|
|
103
104
|
completed = false,
|
|
105
|
+
checkoutSession = void 0,
|
|
106
|
+
paymentMethods = [],
|
|
107
|
+
onPromotionUpdate = _noop.default,
|
|
104
108
|
showFeatures = false,
|
|
105
109
|
...rest
|
|
106
110
|
}) {
|
|
@@ -111,7 +115,10 @@ function PaymentSummary({
|
|
|
111
115
|
const {
|
|
112
116
|
isMobile
|
|
113
117
|
} = (0, _mobile.useMobile)();
|
|
114
|
-
const
|
|
118
|
+
const {
|
|
119
|
+
paymentState,
|
|
120
|
+
...settings
|
|
121
|
+
} = (0, _payment.usePaymentContext)();
|
|
115
122
|
const [state, setState] = (0, _ahooks.useSetState)({
|
|
116
123
|
loading: false,
|
|
117
124
|
shake: false,
|
|
@@ -121,12 +128,40 @@ function PaymentSummary({
|
|
|
121
128
|
data,
|
|
122
129
|
runAsync
|
|
123
130
|
} = (0, _ahooks.useRequest)(() => checkoutSessionId ? fetchCrossSell(checkoutSessionId) : Promise.resolve(null));
|
|
124
|
-
const
|
|
131
|
+
const sessionDiscounts = checkoutSession?.discounts || [];
|
|
132
|
+
const allowPromotionCodes = !!checkoutSession?.allow_promotion_codes;
|
|
133
|
+
const hasDiscounts = sessionDiscounts?.length > 0;
|
|
134
|
+
const discountCurrency = paymentMethods && checkoutSession ? (0, _util2.findCurrency)(paymentMethods, hasDiscounts ? checkoutSession?.currency_id || currency.id : currency.id) || settings.settings?.baseCurrency : currency;
|
|
135
|
+
const headlines = (0, _util2.formatCheckoutHeadlines)(items, discountCurrency, {
|
|
125
136
|
trialEnd,
|
|
126
137
|
trialInDays
|
|
127
138
|
}, locale);
|
|
128
|
-
const staking = showStaking ? getStakingSetup(items,
|
|
129
|
-
const
|
|
139
|
+
const staking = showStaking ? getStakingSetup(items, discountCurrency, billingThreshold) : "0";
|
|
140
|
+
const getAppliedPromotionCodes = () => {
|
|
141
|
+
if (!sessionDiscounts?.length) return [];
|
|
142
|
+
return sessionDiscounts.map(discount => ({
|
|
143
|
+
id: discount.promotion_code || discount.coupon,
|
|
144
|
+
code: discount.verification_data?.code || "APPLIED",
|
|
145
|
+
discount_amount: discount.discount_amount
|
|
146
|
+
}));
|
|
147
|
+
};
|
|
148
|
+
const handlePromotionUpdate = () => {
|
|
149
|
+
onPromotionUpdate?.();
|
|
150
|
+
};
|
|
151
|
+
const handleRemovePromotion = async sessionId => {
|
|
152
|
+
if (paymentState.paying || paymentState.stripePaying) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
await _api.default.delete(`/api/checkout-sessions/${sessionId}/remove-promotion`);
|
|
157
|
+
onPromotionUpdate?.();
|
|
158
|
+
} catch (err) {
|
|
159
|
+
console.error("Failed to remove promotion code:", err);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const discountAmount = new _util.BN(checkoutSession?.total_details?.amount_discount || "0");
|
|
163
|
+
const subtotalAmount = (0, _util.fromUnitToToken)(new _util.BN((0, _util.fromTokenToUnit)(headlines.actualAmount, discountCurrency?.decimal)).add(new _util.BN(staking)).toString(), discountCurrency?.decimal);
|
|
164
|
+
const totalAmount = (0, _util.fromUnitToToken)(new _util.BN((0, _util.fromTokenToUnit)(subtotalAmount, discountCurrency?.decimal)).sub(discountAmount).toString(), discountCurrency?.decimal);
|
|
130
165
|
(0, _useBus.default)("error.REQUIRE_CROSS_SELL", () => {
|
|
131
166
|
setState({
|
|
132
167
|
shake: true
|
|
@@ -194,13 +229,13 @@ function PaymentSummary({
|
|
|
194
229
|
item: x,
|
|
195
230
|
settings: donationSettings,
|
|
196
231
|
onChange: onChangeAmount,
|
|
197
|
-
currency
|
|
198
|
-
}, `${x.price_id}-${
|
|
232
|
+
currency: discountCurrency
|
|
233
|
+
}, `${x.price_id}-${discountCurrency.id}`) : /* @__PURE__ */(0, _jsxRuntime.jsx)(_productItem.default, {
|
|
199
234
|
item: x,
|
|
200
235
|
items,
|
|
201
236
|
trialInDays,
|
|
202
237
|
trialEnd,
|
|
203
|
-
currency,
|
|
238
|
+
currency: discountCurrency,
|
|
204
239
|
onUpsell: handleUpsell,
|
|
205
240
|
onDownsell: handleDownsell,
|
|
206
241
|
adjustableQuantity: x.adjustable_quantity,
|
|
@@ -225,7 +260,7 @@ function PaymentSummary({
|
|
|
225
260
|
children: t("payment.checkout.cross_sell.remove")
|
|
226
261
|
})]
|
|
227
262
|
})
|
|
228
|
-
}, `${x.price_id}-${
|
|
263
|
+
}, `${x.price_id}-${discountCurrency.id}`))
|
|
229
264
|
}), data && items.some(x => x.price_id === data.id) === false && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Grow, {
|
|
230
265
|
in: true,
|
|
231
266
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
|
|
@@ -241,7 +276,7 @@ function PaymentSummary({
|
|
|
241
276
|
},
|
|
242
277
|
items,
|
|
243
278
|
trialInDays,
|
|
244
|
-
currency,
|
|
279
|
+
currency: discountCurrency,
|
|
245
280
|
trialEnd,
|
|
246
281
|
onUpsell: _noop.default,
|
|
247
282
|
onDownsell: _noop.default,
|
|
@@ -400,9 +435,116 @@ function PaymentSummary({
|
|
|
400
435
|
})
|
|
401
436
|
})]
|
|
402
437
|
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
|
|
403
|
-
children: [(0, _util2.formatAmount)(staking,
|
|
438
|
+
children: [(0, _util2.formatAmount)(staking, discountCurrency.decimal), " ", discountCurrency.symbol]
|
|
404
439
|
})]
|
|
405
440
|
})]
|
|
441
|
+
}), (allowPromotionCodes || hasDiscounts) && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
442
|
+
direction: "row",
|
|
443
|
+
spacing: 1,
|
|
444
|
+
sx: {
|
|
445
|
+
justifyContent: "space-between",
|
|
446
|
+
alignItems: "center",
|
|
447
|
+
...(staking > 0 && {
|
|
448
|
+
borderTop: "1px solid",
|
|
449
|
+
borderColor: "divider",
|
|
450
|
+
pt: 1,
|
|
451
|
+
mt: 1
|
|
452
|
+
})
|
|
453
|
+
},
|
|
454
|
+
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
455
|
+
className: "base-label",
|
|
456
|
+
children: t("common.subtotal")
|
|
457
|
+
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
|
|
458
|
+
children: [(0, _util2.formatNumber)(subtotalAmount), " ", discountCurrency.symbol]
|
|
459
|
+
})]
|
|
460
|
+
}), allowPromotionCodes && !hasDiscounts && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
461
|
+
sx: {
|
|
462
|
+
mt: 1
|
|
463
|
+
},
|
|
464
|
+
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_promotionCode.default, {
|
|
465
|
+
checkoutSessionId: checkoutSession.id,
|
|
466
|
+
initialAppliedCodes: getAppliedPromotionCodes(),
|
|
467
|
+
disabled: completed,
|
|
468
|
+
onUpdate: handlePromotionUpdate,
|
|
469
|
+
currencyId: currency.id
|
|
470
|
+
})
|
|
471
|
+
}), hasDiscounts && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
472
|
+
sx: {
|
|
473
|
+
py: 1.5
|
|
474
|
+
},
|
|
475
|
+
children: sessionDiscounts.map(discount => {
|
|
476
|
+
const promotionCodeInfo = discount.promotion_code_details;
|
|
477
|
+
const couponInfo = discount.coupon_details;
|
|
478
|
+
const discountDescription = couponInfo ? (0, _util2.formatCouponTerms)(couponInfo, discountCurrency, locale) : "";
|
|
479
|
+
const notSupported = discountDescription === t("payment.checkout.coupon.noDiscount");
|
|
480
|
+
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
481
|
+
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
482
|
+
direction: "row",
|
|
483
|
+
spacing: 1,
|
|
484
|
+
sx: {
|
|
485
|
+
justifyContent: "space-between",
|
|
486
|
+
alignItems: "center"
|
|
487
|
+
},
|
|
488
|
+
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
489
|
+
direction: "row",
|
|
490
|
+
spacing: 1,
|
|
491
|
+
sx: {
|
|
492
|
+
alignItems: "center",
|
|
493
|
+
backgroundColor: "grey.100",
|
|
494
|
+
width: "fit-content",
|
|
495
|
+
px: 1,
|
|
496
|
+
py: 0.5,
|
|
497
|
+
borderRadius: 1
|
|
498
|
+
},
|
|
499
|
+
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
|
|
500
|
+
sx: {
|
|
501
|
+
fontWeight: "medium",
|
|
502
|
+
fontSize: "small",
|
|
503
|
+
display: "flex",
|
|
504
|
+
alignItems: "center",
|
|
505
|
+
gap: 0.5
|
|
506
|
+
},
|
|
507
|
+
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.LocalOffer, {
|
|
508
|
+
sx: {
|
|
509
|
+
color: "warning.main",
|
|
510
|
+
fontSize: "small"
|
|
511
|
+
}
|
|
512
|
+
}), promotionCodeInfo?.code || discount.verification_data?.code || t("payment.checkout.discount")]
|
|
513
|
+
}), !completed && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
|
|
514
|
+
size: "small",
|
|
515
|
+
disabled: paymentState.paying || paymentState.stripePaying,
|
|
516
|
+
onClick: () => handleRemovePromotion(checkoutSessionId),
|
|
517
|
+
sx: {
|
|
518
|
+
minWidth: "auto",
|
|
519
|
+
width: 16,
|
|
520
|
+
height: 16,
|
|
521
|
+
color: "text.secondary",
|
|
522
|
+
"&.Mui-disabled": {
|
|
523
|
+
color: "text.disabled"
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.Close, {
|
|
527
|
+
sx: {
|
|
528
|
+
fontSize: 14
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
})]
|
|
532
|
+
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
|
|
533
|
+
sx: {
|
|
534
|
+
color: "text.secondary"
|
|
535
|
+
},
|
|
536
|
+
children: ["-", (0, _util2.formatAmount)(discount.discount_amount || "0", discountCurrency.decimal), " ", discountCurrency.symbol]
|
|
537
|
+
})]
|
|
538
|
+
}), discountDescription && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
539
|
+
sx: {
|
|
540
|
+
fontSize: "small",
|
|
541
|
+
color: notSupported ? "error.main" : "text.secondary",
|
|
542
|
+
mt: 0.5
|
|
543
|
+
},
|
|
544
|
+
children: discountDescription
|
|
545
|
+
})]
|
|
546
|
+
}, discount.promotion_code || discount.coupon || `discount-${discount.discount_amount}`);
|
|
547
|
+
})
|
|
406
548
|
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
407
549
|
sx: {
|
|
408
550
|
display: "flex",
|
|
@@ -415,7 +557,7 @@ function PaymentSummary({
|
|
|
415
557
|
className: "base-label",
|
|
416
558
|
children: [t("common.total"), " "]
|
|
417
559
|
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_amount.default, {
|
|
418
|
-
amount: `${totalAmount} ${
|
|
560
|
+
amount: `${totalAmount} ${discountCurrency.symbol}`,
|
|
419
561
|
sx: {
|
|
420
562
|
fontSize: "16px"
|
|
421
563
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.12",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -54,16 +54,16 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@arcblock/did-connect-react": "^3.1.
|
|
58
|
-
"@arcblock/ux": "^3.1.
|
|
59
|
-
"@arcblock/ws": "^1.
|
|
60
|
-
"@blocklet/theme": "^3.1.
|
|
61
|
-
"@blocklet/ui-react": "^3.1.
|
|
57
|
+
"@arcblock/did-connect-react": "^3.1.41",
|
|
58
|
+
"@arcblock/ux": "^3.1.41",
|
|
59
|
+
"@arcblock/ws": "^1.25.1",
|
|
60
|
+
"@blocklet/theme": "^3.1.41",
|
|
61
|
+
"@blocklet/ui-react": "^3.1.41",
|
|
62
62
|
"@mui/icons-material": "^7.1.2",
|
|
63
63
|
"@mui/lab": "7.0.0-beta.14",
|
|
64
64
|
"@mui/material": "^7.1.2",
|
|
65
65
|
"@mui/system": "^7.1.1",
|
|
66
|
-
"@ocap/util": "^1.
|
|
66
|
+
"@ocap/util": "^1.25.1",
|
|
67
67
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
68
68
|
"@stripe/stripe-js": "^2.4.0",
|
|
69
69
|
"@vitejs/plugin-legacy": "^7.0.0",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@babel/core": "^7.27.4",
|
|
95
95
|
"@babel/preset-env": "^7.27.2",
|
|
96
96
|
"@babel/preset-react": "^7.27.1",
|
|
97
|
-
"@blocklet/payment-types": "1.20.
|
|
97
|
+
"@blocklet/payment-types": "1.20.12",
|
|
98
98
|
"@storybook/addon-essentials": "^7.6.20",
|
|
99
99
|
"@storybook/addon-interactions": "^7.6.20",
|
|
100
100
|
"@storybook/addon-links": "^7.6.20",
|
|
@@ -125,5 +125,5 @@
|
|
|
125
125
|
"vite-plugin-babel": "^1.3.1",
|
|
126
126
|
"vite-plugin-node-polyfills": "^0.23.0"
|
|
127
127
|
},
|
|
128
|
-
"gitHead": "
|
|
128
|
+
"gitHead": "e6c432f89c2bf305efc903c0809f4a0557c07774"
|
|
129
129
|
}
|
|
@@ -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
|
};
|