@blocklet/payment-react 1.26.1 → 1.26.3

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.
Files changed (73) hide show
  1. package/es/checkout-v2/components/dialogs/checkout-dialogs.js +2 -0
  2. package/es/checkout-v2/components/left/cross-sell-card.js +3 -3
  3. package/es/checkout-v2/components/left/product-item-card.js +9 -3
  4. package/es/checkout-v2/components/left/promotion-input.d.ts +4 -1
  5. package/es/checkout-v2/components/left/promotion-input.js +8 -13
  6. package/es/checkout-v2/components/right/customer-info-card.d.ts +2 -0
  7. package/es/checkout-v2/components/right/customer-info-card.js +22 -14
  8. package/es/checkout-v2/components/right/status-feedback.js +1 -1
  9. package/es/checkout-v2/components/right/submit-button.js +3 -1
  10. package/es/checkout-v2/layouts/checkout-layout.js +13 -3
  11. package/es/checkout-v2/panels/left/composite-panel.js +27 -6
  12. package/es/checkout-v2/panels/right/payment-panel.js +40 -9
  13. package/es/checkout-v2/utils/format.d.ts +1 -1
  14. package/es/checkout-v2/utils/format.js +1 -0
  15. package/es/checkout-v2/views/error-view.d.ts +1 -1
  16. package/es/checkout-v2/views/error-view.js +9 -0
  17. package/es/checkout-v2/views/success-view.js +3 -1
  18. package/es/components/over-due-invoice-payment.js +5 -3
  19. package/es/components/service-suspended-dialog.d.ts +4 -0
  20. package/es/components/service-suspended-dialog.js +61 -0
  21. package/es/libs/util.d.ts +8 -0
  22. package/es/libs/util.js +3 -0
  23. package/es/locales/en.js +4 -0
  24. package/es/locales/zh.js +4 -0
  25. package/es/payment/form/index.js +17 -0
  26. package/es/payment/index.js +15 -4
  27. package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +4 -0
  28. package/lib/checkout-v2/components/left/cross-sell-card.js +2 -2
  29. package/lib/checkout-v2/components/left/product-item-card.js +9 -2
  30. package/lib/checkout-v2/components/left/promotion-input.d.ts +4 -1
  31. package/lib/checkout-v2/components/left/promotion-input.js +12 -19
  32. package/lib/checkout-v2/components/right/customer-info-card.d.ts +2 -0
  33. package/lib/checkout-v2/components/right/customer-info-card.js +19 -13
  34. package/lib/checkout-v2/components/right/status-feedback.js +1 -1
  35. package/lib/checkout-v2/components/right/submit-button.js +3 -1
  36. package/lib/checkout-v2/layouts/checkout-layout.js +28 -5
  37. package/lib/checkout-v2/panels/left/composite-panel.js +20 -5
  38. package/lib/checkout-v2/panels/right/payment-panel.js +46 -7
  39. package/lib/checkout-v2/utils/format.d.ts +1 -1
  40. package/lib/checkout-v2/utils/format.js +7 -0
  41. package/lib/checkout-v2/views/error-view.d.ts +1 -1
  42. package/lib/checkout-v2/views/error-view.js +9 -0
  43. package/lib/checkout-v2/views/success-view.js +2 -0
  44. package/lib/components/over-due-invoice-payment.js +12 -2
  45. package/lib/components/service-suspended-dialog.d.ts +4 -0
  46. package/lib/components/service-suspended-dialog.js +97 -0
  47. package/lib/libs/util.d.ts +8 -0
  48. package/lib/libs/util.js +4 -0
  49. package/lib/locales/en.js +4 -0
  50. package/lib/locales/zh.js +4 -0
  51. package/lib/payment/form/index.js +23 -0
  52. package/lib/payment/index.js +15 -4
  53. package/package.json +4 -4
  54. package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +4 -0
  55. package/src/checkout-v2/components/left/cross-sell-card.tsx +3 -3
  56. package/src/checkout-v2/components/left/product-item-card.tsx +18 -8
  57. package/src/checkout-v2/components/left/promotion-input.tsx +17 -17
  58. package/src/checkout-v2/components/right/customer-info-card.tsx +29 -16
  59. package/src/checkout-v2/components/right/status-feedback.tsx +2 -2
  60. package/src/checkout-v2/components/right/submit-button.tsx +2 -0
  61. package/src/checkout-v2/layouts/checkout-layout.tsx +25 -10
  62. package/src/checkout-v2/panels/left/composite-panel.tsx +28 -6
  63. package/src/checkout-v2/panels/right/payment-panel.tsx +32 -5
  64. package/src/checkout-v2/utils/format.ts +2 -0
  65. package/src/checkout-v2/views/error-view.tsx +11 -1
  66. package/src/checkout-v2/views/success-view.tsx +3 -1
  67. package/src/components/over-due-invoice-payment.tsx +6 -3
  68. package/src/components/service-suspended-dialog.tsx +64 -0
  69. package/src/libs/util.ts +7 -0
  70. package/src/locales/en.tsx +4 -0
  71. package/src/locales/zh.tsx +4 -0
  72. package/src/payment/form/index.tsx +20 -0
  73. package/src/payment/index.tsx +26 -4
@@ -10,6 +10,7 @@ import {
10
10
  import StripeForm from "../../../payment/form/stripe/index.js";
11
11
  import ConfirmDialog from "../../../components/confirm.js";
12
12
  import PriceChangeConfirm from "../../../components/price-change-confirm.js";
13
+ import ServiceSuspendedDialog from "../../../components/service-suspended-dialog.js";
13
14
  import { formatTokenAmount } from "../../utils/format.js";
14
15
  function getRedirectUrl(session) {
15
16
  try {
@@ -92,6 +93,7 @@ export default function CheckoutDialogs() {
92
93
  color: "primary"
93
94
  }
94
95
  ),
96
+ submit.context?.type === "service_suspended" && /* @__PURE__ */ jsx(ServiceSuspendedDialog, { open: true, onClose: submit.cancel }),
95
97
  submit.status === "credit_insufficient" && submit.context?.type === "credit_insufficient" && /* @__PURE__ */ jsx(
96
98
  ConfirmDialog,
97
99
  {
@@ -3,7 +3,7 @@ import AddShoppingCartIcon from "@mui/icons-material/AddShoppingCart";
3
3
  import ShoppingCartCheckoutIcon from "@mui/icons-material/ShoppingCartCheckout";
4
4
  import { Avatar, Box, Button, Chip, Stack, Typography } from "@mui/material";
5
5
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
6
- import { formatDynamicUnitPrice, tSafe, INTERVAL_LOCALE_KEY } from "../../utils/format.js";
6
+ import { formatDynamicUnitPrice, tSafe, INTERVAL_LOCALE_KEY, primaryContrastColor } from "../../utils/format.js";
7
7
  export default function CrossSellCard({
8
8
  crossSellItem,
9
9
  currency,
@@ -40,7 +40,7 @@ export default function CrossSellCard({
40
40
  fontWeight: 900,
41
41
  letterSpacing: "0.12em",
42
42
  bgcolor: "primary.main",
43
- color: "#fff",
43
+ color: (theme) => primaryContrastColor(theme),
44
44
  boxShadow: "0 4px 12px rgba(45,124,243,0.2)",
45
45
  "& .MuiChip-label": { px: 1.5 }
46
46
  }
@@ -147,7 +147,7 @@ export default function CrossSellCard({
147
147
  transition: "all 0.2s",
148
148
  "&:hover": {
149
149
  bgcolor: "primary.main",
150
- color: "#fff",
150
+ color: (theme) => primaryContrastColor(theme),
151
151
  borderColor: "primary.main"
152
152
  },
153
153
  "&:active": { transform: "scale(0.95)" }
@@ -22,7 +22,13 @@ import {
22
22
  import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
23
23
  import { getPriceUnitAmountByCurrency } from "@blocklet/payment-react-headless";
24
24
  import Toast from "@arcblock/ux/lib/Toast";
25
- import { INTERVAL_LOCALE_KEY, formatDynamicUnitPrice, formatTokenAmount, formatTrialText } from "../../utils/format.js";
25
+ import {
26
+ INTERVAL_LOCALE_KEY,
27
+ formatDynamicUnitPrice,
28
+ formatTokenAmount,
29
+ formatTrialText,
30
+ primaryContrastColor
31
+ } from "../../utils/format.js";
26
32
  export default function ProductItemCard({
27
33
  item,
28
34
  currency,
@@ -217,7 +223,7 @@ export default function ProductItemCard({
217
223
  fontWeight: 900,
218
224
  letterSpacing: "0.12em",
219
225
  bgcolor: "primary.main",
220
- color: "#fff",
226
+ color: (th) => primaryContrastColor(th),
221
227
  boxShadow: "0 4px 12px rgba(45,124,243,0.2)",
222
228
  "& .MuiChip-label": { px: 1.5 }
223
229
  }
@@ -388,7 +394,7 @@ export default function ProductItemCard({
388
394
  ]
389
395
  }
390
396
  ),
391
- discountCode && perItemDiscount && /* @__PURE__ */ jsx(Box, { sx: { mt: 1.5 }, children: /* @__PURE__ */ jsx(
397
+ discountCode && perItemDiscount && /* @__PURE__ */ jsx(Box, { sx: { mt: 1.5 }, children: isRateLoading ? /* @__PURE__ */ jsx(Skeleton, { variant: "rounded", width: 160, height: 22, sx: { borderRadius: "6px" } }) : /* @__PURE__ */ jsx(
392
398
  Chip,
393
399
  {
394
400
  icon: /* @__PURE__ */ jsx(LocalOfferIcon, { sx: { color: "warning.main", fontSize: "small" } }),
@@ -12,8 +12,11 @@ interface PromotionInputProps {
12
12
  };
13
13
  discounts: any[];
14
14
  discountAmount: string | null;
15
+ currency?: any;
15
16
  /** Start with input field visible (skip the "Add promotion code" button) */
16
17
  initialShowInput?: boolean;
18
+ /** Show skeleton for the discount amount while switching */
19
+ isAmountLoading?: boolean;
17
20
  }
18
- export default function PromotionInput({ promotion, discounts, discountAmount, initialShowInput, }: PromotionInputProps): import("react").JSX.Element | null;
21
+ export default function PromotionInput({ promotion, discounts, discountAmount, currency, initialShowInput, isAmountLoading, }: PromotionInputProps): import("react").JSX.Element | null;
19
22
  export {};
@@ -10,18 +10,22 @@ import {
10
10
  CircularProgress,
11
11
  IconButton,
12
12
  InputAdornment,
13
+ Skeleton,
13
14
  Stack,
14
15
  TextField,
15
16
  Typography
16
17
  } from "@mui/material";
17
18
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
19
+ import { formatCouponTerms } from "../../../libs/util.js";
18
20
  export default function PromotionInput({
19
21
  promotion,
20
22
  discounts,
21
23
  discountAmount,
22
- initialShowInput = false
24
+ currency = null,
25
+ initialShowInput = false,
26
+ isAmountLoading = false
23
27
  }) {
24
- const { t } = useLocaleContext();
28
+ const { t, locale } = useLocaleContext();
25
29
  const [showInput, setShowInput] = useState(false);
26
30
  const [code, setCode] = useState("");
27
31
  const [applying, setApplying] = useState(false);
@@ -49,16 +53,7 @@ export default function PromotionInput({
49
53
  return /* @__PURE__ */ jsx(Box, { children: discounts.map((disc, i) => {
50
54
  const discCode = disc.promotion_code_details?.code || disc.verification_data?.code || disc.promotion_code || "";
51
55
  const coupon = disc.coupon_details || {};
52
- const couponOff = coupon.percent_off > 0 ? t("payment.checkout.coupon.percentage", { percent: coupon.percent_off }) : `${coupon.percent_off || 0}%`;
53
- let description = "";
54
- if (coupon.duration === "repeating" && coupon.duration_in_months) {
55
- const months = coupon.duration_in_months;
56
- description = `${couponOff} for ${months} month${months > 1 ? "s" : ""}`;
57
- } else if (coupon.duration === "forever") {
58
- description = t("payment.checkout.coupon.terms.forever", { couponOff });
59
- } else if (coupon.duration === "once") {
60
- description = t("payment.checkout.coupon.terms.once", { couponOff });
61
- }
56
+ const description = coupon && currency ? formatCouponTerms(coupon, currency, locale) : "";
62
57
  return /* @__PURE__ */ jsxs(
63
58
  Stack,
64
59
  {
@@ -91,7 +86,7 @@ export default function PromotionInput({
91
86
  ]
92
87
  }
93
88
  ),
94
- /* @__PURE__ */ jsxs(Typography, { sx: { color: "text.primary", fontWeight: 600, fontSize: 14 }, children: [
89
+ isAmountLoading ? /* @__PURE__ */ jsx(Skeleton, { variant: "text", width: 80, height: 22 }) : /* @__PURE__ */ jsxs(Typography, { sx: { color: "text.primary", fontWeight: 600, fontSize: 14 }, children: [
95
90
  "-",
96
91
  discountAmount || "0"
97
92
  ] })
@@ -9,7 +9,9 @@ interface CustomerInfoCardProps {
9
9
  values: Record<string, any>;
10
10
  onChange: (field: string, value: string | boolean | Record<string, string>) => void;
11
11
  errors: Partial<Record<string, string>>;
12
+ checkValid: () => Promise<boolean>;
12
13
  validateField: (field: string) => Promise<void>;
14
+ prefetched: boolean;
13
15
  };
14
16
  isLoggedIn: boolean;
15
17
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect, useRef } from "react";
2
+ import { useState, useRef, useCallback, useEffect } from "react";
3
3
  import { Box, Button, InputBase, InputAdornment, Stack, Typography } from "@mui/material";
4
4
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
5
5
  import CountrySelect from "../../../components/country-select.js";
@@ -19,16 +19,24 @@ const fieldLabelMap = (t) => ({
19
19
  export default function CustomerInfoCard({ form, isLoggedIn }) {
20
20
  const { t } = useLocaleContext();
21
21
  const labels = fieldLabelMap(t);
22
- const hasRequiredData = !!(form.values.customer_name && form.values.customer_email);
23
- const [showEditForm, setShowEditForm] = useState(!hasRequiredData);
24
- const autoConfirmedRef = useRef(false);
22
+ const [showEditForm, setShowEditForm] = useState(false);
23
+ const [ready, setReady] = useState(false);
24
+ const checkedRef = useRef(false);
25
25
  useEffect(() => {
26
- if (!autoConfirmedRef.current && form.values.customer_name && form.values.customer_email) {
27
- autoConfirmedRef.current = true;
28
- setShowEditForm(false);
29
- }
30
- }, [form.values.customer_name, form.values.customer_email]);
31
- if (!isLoggedIn) return null;
26
+ if (checkedRef.current) return;
27
+ if (!form.prefetched && !form.values.customer_name && !form.values.customer_email) return;
28
+ checkedRef.current = true;
29
+ form.checkValid().then((valid) => {
30
+ setShowEditForm(!valid);
31
+ setReady(true);
32
+ });
33
+ }, [form.prefetched]);
34
+ const handleChange = useCallback(
35
+ (field, value) => form.onChange(field, value),
36
+ [form.onChange]
37
+ // eslint-disable-line react-hooks/exhaustive-deps
38
+ );
39
+ if (!isLoggedIn || !ready) return null;
32
40
  if (!showEditForm) {
33
41
  return /* @__PURE__ */ jsxs(Box, { sx: { mt: 2 }, children: [
34
42
  /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { mb: 1 }, children: [
@@ -108,8 +116,8 @@ export default function CustomerInfoCard({ form, isLoggedIn }) {
108
116
  {
109
117
  value: value || "",
110
118
  country: form.values.billing_address?.country || "",
111
- onChange: (phone) => form.onChange("customer_phone", phone),
112
- onCountryChange: (c) => form.onChange("billing_address.country", c),
119
+ onChange: (phone) => handleChange("customer_phone", phone),
120
+ onCountryChange: (c) => handleChange("billing_address.country", c),
113
121
  onBlur: () => form.validateField(name),
114
122
  label,
115
123
  error: form.errors[name]
@@ -124,13 +132,13 @@ export default function CustomerInfoCard({ form, isLoggedIn }) {
124
132
  {
125
133
  fullWidth: true,
126
134
  value: value || "",
127
- onChange: (e) => form.onChange(name, e.target.value),
135
+ onChange: (e) => handleChange(name, e.target.value),
128
136
  onBlur: () => form.validateField(name),
129
137
  startAdornment: isPostalCode ? /* @__PURE__ */ jsx(InputAdornment, { position: "start", sx: { mr: 0.5, ml: -0.5 }, children: /* @__PURE__ */ jsx(
130
138
  CountrySelect,
131
139
  {
132
140
  value: form.values.billing_address?.country || "",
133
- onChange: (v) => form.onChange("billing_address.country", v),
141
+ onChange: (v) => handleChange("billing_address.country", v),
134
142
  sx: {
135
143
  ".MuiOutlinedInput-notchedOutline": { borderColor: "transparent !important" },
136
144
  "& .MuiSelect-select": { py: 0, pr: "20px !important" }
@@ -8,7 +8,7 @@ export default function StatusFeedback({ status, context, onReset }) {
8
8
  if (status === prevStatusRef.current) return;
9
9
  prevStatusRef.current = status;
10
10
  if (status === "failed" && context?.type === "error") {
11
- if (context.code === "CUSTOMER_LIMITED") return;
11
+ if (context.code === "CUSTOMER_LIMITED" || context.code === "STOP_ACCEPTING_ORDERS") return;
12
12
  Toast.error(context.message || "Payment failed");
13
13
  onReset();
14
14
  }
@@ -1,5 +1,6 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { Button, CircularProgress } from "@mui/material";
3
+ import { primaryContrastColor } from "../../utils/format.js";
3
4
  export default function SubmitButton({
4
5
  canSubmit,
5
6
  isProcessing,
@@ -21,7 +22,8 @@ export default function SubmitButton({
21
22
  py: 1.5,
22
23
  fontSize: "1.3rem",
23
24
  fontWeight: 600,
24
- textTransform: "none"
25
+ textTransform: "none",
26
+ color: (theme) => primaryContrastColor(theme)
25
27
  },
26
28
  children: isProcessing ? processingLabel : label
27
29
  }
@@ -22,7 +22,14 @@ const fadeIn = {
22
22
  },
23
23
  animation: { xs: "none", md: "fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both" }
24
24
  };
25
- const slideInFromRight = {
25
+ const isSafari = typeof navigator !== "undefined" && /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
26
+ const slideInFromRight = isSafari ? {
27
+ "@keyframes panelFadeIn": {
28
+ from: { opacity: 0, transform: "translateX(24px)" },
29
+ to: { opacity: 1, transform: "none" }
30
+ },
31
+ animation: { xs: "none", md: "panelFadeIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both" }
32
+ } : {
26
33
  "@keyframes slideInRight": {
27
34
  from: { transform: "translateX(100%)" },
28
35
  to: { transform: "translateX(0)" }
@@ -79,6 +86,7 @@ export default function CheckoutLayout({ left, right, mode = "inline" }) {
79
86
  {
80
87
  sx: {
81
88
  flex: 1,
89
+ minWidth: 0,
82
90
  bgcolor: (t) => t.palette.mode === "dark" ? "background.default" : "#f8faff",
83
91
  p: { xs: 3, md: 5 },
84
92
  pt: { xs: 3, md: 4 },
@@ -156,7 +164,8 @@ export default function CheckoutLayout({ left, right, mode = "inline" }) {
156
164
  Box,
157
165
  {
158
166
  sx: {
159
- width: { xs: "100%", md: "50%" },
167
+ flex: { xs: "none", md: "0 0 50%" },
168
+ width: { xs: "100%" },
160
169
  height: { xs: "auto", md: "100vh" },
161
170
  display: "flex",
162
171
  justifyContent: { md: "center" },
@@ -185,7 +194,8 @@ export default function CheckoutLayout({ left, right, mode = "inline" }) {
185
194
  Box,
186
195
  {
187
196
  sx: {
188
- width: hideLeft ? "100%" : { xs: "100%", md: "50%" },
197
+ flex: hideLeft ? "none" : { xs: "none", md: "0 0 50%" },
198
+ width: hideLeft ? "100%" : { xs: "100%" },
189
199
  height: hideLeft ? "100vh" : { xs: "auto", md: "100vh" },
190
200
  bgcolor: "background.paper",
191
201
  boxShadow: hideLeft ? "none" : { md: "-4px 0 16px rgba(0,0,0,0.04)" },
@@ -17,7 +17,13 @@ import {
17
17
  useProduct
18
18
  } from "@blocklet/payment-react-headless";
19
19
  import { useMobile } from "../../../hooks/mobile.js";
20
- import { INTERVAL_LOCALE_KEY, formatTrialText, getSessionHeaderMeta, tSafe } from "../../utils/format.js";
20
+ import {
21
+ INTERVAL_LOCALE_KEY,
22
+ formatTrialText,
23
+ getSessionHeaderMeta,
24
+ tSafe,
25
+ primaryContrastColor
26
+ } from "../../utils/format.js";
21
27
  import ProductItemCard from "../../components/left/product-item-card.js";
22
28
  import BillingToggle from "../../components/left/billing-toggle.js";
23
29
  import CrossSellCard from "../../components/left/cross-sell-card.js";
@@ -51,6 +57,9 @@ export default function CompositePanel() {
51
57
  const canUpsell = nonCrossSellItems.length <= 1;
52
58
  const hasTopUpsell = canUpsell && !!upsellPrimaryItem && ["subscription", "setup"].includes(mode);
53
59
  const isUpselled = !!upsellPrimaryItem?.upsell_price;
60
+ const [upsellSwitching, setUpsellSwitching] = useState(false);
61
+ const [pendingUpsell, setPendingUpsell] = useState(null);
62
+ const visualIsUpselled = pendingUpsell !== null ? pendingUpsell : isUpselled;
54
63
  const currentInterval = hasTopUpsell ? upsellPrimaryItem.price?.recurring?.interval : null;
55
64
  const upsellInterval = hasTopUpsell ? upsellTarget?.recurring?.interval : null;
56
65
  let upsellSavings = 0;
@@ -75,7 +84,7 @@ export default function CompositePanel() {
75
84
  const isMultiItem = lineItems.items.length > 1;
76
85
  const activeSx = {
77
86
  bgcolor: "primary.main",
78
- color: "#fff",
87
+ color: (theme) => primaryContrastColor(theme),
79
88
  boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)"
80
89
  };
81
90
  const inactiveSx = {
@@ -223,17 +232,23 @@ export default function CompositePanel() {
223
232
  Box,
224
233
  {
225
234
  onClick: async () => {
226
- if (isUpselled) {
235
+ if (isUpselled && !upsellSwitching) {
236
+ setPendingUpsell(false);
237
+ setUpsellSwitching(true);
227
238
  try {
228
239
  await lineItems.downsell(
229
240
  upsellPrimaryItem.upsell_price?.id || upsellPrimaryItem.price_id
230
241
  );
231
242
  } catch (err) {
243
+ setPendingUpsell(null);
232
244
  Toast.error(err?.response?.data?.error || err?.message || "Failed");
245
+ } finally {
246
+ setUpsellSwitching(false);
247
+ setPendingUpsell(null);
233
248
  }
234
249
  }
235
250
  },
236
- sx: capsuleBtnSx(!isUpselled),
251
+ sx: capsuleBtnSx(!visualIsUpselled),
237
252
  children: /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 14, fontWeight: 700, color: "inherit", lineHeight: 1 }, children: t(INTERVAL_LOCALE_KEY[currentInterval] || "") })
238
253
  }
239
254
  ),
@@ -241,15 +256,21 @@ export default function CompositePanel() {
241
256
  Box,
242
257
  {
243
258
  onClick: async () => {
244
- if (!isUpselled) {
259
+ if (!isUpselled && !upsellSwitching) {
260
+ setPendingUpsell(true);
261
+ setUpsellSwitching(true);
245
262
  try {
246
263
  await lineItems.upsell(upsellPrimaryItem.price_id, upsellTarget.id);
247
264
  } catch (err) {
265
+ setPendingUpsell(null);
248
266
  Toast.error(err?.response?.data?.error || err?.message || "Failed");
267
+ } finally {
268
+ setUpsellSwitching(false);
269
+ setPendingUpsell(null);
249
270
  }
250
271
  }
251
272
  },
252
- sx: capsuleBtnSx(isUpselled),
273
+ sx: capsuleBtnSx(visualIsUpselled),
253
274
  children: /* @__PURE__ */ jsx(Typography, { component: "span", sx: { fontSize: 14, fontWeight: 700, color: "inherit", lineHeight: 1 }, children: t(INTERVAL_LOCALE_KEY[upsellInterval] || "") })
254
275
  }
255
276
  )
@@ -40,9 +40,9 @@ import {
40
40
  import { joinURL } from "ufo";
41
41
  import { usePaymentContext } from "../../../contexts/payment.js";
42
42
  import { useMobile } from "../../../hooks/mobile.js";
43
- import { getPrefix } from "../../../libs/util.js";
43
+ import { getPrefix, getStatementDescriptor } from "../../../libs/util.js";
44
44
  import OverdueInvoicePayment from "../../../components/over-due-invoice-payment.js";
45
- import { tSafe, whiteTooltipSx } from "../../utils/format.js";
45
+ import { tSafe, whiteTooltipSx, primaryContrastColor } from "../../utils/format.js";
46
46
  import CustomerInfoCard from "../../components/right/customer-info-card.js";
47
47
  import SubscriptionDisclaimer from "../../components/right/subscription-disclaimer.js";
48
48
  import StatusFeedback from "../../components/right/status-feedback.js";
@@ -412,7 +412,9 @@ export default function PaymentPanel() {
412
412
  remove: promotion.remove
413
413
  },
414
414
  discounts,
415
- discountAmount: pricing.discount
415
+ discountAmount: pricing.discount,
416
+ currency,
417
+ isAmountLoading
416
418
  }
417
419
  )
418
420
  ] }),
@@ -502,7 +504,7 @@ export default function PaymentPanel() {
502
504
  ] })
503
505
  ] });
504
506
  })(),
505
- /* @__PURE__ */ jsx(
507
+ /* @__PURE__ */ jsxs(
506
508
  Button,
507
509
  {
508
510
  variant: "contained",
@@ -511,15 +513,43 @@ export default function PaymentPanel() {
511
513
  disabled: !canSubmit || submit.status === "waiting_stripe",
512
514
  onClick: handleAction,
513
515
  startIcon: isProcessing ? /* @__PURE__ */ jsx(CircularProgress, { size: 20, color: "inherit" }) : null,
514
- endIcon: !isProcessing ? /* @__PURE__ */ jsx(ArrowForwardIcon, {}) : void 0,
515
516
  sx: {
516
517
  py: 1.5,
517
518
  fontSize: "1.1rem",
518
519
  fontWeight: 600,
519
520
  textTransform: "none",
520
- borderRadius: "12px"
521
+ borderRadius: "12px",
522
+ color: (theme) => primaryContrastColor(theme),
523
+ position: "relative",
524
+ overflow: "hidden",
525
+ "&:hover": { bgcolor: "primary.main" },
526
+ "&:hover .arrow-icon": { transform: "translateX(4px)" },
527
+ "&:hover .shine-layer": { transform: "translateX(100%)" }
521
528
  },
522
- children: isProcessing ? `${t("payment.checkout.processing")}...` : buttonLabel
529
+ children: [
530
+ /* @__PURE__ */ jsx(Box, { component: "span", sx: { position: "relative", zIndex: 1 }, children: isProcessing ? `${t("payment.checkout.processing")}...` : buttonLabel }),
531
+ !isProcessing && /* @__PURE__ */ jsx(
532
+ ArrowForwardIcon,
533
+ {
534
+ className: "arrow-icon",
535
+ sx: { ml: 1, position: "relative", zIndex: 1, transition: "transform 0.2s ease" }
536
+ }
537
+ ),
538
+ /* @__PURE__ */ jsx(
539
+ Box,
540
+ {
541
+ className: "shine-layer",
542
+ sx: {
543
+ position: "absolute",
544
+ inset: 0,
545
+ background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.12), transparent)",
546
+ transform: "translateX(-100%)",
547
+ transition: "transform 0.7s ease",
548
+ pointerEvents: "none"
549
+ }
550
+ }
551
+ )
552
+ ]
523
553
  }
524
554
  ),
525
555
  isMobile && /* @__PURE__ */ jsxs(
@@ -575,7 +605,8 @@ export default function PaymentPanel() {
575
605
  remove: promotion.remove
576
606
  },
577
607
  discounts,
578
- discountAmount: pricing.discount
608
+ discountAmount: pricing.discount,
609
+ currency
579
610
  }
580
611
  )
581
612
  ]
@@ -588,7 +619,7 @@ export default function PaymentPanel() {
588
619
  mode,
589
620
  subscription,
590
621
  staking: pricing.staking,
591
- appName: session?.metadata?.app_name || "New Payment Kit"
622
+ appName: getStatementDescriptor(session?.line_items || [])
592
623
  }
593
624
  ),
594
625
  !isMobile && /* @__PURE__ */ jsxs(
@@ -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 {};
@@ -1,4 +1,5 @@
1
1
  import { fromUnitToToken } from "@ocap/util";
2
+ export { primaryContrastColor } from "../../libs/util.js";
2
3
  export const INTERVAL_LOCALE_KEY = {
3
4
  day: "common.daily",
4
5
  week: "common.weekly",
@@ -1,6 +1,6 @@
1
1
  interface ErrorViewProps {
2
2
  error: string;
3
- errorCode?: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
3
+ errorCode?: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
4
4
  mode?: string;
5
5
  }
6
6
  export default function ErrorView({ error, errorCode, mode }: ErrorViewProps): import("react").JSX.Element;
@@ -4,6 +4,7 @@ import { alpha, useTheme } from "@mui/material/styles";
4
4
  import ArrowBackIcon from "@mui/icons-material/ArrowBack";
5
5
  import Header from "@blocklet/ui-react/lib/Header";
6
6
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
7
+ import { primaryContrastColor } from "../utils/format.js";
7
8
  function GeometricDecoration() {
8
9
  const theme = useTheme();
9
10
  const gridColor = alpha(theme.palette.primary.main, 0.06);
@@ -113,6 +114,13 @@ function getErrorConfig(errorCode, error, t) {
113
114
  color: "#94a3b8"
114
115
  };
115
116
  }
117
+ if (errorCode === "STOP_ACCEPTING_ORDERS") {
118
+ return {
119
+ title: t("payment.checkout.stopAcceptingOrders.title"),
120
+ description: t("payment.checkout.stopAcceptingOrders.description"),
121
+ color: "#f59e0b"
122
+ };
123
+ }
116
124
  return {
117
125
  title: t("payment.checkout.error.title"),
118
126
  description: error,
@@ -173,6 +181,7 @@ function ErrorContent({ error, errorCode = void 0 }) {
173
181
  fontWeight: 600,
174
182
  fontSize: 16,
175
183
  letterSpacing: "0.02em",
184
+ color: (th) => primaryContrastColor(th),
176
185
  boxShadow: `0 8px 32px -4px ${alpha(primaryColor, 0.3)}`,
177
186
  "&:hover": {
178
187
  boxShadow: `0 12px 40px -4px ${alpha(primaryColor, 0.4)}`,
@@ -22,7 +22,7 @@ import { joinURL } from "ufo";
22
22
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
23
23
  import { usePaymentMethodContext } from "@blocklet/payment-react-headless";
24
24
  import { getPrefix } from "../../libs/util.js";
25
- import { formatTokenAmount } from "../utils/format.js";
25
+ import { formatTokenAmount, primaryContrastColor } from "../utils/format.js";
26
26
  const scaleIn = keyframes`
27
27
  from { transform: scale(0); opacity: 0; }
28
28
  60% { transform: scale(1.15); }
@@ -452,6 +452,7 @@ function SubscriptionLinks({
452
452
  fontWeight: 700,
453
453
  fontSize: { xs: 16, md: 17 },
454
454
  letterSpacing: "0.02em",
455
+ color: (theme) => primaryContrastColor(theme),
455
456
  boxShadow: "0 8px 24px -4px rgba(59,130,246,0.25)",
456
457
  "&:hover": {
457
458
  boxShadow: "0 12px 28px -4px rgba(59,130,246,0.35)"
@@ -486,6 +487,7 @@ function InvoiceLink({
486
487
  fontWeight: 700,
487
488
  fontSize: { xs: 16, md: 17 },
488
489
  letterSpacing: "0.02em",
490
+ color: (theme) => primaryContrastColor(theme),
489
491
  boxShadow: "0 8px 24px -4px rgba(59,130,246,0.25)",
490
492
  "&:hover": {
491
493
  boxShadow: "0 12px 28px -4px rgba(59,130,246,0.35)"
@@ -11,7 +11,7 @@ import Dialog from "@arcblock/ux/lib/Dialog/dialog";
11
11
  import { CheckCircle as CheckCircleIcon } from "@mui/icons-material";
12
12
  import debounce from "lodash/debounce";
13
13
  import { usePaymentContext } from "../contexts/payment.js";
14
- import { formatAmount, formatError, getPrefix, isCrossOrigin } from "../libs/util.js";
14
+ import { formatAmount, formatError, getPrefix, isCrossOrigin, primaryContrastColor } from "../libs/util.js";
15
15
  import { useSubscription } from "../hooks/subscription.js";
16
16
  import api from "../libs/api.js";
17
17
  import LoadingButton from "./loading-button.js";
@@ -290,6 +290,7 @@ function OverdueInvoicePayment({
290
290
  const { currency } = item;
291
291
  const inProcess = payLoading && selectCurrencyId === currency.id;
292
292
  const status = paymentStatus[currency.id] || "idle";
293
+ const containedColorSx = (options?.variant || "contained") === "contained" ? { color: (th) => primaryContrastColor(th) } : {};
293
294
  if (status === "success") {
294
295
  return /* @__PURE__ */ jsx(
295
296
  Button,
@@ -297,6 +298,7 @@ function OverdueInvoicePayment({
297
298
  variant: options?.variant || "contained",
298
299
  size: "small",
299
300
  onClick: () => checkAndHandleInvoicePaid(currency.id),
301
+ sx: containedColorSx,
300
302
  ...primaryButton ? {} : {
301
303
  color: "success",
302
304
  startIcon: /* @__PURE__ */ jsx(CheckCircleIcon, {})
@@ -334,7 +336,7 @@ function OverdueInvoicePayment({
334
336
  disabled: paying || status === "processing",
335
337
  loading: paying || status === "processing",
336
338
  onClick: onPay,
337
- sx: options?.sx,
339
+ sx: { ...containedColorSx, ...options?.sx || {} },
338
340
  children: buttonText
339
341
  }
340
342
  )
@@ -349,7 +351,7 @@ function OverdueInvoicePayment({
349
351
  disabled: inProcess,
350
352
  loading: inProcess,
351
353
  onClick: () => handlePay(item),
352
- sx: options?.sx,
354
+ sx: { ...containedColorSx, ...options?.sx || {} },
353
355
  children: status === "error" ? t("payment.subscription.overdue.retry") : t("payment.subscription.overdue.payNow")
354
356
  }
355
357
  );
@@ -0,0 +1,4 @@
1
+ export default function ServiceSuspendedDialog({ open, onClose }: {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ }): import("react").JSX.Element;