@blocklet/payment-react 1.18.25 → 1.18.26

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 (59) hide show
  1. package/es/checkout/donate.js +11 -1
  2. package/es/components/country-select.js +243 -21
  3. package/es/components/over-due-invoice-payment.d.ts +3 -1
  4. package/es/components/over-due-invoice-payment.js +6 -4
  5. package/es/contexts/payment.d.ts +2 -1
  6. package/es/contexts/payment.js +8 -1
  7. package/es/index.d.ts +1 -0
  8. package/es/index.js +1 -0
  9. package/es/libs/api.js +4 -0
  10. package/es/libs/currency.d.ts +3 -0
  11. package/es/libs/currency.js +22 -0
  12. package/es/libs/phone-validator.js +2 -0
  13. package/es/libs/validator.d.ts +1 -0
  14. package/es/libs/validator.js +70 -0
  15. package/es/payment/form/address.js +17 -3
  16. package/es/payment/form/index.js +10 -1
  17. package/es/payment/form/phone.js +12 -1
  18. package/es/payment/form/stripe/form.js +14 -5
  19. package/es/payment/index.js +33 -11
  20. package/es/payment/product-donation.js +110 -12
  21. package/es/types/shims.d.ts +2 -0
  22. package/lib/checkout/donate.js +11 -1
  23. package/lib/components/country-select.js +243 -39
  24. package/lib/components/over-due-invoice-payment.d.ts +3 -1
  25. package/lib/components/over-due-invoice-payment.js +7 -4
  26. package/lib/contexts/payment.d.ts +2 -1
  27. package/lib/contexts/payment.js +9 -1
  28. package/lib/index.d.ts +1 -0
  29. package/lib/index.js +12 -0
  30. package/lib/libs/api.js +4 -0
  31. package/lib/libs/currency.d.ts +3 -0
  32. package/lib/libs/currency.js +31 -0
  33. package/lib/libs/phone-validator.js +1 -0
  34. package/lib/libs/validator.d.ts +1 -0
  35. package/lib/libs/validator.js +20 -0
  36. package/lib/payment/form/address.js +15 -2
  37. package/lib/payment/form/index.js +12 -1
  38. package/lib/payment/form/phone.js +13 -1
  39. package/lib/payment/form/stripe/form.js +21 -5
  40. package/lib/payment/index.js +34 -10
  41. package/lib/payment/product-donation.js +106 -15
  42. package/lib/types/shims.d.ts +2 -0
  43. package/package.json +8 -8
  44. package/src/checkout/donate.tsx +11 -1
  45. package/src/components/country-select.tsx +265 -20
  46. package/src/components/over-due-invoice-payment.tsx +6 -2
  47. package/src/contexts/payment.tsx +11 -1
  48. package/src/index.ts +1 -0
  49. package/src/libs/api.ts +4 -1
  50. package/src/libs/currency.ts +25 -0
  51. package/src/libs/phone-validator.ts +1 -0
  52. package/src/libs/validator.ts +70 -0
  53. package/src/payment/form/address.tsx +17 -4
  54. package/src/payment/form/index.tsx +11 -1
  55. package/src/payment/form/phone.tsx +15 -1
  56. package/src/payment/form/stripe/form.tsx +20 -9
  57. package/src/payment/index.tsx +45 -14
  58. package/src/payment/product-donation.tsx +129 -10
  59. package/src/types/shims.d.ts +2 -0
@@ -65,6 +65,7 @@ export function DonateDetails({ supporters = [], currency, method }) {
65
65
  return /* @__PURE__ */ jsx(
66
66
  Stack,
67
67
  {
68
+ className: "cko-donate-details",
68
69
  sx: {
69
70
  width: "100%",
70
71
  minWidth: "256px",
@@ -246,8 +247,17 @@ function SupporterAvatar({
246
247
  onClose: () => setOpen(false),
247
248
  sx: {
248
249
  ".MuiDialogContent-root": {
249
- width: "450px",
250
+ width: {
251
+ xs: "100%",
252
+ md: "450px"
253
+ },
250
254
  padding: "8px"
255
+ },
256
+ ".cko-donate-details": {
257
+ maxHeight: {
258
+ xs: "100%",
259
+ md: "300px"
260
+ }
251
261
  }
252
262
  },
253
263
  title: `${customersNum} supporter${customersNum > 1 ? "s" : ""}`,
@@ -1,10 +1,110 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
- import { useMemo, forwardRef } from "react";
3
- import { Box, MenuItem, Select, Typography } from "@mui/material";
2
+ import { useMemo, forwardRef, useState, useEffect, useRef } from "react";
3
+ import { Box, MenuItem, Select, Typography, TextField } from "@mui/material";
4
4
  import { useFormContext } from "react-hook-form";
5
5
  import { FlagEmoji, defaultCountries, parseCountry } from "react-international-phone";
6
+ import { useMobile } from "../hooks/mobile.js";
6
7
  const CountrySelect = forwardRef(({ value, onChange, name, sx }, ref) => {
7
8
  const { setValue } = useFormContext();
9
+ const [open, setOpen] = useState(false);
10
+ const [searchText, setSearchText] = useState("");
11
+ const inputRef = useRef(null);
12
+ const menuRef = useRef(null);
13
+ const listRef = useRef(null);
14
+ const [focusedIndex, setFocusedIndex] = useState(-1);
15
+ const itemHeightRef = useRef(40);
16
+ const { isMobile } = useMobile();
17
+ const measuredRef = useRef(false);
18
+ useEffect(() => {
19
+ if (!open)
20
+ return () => {
21
+ };
22
+ const handleResize = () => {
23
+ measuredRef.current = false;
24
+ };
25
+ window.addEventListener("resize", handleResize);
26
+ return () => {
27
+ window.removeEventListener("resize", handleResize);
28
+ };
29
+ }, [open]);
30
+ const scrollToTop = () => {
31
+ if (listRef.current) {
32
+ listRef.current.scrollTop = 0;
33
+ }
34
+ };
35
+ const measureItemHeight = () => {
36
+ if (!listRef.current || !open)
37
+ return;
38
+ const items = listRef.current.querySelectorAll(".MuiMenuItem-root");
39
+ if (items.length > 0) {
40
+ const firstItem = items[0];
41
+ if (firstItem.offsetHeight > 0) {
42
+ itemHeightRef.current = firstItem.offsetHeight;
43
+ }
44
+ }
45
+ };
46
+ const controlScrollPosition = (index) => {
47
+ if (!listRef.current)
48
+ return;
49
+ if (open && !measuredRef.current) {
50
+ measureItemHeight();
51
+ measuredRef.current = true;
52
+ }
53
+ const listHeight = listRef.current.clientHeight;
54
+ const targetPosition = index * itemHeightRef.current;
55
+ if (index < 2) {
56
+ listRef.current.scrollTop = 0;
57
+ } else if (index > filteredCountries.length - 3) {
58
+ listRef.current.scrollTop = listRef.current.scrollHeight - listHeight;
59
+ } else {
60
+ const scrollPosition = targetPosition - listHeight / 2 + itemHeightRef.current / 2;
61
+ listRef.current.scrollTop = Math.max(0, scrollPosition);
62
+ }
63
+ };
64
+ useEffect(() => {
65
+ let timeout = null;
66
+ if (open) {
67
+ timeout = setTimeout(() => {
68
+ scrollToTop();
69
+ if (!isMobile && inputRef.current) {
70
+ inputRef.current.focus();
71
+ }
72
+ }, 100);
73
+ } else {
74
+ setSearchText("");
75
+ setFocusedIndex(-1);
76
+ }
77
+ return () => {
78
+ if (timeout) {
79
+ clearTimeout(timeout);
80
+ }
81
+ };
82
+ }, [open, isMobile]);
83
+ const filteredCountries = useMemo(() => {
84
+ if (!searchText)
85
+ return defaultCountries;
86
+ return defaultCountries.filter((c) => {
87
+ const parsed = parseCountry(c);
88
+ return parsed.name.toLowerCase().includes(searchText.toLowerCase()) || parsed.iso2.toLowerCase().includes(searchText.toLowerCase()) || `+${parsed.dialCode}`.includes(searchText);
89
+ });
90
+ }, [searchText]);
91
+ useEffect(() => {
92
+ scrollToTop();
93
+ setFocusedIndex(-1);
94
+ }, [searchText]);
95
+ useEffect(() => {
96
+ let timeout = null;
97
+ if (focusedIndex >= 0) {
98
+ timeout = setTimeout(() => {
99
+ controlScrollPosition(focusedIndex);
100
+ }, 10);
101
+ }
102
+ return () => {
103
+ if (timeout) {
104
+ clearTimeout(timeout);
105
+ }
106
+ };
107
+ }, [focusedIndex, filteredCountries.length]);
8
108
  const countryDetail = useMemo(() => {
9
109
  const item = defaultCountries.find((v) => v[1] === value);
10
110
  return value && item ? parseCountry(item) : { name: "" };
@@ -13,13 +113,62 @@ const CountrySelect = forwardRef(({ value, onChange, name, sx }, ref) => {
13
113
  onChange(e.target.value);
14
114
  setValue(name, e.target.value);
15
115
  };
16
- return /* @__PURE__ */ jsx(
116
+ const handleCountryClick = (code) => {
117
+ onChange(code);
118
+ setValue(name, code);
119
+ setOpen(false);
120
+ };
121
+ const handleSearchChange = (e) => {
122
+ e.stopPropagation();
123
+ setSearchText(e.target.value);
124
+ };
125
+ const handleKeyDown = (e) => {
126
+ e.stopPropagation();
127
+ if (e.key === "Escape") {
128
+ setOpen(false);
129
+ return;
130
+ }
131
+ const handleNavigation = (direction) => {
132
+ e.preventDefault();
133
+ setFocusedIndex((prev) => {
134
+ if (direction === "next") {
135
+ if (prev === -1)
136
+ return 0;
137
+ return prev >= filteredCountries.length - 1 ? 0 : prev + 1;
138
+ }
139
+ if (prev === -1)
140
+ return filteredCountries.length - 1;
141
+ return prev <= 0 ? filteredCountries.length - 1 : prev - 1;
142
+ });
143
+ };
144
+ if (e.key === "Tab") {
145
+ handleNavigation(e.shiftKey ? "prev" : "next");
146
+ return;
147
+ }
148
+ if (e.key === "ArrowDown") {
149
+ handleNavigation("next");
150
+ return;
151
+ }
152
+ if (e.key === "ArrowUp") {
153
+ handleNavigation("prev");
154
+ return;
155
+ }
156
+ if (e.key === "Enter" && focusedIndex >= 0 && focusedIndex < filteredCountries.length) {
157
+ e.preventDefault();
158
+ const country = parseCountry(filteredCountries[focusedIndex]);
159
+ handleCountryClick(country.iso2);
160
+ }
161
+ };
162
+ return /* @__PURE__ */ jsxs(
17
163
  Select,
18
164
  {
19
165
  ref,
166
+ open,
167
+ onOpen: () => setOpen(true),
168
+ onClose: () => setOpen(false),
20
169
  MenuProps: {
21
170
  style: {
22
- height: "300px",
171
+ maxHeight: "300px",
23
172
  top: "10px"
24
173
  },
25
174
  anchorOrigin: {
@@ -29,6 +178,18 @@ const CountrySelect = forwardRef(({ value, onChange, name, sx }, ref) => {
29
178
  transformOrigin: {
30
179
  vertical: "top",
31
180
  horizontal: "left"
181
+ },
182
+ PaperProps: {
183
+ ref: menuRef,
184
+ sx: {
185
+ display: "flex",
186
+ "& .MuiList-root": {
187
+ pt: 0,
188
+ display: "flex",
189
+ flexDirection: "column",
190
+ overflowY: "hidden"
191
+ }
192
+ }
32
193
  }
33
194
  },
34
195
  sx: {
@@ -55,23 +216,84 @@ const CountrySelect = forwardRef(({ value, onChange, name, sx }, ref) => {
55
216
  },
56
217
  value,
57
218
  onChange: onCountryChange,
58
- renderValue: (code) => {
59
- return /* @__PURE__ */ jsxs(Box, { display: "flex", alignItems: "center", flexWrap: "nowrap", gap: 0.5, sx: { cursor: "pointer" }, children: [
60
- /* @__PURE__ */ jsx(FlagEmoji, { iso2: code, style: { display: "flex" } }),
61
- /* @__PURE__ */ jsx(Typography, { children: countryDetail?.name })
62
- ] });
63
- },
64
- children: defaultCountries.map((c) => {
65
- const parsed = parseCountry(c);
66
- return /* @__PURE__ */ jsxs(MenuItem, { value: parsed.iso2, children: [
67
- /* @__PURE__ */ jsx(FlagEmoji, { iso2: parsed.iso2, style: { marginRight: "8px" } }),
68
- /* @__PURE__ */ jsx(Typography, { marginRight: "8px", children: parsed.name }),
69
- /* @__PURE__ */ jsxs(Typography, { color: "gray", children: [
70
- "+",
71
- parsed.dialCode
72
- ] })
73
- ] }, parsed.iso2);
74
- })
219
+ renderValue: (code) => /* @__PURE__ */ jsxs(Box, { display: "flex", alignItems: "center", flexWrap: "nowrap", gap: 0.5, sx: { cursor: "pointer" }, children: [
220
+ /* @__PURE__ */ jsx(FlagEmoji, { iso2: code, style: { display: "flex" } }),
221
+ /* @__PURE__ */ jsx(Typography, { children: countryDetail?.name })
222
+ ] }),
223
+ children: [
224
+ /* @__PURE__ */ jsx(
225
+ Box,
226
+ {
227
+ sx: {
228
+ position: "sticky",
229
+ top: 0,
230
+ zIndex: 1,
231
+ bgcolor: "background.paper",
232
+ p: 1
233
+ },
234
+ onClick: (e) => {
235
+ e.stopPropagation();
236
+ },
237
+ children: /* @__PURE__ */ jsx(
238
+ TextField,
239
+ {
240
+ inputRef,
241
+ autoFocus: !isMobile,
242
+ fullWidth: true,
243
+ placeholder: "Search country...",
244
+ value: searchText,
245
+ onChange: handleSearchChange,
246
+ onKeyDown: handleKeyDown,
247
+ onClick: (e) => e.stopPropagation(),
248
+ size: "small",
249
+ variant: "outlined"
250
+ }
251
+ )
252
+ }
253
+ ),
254
+ /* @__PURE__ */ jsx(
255
+ Box,
256
+ {
257
+ ref: listRef,
258
+ sx: {
259
+ flex: 1,
260
+ overflowY: "auto",
261
+ overflowX: "hidden",
262
+ maxHeight: "calc(300px - 65px)",
263
+ scrollBehavior: "smooth"
264
+ },
265
+ children: filteredCountries.length > 0 ? filteredCountries.map((c, index) => {
266
+ const parsed = parseCountry(c);
267
+ const isFocused = index === focusedIndex;
268
+ return /* @__PURE__ */ jsxs(
269
+ MenuItem,
270
+ {
271
+ value: parsed.iso2,
272
+ selected: parsed.iso2 === value,
273
+ onClick: () => handleCountryClick(parsed.iso2),
274
+ sx: {
275
+ "&.Mui-selected": {
276
+ backgroundColor: "rgba(0, 0, 0, 0.04)"
277
+ },
278
+ "&:hover": {
279
+ backgroundColor: "var(--backgrounds-bg-highlight, #eff6ff)"
280
+ },
281
+ ...isFocused ? {
282
+ backgroundColor: "var(--backgrounds-bg-highlight, #eff6ff)",
283
+ outline: "none"
284
+ } : {}
285
+ },
286
+ children: [
287
+ /* @__PURE__ */ jsx(FlagEmoji, { iso2: parsed.iso2, style: { marginRight: "8px" } }),
288
+ /* @__PURE__ */ jsx(Typography, { children: parsed.name })
289
+ ]
290
+ },
291
+ parsed.iso2
292
+ );
293
+ }) : /* @__PURE__ */ jsx(MenuItem, { disabled: true, children: /* @__PURE__ */ jsx(Typography, { color: "text.secondary", children: "No countries found" }) })
294
+ }
295
+ )
296
+ ]
75
297
  }
76
298
  );
77
299
  });
@@ -27,13 +27,14 @@ type Props = {
27
27
  subscriptionCount?: number;
28
28
  detailUrl: string;
29
29
  }) => React.ReactNode;
30
+ authToken?: string;
30
31
  };
31
32
  type SummaryItem = {
32
33
  amount: string;
33
34
  currency: PaymentCurrency;
34
35
  method: PaymentMethod;
35
36
  };
36
- declare function OverdueInvoicePayment({ subscriptionId, customerId, mode, dialogProps, children, onPaid, detailLinkOptions, successToast, alertMessage, }: Props): import("react").JSX.Element | null;
37
+ declare function OverdueInvoicePayment({ subscriptionId, customerId, mode, dialogProps, children, onPaid, detailLinkOptions, successToast, alertMessage, authToken, }: Props): import("react").JSX.Element | null;
37
38
  declare namespace OverdueInvoicePayment {
38
39
  var defaultProps: {
39
40
  mode: string;
@@ -49,6 +50,7 @@ declare namespace OverdueInvoicePayment {
49
50
  customerId: undefined;
50
51
  successToast: boolean;
51
52
  alertMessage: string;
53
+ authToken: undefined;
52
54
  };
53
55
  }
54
56
  export default OverdueInvoicePayment;
@@ -23,7 +23,7 @@ const fetchOverdueInvoices = async (params) => {
23
23
  } else {
24
24
  url = `/api/customers/${params.customerId}/overdue/invoices`;
25
25
  }
26
- const res = await api.get(url);
26
+ const res = await api.get(params.authToken ? joinURL(url, `?authToken=${params.authToken}`) : url);
27
27
  return res.data;
28
28
  };
29
29
  function OverdueInvoicePayment({
@@ -36,7 +36,8 @@ function OverdueInvoicePayment({
36
36
  },
37
37
  detailLinkOptions = { enabled: true },
38
38
  successToast = true,
39
- alertMessage = ""
39
+ alertMessage = "",
40
+ authToken
40
41
  }) {
41
42
  const { t } = useLocaleContext();
42
43
  const { connect } = usePaymentContext();
@@ -55,7 +56,7 @@ function OverdueInvoicePayment({
55
56
  error,
56
57
  loading,
57
58
  runAsync: refresh
58
- } = useRequest(() => fetchOverdueInvoices({ subscriptionId, customerId }), {
59
+ } = useRequest(() => fetchOverdueInvoices({ subscriptionId, customerId, authToken }), {
59
60
  ready: !!subscriptionId || !!customerId
60
61
  });
61
62
  const detailUrl = useMemo(() => {
@@ -396,6 +397,7 @@ OverdueInvoicePayment.defaultProps = {
396
397
  subscriptionId: void 0,
397
398
  customerId: void 0,
398
399
  successToast: true,
399
- alertMessage: ""
400
+ alertMessage: "",
401
+ authToken: void 0
400
402
  };
401
403
  export default OverdueInvoicePayment;
@@ -25,10 +25,11 @@ export type PaymentContextProps = {
25
25
  connect: import('@arcblock/did-connect/lib/types').SessionContext['connectApi'];
26
26
  children: any;
27
27
  baseUrl?: string;
28
+ authToken?: string;
28
29
  };
29
30
  declare const PaymentContext: import("react").Context<PaymentContextType>;
30
31
  declare const Consumer: import("react").Consumer<PaymentContextType>;
31
- declare function PaymentProvider({ session, connect, children, baseUrl }: PaymentContextProps): import("react").JSX.Element | null;
32
+ declare function PaymentProvider({ session, connect, children, baseUrl, authToken }: PaymentContextProps): import("react").JSX.Element | null;
32
33
  declare namespace PaymentProvider {
33
34
  var defaultProps: {};
34
35
  }
@@ -33,9 +33,16 @@ const getCurrency = (currencyId, methods) => {
33
33
  const getMethod = (methodId, methods) => {
34
34
  return methods.find((x) => x.id === methodId);
35
35
  };
36
- function PaymentProvider({ session, connect, children, baseUrl }) {
36
+ function PaymentProvider({ session, connect, children, baseUrl, authToken }) {
37
37
  if (baseUrl) {
38
38
  window.__PAYMENT_KIT_BASE_URL = baseUrl;
39
+ } else {
40
+ window.__PAYMENT_KIT_BASE_URL = "";
41
+ }
42
+ if (authToken) {
43
+ window.__PAYMENT_KIT_AUTH_TOKEN = authToken;
44
+ } else {
45
+ window.__PAYMENT_KIT_AUTH_TOKEN = "";
39
46
  }
40
47
  const [livemode, setLivemode] = useLocalStorageState("livemode", { defaultValue: true });
41
48
  const {
package/es/index.d.ts CHANGED
@@ -43,5 +43,6 @@ export * from './hooks/mobile';
43
43
  export * from './hooks/table';
44
44
  export * from './hooks/scroll';
45
45
  export * from './hooks/keyboard';
46
+ export * from './libs/validator';
46
47
  export { translations, createTranslator } from './locales';
47
48
  export { createLazyComponent, api, dayjs, FormInput, PhoneInput, AddressForm, StripeForm, Status, Livemode, Switch, ConfirmDialog, CheckoutForm, CheckoutTable, CheckoutDonate, CurrencySelector, Payment, PaymentSummary, PricingTable, ProductSkeleton, Amount, CustomerInvoiceList, CustomerPaymentList, TxLink, TxGas, SafeGuard, PricingItem, CountrySelect, Table, TruncatedText, Link, OverdueInvoicePayment, PaymentBeneficiaries, LoadingButton, DonateDetails, };
package/es/index.js CHANGED
@@ -43,6 +43,7 @@ export * from "./hooks/mobile.js";
43
43
  export * from "./hooks/table.js";
44
44
  export * from "./hooks/scroll.js";
45
45
  export * from "./hooks/keyboard.js";
46
+ export * from "./libs/validator.js";
46
47
  export { translations, createTranslator } from "./locales/index.js";
47
48
  export {
48
49
  createLazyComponent,
package/es/libs/api.js CHANGED
@@ -10,6 +10,10 @@ api.interceptors.request.use(
10
10
  const locale = getLocale(window.blocklet?.languages);
11
11
  const query = new URLSearchParams(config.url?.split("?").pop());
12
12
  config.params = { ...config.params || {}, locale };
13
+ const authToken = window.__PAYMENT_KIT_AUTH_TOKEN;
14
+ if (authToken && typeof config.params.authToken === "undefined" && !query.has("authToken")) {
15
+ config.params.authToken = authToken;
16
+ }
13
17
  if (typeof config.params.livemode === "undefined" && query.has("livemode") === false) {
14
18
  const livemode = localStorage.getItem("livemode");
15
19
  config.params.livemode = isNull(livemode) ? true : JSON.parse(livemode);
@@ -0,0 +1,3 @@
1
+ export declare const getUserStorageKey: (base: string, did?: string) => string;
2
+ export declare const saveCurrencyPreference: (currencyId: string, did?: string) => void;
3
+ export declare const getCurrencyPreference: (did?: string, availableCurrencyIds?: string[]) => string | null;
@@ -0,0 +1,22 @@
1
+ const CURRENCY_PREFERENCE_KEY_BASE = "payment-currency-preference";
2
+ export const getUserStorageKey = (base, did) => {
3
+ return did ? `${base}:${did}` : base;
4
+ };
5
+ export const saveCurrencyPreference = (currencyId, did) => {
6
+ try {
7
+ localStorage.setItem(getUserStorageKey(CURRENCY_PREFERENCE_KEY_BASE, did), currencyId);
8
+ } catch (e) {
9
+ console.warn("Failed to save currency preference", e);
10
+ }
11
+ };
12
+ export const getCurrencyPreference = (did, availableCurrencyIds) => {
13
+ try {
14
+ const saved = localStorage.getItem(getUserStorageKey(CURRENCY_PREFERENCE_KEY_BASE, did));
15
+ if (saved && (!availableCurrencyIds || availableCurrencyIds.includes(saved))) {
16
+ return saved;
17
+ }
18
+ } catch (e) {
19
+ console.warn("Failed to access localStorage", e);
20
+ }
21
+ return null;
22
+ };
@@ -14,6 +14,8 @@ export const getPhoneUtil = async () => {
14
14
  return phoneUtil;
15
15
  };
16
16
  export const validatePhoneNumber = async (phoneNumber) => {
17
+ if (!phoneNumber)
18
+ return true;
17
19
  try {
18
20
  let util = null;
19
21
  try {
@@ -0,0 +1 @@
1
+ export declare function validatePostalCode(postalCode: string, country?: string): boolean;
@@ -0,0 +1,70 @@
1
+ import isPostalCode from "validator/lib/isPostalCode";
2
+ const POSTAL_CODE_SUPPORTED_COUNTRIES = [
3
+ "AD",
4
+ "AT",
5
+ "AU",
6
+ "BE",
7
+ "BG",
8
+ "BR",
9
+ "CA",
10
+ "CH",
11
+ "CN",
12
+ "CZ",
13
+ "DE",
14
+ "DK",
15
+ "DZ",
16
+ "EE",
17
+ "ES",
18
+ "FI",
19
+ "FR",
20
+ "GB",
21
+ "GR",
22
+ "HR",
23
+ "HU",
24
+ "ID",
25
+ "IE",
26
+ "IL",
27
+ "IN",
28
+ "IR",
29
+ "IS",
30
+ "IT",
31
+ "JP",
32
+ "KE",
33
+ "KR",
34
+ "LI",
35
+ "LT",
36
+ "LU",
37
+ "LV",
38
+ "MX",
39
+ "MT",
40
+ "NL",
41
+ "NO",
42
+ "NZ",
43
+ "PL",
44
+ "PR",
45
+ "PT",
46
+ "RO",
47
+ "RU",
48
+ "SA",
49
+ "SE",
50
+ "SI",
51
+ "SK",
52
+ "TN",
53
+ "TW",
54
+ "UA",
55
+ "US",
56
+ "ZA",
57
+ "ZM"
58
+ ];
59
+ export function validatePostalCode(postalCode, country) {
60
+ if (!postalCode)
61
+ return true;
62
+ const countryUpper = country?.toUpperCase();
63
+ const isSupported = country && POSTAL_CODE_SUPPORTED_COUNTRIES.includes(countryUpper);
64
+ try {
65
+ return isPostalCode(postalCode, isSupported ? countryUpper : "any");
66
+ } catch (error) {
67
+ console.error(error);
68
+ return false;
69
+ }
70
+ }
@@ -1,15 +1,17 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
3
3
  import { Fade, FormLabel, InputAdornment, Stack } from "@mui/material";
4
- import { Controller, useFormContext } from "react-hook-form";
4
+ import { Controller, useFormContext, useWatch } from "react-hook-form";
5
5
  import FormInput from "../../components/input.js";
6
6
  import CountrySelect from "../../components/country-select.js";
7
+ import { validatePostalCode } from "../../libs/validator.js";
7
8
  AddressForm.defaultProps = {
8
9
  sx: {}
9
10
  };
10
11
  export default function AddressForm({ mode, stripe, sx = {} }) {
11
12
  const { t } = useLocaleContext();
12
13
  const { control } = useFormContext();
14
+ const country = useWatch({ control, name: "billing_address.country" });
13
15
  if (mode === "required") {
14
16
  return /* @__PURE__ */ jsx(Fade, { in: true, children: /* @__PURE__ */ jsx(Stack, { className: "cko-payment-address cko-payment-form", sx, children: /* @__PURE__ */ jsxs(Stack, { direction: "column", className: "cko-payment-form", spacing: 0, children: [
15
17
  /* @__PURE__ */ jsx(FormLabel, { className: "base-label", children: t("payment.checkout.billing.line1") }),
@@ -50,7 +52,13 @@ export default function AddressForm({ mode, stripe, sx = {} }) {
50
52
  FormInput,
51
53
  {
52
54
  name: "billing_address.postal_code",
53
- rules: { required: t("payment.checkout.required") },
55
+ rules: {
56
+ required: t("payment.checkout.required"),
57
+ validate: (x) => {
58
+ const isValid = validatePostalCode(x, country);
59
+ return isValid ? true : t("payment.checkout.invalid");
60
+ }
61
+ },
54
62
  errorPosition: "right",
55
63
  variant: "outlined",
56
64
  placeholder: t("payment.checkout.billing.postal_code"),
@@ -85,7 +93,13 @@ export default function AddressForm({ mode, stripe, sx = {} }) {
85
93
  FormInput,
86
94
  {
87
95
  name: "billing_address.postal_code",
88
- rules: { required: t("payment.checkout.required") },
96
+ rules: {
97
+ required: t("payment.checkout.required"),
98
+ validate: (x) => {
99
+ const isValid = validatePostalCode(x, country);
100
+ return isValid ? true : t("payment.checkout.invalid");
101
+ }
102
+ },
89
103
  errorPosition: "right",
90
104
  variant: "outlined",
91
105
  placeholder: t("payment.checkout.billing.postal_code"),
@@ -31,6 +31,7 @@ import { useMobile } from "../../hooks/mobile.js";
31
31
  import { formatPhone, validatePhoneNumber } from "../../libs/phone-validator.js";
32
32
  import LoadingButton from "../../components/loading-button.js";
33
33
  import OverdueInvoicePayment from "../../components/over-due-invoice-payment.js";
34
+ import { saveCurrencyPreference } from "../../libs/currency.js";
34
35
  export const waitForCheckoutComplete = async (sessionId) => {
35
36
  let result;
36
37
  await pWaitFor(
@@ -135,6 +136,13 @@ export default function PaymentForm({
135
136
  const index = currencies.findIndex((x) => x.id === queryCurrencyId);
136
137
  return index >= 0 ? index : 0;
137
138
  });
139
+ const handleCurrencyChange = (index) => {
140
+ setPaymentCurrencyIndex(index);
141
+ const selectedCurrencyId = currencies[index]?.id;
142
+ if (selectedCurrencyId) {
143
+ saveCurrencyPreference(selectedCurrencyId, session?.user?.did);
144
+ }
145
+ };
138
146
  const onCheckoutComplete = useMemoizedFn(async ({ response }) => {
139
147
  if (response.id === checkoutSession.id && state.paid === false) {
140
148
  await handleConnected();
@@ -248,6 +256,7 @@ export default function PaymentForm({
248
256
  const showForm = !!session?.user;
249
257
  const skipBindWallet = method.type === "stripe";
250
258
  const handleConnected = async () => {
259
+ setState({ paying: true });
251
260
  try {
252
261
  const result = await waitForCheckoutComplete(checkoutSession.id);
253
262
  if (state.paid === false) {
@@ -485,7 +494,7 @@ export default function PaymentForm({
485
494
  {
486
495
  value: paymentCurrencyIndex,
487
496
  currencies,
488
- onChange: setPaymentCurrencyIndex
497
+ onChange: handleCurrencyChange
489
498
  }
490
499
  )
491
500
  }