@blocklet/payment-react 1.26.0 → 1.26.2
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/checkout-v2/components/left/cross-sell-card.js +3 -3
- package/es/checkout-v2/components/left/product-item-card.js +13 -7
- package/es/checkout-v2/components/left/promotion-input.d.ts +3 -1
- package/es/checkout-v2/components/left/promotion-input.js +4 -2
- package/es/checkout-v2/components/right/submit-button.js +3 -1
- package/es/checkout-v2/panels/left/composite-panel.js +27 -6
- package/es/checkout-v2/panels/left/credit-topup-panel.js +1 -5
- package/es/checkout-v2/panels/right/payment-panel.js +37 -8
- package/es/checkout-v2/utils/format.d.ts +1 -1
- package/es/checkout-v2/utils/format.js +3 -2
- package/es/checkout-v2/views/error-view.js +2 -0
- package/es/checkout-v2/views/success-view.js +3 -1
- package/es/components/over-due-invoice-payment.js +5 -3
- package/es/libs/util.d.ts +8 -0
- package/es/libs/util.js +3 -0
- package/lib/checkout-v2/components/left/cross-sell-card.js +2 -2
- package/lib/checkout-v2/components/left/product-item-card.js +13 -6
- package/lib/checkout-v2/components/left/promotion-input.d.ts +3 -1
- package/lib/checkout-v2/components/left/promotion-input.js +7 -2
- package/lib/checkout-v2/components/right/submit-button.js +3 -1
- package/lib/checkout-v2/panels/left/composite-panel.js +20 -5
- package/lib/checkout-v2/panels/left/credit-topup-panel.js +1 -5
- package/lib/checkout-v2/panels/right/payment-panel.js +43 -6
- package/lib/checkout-v2/utils/format.d.ts +1 -1
- package/lib/checkout-v2/utils/format.js +9 -2
- package/lib/checkout-v2/views/error-view.js +2 -0
- package/lib/checkout-v2/views/success-view.js +2 -0
- package/lib/components/over-due-invoice-payment.js +12 -2
- package/lib/libs/util.d.ts +8 -0
- package/lib/libs/util.js +4 -0
- package/package.json +4 -4
- package/src/checkout-v2/components/left/cross-sell-card.tsx +3 -3
- package/src/checkout-v2/components/left/product-item-card.tsx +30 -12
- package/src/checkout-v2/components/left/promotion-input.tsx +11 -3
- package/src/checkout-v2/components/right/submit-button.tsx +2 -0
- package/src/checkout-v2/panels/left/composite-panel.tsx +28 -6
- package/src/checkout-v2/panels/left/credit-topup-panel.tsx +1 -5
- package/src/checkout-v2/panels/right/payment-panel.tsx +30 -5
- package/src/checkout-v2/utils/format.ts +5 -2
- package/src/checkout-v2/views/error-view.tsx +2 -0
- package/src/checkout-v2/views/success-view.tsx +3 -1
- package/src/components/over-due-invoice-payment.tsx +6 -3
- package/src/libs/util.ts +7 -0
|
@@ -61,6 +61,9 @@ function CompositePanel() {
|
|
|
61
61
|
const canUpsell = nonCrossSellItems.length <= 1;
|
|
62
62
|
const hasTopUpsell = canUpsell && !!upsellPrimaryItem && ["subscription", "setup"].includes(mode);
|
|
63
63
|
const isUpselled = !!upsellPrimaryItem?.upsell_price;
|
|
64
|
+
const [upsellSwitching, setUpsellSwitching] = (0, _react.useState)(false);
|
|
65
|
+
const [pendingUpsell, setPendingUpsell] = (0, _react.useState)(null);
|
|
66
|
+
const visualIsUpselled = pendingUpsell !== null ? pendingUpsell : isUpselled;
|
|
64
67
|
const currentInterval = hasTopUpsell ? upsellPrimaryItem.price?.recurring?.interval : null;
|
|
65
68
|
const upsellInterval = hasTopUpsell ? upsellTarget?.recurring?.interval : null;
|
|
66
69
|
let upsellSavings = 0;
|
|
@@ -88,7 +91,7 @@ function CompositePanel() {
|
|
|
88
91
|
const isMultiItem = lineItems.items.length > 1;
|
|
89
92
|
const activeSx = {
|
|
90
93
|
bgcolor: "primary.main",
|
|
91
|
-
color:
|
|
94
|
+
color: theme => (0, _format.primaryContrastColor)(theme),
|
|
92
95
|
boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)"
|
|
93
96
|
};
|
|
94
97
|
const inactiveSx = {
|
|
@@ -277,15 +280,21 @@ function CompositePanel() {
|
|
|
277
280
|
},
|
|
278
281
|
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
279
282
|
onClick: async () => {
|
|
280
|
-
if (isUpselled) {
|
|
283
|
+
if (isUpselled && !upsellSwitching) {
|
|
284
|
+
setPendingUpsell(false);
|
|
285
|
+
setUpsellSwitching(true);
|
|
281
286
|
try {
|
|
282
287
|
await lineItems.downsell(upsellPrimaryItem.upsell_price?.id || upsellPrimaryItem.price_id);
|
|
283
288
|
} catch (err) {
|
|
289
|
+
setPendingUpsell(null);
|
|
284
290
|
_Toast.default.error(err?.response?.data?.error || err?.message || "Failed");
|
|
291
|
+
} finally {
|
|
292
|
+
setUpsellSwitching(false);
|
|
293
|
+
setPendingUpsell(null);
|
|
285
294
|
}
|
|
286
295
|
}
|
|
287
296
|
},
|
|
288
|
-
sx: capsuleBtnSx(!
|
|
297
|
+
sx: capsuleBtnSx(!visualIsUpselled),
|
|
289
298
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
290
299
|
component: "span",
|
|
291
300
|
sx: {
|
|
@@ -298,15 +307,21 @@ function CompositePanel() {
|
|
|
298
307
|
})
|
|
299
308
|
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
300
309
|
onClick: async () => {
|
|
301
|
-
if (!isUpselled) {
|
|
310
|
+
if (!isUpselled && !upsellSwitching) {
|
|
311
|
+
setPendingUpsell(true);
|
|
312
|
+
setUpsellSwitching(true);
|
|
302
313
|
try {
|
|
303
314
|
await lineItems.upsell(upsellPrimaryItem.price_id, upsellTarget.id);
|
|
304
315
|
} catch (err) {
|
|
316
|
+
setPendingUpsell(null);
|
|
305
317
|
_Toast.default.error(err?.response?.data?.error || err?.message || "Failed");
|
|
318
|
+
} finally {
|
|
319
|
+
setUpsellSwitching(false);
|
|
320
|
+
setPendingUpsell(null);
|
|
306
321
|
}
|
|
307
322
|
}
|
|
308
323
|
},
|
|
309
|
-
sx: capsuleBtnSx(
|
|
324
|
+
sx: capsuleBtnSx(visualIsUpselled),
|
|
310
325
|
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
|
|
311
326
|
component: "span",
|
|
312
327
|
sx: {
|
|
@@ -144,12 +144,8 @@ function CreditTopupPanel() {
|
|
|
144
144
|
interval: intervalDisplay
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
const productDesc = product?.description || "";
|
|
148
|
-
if (productDesc && productDesc.length > 10 && productDesc !== creditName) {
|
|
149
|
-
return productDesc;
|
|
150
|
-
}
|
|
151
147
|
return "";
|
|
152
|
-
}, [creditAmount, currencySymbol, hasSchedule, scheduleConfig,
|
|
148
|
+
}, [creditAmount, currencySymbol, hasSchedule, scheduleConfig, t]);
|
|
153
149
|
const validityText = (0, _react.useMemo)(() => {
|
|
154
150
|
if (!hasExpiry) return "";
|
|
155
151
|
return t("payment.checkout.creditTopup.validFor", {
|
|
@@ -561,7 +561,8 @@ function PaymentPanel() {
|
|
|
561
561
|
remove: promotion.remove
|
|
562
562
|
},
|
|
563
563
|
discounts,
|
|
564
|
-
discountAmount: pricing.discount
|
|
564
|
+
discountAmount: pricing.discount,
|
|
565
|
+
isAmountLoading
|
|
565
566
|
})]
|
|
566
567
|
}), (() => {
|
|
567
568
|
const totalStr = pricing.total || "0";
|
|
@@ -710,7 +711,7 @@ function PaymentPanel() {
|
|
|
710
711
|
})]
|
|
711
712
|
})]
|
|
712
713
|
});
|
|
713
|
-
})(), /* @__PURE__ */(0, _jsxRuntime.
|
|
714
|
+
})(), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Button, {
|
|
714
715
|
variant: "contained",
|
|
715
716
|
size: "large",
|
|
716
717
|
fullWidth: true,
|
|
@@ -720,15 +721,51 @@ function PaymentPanel() {
|
|
|
720
721
|
size: 20,
|
|
721
722
|
color: "inherit"
|
|
722
723
|
}) : null,
|
|
723
|
-
endIcon: !isProcessing ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_ArrowForward.default, {}) : void 0,
|
|
724
724
|
sx: {
|
|
725
725
|
py: 1.5,
|
|
726
726
|
fontSize: "1.1rem",
|
|
727
727
|
fontWeight: 600,
|
|
728
728
|
textTransform: "none",
|
|
729
|
-
borderRadius: "12px"
|
|
729
|
+
borderRadius: "12px",
|
|
730
|
+
color: theme => (0, _format.primaryContrastColor)(theme),
|
|
731
|
+
position: "relative",
|
|
732
|
+
overflow: "hidden",
|
|
733
|
+
"&:hover": {
|
|
734
|
+
bgcolor: "primary.main"
|
|
735
|
+
},
|
|
736
|
+
"&:hover .arrow-icon": {
|
|
737
|
+
transform: "translateX(4px)"
|
|
738
|
+
},
|
|
739
|
+
"&:hover .shine-layer": {
|
|
740
|
+
transform: "translateX(100%)"
|
|
741
|
+
}
|
|
730
742
|
},
|
|
731
|
-
children:
|
|
743
|
+
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
744
|
+
component: "span",
|
|
745
|
+
sx: {
|
|
746
|
+
position: "relative",
|
|
747
|
+
zIndex: 1
|
|
748
|
+
},
|
|
749
|
+
children: isProcessing ? `${t("payment.checkout.processing")}...` : buttonLabel
|
|
750
|
+
}), !isProcessing && /* @__PURE__ */(0, _jsxRuntime.jsx)(_ArrowForward.default, {
|
|
751
|
+
className: "arrow-icon",
|
|
752
|
+
sx: {
|
|
753
|
+
ml: 1,
|
|
754
|
+
position: "relative",
|
|
755
|
+
zIndex: 1,
|
|
756
|
+
transition: "transform 0.2s ease"
|
|
757
|
+
}
|
|
758
|
+
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
|
|
759
|
+
className: "shine-layer",
|
|
760
|
+
sx: {
|
|
761
|
+
position: "absolute",
|
|
762
|
+
inset: 0,
|
|
763
|
+
background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.12), transparent)",
|
|
764
|
+
transform: "translateX(-100%)",
|
|
765
|
+
transition: "transform 0.7s ease",
|
|
766
|
+
pointerEvents: "none"
|
|
767
|
+
}
|
|
768
|
+
})]
|
|
732
769
|
}), isMobile && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
733
770
|
direction: "row",
|
|
734
771
|
alignItems: "center",
|
|
@@ -828,7 +865,7 @@ function PaymentPanel() {
|
|
|
828
865
|
mode,
|
|
829
866
|
subscription,
|
|
830
867
|
staking: pricing.staking,
|
|
831
|
-
appName: session?.
|
|
868
|
+
appName: (0, _util.getStatementDescriptor)(session?.line_items || [])
|
|
832
869
|
}), !isMobile && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
|
|
833
870
|
direction: "row",
|
|
834
871
|
alignItems: "center",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TPaymentCurrency } from '@blocklet/payment-types';
|
|
2
|
+
export { primaryContrastColor } from '../../libs/util';
|
|
2
3
|
export declare const INTERVAL_LOCALE_KEY: Record<string, string>;
|
|
3
4
|
export declare function countryCodeToFlag(code: string): string;
|
|
4
5
|
export declare function formatTokenAmount(unitAmount: string | number | bigint, currency: TPaymentCurrency | null): string;
|
|
@@ -56,4 +57,3 @@ interface ItemMeta {
|
|
|
56
57
|
* Works for the "primary product" header above the item list.
|
|
57
58
|
*/
|
|
58
59
|
export declare function getSessionHeaderMeta(t: TFn, session: any, product: any, items: any[]): ItemMeta;
|
|
59
|
-
export {};
|
|
@@ -10,9 +10,16 @@ exports.formatTokenAmount = formatTokenAmount;
|
|
|
10
10
|
exports.formatTrialText = formatTrialText;
|
|
11
11
|
exports.getSessionHeaderMeta = getSessionHeaderMeta;
|
|
12
12
|
exports.getUnitAmountForCurrency = getUnitAmountForCurrency;
|
|
13
|
+
Object.defineProperty(exports, "primaryContrastColor", {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () {
|
|
16
|
+
return _util2.primaryContrastColor;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
13
19
|
exports.tSafe = tSafe;
|
|
14
20
|
exports.whiteTooltipSx = void 0;
|
|
15
21
|
var _util = require("@ocap/util");
|
|
22
|
+
var _util2 = require("../../libs/util");
|
|
16
23
|
const INTERVAL_LOCALE_KEY = exports.INTERVAL_LOCALE_KEY = {
|
|
17
24
|
day: "common.daily",
|
|
18
25
|
week: "common.weekly",
|
|
@@ -35,7 +42,7 @@ function formatTokenAmount(unitAmount, currency) {
|
|
|
35
42
|
minimumFractionDigits: 0,
|
|
36
43
|
maximumFractionDigits: precision
|
|
37
44
|
});
|
|
38
|
-
return formatted.replace(
|
|
45
|
+
return formatted.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "") || "0";
|
|
39
46
|
} catch {
|
|
40
47
|
return "0";
|
|
41
48
|
}
|
|
@@ -69,7 +76,7 @@ function formatDynamicUnitPrice(price, currency, exchangeRate) {
|
|
|
69
76
|
return tokenAmount.toLocaleString("en-US", {
|
|
70
77
|
minimumFractionDigits: 0,
|
|
71
78
|
maximumFractionDigits: precision
|
|
72
|
-
}).replace(
|
|
79
|
+
}).replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "") || "0";
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
}
|
|
@@ -10,6 +10,7 @@ var _styles = require("@mui/material/styles");
|
|
|
10
10
|
var _ArrowBack = _interopRequireDefault(require("@mui/icons-material/ArrowBack"));
|
|
11
11
|
var _Header = _interopRequireDefault(require("@blocklet/ui-react/lib/Header"));
|
|
12
12
|
var _context = require("@arcblock/ux/lib/Locale/context");
|
|
13
|
+
var _format = require("../utils/format");
|
|
13
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
15
|
function GeometricDecoration() {
|
|
15
16
|
const theme = (0, _styles.useTheme)();
|
|
@@ -223,6 +224,7 @@ function ErrorContent({
|
|
|
223
224
|
fontWeight: 600,
|
|
224
225
|
fontSize: 16,
|
|
225
226
|
letterSpacing: "0.02em",
|
|
227
|
+
color: th => (0, _format.primaryContrastColor)(th),
|
|
226
228
|
boxShadow: `0 8px 32px -4px ${(0, _styles.alpha)(primaryColor, 0.3)}`,
|
|
227
229
|
"&:hover": {
|
|
228
230
|
boxShadow: `0 12px 40px -4px ${(0, _styles.alpha)(primaryColor, 0.4)}`,
|
|
@@ -537,6 +537,7 @@ function SubscriptionLinks({
|
|
|
537
537
|
md: 17
|
|
538
538
|
},
|
|
539
539
|
letterSpacing: "0.02em",
|
|
540
|
+
color: theme => (0, _format.primaryContrastColor)(theme),
|
|
540
541
|
boxShadow: "0 8px 24px -4px rgba(59,130,246,0.25)",
|
|
541
542
|
"&:hover": {
|
|
542
543
|
boxShadow: "0 12px 28px -4px rgba(59,130,246,0.35)"
|
|
@@ -577,6 +578,7 @@ function InvoiceLink({
|
|
|
577
578
|
md: 17
|
|
578
579
|
},
|
|
579
580
|
letterSpacing: "0.02em",
|
|
581
|
+
color: theme => (0, _format.primaryContrastColor)(theme),
|
|
580
582
|
boxShadow: "0 8px 24px -4px rgba(59,130,246,0.25)",
|
|
581
583
|
"&:hover": {
|
|
582
584
|
boxShadow: "0 12px 28px -4px rgba(59,130,246,0.35)"
|
|
@@ -335,11 +335,15 @@ function OverdueInvoicePayment({
|
|
|
335
335
|
} = item;
|
|
336
336
|
const inProcess = payLoading && selectCurrencyId === currency.id;
|
|
337
337
|
const status = paymentStatus[currency.id] || "idle";
|
|
338
|
+
const containedColorSx = (options?.variant || "contained") === "contained" ? {
|
|
339
|
+
color: th => (0, _util.primaryContrastColor)(th)
|
|
340
|
+
} : {};
|
|
338
341
|
if (status === "success") {
|
|
339
342
|
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
|
|
340
343
|
variant: options?.variant || "contained",
|
|
341
344
|
size: "small",
|
|
342
345
|
onClick: () => checkAndHandleInvoicePaid(currency.id),
|
|
346
|
+
sx: containedColorSx,
|
|
343
347
|
...(primaryButton ? {} : {
|
|
344
348
|
color: "success",
|
|
345
349
|
startIcon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.CheckCircle, {})
|
|
@@ -378,7 +382,10 @@ function OverdueInvoicePayment({
|
|
|
378
382
|
disabled: paying || status === "processing",
|
|
379
383
|
loading: paying || status === "processing",
|
|
380
384
|
onClick: onPay,
|
|
381
|
-
sx:
|
|
385
|
+
sx: {
|
|
386
|
+
...containedColorSx,
|
|
387
|
+
...(options?.sx || {})
|
|
388
|
+
},
|
|
382
389
|
children: buttonText
|
|
383
390
|
})
|
|
384
391
|
});
|
|
@@ -389,7 +396,10 @@ function OverdueInvoicePayment({
|
|
|
389
396
|
disabled: inProcess,
|
|
390
397
|
loading: inProcess,
|
|
391
398
|
onClick: () => handlePay(item),
|
|
392
|
-
sx:
|
|
399
|
+
sx: {
|
|
400
|
+
...containedColorSx,
|
|
401
|
+
...(options?.sx || {})
|
|
402
|
+
},
|
|
393
403
|
children: status === "error" ? t("payment.subscription.overdue.retry") : t("payment.subscription.overdue.payNow")
|
|
394
404
|
});
|
|
395
405
|
};
|
package/lib/libs/util.d.ts
CHANGED
|
@@ -194,3 +194,11 @@ export declare function getTokenBalanceLink(method: TPaymentMethod, address: str
|
|
|
194
194
|
export declare function isCreditMetered(price: TPrice): boolean;
|
|
195
195
|
export declare function showStaking(method: TPaymentMethod, currency: TPaymentCurrency, noStake: boolean): boolean;
|
|
196
196
|
export declare function formatLinkWithLocale(url: string, locale?: string): string;
|
|
197
|
+
export declare function primaryContrastColor(theme: {
|
|
198
|
+
palette: {
|
|
199
|
+
primary: {
|
|
200
|
+
main: string;
|
|
201
|
+
};
|
|
202
|
+
getContrastText: (bg: string) => string;
|
|
203
|
+
};
|
|
204
|
+
}): string;
|
package/lib/libs/util.js
CHANGED
|
@@ -75,6 +75,7 @@ exports.lazyLoad = lazyLoad;
|
|
|
75
75
|
exports.mergeExtraParams = void 0;
|
|
76
76
|
exports.openDonationSettings = openDonationSettings;
|
|
77
77
|
exports.parseMarkedText = parseMarkedText;
|
|
78
|
+
exports.primaryContrastColor = primaryContrastColor;
|
|
78
79
|
exports.showStaking = showStaking;
|
|
79
80
|
exports.sleep = sleep;
|
|
80
81
|
exports.stopEvent = stopEvent;
|
|
@@ -1644,4 +1645,7 @@ function formatLinkWithLocale(url, locale) {
|
|
|
1644
1645
|
const separator = url.includes("?") ? "&" : "?";
|
|
1645
1646
|
return `${url}${separator}locale=${locale}`;
|
|
1646
1647
|
}
|
|
1648
|
+
}
|
|
1649
|
+
function primaryContrastColor(theme) {
|
|
1650
|
+
return theme.palette.getContrastText(theme.palette.primary.main);
|
|
1647
1651
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/payment-react",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.2",
|
|
4
4
|
"description": "Reusable react components for payment kit v2",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@arcblock/react-hooks": "^3.5.1",
|
|
60
60
|
"@arcblock/ux": "^3.5.1",
|
|
61
61
|
"@arcblock/ws": "^1.28.5",
|
|
62
|
-
"@blocklet/payment-react-headless": "1.26.
|
|
62
|
+
"@blocklet/payment-react-headless": "1.26.2",
|
|
63
63
|
"@blocklet/theme": "^3.5.1",
|
|
64
64
|
"@blocklet/ui-react": "^3.5.1",
|
|
65
65
|
"@mui/icons-material": "^7.1.2",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"@babel/core": "^7.27.4",
|
|
98
98
|
"@babel/preset-env": "^7.27.2",
|
|
99
99
|
"@babel/preset-react": "^7.27.1",
|
|
100
|
-
"@blocklet/payment-types": "1.26.
|
|
100
|
+
"@blocklet/payment-types": "1.26.2",
|
|
101
101
|
"@storybook/addon-essentials": "^7.6.20",
|
|
102
102
|
"@storybook/addon-interactions": "^7.6.20",
|
|
103
103
|
"@storybook/addon-links": "^7.6.20",
|
|
@@ -128,5 +128,5 @@
|
|
|
128
128
|
"vite-plugin-babel": "^1.3.1",
|
|
129
129
|
"vite-plugin-node-polyfills": "^0.23.0"
|
|
130
130
|
},
|
|
131
|
-
"gitHead": "
|
|
131
|
+
"gitHead": "71242a68d27d56666487176425153dc08071960f"
|
|
132
132
|
}
|
|
@@ -3,7 +3,7 @@ import ShoppingCartCheckoutIcon from '@mui/icons-material/ShoppingCartCheckout';
|
|
|
3
3
|
import { Avatar, Box, Button, Chip, Stack, Typography } from '@mui/material';
|
|
4
4
|
import type { TPaymentCurrency, TPrice } from '@blocklet/payment-types';
|
|
5
5
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
|
-
import { formatDynamicUnitPrice, tSafe, INTERVAL_LOCALE_KEY } from '../../utils/format';
|
|
6
|
+
import { formatDynamicUnitPrice, tSafe, INTERVAL_LOCALE_KEY, primaryContrastColor } from '../../utils/format';
|
|
7
7
|
|
|
8
8
|
interface CrossSellCardProps {
|
|
9
9
|
crossSellItem: TPrice;
|
|
@@ -53,7 +53,7 @@ export default function CrossSellCard({
|
|
|
53
53
|
fontWeight: 900,
|
|
54
54
|
letterSpacing: '0.12em',
|
|
55
55
|
bgcolor: 'primary.main',
|
|
56
|
-
color:
|
|
56
|
+
color: (theme: any) => primaryContrastColor(theme),
|
|
57
57
|
boxShadow: '0 4px 12px rgba(45,124,243,0.2)',
|
|
58
58
|
'& .MuiChip-label': { px: 1.5 },
|
|
59
59
|
}}
|
|
@@ -150,7 +150,7 @@ export default function CrossSellCard({
|
|
|
150
150
|
transition: 'all 0.2s',
|
|
151
151
|
'&:hover': {
|
|
152
152
|
bgcolor: 'primary.main',
|
|
153
|
-
color:
|
|
153
|
+
color: (theme: any) => primaryContrastColor(theme),
|
|
154
154
|
borderColor: 'primary.main',
|
|
155
155
|
},
|
|
156
156
|
'&:active': { transform: 'scale(0.95)' },
|
|
@@ -22,7 +22,13 @@ import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
|
22
22
|
import type { TLineItemExpanded, TPaymentCurrency } from '@blocklet/payment-types';
|
|
23
23
|
import { getPriceUnitAmountByCurrency } from '@blocklet/payment-react-headless';
|
|
24
24
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
INTERVAL_LOCALE_KEY,
|
|
27
|
+
formatDynamicUnitPrice,
|
|
28
|
+
formatTokenAmount,
|
|
29
|
+
formatTrialText,
|
|
30
|
+
primaryContrastColor,
|
|
31
|
+
} from '../../utils/format';
|
|
26
32
|
|
|
27
33
|
interface ProductItemCardProps {
|
|
28
34
|
item: TLineItemExpanded & { adjustable_quantity?: { enabled: boolean; minimum?: number; maximum?: number } };
|
|
@@ -109,7 +115,8 @@ export default function ProductItemCard({
|
|
|
109
115
|
return (
|
|
110
116
|
tokenAmount
|
|
111
117
|
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
112
|
-
.replace(
|
|
118
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
119
|
+
.replace(/\.$/, '') || '0'
|
|
113
120
|
);
|
|
114
121
|
}
|
|
115
122
|
}
|
|
@@ -147,7 +154,12 @@ export default function ProductItemCard({
|
|
|
147
154
|
const discAmount = (numericTotal * couponDetails.percent_off) / 100;
|
|
148
155
|
const abs = Math.abs(discAmount);
|
|
149
156
|
const precision = abs > 0 && abs < 0.01 ? 6 : 2;
|
|
150
|
-
return `${
|
|
157
|
+
return `${
|
|
158
|
+
discAmount
|
|
159
|
+
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
160
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
161
|
+
.replace(/\.$/, '') || '0'
|
|
162
|
+
} ${currency?.symbol || ''}`;
|
|
151
163
|
}
|
|
152
164
|
}
|
|
153
165
|
if ((item as any).discount_amounts?.length > 0 && currency) {
|
|
@@ -207,7 +219,8 @@ export default function ProductItemCard({
|
|
|
207
219
|
const formatted =
|
|
208
220
|
tokenAmount
|
|
209
221
|
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
210
|
-
.replace(
|
|
222
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
223
|
+
.replace(/\.$/, '') || '0';
|
|
211
224
|
return `${formatted} ${currency?.symbol || ''} ${slashText}`;
|
|
212
225
|
}
|
|
213
226
|
}
|
|
@@ -238,7 +251,8 @@ export default function ProductItemCard({
|
|
|
238
251
|
const formatted =
|
|
239
252
|
tokenAmount
|
|
240
253
|
.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: precision })
|
|
241
|
-
.replace(
|
|
254
|
+
.replace(/(\.\d*?)0+$/, '$1')
|
|
255
|
+
.replace(/\.$/, '') || '0';
|
|
242
256
|
return `${formatted} ${currency?.symbol || ''} ${originalSlash}`;
|
|
243
257
|
}
|
|
244
258
|
}
|
|
@@ -284,7 +298,7 @@ export default function ProductItemCard({
|
|
|
284
298
|
fontWeight: 900,
|
|
285
299
|
letterSpacing: '0.12em',
|
|
286
300
|
bgcolor: 'primary.main',
|
|
287
|
-
color:
|
|
301
|
+
color: (th: any) => primaryContrastColor(th),
|
|
288
302
|
boxShadow: '0 4px 12px rgba(45,124,243,0.2)',
|
|
289
303
|
'& .MuiChip-label': { px: 1.5 },
|
|
290
304
|
}}
|
|
@@ -459,12 +473,16 @@ export default function ProductItemCard({
|
|
|
459
473
|
{/* Discount chip */}
|
|
460
474
|
{discountCode && perItemDiscount && (
|
|
461
475
|
<Box sx={{ mt: 1.5 }}>
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
476
|
+
{isRateLoading ? (
|
|
477
|
+
<Skeleton variant="rounded" width={160} height={22} sx={{ borderRadius: '6px' }} />
|
|
478
|
+
) : (
|
|
479
|
+
<Chip
|
|
480
|
+
icon={<LocalOfferIcon sx={{ color: 'warning.main', fontSize: 'small' }} />}
|
|
481
|
+
label={`${discountCode} (-${perItemDiscount})`}
|
|
482
|
+
size="small"
|
|
483
|
+
sx={{ height: 22, borderRadius: '6px', '& .MuiChip-label': { fontSize: 12 } }}
|
|
484
|
+
/>
|
|
485
|
+
)}
|
|
468
486
|
</Box>
|
|
469
487
|
)}
|
|
470
488
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CircularProgress,
|
|
10
10
|
IconButton,
|
|
11
11
|
InputAdornment,
|
|
12
|
+
Skeleton,
|
|
12
13
|
Stack,
|
|
13
14
|
TextField,
|
|
14
15
|
Typography,
|
|
@@ -28,6 +29,8 @@ interface PromotionInputProps {
|
|
|
28
29
|
discountAmount: string | null;
|
|
29
30
|
/** Start with input field visible (skip the "Add promotion code" button) */
|
|
30
31
|
initialShowInput?: boolean;
|
|
32
|
+
/** Show skeleton for the discount amount while switching */
|
|
33
|
+
isAmountLoading?: boolean;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export default function PromotionInput({
|
|
@@ -35,6 +38,7 @@ export default function PromotionInput({
|
|
|
35
38
|
discounts,
|
|
36
39
|
discountAmount,
|
|
37
40
|
initialShowInput = false,
|
|
41
|
+
isAmountLoading = false,
|
|
38
42
|
}: PromotionInputProps) {
|
|
39
43
|
const { t } = useLocaleContext();
|
|
40
44
|
const [showInput, setShowInput] = useState(false);
|
|
@@ -115,9 +119,13 @@ export default function PromotionInput({
|
|
|
115
119
|
<CloseIcon sx={{ fontSize: 12, color: '#12b886' }} />
|
|
116
120
|
</IconButton>
|
|
117
121
|
</Stack>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
{isAmountLoading ? (
|
|
123
|
+
<Skeleton variant="text" width={80} height={22} />
|
|
124
|
+
) : (
|
|
125
|
+
<Typography sx={{ color: 'text.primary', fontWeight: 600, fontSize: 14 }}>
|
|
126
|
+
-{discountAmount || '0'}
|
|
127
|
+
</Typography>
|
|
128
|
+
)}
|
|
121
129
|
</Stack>
|
|
122
130
|
);
|
|
123
131
|
})}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Button, CircularProgress } from '@mui/material';
|
|
2
|
+
import { primaryContrastColor } from '../../utils/format';
|
|
2
3
|
|
|
3
4
|
interface SubmitButtonProps {
|
|
4
5
|
canSubmit: boolean;
|
|
@@ -30,6 +31,7 @@ export default function SubmitButton({
|
|
|
30
31
|
fontSize: '1.3rem',
|
|
31
32
|
fontWeight: 600,
|
|
32
33
|
textTransform: 'none',
|
|
34
|
+
color: (theme) => primaryContrastColor(theme),
|
|
33
35
|
}}>
|
|
34
36
|
{isProcessing ? processingLabel : label}
|
|
35
37
|
</Button>
|
|
@@ -17,7 +17,13 @@ import {
|
|
|
17
17
|
} from '@blocklet/payment-react-headless';
|
|
18
18
|
|
|
19
19
|
import { useMobile } from '../../../hooks/mobile';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
INTERVAL_LOCALE_KEY,
|
|
22
|
+
formatTrialText,
|
|
23
|
+
getSessionHeaderMeta,
|
|
24
|
+
tSafe,
|
|
25
|
+
primaryContrastColor,
|
|
26
|
+
} from '../../utils/format';
|
|
21
27
|
import ProductItemCard from '../../components/left/product-item-card';
|
|
22
28
|
import BillingToggle from '../../components/left/billing-toggle';
|
|
23
29
|
import CrossSellCard from '../../components/left/cross-sell-card';
|
|
@@ -60,6 +66,10 @@ export default function CompositePanel() {
|
|
|
60
66
|
const canUpsell = nonCrossSellItems.length <= 1;
|
|
61
67
|
const hasTopUpsell = canUpsell && !!upsellPrimaryItem && ['subscription', 'setup'].includes(mode);
|
|
62
68
|
const isUpselled = !!(upsellPrimaryItem as any)?.upsell_price;
|
|
69
|
+
const [upsellSwitching, setUpsellSwitching] = useState(false);
|
|
70
|
+
// Optimistic: track which tab the user clicked so highlight switches immediately
|
|
71
|
+
const [pendingUpsell, setPendingUpsell] = useState<boolean | null>(null);
|
|
72
|
+
const visualIsUpselled = pendingUpsell !== null ? pendingUpsell : isUpselled;
|
|
63
73
|
|
|
64
74
|
// Intervals for capsule toggle
|
|
65
75
|
const currentInterval = hasTopUpsell ? (upsellPrimaryItem!.price as any)?.recurring?.interval : null;
|
|
@@ -94,7 +104,7 @@ export default function CompositePanel() {
|
|
|
94
104
|
// Capsule button sx helper
|
|
95
105
|
const activeSx = {
|
|
96
106
|
bgcolor: 'primary.main',
|
|
97
|
-
color:
|
|
107
|
+
color: (theme: any) => primaryContrastColor(theme),
|
|
98
108
|
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)',
|
|
99
109
|
};
|
|
100
110
|
const inactiveSx = {
|
|
@@ -260,17 +270,23 @@ export default function CompositePanel() {
|
|
|
260
270
|
{/* Current interval */}
|
|
261
271
|
<Box
|
|
262
272
|
onClick={async () => {
|
|
263
|
-
if (isUpselled) {
|
|
273
|
+
if (isUpselled && !upsellSwitching) {
|
|
274
|
+
setPendingUpsell(false);
|
|
275
|
+
setUpsellSwitching(true);
|
|
264
276
|
try {
|
|
265
277
|
await lineItems.downsell(
|
|
266
278
|
(upsellPrimaryItem as any).upsell_price?.id || upsellPrimaryItem!.price_id
|
|
267
279
|
);
|
|
268
280
|
} catch (err: any) {
|
|
281
|
+
setPendingUpsell(null);
|
|
269
282
|
Toast.error(err?.response?.data?.error || err?.message || 'Failed');
|
|
283
|
+
} finally {
|
|
284
|
+
setUpsellSwitching(false);
|
|
285
|
+
setPendingUpsell(null);
|
|
270
286
|
}
|
|
271
287
|
}
|
|
272
288
|
}}
|
|
273
|
-
sx={capsuleBtnSx(!
|
|
289
|
+
sx={capsuleBtnSx(!visualIsUpselled)}>
|
|
274
290
|
<Typography component="span" sx={{ fontSize: 14, fontWeight: 700, color: 'inherit', lineHeight: 1 }}>
|
|
275
291
|
{t(INTERVAL_LOCALE_KEY[currentInterval!] || '')}
|
|
276
292
|
</Typography>
|
|
@@ -278,15 +294,21 @@ export default function CompositePanel() {
|
|
|
278
294
|
{/* Upsell interval */}
|
|
279
295
|
<Box
|
|
280
296
|
onClick={async () => {
|
|
281
|
-
if (!isUpselled) {
|
|
297
|
+
if (!isUpselled && !upsellSwitching) {
|
|
298
|
+
setPendingUpsell(true);
|
|
299
|
+
setUpsellSwitching(true);
|
|
282
300
|
try {
|
|
283
301
|
await lineItems.upsell(upsellPrimaryItem!.price_id, upsellTarget.id);
|
|
284
302
|
} catch (err: any) {
|
|
303
|
+
setPendingUpsell(null);
|
|
285
304
|
Toast.error(err?.response?.data?.error || err?.message || 'Failed');
|
|
305
|
+
} finally {
|
|
306
|
+
setUpsellSwitching(false);
|
|
307
|
+
setPendingUpsell(null);
|
|
286
308
|
}
|
|
287
309
|
}
|
|
288
310
|
}}
|
|
289
|
-
sx={capsuleBtnSx(
|
|
311
|
+
sx={capsuleBtnSx(visualIsUpselled)}>
|
|
290
312
|
<Typography component="span" sx={{ fontSize: 14, fontWeight: 700, color: 'inherit', lineHeight: 1 }}>
|
|
291
313
|
{t(INTERVAL_LOCALE_KEY[upsellInterval!] || '')}
|
|
292
314
|
</Typography>
|
|
@@ -154,12 +154,8 @@ export default function CreditTopupPanel() {
|
|
|
154
154
|
? t('payment.checkout.credit.schedule.withRefresh', { amount: formattedAmount, interval: intervalDisplay })
|
|
155
155
|
: t('payment.checkout.credit.schedule.periodic', { amount: formattedAmount, interval: intervalDisplay });
|
|
156
156
|
}
|
|
157
|
-
const productDesc = product?.description || '';
|
|
158
|
-
if (productDesc && productDesc.length > 10 && productDesc !== creditName) {
|
|
159
|
-
return productDesc;
|
|
160
|
-
}
|
|
161
157
|
return '';
|
|
162
|
-
}, [creditAmount, currencySymbol, hasSchedule, scheduleConfig,
|
|
158
|
+
}, [creditAmount, currencySymbol, hasSchedule, scheduleConfig, t]);
|
|
163
159
|
|
|
164
160
|
// Validity text: "Credits are valid for X days after purchase."
|
|
165
161
|
const validityText = useMemo(() => {
|