@blocklet/payment-react 1.18.25 → 1.18.27

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
@@ -4,12 +4,14 @@ import omit from "lodash/omit";
4
4
  import { useEffect, useRef, useCallback } from "react";
5
5
  import { useFormContext, useWatch } from "react-hook-form";
6
6
  import { defaultCountries, usePhoneInput } from "react-international-phone";
7
+ import { useMount } from "ahooks";
7
8
  import FormInput from "../../components/input.js";
8
9
  import { isValidCountry } from "../../libs/util.js";
9
10
  import CountrySelect from "../../components/country-select.js";
11
+ import { getPhoneUtil } from "../../libs/phone-validator.js";
10
12
  export default function PhoneInput({ ...props }) {
11
13
  const countryFieldName = props.countryFieldName || "billing_address.country";
12
- const { control, getValues, setValue } = useFormContext();
14
+ const { control, getValues, setValue, trigger } = useFormContext();
13
15
  const values = getValues();
14
16
  const isUpdatingRef = useRef(false);
15
17
  const safeUpdate = useCallback((callback) => {
@@ -43,6 +45,11 @@ export default function PhoneInput({ ...props }) {
43
45
  setCountry(userCountry);
44
46
  });
45
47
  }, [userCountry, country, setCountry, safeUpdate]);
48
+ useMount(() => {
49
+ getPhoneUtil().catch((err) => {
50
+ console.error("Failed to preload phone validator:", err);
51
+ });
52
+ });
46
53
  const onCountryChange = useCallback(
47
54
  (v) => {
48
55
  safeUpdate(() => {
@@ -51,6 +58,9 @@ export default function PhoneInput({ ...props }) {
51
58
  },
52
59
  [setCountry, safeUpdate]
53
60
  );
61
+ const handleBlur = useCallback(() => {
62
+ trigger(props.name);
63
+ }, [props.name]);
54
64
  return (
55
65
  // @ts-ignore
56
66
  /* @__PURE__ */ jsx(
@@ -60,6 +70,7 @@ export default function PhoneInput({ ...props }) {
60
70
  onChange: handlePhoneValueChange,
61
71
  type: "tel",
62
72
  inputRef,
73
+ onBlur: handleBlur,
63
74
  InputProps: {
64
75
  startAdornment: /* @__PURE__ */ jsx(InputAdornment, { position: "start", style: { marginRight: "2px", marginLeft: "-8px" }, children: /* @__PURE__ */ jsx(
65
76
  CountrySelect,
@@ -32,7 +32,8 @@ function StripeCheckoutForm({
32
32
  confirming: false,
33
33
  loaded: false,
34
34
  showBillingForm: false,
35
- isTransitioning: false
35
+ isTransitioning: false,
36
+ paymentMethod: "card"
36
37
  });
37
38
  const handlePaymentMethodChange = (event) => {
38
39
  const method = event.value?.type;
@@ -43,12 +44,14 @@ function StripeCheckoutForm({
43
44
  setTimeout(() => {
44
45
  setState({
45
46
  isTransitioning: false,
47
+ paymentMethod: method,
46
48
  showBillingForm: true
47
49
  });
48
50
  }, 300);
49
51
  } else {
50
52
  setState({
51
53
  showBillingForm: false,
54
+ paymentMethod: method,
52
55
  isTransitioning: false
53
56
  });
54
57
  }
@@ -86,13 +89,14 @@ function StripeCheckoutForm({
86
89
  return;
87
90
  }
88
91
  try {
89
- setState({ confirming: true });
92
+ setState({ confirming: true, message: "" });
90
93
  const method = intentType === "payment_intent" ? "confirmPayment" : "confirmSetup";
91
94
  const { error: submitError } = await elements.submit();
92
95
  if (submitError) {
96
+ setState({ confirming: false });
93
97
  return;
94
98
  }
95
- const { error } = await stripe[method]({
99
+ const { error, paymentIntent, setupIntent } = await stripe[method]({
96
100
  elements,
97
101
  redirect: "if_required",
98
102
  confirmParams: {
@@ -117,6 +121,11 @@ function StripeCheckoutForm({
117
121
  } : {}
118
122
  }
119
123
  });
124
+ const intent = paymentIntent || setupIntent;
125
+ if (intent?.status === "canceled" || intent?.status === "requires_payment_method") {
126
+ setState({ confirming: false });
127
+ return;
128
+ }
120
129
  setState({ confirming: false });
121
130
  if (error) {
122
131
  if (error.type === "validation_error") {
@@ -131,11 +140,11 @@ function StripeCheckoutForm({
131
140
  setState({ confirming: false, message: err.message });
132
141
  }
133
142
  },
134
- [customer, intentType, stripe]
143
+ [customer, intentType, stripe, state.showBillingForm, returnUrl]
135
144
  // eslint-disable-line
136
145
  );
137
146
  return /* @__PURE__ */ jsxs(Content, { onSubmit: handleSubmit, children: [
138
- /* @__PURE__ */ jsx(
147
+ (!state.paymentMethod || state.paymentMethod === "card") && /* @__PURE__ */ jsx(
139
148
  LinkAuthenticationElement,
140
149
  {
141
150
  options: {
@@ -7,7 +7,7 @@ import { Box, Fade, Stack } from "@mui/material";
7
7
  import { styled } from "@mui/system";
8
8
  import { fromTokenToUnit } from "@ocap/util";
9
9
  import { useSetState } from "ahooks";
10
- import { useEffect, useState } from "react";
10
+ import { useEffect, useState, useMemo } from "react";
11
11
  import { FormProvider, useForm, useWatch } from "react-hook-form";
12
12
  import trim from "lodash/trim";
13
13
  import { usePaymentContext } from "../contexts/payment.js";
@@ -22,13 +22,14 @@ import {
22
22
  } from "../libs/util.js";
23
23
  import PaymentError from "./error.js";
24
24
  import CheckoutFooter from "./footer.js";
25
- import PaymentForm from "./form/index.js";
25
+ import PaymentForm, { hasDidWallet } from "./form/index.js";
26
26
  import OverviewSkeleton from "./skeleton/overview.js";
27
27
  import PaymentSkeleton from "./skeleton/payment.js";
28
28
  import PaymentSuccess from "./success.js";
29
29
  import PaymentSummary from "./summary.js";
30
30
  import { useMobile } from "../hooks/mobile.js";
31
31
  import { formatPhone } from "../libs/phone-validator.js";
32
+ import { getCurrencyPreference } from "../libs/currency.js";
32
33
  PaymentInner.defaultProps = {
33
34
  completed: false,
34
35
  showCheckoutSummary: true
@@ -52,7 +53,36 @@ function PaymentInner({
52
53
  const { isMobile } = useMobile();
53
54
  const [state, setState] = useSetState({ checkoutSession });
54
55
  const query = getQueryParams(window.location.href);
55
- const defaultCurrencyId = query.currencyId || state.checkoutSession.currency_id || state.checkoutSession.line_items[0]?.price.currency_id;
56
+ const availableCurrencyIds = useMemo(() => {
57
+ const currencyIds = /* @__PURE__ */ new Set();
58
+ paymentMethods.forEach((method2) => {
59
+ method2.payment_currencies.forEach((currency2) => {
60
+ if (currency2.active) {
61
+ currencyIds.add(currency2.id);
62
+ }
63
+ });
64
+ });
65
+ return Array.from(currencyIds);
66
+ }, [paymentMethods]);
67
+ const defaultCurrencyId = useMemo(() => {
68
+ if (query.currencyId && availableCurrencyIds.includes(query.currencyId)) {
69
+ return query.currencyId;
70
+ }
71
+ if (session?.user && !hasDidWallet(session.user)) {
72
+ const stripeCurrencyId = paymentMethods.find((m) => m.type === "stripe")?.payment_currencies.find((c) => c.active)?.id;
73
+ if (stripeCurrencyId) {
74
+ return stripeCurrencyId;
75
+ }
76
+ }
77
+ const savedPreference = getCurrencyPreference(session?.user?.did, availableCurrencyIds);
78
+ if (savedPreference) {
79
+ return savedPreference;
80
+ }
81
+ if (state.checkoutSession.currency_id && availableCurrencyIds.includes(state.checkoutSession.currency_id)) {
82
+ return state.checkoutSession.currency_id;
83
+ }
84
+ return availableCurrencyIds?.[0];
85
+ }, [query.currencyId, availableCurrencyIds, session?.user, state.checkoutSession.currency_id, paymentMethods]);
56
86
  const defaultMethodId = paymentMethods.find((m) => m.payment_currencies.some((c) => c.id === defaultCurrencyId))?.id;
57
87
  const hideSummaryCard = mode.endsWith("-minimal") || !showCheckoutSummary;
58
88
  const methods = useForm({
@@ -97,14 +127,6 @@ function PaymentInner({
97
127
  document.body.removeEventListener("focusout", focusoutHandler);
98
128
  };
99
129
  }, []);
100
- useEffect(() => {
101
- if (!methods || query.currencyId) {
102
- return;
103
- }
104
- if (state.checkoutSession.currency_id !== defaultCurrencyId) {
105
- methods.setValue("payment_currency", state.checkoutSession.currency_id);
106
- }
107
- }, [state.checkoutSession, defaultCurrencyId, query.currencyId]);
108
130
  const currencyId = useWatch({ control: methods.control, name: "payment_currency", defaultValue: defaultCurrencyId });
109
131
  const currency = findCurrency(paymentMethods, currencyId) || settings.baseCurrency;
110
132
  const method = paymentMethods.find((x) => x.id === currency.payment_method_id);
@@ -1,6 +1,7 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
3
- import { Avatar, Box, Card, CardActionArea, Grid, Stack, TextField, Typography } from "@mui/material";
3
+ import { Avatar, Box, Card, CardActionArea, Grid, Stack, TextField, Typography, IconButton } from "@mui/material";
4
+ import AutoAwesomeIcon from "@mui/icons-material/AutoAwesome";
4
5
  import { useSetState } from "ahooks";
5
6
  import { useEffect, useRef } from "react";
6
7
  import { formatAmountPrecisionLimit } from "../libs/util.js";
@@ -62,7 +63,8 @@ export default function ProductDonation({
62
63
  selected: defaultPreset === "custom" ? "" : defaultPreset,
63
64
  input: defaultCustomAmount,
64
65
  custom: !supportPreset || defaultPreset === "custom",
65
- error: ""
66
+ error: "",
67
+ animating: false
66
68
  });
67
69
  const customInputRef = useRef(null);
68
70
  const containerRef = useRef(null);
@@ -73,15 +75,73 @@ export default function ProductDonation({
73
75
  localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), formatAmount(amount));
74
76
  };
75
77
  const handleCustomSelect = () => {
76
- setState({ custom: true, selected: "", error: "" });
77
- const savedCustomAmount = getSavedCustomAmount();
78
- if (savedCustomAmount) {
79
- setState({ input: savedCustomAmount });
80
- onChange({ priceId: item.price_id, amount: savedCustomAmount });
81
- setPayable(true);
82
- } else if (!state.input) {
83
- setPayable(false);
78
+ setState({
79
+ custom: true,
80
+ selected: "",
81
+ animating: true
82
+ });
83
+ const hasPresets = presets.length > 0;
84
+ let sortedPresets = [];
85
+ if (hasPresets) {
86
+ sortedPresets = [...presets].map((p) => parseFloat(p)).sort((a, b) => a - b);
87
+ }
88
+ const minPreset = hasPresets ? sortedPresets[0] : 1;
89
+ const middleIndex = Math.floor(sortedPresets.length / 2);
90
+ const maxPreset = hasPresets ? sortedPresets[middleIndex] : 10;
91
+ const detectPrecision = () => {
92
+ let maxPrecision = 2;
93
+ if (!hasPresets)
94
+ return 0;
95
+ const allIntegers = presets.every((preset) => {
96
+ const num = parseFloat(preset);
97
+ return num === Math.floor(num);
98
+ });
99
+ if (allIntegers)
100
+ return 0;
101
+ presets.forEach((preset) => {
102
+ const decimalPart = preset.toString().split(".")[1];
103
+ if (decimalPart) {
104
+ maxPrecision = Math.max(maxPrecision, decimalPart.length);
105
+ }
106
+ });
107
+ return maxPrecision;
108
+ };
109
+ const precision = detectPrecision();
110
+ let randomAmount;
111
+ if (precision === 0) {
112
+ randomAmount = (Math.round(Math.random() * (maxPreset - minPreset) + minPreset) || 1).toString();
113
+ } else {
114
+ randomAmount = (Math.random() * (maxPreset - minPreset) + minPreset).toFixed(precision);
84
115
  }
116
+ const startValue = state.input ? parseFloat(state.input) : 0;
117
+ const targetValue = parseFloat(randomAmount);
118
+ const difference = targetValue - startValue;
119
+ const startTime = Date.now();
120
+ const duration = 800;
121
+ const updateCounter = () => {
122
+ const currentTime = Date.now();
123
+ const elapsed = currentTime - startTime;
124
+ if (elapsed < duration) {
125
+ const progress = elapsed / duration;
126
+ const intermediateValue = startValue + difference * progress;
127
+ const currentValue = precision === 0 ? Math.floor(intermediateValue).toString() : intermediateValue.toFixed(precision);
128
+ setState({ input: currentValue });
129
+ requestAnimationFrame(updateCounter);
130
+ } else {
131
+ setState({
132
+ input: randomAmount,
133
+ animating: false,
134
+ error: ""
135
+ });
136
+ onChange({ priceId: item.price_id, amount: formatAmount(randomAmount) });
137
+ setPayable(true);
138
+ localStorage.setItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE), formatAmount(randomAmount));
139
+ setTimeout(() => {
140
+ customInputRef.current?.focus();
141
+ }, 200);
142
+ }
143
+ };
144
+ requestAnimationFrame(updateCounter);
85
145
  localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), "custom");
86
146
  };
87
147
  const handleTabSelect = (selectedItem) => {
@@ -261,13 +321,51 @@ export default function ProductDonation({
261
321
  inputRef: customInputRef,
262
322
  InputProps: {
263
323
  endAdornment: /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 0.5, alignItems: "center", sx: { ml: 1 }, children: [
324
+ /* @__PURE__ */ jsx(
325
+ IconButton,
326
+ {
327
+ size: "small",
328
+ onClick: handleCustomSelect,
329
+ disabled: state.animating,
330
+ sx: {
331
+ mr: 0.5,
332
+ opacity: state.animating ? 0.5 : 1,
333
+ transition: "all 0.2s ease",
334
+ "&:hover": {
335
+ transform: "scale(1.2)",
336
+ transition: "transform 0.3s ease"
337
+ }
338
+ },
339
+ "aria-label": t("common.random"),
340
+ children: /* @__PURE__ */ jsx(AutoAwesomeIcon, { fontSize: "small" })
341
+ }
342
+ ),
264
343
  /* @__PURE__ */ jsx(Avatar, { src: currency?.logo, sx: { width: 16, height: 16 }, alt: currency?.symbol }),
265
344
  /* @__PURE__ */ jsx(Typography, { children: currency?.symbol })
266
345
  ] }),
267
- autoComplete: "off"
346
+ autoComplete: "off",
347
+ sx: {
348
+ "& input": {
349
+ transition: "all 0.25s ease"
350
+ }
351
+ }
268
352
  },
269
353
  sx: {
270
- mt: defaultPreset !== "0" ? 0 : 1
354
+ mt: defaultPreset !== "0" ? 0 : 1,
355
+ "& .MuiInputBase-root": {
356
+ transition: "all 0.3s ease"
357
+ },
358
+ "& input[type=number]": {
359
+ MozAppearance: "textfield"
360
+ },
361
+ "& input[type=number]::-webkit-outer-spin-button": {
362
+ WebkitAppearance: "none",
363
+ margin: 0
364
+ },
365
+ "& input[type=number]::-webkit-inner-spin-button": {
366
+ WebkitAppearance: "none",
367
+ margin: 0
368
+ }
271
369
  }
272
370
  }
273
371
  )
@@ -16,3 +16,5 @@ declare module 'pretty-ms-i18n';
16
16
  declare var blocklet: import('@blocklet/sdk').WindowBlocklet;
17
17
 
18
18
  declare var __PAYMENT_KIT_BASE_URL: string;
19
+
20
+ declare var __PAYMENT_KIT_AUTH_TOKEN: string;
@@ -63,6 +63,7 @@ function DonateDetails({
63
63
  locale
64
64
  } = (0, _context.useLocaleContext)();
65
65
  return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
66
+ className: "cko-donate-details",
66
67
  sx: {
67
68
  width: "100%",
68
69
  minWidth: "256px",
@@ -227,8 +228,17 @@ function SupporterAvatar({
227
228
  onClose: () => setOpen(false),
228
229
  sx: {
229
230
  ".MuiDialogContent-root": {
230
- width: "450px",
231
+ width: {
232
+ xs: "100%",
233
+ md: "450px"
234
+ },
231
235
  padding: "8px"
236
+ },
237
+ ".cko-donate-details": {
238
+ maxHeight: {
239
+ xs: "100%",
240
+ md: "300px"
241
+ }
232
242
  }
233
243
  },
234
244
  title: `${customersNum} supporter${customersNum > 1 ? "s" : ""}`,