@blocklet/payment-react 1.26.2 → 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 (45) hide show
  1. package/es/checkout-v2/components/dialogs/checkout-dialogs.js +2 -0
  2. package/es/checkout-v2/components/left/promotion-input.d.ts +2 -1
  3. package/es/checkout-v2/components/left/promotion-input.js +4 -11
  4. package/es/checkout-v2/components/right/customer-info-card.d.ts +2 -0
  5. package/es/checkout-v2/components/right/customer-info-card.js +22 -14
  6. package/es/checkout-v2/components/right/status-feedback.js +1 -1
  7. package/es/checkout-v2/layouts/checkout-layout.js +13 -3
  8. package/es/checkout-v2/panels/right/payment-panel.js +3 -1
  9. package/es/checkout-v2/views/error-view.d.ts +1 -1
  10. package/es/checkout-v2/views/error-view.js +7 -0
  11. package/es/components/service-suspended-dialog.d.ts +4 -0
  12. package/es/components/service-suspended-dialog.js +61 -0
  13. package/es/locales/en.js +4 -0
  14. package/es/locales/zh.js +4 -0
  15. package/es/payment/form/index.js +17 -0
  16. package/es/payment/index.js +15 -4
  17. package/lib/checkout-v2/components/dialogs/checkout-dialogs.js +4 -0
  18. package/lib/checkout-v2/components/left/promotion-input.d.ts +2 -1
  19. package/lib/checkout-v2/components/left/promotion-input.js +5 -17
  20. package/lib/checkout-v2/components/right/customer-info-card.d.ts +2 -0
  21. package/lib/checkout-v2/components/right/customer-info-card.js +19 -13
  22. package/lib/checkout-v2/components/right/status-feedback.js +1 -1
  23. package/lib/checkout-v2/layouts/checkout-layout.js +28 -5
  24. package/lib/checkout-v2/panels/right/payment-panel.js +3 -1
  25. package/lib/checkout-v2/views/error-view.d.ts +1 -1
  26. package/lib/checkout-v2/views/error-view.js +7 -0
  27. package/lib/components/service-suspended-dialog.d.ts +4 -0
  28. package/lib/components/service-suspended-dialog.js +97 -0
  29. package/lib/locales/en.js +4 -0
  30. package/lib/locales/zh.js +4 -0
  31. package/lib/payment/form/index.js +23 -0
  32. package/lib/payment/index.js +15 -4
  33. package/package.json +4 -4
  34. package/src/checkout-v2/components/dialogs/checkout-dialogs.tsx +4 -0
  35. package/src/checkout-v2/components/left/promotion-input.tsx +6 -14
  36. package/src/checkout-v2/components/right/customer-info-card.tsx +29 -16
  37. package/src/checkout-v2/components/right/status-feedback.tsx +2 -2
  38. package/src/checkout-v2/layouts/checkout-layout.tsx +25 -10
  39. package/src/checkout-v2/panels/right/payment-panel.tsx +2 -0
  40. package/src/checkout-v2/views/error-view.tsx +9 -1
  41. package/src/components/service-suspended-dialog.tsx +64 -0
  42. package/src/locales/en.tsx +4 -0
  43. package/src/locales/zh.tsx +4 -0
  44. package/src/payment/form/index.tsx +20 -0
  45. package/src/payment/index.tsx +26 -4
@@ -47,7 +47,23 @@ const fadeIn = {
47
47
  md: "fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both"
48
48
  }
49
49
  };
50
- const slideInFromRight = {
50
+ const isSafari = typeof navigator !== "undefined" && /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
51
+ const slideInFromRight = isSafari ? {
52
+ "@keyframes panelFadeIn": {
53
+ from: {
54
+ opacity: 0,
55
+ transform: "translateX(24px)"
56
+ },
57
+ to: {
58
+ opacity: 1,
59
+ transform: "none"
60
+ }
61
+ },
62
+ animation: {
63
+ xs: "none",
64
+ md: "panelFadeIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both"
65
+ }
66
+ } : {
51
67
  "@keyframes slideInRight": {
52
68
  from: {
53
69
  transform: "translateX(100%)"
@@ -117,6 +133,7 @@ function CheckoutLayout({
117
133
  children: [!hideLeft && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
118
134
  sx: {
119
135
  flex: 1,
136
+ minWidth: 0,
120
137
  bgcolor: t => t.palette.mode === "dark" ? "background.default" : "#f8faff",
121
138
  p: {
122
139
  xs: 3,
@@ -205,9 +222,12 @@ function CheckoutLayout({
205
222
  }
206
223
  }), !hideLeft && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
207
224
  sx: {
225
+ flex: {
226
+ xs: "none",
227
+ md: "0 0 50%"
228
+ },
208
229
  width: {
209
- xs: "100%",
210
- md: "50%"
230
+ xs: "100%"
211
231
  },
212
232
  height: {
213
233
  xs: "auto",
@@ -246,9 +266,12 @@ function CheckoutLayout({
246
266
  })
247
267
  }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
248
268
  sx: {
269
+ flex: hideLeft ? "none" : {
270
+ xs: "none",
271
+ md: "0 0 50%"
272
+ },
249
273
  width: hideLeft ? "100%" : {
250
- xs: "100%",
251
- md: "50%"
274
+ xs: "100%"
252
275
  },
253
276
  height: hideLeft ? "100vh" : {
254
277
  xs: "auto",
@@ -562,6 +562,7 @@ function PaymentPanel() {
562
562
  },
563
563
  discounts,
564
564
  discountAmount: pricing.discount,
565
+ currency,
565
566
  isAmountLoading
566
567
  })]
567
568
  }), (() => {
@@ -855,7 +856,8 @@ function PaymentPanel() {
855
856
  remove: promotion.remove
856
857
  },
857
858
  discounts,
858
- discountAmount: pricing.discount
859
+ discountAmount: pricing.discount,
860
+ currency
859
861
  })]
860
862
  }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
861
863
  sx: {
@@ -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;
@@ -150,6 +150,13 @@ function getErrorConfig(errorCode, error, t) {
150
150
  color: "#94a3b8"
151
151
  };
152
152
  }
153
+ if (errorCode === "STOP_ACCEPTING_ORDERS") {
154
+ return {
155
+ title: t("payment.checkout.stopAcceptingOrders.title"),
156
+ description: t("payment.checkout.stopAcceptingOrders.description"),
157
+ color: "#f59e0b"
158
+ };
159
+ }
153
160
  return {
154
161
  title: t("payment.checkout.error.title"),
155
162
  description: error,
@@ -0,0 +1,4 @@
1
+ export default function ServiceSuspendedDialog({ open, onClose }: {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ }): import("react").JSX.Element;
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ module.exports = ServiceSuspendedDialog;
7
+ var _jsxRuntime = require("react/jsx-runtime");
8
+ var _material = require("@mui/material");
9
+ var _styles = require("@mui/material/styles");
10
+ var _PauseCircleOutline = _interopRequireDefault(require("@mui/icons-material/PauseCircleOutline"));
11
+ var _context = require("@arcblock/ux/lib/Locale/context");
12
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
13
+ function ServiceSuspendedDialog({
14
+ open,
15
+ onClose
16
+ }) {
17
+ const {
18
+ t
19
+ } = (0, _context.useLocaleContext)();
20
+ return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Dialog, {
21
+ open,
22
+ onClose,
23
+ PaperProps: {
24
+ sx: {
25
+ borderRadius: 3,
26
+ maxWidth: 400,
27
+ mx: "auto",
28
+ overflow: "hidden"
29
+ }
30
+ },
31
+ children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.DialogContent, {
32
+ sx: {
33
+ p: 0
34
+ },
35
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
36
+ alignItems: "center",
37
+ sx: {
38
+ pt: 4,
39
+ pb: 3,
40
+ px: 4,
41
+ textAlign: "center"
42
+ },
43
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
44
+ sx: {
45
+ width: 64,
46
+ height: 64,
47
+ borderRadius: "50%",
48
+ display: "flex",
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ bgcolor: theme => (0, _styles.alpha)(theme.palette.warning.main, 0.1),
52
+ mb: 2.5
53
+ },
54
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_PauseCircleOutline.default, {
55
+ sx: {
56
+ fontSize: 36,
57
+ color: "warning.main"
58
+ }
59
+ })
60
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
61
+ sx: {
62
+ fontWeight: 700,
63
+ fontSize: 18,
64
+ mb: 1,
65
+ color: "text.primary"
66
+ },
67
+ children: t("payment.checkout.stopAcceptingOrders.title")
68
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
69
+ sx: {
70
+ color: "text.secondary",
71
+ fontSize: 14,
72
+ lineHeight: 1.6
73
+ },
74
+ children: t("payment.checkout.stopAcceptingOrders.description")
75
+ })]
76
+ }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
77
+ sx: {
78
+ px: 4,
79
+ pb: 3
80
+ },
81
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
82
+ fullWidth: true,
83
+ variant: "contained",
84
+ disableElevation: true,
85
+ onClick: onClose,
86
+ sx: {
87
+ borderRadius: 2,
88
+ textTransform: "none",
89
+ fontWeight: 600,
90
+ py: 1
91
+ },
92
+ children: t("common.know")
93
+ })
94
+ })]
95
+ })
96
+ });
97
+ }
package/lib/locales/en.js CHANGED
@@ -387,6 +387,10 @@ module.exports = (0, _flat.default)({
387
387
  title: "Nothing to show here",
388
388
  description: "It seems this checkout session is not configured properly"
389
389
  },
390
+ stopAcceptingOrders: {
391
+ title: "Service Suspended",
392
+ description: "New order placement is temporarily unavailable due to a system-level service suspension."
393
+ },
390
394
  error: {
391
395
  title: "Something went wrong"
392
396
  },
package/lib/locales/zh.js CHANGED
@@ -416,6 +416,10 @@ module.exports = (0, _flat.default)({
416
416
  title: "\u6CA1\u6709\u4EFB\u4F55\u8D2D\u4E70\u9879\u76EE",
417
417
  description: "\u53EF\u80FD\u8FD9\u4E2A\u4ED8\u6B3E\u94FE\u63A5\u6CA1\u6709\u6B63\u786E\u914D\u7F6E"
418
418
  },
419
+ stopAcceptingOrders: {
420
+ title: "\u6682\u505C\u670D\u52A1",
421
+ description: "\u56E0\u7CFB\u7EDF\u7B56\u7565\u8C03\u6574\uFF0C\u5F53\u524D\u5DF2\u6682\u505C\u65B0\u8BA2\u5355\u670D\u52A1\u3002"
422
+ },
419
423
  error: {
420
424
  title: "\u51FA\u4E86\u70B9\u95EE\u9898"
421
425
  },
@@ -40,6 +40,7 @@ var _loadingButton = _interopRequireDefault(require("../../components/loading-bu
40
40
  var _overDueInvoicePayment = _interopRequireDefault(require("../../components/over-due-invoice-payment"));
41
41
  var _currency2 = require("../../libs/currency");
42
42
  var _confirm = _interopRequireDefault(require("../../components/confirm"));
43
+ var _serviceSuspendedDialog = _interopRequireDefault(require("../../components/service-suspended-dialog"));
43
44
  var _priceChangeConfirm = _interopRequireDefault(require("../../components/price-change-confirm"));
44
45
  var _validator = require("../../libs/validator");
45
46
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -181,6 +182,7 @@ function PaymentForm({
181
182
  stripeContext: void 0,
182
183
  customer,
183
184
  customerLimited: false,
185
+ serviceSuspended: false,
184
186
  stripePaying: false,
185
187
  fastCheckoutInfo: null,
186
188
  creditInsufficientInfo: null,
@@ -1039,6 +1041,12 @@ function PaymentForm({
1039
1041
  customerLimited: true
1040
1042
  });
1041
1043
  }
1044
+ if (errorCode === "STOP_ACCEPTING_ORDERS") {
1045
+ shouldToast = false;
1046
+ setState({
1047
+ serviceSuspended: true
1048
+ });
1049
+ }
1042
1050
  }
1043
1051
  if (shouldToast) {
1044
1052
  _Toast.default.error((0, _util2.formatError)(err));
@@ -1307,6 +1315,11 @@ function PaymentForm({
1307
1315
  }),
1308
1316
  title: t("payment.customer.pastDue.alert.title")
1309
1317
  }
1318
+ }), state.serviceSuspended && /* @__PURE__ */(0, _jsxRuntime.jsx)(_serviceSuspendedDialog.default, {
1319
+ open: true,
1320
+ onClose: () => setState({
1321
+ serviceSuspended: false
1322
+ })
1310
1323
  }), FastCheckoutConfirmDialog, CreditInsufficientDialog, PriceUpdatedDialog, state.priceChangeConfirm?.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_priceChangeConfirm.default, {
1311
1324
  open: true,
1312
1325
  changePercent: state.priceChangeConfirm.changePercent,
@@ -1617,6 +1630,16 @@ function PaymentForm({
1617
1630
  }),
1618
1631
  title: t("payment.customer.pastDue.alert.title")
1619
1632
  }
1633
+ }), state.serviceSuspended && /* @__PURE__ */(0, _jsxRuntime.jsx)(_confirm.default, {
1634
+ onConfirm: () => setState({
1635
+ serviceSuspended: false
1636
+ }),
1637
+ onCancel: () => setState({
1638
+ serviceSuspended: false
1639
+ }),
1640
+ title: t("payment.checkout.stopAcceptingOrders.title"),
1641
+ message: t("payment.checkout.stopAcceptingOrders.description"),
1642
+ confirm: t("common.confirm")
1620
1643
  }), FastCheckoutConfirmDialog, CreditInsufficientDialog, PriceUpdatedDialog, state.priceChangeConfirm?.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_priceChangeConfirm.default, {
1621
1644
  open: true,
1622
1645
  changePercent: state.priceChangeConfirm.changePercent,
@@ -87,6 +87,10 @@ function PaymentInner({
87
87
  return Array.from(currencyIds);
88
88
  }, [paymentMethods]);
89
89
  const defaultCurrencyId = (0, _react.useMemo)(() => {
90
+ const hasAppliedDiscount = Boolean(state.checkoutSession?.discounts?.length);
91
+ if (hasAppliedDiscount && state.checkoutSession.currency_id && availableCurrencyIds.includes(state.checkoutSession.currency_id)) {
92
+ return state.checkoutSession.currency_id;
93
+ }
90
94
  if (query.currencyId && availableCurrencyIds.includes(query.currencyId)) {
91
95
  return query.currencyId;
92
96
  }
@@ -104,7 +108,7 @@ function PaymentInner({
104
108
  return state.checkoutSession.currency_id;
105
109
  }
106
110
  return availableCurrencyIds?.[0];
107
- }, [query.currencyId, availableCurrencyIds, session?.user, state.checkoutSession.currency_id, paymentMethods]);
111
+ }, [query.currencyId, availableCurrencyIds, session?.user, state.checkoutSession, paymentMethods]);
108
112
  const defaultMethodId = paymentMethods.find(m => m.payment_currencies.some(c => c.id === defaultCurrencyId))?.id;
109
113
  const hideSummaryCard = mode.endsWith("-minimal") || !showCheckoutSummary;
110
114
  const methods = (0, _reactHookForm.useForm)({
@@ -127,13 +131,20 @@ function PaymentInner({
127
131
  }
128
132
  });
129
133
  (0, _react.useEffect)(() => {
134
+ const hasAppliedDiscount = Boolean(state.checkoutSession?.discounts?.length);
135
+ const currentCurrency = methods.getValues("payment_currency");
136
+ const currentMethod = methods.getValues("payment_method");
130
137
  if (defaultCurrencyId) {
131
- methods.setValue("payment_currency", defaultCurrencyId);
138
+ if (!hasAppliedDiscount || !currentCurrency) {
139
+ methods.setValue("payment_currency", defaultCurrencyId);
140
+ }
132
141
  }
133
142
  if (defaultMethodId) {
134
- methods.setValue("payment_method", defaultMethodId);
143
+ if (!hasAppliedDiscount || !currentMethod) {
144
+ methods.setValue("payment_method", defaultMethodId);
145
+ }
135
146
  }
136
- }, [defaultCurrencyId, defaultMethodId]);
147
+ }, [defaultCurrencyId, defaultMethodId, state.checkoutSession.discounts]);
137
148
  (0, _react.useEffect)(() => {
138
149
  if (!(0, _util2.isMobileSafari)()) {
139
150
  return () => {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.26.2",
3
+ "version": "1.26.3",
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.2",
62
+ "@blocklet/payment-react-headless": "1.26.3",
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.2",
100
+ "@blocklet/payment-types": "1.26.3",
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": "71242a68d27d56666487176425153dc08071960f"
131
+ "gitHead": "18c5d045139c572b52465e15c4c63b3e327efab5"
132
132
  }
@@ -9,6 +9,7 @@ import {
9
9
  import StripeForm from '../../../payment/form/stripe';
10
10
  import ConfirmDialog from '../../../components/confirm';
11
11
  import PriceChangeConfirm from '../../../components/price-change-confirm';
12
+ import ServiceSuspendedDialog from '../../../components/service-suspended-dialog';
12
13
  import { formatTokenAmount } from '../../utils/format';
13
14
 
14
15
  function getRedirectUrl(session: any): string | undefined {
@@ -119,6 +120,9 @@ export default function CheckoutDialogs() {
119
120
  />
120
121
  )}
121
122
 
123
+ {/* Service Suspended Dialog */}
124
+ {(submit.context as any)?.type === 'service_suspended' && <ServiceSuspendedDialog open onClose={submit.cancel} />}
125
+
122
126
  {/* Credit Insufficient Dialog (matches V1 ConfirmDialog) */}
123
127
  {submit.status === 'credit_insufficient' && submit.context?.type === 'credit_insufficient' && (
124
128
  <ConfirmDialog
@@ -15,6 +15,7 @@ import {
15
15
  Typography,
16
16
  } from '@mui/material';
17
17
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
18
+ import { formatCouponTerms } from '../../../libs/util';
18
19
 
19
20
  interface PromotionInputProps {
20
21
  promotion: {
@@ -27,6 +28,8 @@ interface PromotionInputProps {
27
28
  };
28
29
  discounts: any[];
29
30
  discountAmount: string | null;
31
+ // eslint-disable-next-line react/require-default-props
32
+ currency?: any;
30
33
  /** Start with input field visible (skip the "Add promotion code" button) */
31
34
  initialShowInput?: boolean;
32
35
  /** Show skeleton for the discount amount while switching */
@@ -37,10 +40,11 @@ export default function PromotionInput({
37
40
  promotion,
38
41
  discounts,
39
42
  discountAmount,
43
+ currency = null,
40
44
  initialShowInput = false,
41
45
  isAmountLoading = false,
42
46
  }: PromotionInputProps) {
43
- const { t } = useLocaleContext();
47
+ const { t, locale } = useLocaleContext();
44
48
  const [showInput, setShowInput] = useState(false);
45
49
  const [code, setCode] = useState('');
46
50
  const [applying, setApplying] = useState(false);
@@ -77,19 +81,7 @@ export default function PromotionInput({
77
81
  const discCode =
78
82
  disc.promotion_code_details?.code || disc.verification_data?.code || disc.promotion_code || '';
79
83
  const coupon = disc.coupon_details || {};
80
- const couponOff =
81
- coupon.percent_off > 0
82
- ? t('payment.checkout.coupon.percentage', { percent: coupon.percent_off })
83
- : `${coupon.percent_off || 0}%`;
84
- let description = '';
85
- if (coupon.duration === 'repeating' && coupon.duration_in_months) {
86
- const months = coupon.duration_in_months;
87
- description = `${couponOff} for ${months} month${months > 1 ? 's' : ''}`;
88
- } else if (coupon.duration === 'forever') {
89
- description = t('payment.checkout.coupon.terms.forever', { couponOff });
90
- } else if (coupon.duration === 'once') {
91
- description = t('payment.checkout.coupon.terms.once', { couponOff });
92
- }
84
+ const description = coupon && currency ? formatCouponTerms(coupon, currency, locale) : '';
93
85
  return (
94
86
  <Stack
95
87
  key={disc.promotion_code || disc.coupon || i}
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from 'react';
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import { Box, Button, InputBase, InputAdornment, Stack, Typography } from '@mui/material';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import CountrySelect from '../../../components/country-select';
@@ -17,7 +17,9 @@ interface CustomerInfoCardProps {
17
17
  values: Record<string, any>;
18
18
  onChange: (field: string, value: string | boolean | Record<string, string>) => void;
19
19
  errors: Partial<Record<string, string>>;
20
+ checkValid: () => Promise<boolean>;
20
21
  validateField: (field: string) => Promise<void>;
22
+ prefetched: boolean;
21
23
  };
22
24
  isLoggedIn: boolean;
23
25
  }
@@ -39,20 +41,31 @@ export default function CustomerInfoCard({ form, isLoggedIn }: CustomerInfoCardP
39
41
  const { t } = useLocaleContext();
40
42
  const labels = fieldLabelMap(t);
41
43
 
42
- // Default to confirmed view if required fields have values
43
- const hasRequiredData = !!(form.values.customer_name && form.values.customer_email);
44
- const [showEditForm, setShowEditForm] = useState(!hasRequiredData);
45
- const autoConfirmedRef = useRef(false);
44
+ // Don't render until first validation completes avoids flash.
45
+ // Valid show collapsed summary; invalid → show expanded form.
46
+ // After first check, only manual "Edit"/"Confirm" toggles.
47
+ const [showEditForm, setShowEditForm] = useState(false);
48
+ const [ready, setReady] = useState(false);
49
+ const checkedRef = useRef(false);
46
50
 
47
- // When data arrives (e.g. prefetch), auto-switch to confirmed if valid
48
51
  useEffect(() => {
49
- if (!autoConfirmedRef.current && form.values.customer_name && form.values.customer_email) {
50
- autoConfirmedRef.current = true;
51
- setShowEditForm(false);
52
- }
53
- }, [form.values.customer_name, form.values.customer_email]);
52
+ if (checkedRef.current) return;
53
+ // Wait for data to arrive before making a decision
54
+ if (!form.prefetched && !form.values.customer_name && !form.values.customer_email) return;
55
+ checkedRef.current = true;
56
+ form.checkValid().then((valid) => {
57
+ setShowEditForm(!valid);
58
+ setReady(true);
59
+ });
60
+ }, [form.prefetched]); // eslint-disable-line react-hooks/exhaustive-deps
54
61
 
55
- if (!isLoggedIn) return null;
62
+ // Wrap onChange to delegate to parent form
63
+ const handleChange: typeof form.onChange = useCallback(
64
+ (field, value) => form.onChange(field, value),
65
+ [form.onChange] // eslint-disable-line react-hooks/exhaustive-deps
66
+ );
67
+
68
+ if (!isLoggedIn || !ready) return null;
56
69
 
57
70
  // Summary view
58
71
  if (!showEditForm) {
@@ -162,8 +175,8 @@ export default function CustomerInfoCard({ form, isLoggedIn }: CustomerInfoCardP
162
175
  key={name}
163
176
  value={value || ''}
164
177
  country={form.values.billing_address?.country || ''}
165
- onChange={(phone) => form.onChange('customer_phone', phone)}
166
- onCountryChange={(c) => form.onChange('billing_address.country', c)}
178
+ onChange={(phone) => handleChange('customer_phone', phone)}
179
+ onCountryChange={(c) => handleChange('billing_address.country', c)}
167
180
  onBlur={() => form.validateField(name)}
168
181
  label={label}
169
182
  error={form.errors[name]}
@@ -177,14 +190,14 @@ export default function CustomerInfoCard({ form, isLoggedIn }: CustomerInfoCardP
177
190
  <InputBase
178
191
  fullWidth
179
192
  value={value || ''}
180
- onChange={(e) => form.onChange(name, e.target.value)}
193
+ onChange={(e) => handleChange(name, e.target.value)}
181
194
  onBlur={() => form.validateField(name)}
182
195
  startAdornment={
183
196
  isPostalCode ? (
184
197
  <InputAdornment position="start" sx={{ mr: 0.5, ml: -0.5 }}>
185
198
  <CountrySelect
186
199
  value={form.values.billing_address?.country || ''}
187
- onChange={(v) => form.onChange('billing_address.country', v)}
200
+ onChange={(v) => handleChange('billing_address.country', v)}
188
201
  sx={{
189
202
  '.MuiOutlinedInput-notchedOutline': { borderColor: 'transparent !important' },
190
203
  '& .MuiSelect-select': { py: 0, pr: '20px !important' },
@@ -19,8 +19,8 @@ export default function StatusFeedback({ status, context, onReset }: StatusFeedb
19
19
  prevStatusRef.current = status;
20
20
 
21
21
  if (status === 'failed' && context?.type === 'error') {
22
- // CUSTOMER_LIMITED is handled by parent (overdue invoice dialog)
23
- if (context.code === 'CUSTOMER_LIMITED') return;
22
+ // CUSTOMER_LIMITED / STOP_ACCEPTING_ORDERS are handled by CheckoutDialogs
23
+ if (context.code === 'CUSTOMER_LIMITED' || context.code === 'STOP_ACCEPTING_ORDERS') return;
24
24
 
25
25
  Toast.error(context.message || 'Payment failed');
26
26
  onReset();
@@ -27,14 +27,26 @@ const fadeIn = {
27
27
  animation: { xs: 'none', md: 'fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) 0.15s both' },
28
28
  } as const;
29
29
 
30
- // Desktop: right panel slides in from the right
31
- const slideInFromRight = {
32
- '@keyframes slideInRight': {
33
- from: { transform: 'translateX(100%)' },
34
- to: { transform: 'translateX(0)' },
35
- },
36
- animation: { xs: 'none', md: 'slideInRight 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards' },
37
- } as const;
30
+ // Desktop: right panel slides in from right (non-Safari) or fades in (Safari)
31
+ // Safari has flexbox reflow bugs with translateX(100%), so we use a subtle fade instead
32
+ const isSafari =
33
+ typeof navigator !== 'undefined' && /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
34
+
35
+ const slideInFromRight = isSafari
36
+ ? ({
37
+ '@keyframes panelFadeIn': {
38
+ from: { opacity: 0, transform: 'translateX(24px)' },
39
+ to: { opacity: 1, transform: 'none' },
40
+ },
41
+ animation: { xs: 'none', md: 'panelFadeIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' },
42
+ } as const)
43
+ : ({
44
+ '@keyframes slideInRight': {
45
+ from: { transform: 'translateX(100%)' },
46
+ to: { transform: 'translateX(0)' },
47
+ },
48
+ animation: { xs: 'none', md: 'slideInRight 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards' },
49
+ } as const);
38
50
 
39
51
  interface CheckoutLayoutProps {
40
52
  left: React.ReactNode;
@@ -94,6 +106,7 @@ export default function CheckoutLayout({ left, right, mode = 'inline' }: Checkou
94
106
  <Box
95
107
  sx={{
96
108
  flex: 1,
109
+ minWidth: 0,
97
110
  bgcolor: (t) => (t.palette.mode === 'dark' ? 'background.default' : '#f8faff'),
98
111
  p: { xs: 3, md: 5 },
99
112
  pt: { xs: 3, md: 4 },
@@ -170,7 +183,8 @@ export default function CheckoutLayout({ left, right, mode = 'inline' }: Checkou
170
183
  {!hideLeft && (
171
184
  <Box
172
185
  sx={{
173
- width: { xs: '100%', md: '50%' },
186
+ flex: { xs: 'none', md: '0 0 50%' },
187
+ width: { xs: '100%' },
174
188
  height: { xs: 'auto', md: '100vh' },
175
189
  display: 'flex',
176
190
  justifyContent: { md: 'center' },
@@ -196,7 +210,8 @@ export default function CheckoutLayout({ left, right, mode = 'inline' }: Checkou
196
210
  {/* Right panel — full width when left is hidden */}
197
211
  <Box
198
212
  sx={{
199
- width: hideLeft ? '100%' : { xs: '100%', md: '50%' },
213
+ flex: hideLeft ? 'none' : { xs: 'none', md: '0 0 50%' },
214
+ width: hideLeft ? '100%' : { xs: '100%' },
200
215
  height: hideLeft ? '100vh' : { xs: 'auto', md: '100vh' },
201
216
  bgcolor: 'background.paper',
202
217
  boxShadow: hideLeft ? 'none' : { md: '-4px 0 16px rgba(0,0,0,0.04)' },
@@ -459,6 +459,7 @@ export default function PaymentPanel() {
459
459
  }}
460
460
  discounts={discounts}
461
461
  discountAmount={pricing.discount}
462
+ currency={currency}
462
463
  isAmountLoading={isAmountLoading}
463
464
  />
464
465
  </>
@@ -657,6 +658,7 @@ export default function PaymentPanel() {
657
658
  }}
658
659
  discounts={discounts}
659
660
  discountAmount={pricing.discount}
661
+ currency={currency}
660
662
  />
661
663
  </Drawer>
662
664
  )}
@@ -7,7 +7,7 @@ import { primaryContrastColor } from '../utils/format';
7
7
 
8
8
  interface ErrorViewProps {
9
9
  error: string;
10
- errorCode?: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
10
+ errorCode?: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
11
11
  mode?: string;
12
12
  }
13
13
 
@@ -128,6 +128,14 @@ function getErrorConfig(
128
128
  };
129
129
  }
130
130
 
131
+ if (errorCode === 'STOP_ACCEPTING_ORDERS') {
132
+ return {
133
+ title: t('payment.checkout.stopAcceptingOrders.title'),
134
+ description: t('payment.checkout.stopAcceptingOrders.description'),
135
+ color: '#f59e0b',
136
+ };
137
+ }
138
+
131
139
  return {
132
140
  title: t('payment.checkout.error.title'),
133
141
  description: error,