@blocklet/payment-react 1.14.28 → 1.14.30

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.
@@ -12,7 +12,7 @@ export default function Livemode({ color, backgroundColor, sx }) {
12
12
  sx: {
13
13
  ml: 2,
14
14
  height: 18,
15
- lineHeight: 1,
15
+ lineHeight: 1.2,
16
16
  textTransform: "uppercase",
17
17
  fontSize: "0.8rem",
18
18
  fontWeight: "bold",
@@ -83,7 +83,7 @@ const Wrapped = styled(Datatable)`
83
83
  }
84
84
 
85
85
  tr.MuiTableRow-root:not(.MuiTableRow-footer):hover {
86
- background: #f5f5f5;
86
+ background: var(--backgrounds-bg-highlight, #eff6ff);
87
87
  }
88
88
  tr.MuiTableRow-root:last-of-type td:first-of-type {
89
89
  border-bottom-left-radius: 8px;
@@ -94,28 +94,30 @@ const InvoiceTable = React.memo((props) => {
94
94
  );
95
95
  const columns = [
96
96
  {
97
- label: t("payment.customer.invoice.invoiceNumber"),
98
- name: "number",
97
+ label: t("common.amount"),
98
+ name: "total",
99
+ width: 60,
100
+ align: "right",
99
101
  options: {
100
102
  customBodyRenderLite: (_, index) => {
101
103
  const invoice = data?.list[index];
102
104
  const link = getInvoiceLink(invoice, action);
103
- return /* @__PURE__ */ jsx("a", { href: link.url, target: link.external ? "_blank" : target, rel: "noreferrer", children: invoice?.number });
105
+ return /* @__PURE__ */ jsx("a", { href: link.url, target: link.external ? "_blank" : target, rel: "noreferrer", children: /* @__PURE__ */ jsxs(Typography, { children: [
106
+ formatBNStr(invoice.total, invoice.paymentCurrency.decimal),
107
+ "\xA0",
108
+ invoice.paymentCurrency.symbol
109
+ ] }) });
104
110
  }
105
111
  }
106
112
  },
107
113
  {
108
- label: t("common.amount"),
109
- name: "total",
114
+ label: t("payment.customer.invoice.invoiceNumber"),
115
+ name: "number",
110
116
  options: {
111
117
  customBodyRenderLite: (_, index) => {
112
118
  const invoice = data?.list[index];
113
119
  const link = getInvoiceLink(invoice, action);
114
- return /* @__PURE__ */ jsx("a", { href: link.url, target: link.external ? "_blank" : target, rel: "noreferrer", children: /* @__PURE__ */ jsxs(Typography, { children: [
115
- formatBNStr(invoice.total, invoice.paymentCurrency.decimal),
116
- "\xA0",
117
- invoice.paymentCurrency.symbol
118
- ] }) });
120
+ return /* @__PURE__ */ jsx("a", { href: link.url, target: link.external ? "_blank" : target, rel: "noreferrer", children: invoice?.number });
119
121
  }
120
122
  }
121
123
  },
package/es/libs/util.d.ts CHANGED
@@ -105,3 +105,4 @@ export declare function formatQuantityInventory(price: TPrice, quantity: string
105
105
  export declare function formatSubscriptionStatus(status: string): string;
106
106
  export declare function formatAmountPrecisionLimit(amount: string, locale?: string, precision?: number): string;
107
107
  export declare function getWordBreakStyle(value: any): 'break-word' | 'break-all';
108
+ export declare function isMobileSafari(): boolean;
package/es/libs/util.js CHANGED
@@ -805,3 +805,10 @@ export function getWordBreakStyle(value) {
805
805
  }
806
806
  return "break-all";
807
807
  }
808
+ export function isMobileSafari() {
809
+ const ua = navigator.userAgent.toLowerCase();
810
+ const isSafari = ua.indexOf("safari") > -1 && ua.indexOf("chrome") === -1;
811
+ const isMobile = ua.indexOf("mobile") > -1 || /iphone|ipad|ipod/.test(ua);
812
+ const isIOS = /iphone|ipad|ipod/.test(ua);
813
+ return isSafari && isMobile && isIOS;
814
+ }
package/es/locales/en.js CHANGED
@@ -87,7 +87,8 @@ export default flat({
87
87
  recoverFrom: "Recovered From",
88
88
  quantityLimitPerCheckout: "Exceed purchase limit",
89
89
  quantityNotEnough: "Exceed inventory",
90
- amountPrecisionLimit: "Amount decimal places must be less than or equal to {precision}"
90
+ amountPrecisionLimit: "Amount decimal places must be less than or equal to {precision}",
91
+ saveAsDefaultPriceSuccess: "Set default price successfully"
91
92
  },
92
93
  payment: {
93
94
  checkout: {
@@ -226,7 +227,7 @@ export default flat({
226
227
  recover: {
227
228
  button: "Renew",
228
229
  title: "Renew your subscription",
229
- description: "Your subscription will no longer be canceled, it will renew on {date}"
230
+ description: "Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue"
230
231
  },
231
232
  changePlan: {
232
233
  button: "Update",
package/es/locales/zh.js CHANGED
@@ -87,7 +87,8 @@ export default flat({
87
87
  recoverFrom: "\u6062\u590D\u81EA",
88
88
  quantityLimitPerCheckout: "\u8D85\u51FA\u8D2D\u4E70\u9650\u5236",
89
89
  quantityNotEnough: "\u5E93\u5B58\u4E0D\u8DB3",
90
- amountPrecisionLimit: "\u91D1\u989D\u5C0F\u6570\u4F4D\u6570\u5FC5\u987B\u5728 {precision} \u4F4D\u4EE5\u5185"
90
+ amountPrecisionLimit: "\u91D1\u989D\u5C0F\u6570\u4F4D\u6570\u5FC5\u987B\u5728 {precision} \u4F4D\u4EE5\u5185",
91
+ saveAsDefaultPriceSuccess: "\u8BBE\u7F6E\u9ED8\u8BA4\u4EF7\u683C\u6210\u529F"
91
92
  },
92
93
  payment: {
93
94
  checkout: {
@@ -226,7 +227,7 @@ export default flat({
226
227
  recover: {
227
228
  button: "\u7EED\u8BA2",
228
229
  title: "\u7EED\u8BA2\u60A8\u7684\u8BA2\u9605",
229
- description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u518D\u88AB\u53D6\u6D88\uFF0C\u5C06\u5728{date}\u7EED\u8BA2"
230
+ description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u4F1A\u88AB\u53D6\u6D88\uFF0C\u5E76\u5C06\u5728{date}\u81EA\u52A8\u7EED\u8BA2\uFF0C\u8BF7\u786E\u8BA4\u662F\u5426\u7EE7\u7EED"
230
231
  },
231
232
  changePlan: {
232
233
  button: "\u66F4\u65B0",
@@ -3,15 +3,16 @@ import "react-international-phone/style.css";
3
3
  import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
4
4
  import Toast from "@arcblock/ux/lib/Toast";
5
5
  import { LoadingButton } from "@mui/lab";
6
- import { Divider, Fade, FormLabel, Stack, Typography } from "@mui/material";
6
+ import { Box, Divider, Fade, FormLabel, Stack, Typography } from "@mui/material";
7
7
  import { useMemoizedFn, useSetState } from "ahooks";
8
8
  import { PhoneNumberUtil } from "google-libphonenumber";
9
9
  import pWaitFor from "p-wait-for";
10
- import { useEffect, useMemo, useState } from "react";
10
+ import { useEffect, useMemo, useRef, useState } from "react";
11
11
  import { Controller, useFormContext, useWatch } from "react-hook-form";
12
12
  import { joinURL } from "ufo";
13
13
  import { dispatch } from "use-bus";
14
14
  import isEmail from "validator/es/lib/isEmail";
15
+ import { isEmpty } from "lodash";
15
16
  import ConfirmDialog from "../../components/confirm.js";
16
17
  import FormInput from "../../components/input.js";
17
18
  import { usePaymentContext } from "../../contexts/payment.js";
@@ -29,6 +30,7 @@ import AddressForm from "./address.js";
29
30
  import CurrencySelector from "./currency.js";
30
31
  import PhoneInput from "./phone.js";
31
32
  import StripeCheckout from "./stripe.js";
33
+ import { useMobile } from "../../hooks/mobile.js";
32
34
  const phoneUtil = PhoneNumberUtil.getInstance();
33
35
  const waitForCheckoutComplete = async (sessionId) => {
34
36
  let result;
@@ -62,9 +64,17 @@ export default function PaymentForm({
62
64
  action
63
65
  }) {
64
66
  const { t } = useLocaleContext();
67
+ const { isMobile } = useMobile();
65
68
  const { session, connect, payable } = usePaymentContext();
66
69
  const subscription = useSubscription("events");
67
- const { control, getValues, setValue, handleSubmit } = useFormContext();
70
+ const {
71
+ control,
72
+ getValues,
73
+ setValue,
74
+ handleSubmit,
75
+ formState: { errors }
76
+ } = useFormContext();
77
+ const errorRef = useRef(null);
68
78
  const quantityInventoryStatus = useMemo(() => {
69
79
  let status = true;
70
80
  for (const item of checkoutSession.line_items) {
@@ -163,6 +173,11 @@ export default function PaymentForm({
163
173
  setState({ paying: false });
164
174
  }
165
175
  };
176
+ useEffect(() => {
177
+ if (errorRef.current && !isEmpty(errors) && isMobile) {
178
+ errorRef.current.scrollIntoView({ behavior: "smooth" });
179
+ }
180
+ }, [errors, isMobile]);
166
181
  const onUserLoggedIn = async () => {
167
182
  const { data: profile } = await api.get("/api/customers/me?fallback=1");
168
183
  if (profile) {
@@ -267,6 +282,9 @@ export default function PaymentForm({
267
282
  setState({ submitting: false });
268
283
  };
269
284
  const onAction = () => {
285
+ if (errorRef.current && !isEmpty(errors) && isMobile) {
286
+ errorRef.current.scrollIntoView({ behavior: "smooth" });
287
+ }
270
288
  if (session?.user) {
271
289
  if (hasDidWallet(session.user)) {
272
290
  handleSubmit(onFormSubmit, onFormError)();
@@ -343,68 +361,79 @@ export default function PaymentForm({
343
361
  )
344
362
  ] }) }),
345
363
  /* @__PURE__ */ jsx(Stack, { direction: "row", sx: { mb: 1 }, alignItems: "center", justifyContent: "space-between" }),
346
- /* @__PURE__ */ jsxs(Stack, { direction: "column", className: "cko-payment-form", spacing: 0, sx: { flex: 1, overflow: "auto" }, children: [
347
- /* @__PURE__ */ jsx(FormLabel, { className: "base-label", children: t("payment.checkout.customer.name") }),
348
- /* @__PURE__ */ jsx(
349
- FormInput,
350
- {
351
- name: "customer_name",
352
- variant: "outlined",
353
- errorPosition: "right",
354
- rules: {
355
- required: t("payment.checkout.required")
356
- }
357
- }
358
- ),
359
- /* @__PURE__ */ jsx(FormLabel, { className: "base-label", children: t("payment.checkout.customer.email") }),
360
- /* @__PURE__ */ jsx(
361
- FormInput,
362
- {
363
- name: "customer_email",
364
- variant: "outlined",
365
- errorPosition: "right",
366
- rules: {
367
- required: t("payment.checkout.required"),
368
- validate: (x) => isEmail(x) ? true : t("payment.checkout.invalid")
369
- }
370
- }
371
- ),
372
- checkoutSession.phone_number_collection?.enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
373
- /* @__PURE__ */ jsx(FormLabel, { className: "base-label", children: t("payment.checkout.customer.phone") }),
374
- /* @__PURE__ */ jsx(
375
- PhoneInput,
376
- {
377
- name: "customer_phone",
378
- variant: "outlined",
379
- errorPosition: "right",
380
- placeholder: "Phone number",
381
- rules: {
382
- required: t("payment.checkout.required"),
383
- validate: (x) => {
384
- try {
385
- const parsed = phoneUtil.parseAndKeepRawInput(x);
386
- return phoneUtil.isValidNumber(parsed) ? true : t("payment.checkout.invalid");
387
- } catch {
388
- return t("payment.checkout.invalid");
364
+ /* @__PURE__ */ jsxs(
365
+ Stack,
366
+ {
367
+ direction: "column",
368
+ className: "cko-payment-form",
369
+ id: "cko-payment-form",
370
+ spacing: 0,
371
+ ref: !isEmpty(errors) ? errorRef : void 0,
372
+ sx: { flex: 1, overflow: "auto" },
373
+ children: [
374
+ /* @__PURE__ */ jsx(FormLabel, { className: "base-label", children: t("payment.checkout.customer.name") }),
375
+ /* @__PURE__ */ jsx(
376
+ FormInput,
377
+ {
378
+ name: "customer_name",
379
+ variant: "outlined",
380
+ errorPosition: "right",
381
+ rules: {
382
+ required: t("payment.checkout.required")
383
+ }
384
+ }
385
+ ),
386
+ /* @__PURE__ */ jsx(FormLabel, { className: "base-label", children: t("payment.checkout.customer.email") }),
387
+ /* @__PURE__ */ jsx(
388
+ FormInput,
389
+ {
390
+ name: "customer_email",
391
+ variant: "outlined",
392
+ errorPosition: "right",
393
+ rules: {
394
+ required: t("payment.checkout.required"),
395
+ validate: (x) => isEmail(x) ? true : t("payment.checkout.invalid")
396
+ }
397
+ }
398
+ ),
399
+ checkoutSession.phone_number_collection?.enabled && /* @__PURE__ */ jsxs(Fragment, { children: [
400
+ /* @__PURE__ */ jsx(FormLabel, { className: "base-label", children: t("payment.checkout.customer.phone") }),
401
+ /* @__PURE__ */ jsx(
402
+ PhoneInput,
403
+ {
404
+ name: "customer_phone",
405
+ variant: "outlined",
406
+ errorPosition: "right",
407
+ placeholder: "Phone number",
408
+ rules: {
409
+ required: t("payment.checkout.required"),
410
+ validate: (x) => {
411
+ try {
412
+ const parsed = phoneUtil.parseAndKeepRawInput(x);
413
+ return phoneUtil.isValidNumber(parsed) ? true : t("payment.checkout.invalid");
414
+ } catch {
415
+ return t("payment.checkout.invalid");
416
+ }
417
+ }
389
418
  }
390
419
  }
420
+ )
421
+ ] }),
422
+ /* @__PURE__ */ jsx(
423
+ AddressForm,
424
+ {
425
+ mode: checkoutSession.billing_address_collection,
426
+ stripe: method?.type === "stripe",
427
+ sx: { marginTop: "0 !important" }
391
428
  }
392
- }
393
- )
394
- ] }),
395
- /* @__PURE__ */ jsx(
396
- AddressForm,
397
- {
398
- mode: checkoutSession.billing_address_collection,
399
- stripe: method?.type === "stripe",
400
- sx: { marginTop: "0 !important" }
401
- }
402
- )
403
- ] })
429
+ )
430
+ ]
431
+ }
432
+ )
404
433
  ] }) }),
405
434
  /* @__PURE__ */ jsx(Divider, { sx: { mt: 2.5, mb: 2.5 } }),
406
435
  /* @__PURE__ */ jsx(Fade, { in: true, children: /* @__PURE__ */ jsxs(Stack, { className: "cko-payment-submit", children: [
407
- /* @__PURE__ */ jsx(
436
+ /* @__PURE__ */ jsx(Box, { className: "cko-payment-submit-btn", children: /* @__PURE__ */ jsx(
408
437
  LoadingButton,
409
438
  {
410
439
  variant: "contained",
@@ -417,7 +446,7 @@ export default function PaymentForm({
417
446
  loading: state.submitting || state.paying,
418
447
  children: state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
419
448
  }
420
- ),
449
+ ) }),
421
450
  ["subscription", "setup"].includes(checkoutSession.mode) && /* @__PURE__ */ jsx(Typography, { sx: { mt: 2.5, color: "text.lighter", fontSize: "0.9rem", lineHeight: "1.1rem" }, children: t("payment.checkout.confirm", { payee }) })
422
451
  ] }) }),
423
452
  state.customerLimited && /* @__PURE__ */ jsx(
@@ -11,7 +11,7 @@ import { useEffect, useState } from "react";
11
11
  import { FormProvider, useForm, useWatch } from "react-hook-form";
12
12
  import { usePaymentContext } from "../contexts/payment.js";
13
13
  import api from "../libs/api.js";
14
- import { findCurrency, formatError, getStatementDescriptor, isValidCountry } from "../libs/util.js";
14
+ import { findCurrency, formatError, getStatementDescriptor, isMobileSafari, isValidCountry } from "../libs/util.js";
15
15
  import PaymentError from "./error.js";
16
16
  import CheckoutFooter from "./footer.js";
17
17
  import PaymentForm from "./form/index.js";
@@ -66,6 +66,25 @@ function PaymentInner({
66
66
  )
67
67
  }
68
68
  });
69
+ useEffect(() => {
70
+ if (!isMobileSafari()) {
71
+ return () => {
72
+ };
73
+ }
74
+ let scrollTop = 0;
75
+ const focusinHandler = () => {
76
+ scrollTop = window.scrollY;
77
+ };
78
+ const focusoutHandler = () => {
79
+ window.scrollTo(0, scrollTop);
80
+ };
81
+ document.body.addEventListener("focusin", focusinHandler);
82
+ document.body.addEventListener("focusout", focusoutHandler);
83
+ return () => {
84
+ document.body.removeEventListener("focusin", focusinHandler);
85
+ document.body.removeEventListener("focusout", focusoutHandler);
86
+ };
87
+ }, []);
69
88
  const currencyId = useWatch({ control: methods.control, name: "payment_currency", defaultValue: defaultCurrencyId });
70
89
  const currency = findCurrency(paymentMethods, currencyId) || settings.baseCurrency;
71
90
  const method = paymentMethods.find((x) => x.id === currency.payment_method_id);
@@ -221,7 +240,9 @@ export default function Payment({
221
240
  const { t } = useLocaleContext();
222
241
  const { refresh, livemode, setLivemode } = usePaymentContext();
223
242
  const [delay, setDelay] = useState(false);
243
+ const { isMobile } = useMobile();
224
244
  const hideSummaryCard = mode.endsWith("-minimal") || !showCheckoutSummary;
245
+ const isMobileSafariEnv = isMobileSafari();
225
246
  useEffect(() => {
226
247
  setTimeout(() => {
227
248
  setDelay(true);
@@ -291,7 +312,10 @@ export default function Payment({
291
312
  {
292
313
  display: "flex",
293
314
  flexDirection: "column",
294
- sx: { height: mode === "standalone" ? "100vh" : "auto", overflow: "hidden" },
315
+ sx: {
316
+ height: mode === "standalone" ? "100vh" : "auto",
317
+ overflow: isMobileSafariEnv ? "visible" : "hidden"
318
+ },
295
319
  children: [
296
320
  mode === "standalone" ? /* @__PURE__ */ jsx(
297
321
  Header,
@@ -306,17 +330,58 @@ export default function Payment({
306
330
  sx: { borderBottom: "1px solid var(--stroke-border-base, #EFF1F5)" }
307
331
  }
308
332
  ) : null,
309
- /* @__PURE__ */ jsxs(Root, { mode, sx: { flex: 1 }, children: [
310
- goBack && /* @__PURE__ */ jsx(
311
- ArrowBackOutlined,
312
- {
313
- sx: { mr: 0.5, color: "text.secondary", alignSelf: "flex-start", margin: "16px 0", cursor: "pointer" },
314
- onClick: goBack,
315
- fontSize: "medium"
316
- }
317
- ),
318
- /* @__PURE__ */ jsx(Stack, { className: "cko-container", sx: { gap: { sm: mode === "standalone" ? 0 : mode === "inline" ? 4 : 8 } }, children: renderContent() })
319
- ] })
333
+ /* @__PURE__ */ jsxs(
334
+ Root,
335
+ {
336
+ mode,
337
+ sx: {
338
+ flex: 1,
339
+ overflow: {
340
+ xs: isMobileSafariEnv ? "visible" : "auto",
341
+ md: "hidden"
342
+ },
343
+ ...isMobile && mode === "standalone" ? {
344
+ ".cko-payment-submit-btn": {
345
+ position: "fixed",
346
+ bottom: 20,
347
+ left: 0,
348
+ right: 0,
349
+ zIndex: 999,
350
+ background: "#fff",
351
+ padding: "10px",
352
+ textAlign: "center",
353
+ button: {
354
+ color: "#fff",
355
+ maxWidth: 542
356
+ }
357
+ },
358
+ ".cko-footer": {
359
+ position: "fixed",
360
+ bottom: 0,
361
+ left: 0,
362
+ right: 0,
363
+ zIndex: 999,
364
+ background: "#fff",
365
+ marginBottom: 0
366
+ },
367
+ ".cko-payment": {
368
+ paddingBottom: "100px"
369
+ }
370
+ } : {}
371
+ },
372
+ children: [
373
+ goBack && /* @__PURE__ */ jsx(
374
+ ArrowBackOutlined,
375
+ {
376
+ sx: { mr: 0.5, color: "text.secondary", alignSelf: "flex-start", margin: "16px 0", cursor: "pointer" },
377
+ onClick: goBack,
378
+ fontSize: "medium"
379
+ }
380
+ ),
381
+ /* @__PURE__ */ jsx(Stack, { className: "cko-container", sx: { gap: { sm: mode === "standalone" ? 0 : mode === "inline" ? 4 : 8 } }, children: renderContent() })
382
+ ]
383
+ }
384
+ )
320
385
  ]
321
386
  }
322
387
  );
@@ -461,7 +526,6 @@ export const Root = styled(Box)`
461
526
  }
462
527
 
463
528
  @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
464
- background: ${(props) => props.mode === "standalone" ? "var(--backgrounds-bg-subtle, #F9FAFB)" : "transparent"};
465
529
  padding-top: 0;
466
530
  overflow: auto;
467
531
  &:before {
@@ -470,7 +534,8 @@ export const Root = styled(Box)`
470
534
  .cko-container {
471
535
  flex-direction: column;
472
536
  align-items: center;
473
- gap: 32px;
537
+ justify-content: flex-start;
538
+ gap: 0;
474
539
  overflow: visible;
475
540
  min-width: 200px;
476
541
  }
@@ -483,6 +548,7 @@ export const Root = styled(Box)`
483
548
  width: 100%;
484
549
  height: fit-content;
485
550
  flex: none;
551
+ border-top: 1px solid var(--stroke-border-base, #eff1f5);
486
552
  &:before {
487
553
  display: none;
488
554
  }
@@ -24,7 +24,7 @@ export default function ProductCard({ size, variant, name, logo, description, ex
24
24
  className: "cko-ellipsis",
25
25
  variant: "body1",
26
26
  title: name,
27
- sx: { fontWeight: 500, mb: 0.5, lineHeight: 1, fontSize: 16 },
27
+ sx: { fontWeight: 500, mb: 0.5, lineHeight: 1.2, fontSize: 16 },
28
28
  color: "text.primary",
29
29
  children: name
30
30
  }
@@ -34,7 +34,7 @@ export default function ProductCard({ size, variant, name, logo, description, ex
34
34
  {
35
35
  variant: "body1",
36
36
  title: description,
37
- sx: { fontSize: "0.85rem", mb: 0.5, lineHeight: 1, textAlign: "left" },
37
+ sx: { fontSize: "0.85rem", mb: 0.5, lineHeight: 1.2, textAlign: "left" },
38
38
  color: "text.lighter",
39
39
  children: description
40
40
  }
@@ -22,7 +22,7 @@ function Livemode({
22
22
  sx: {
23
23
  ml: 2,
24
24
  height: 18,
25
- lineHeight: 1,
25
+ lineHeight: 1.2,
26
26
  textTransform: "uppercase",
27
27
  fontSize: "0.8rem",
28
28
  fontWeight: "bold",
@@ -103,7 +103,7 @@ const Wrapped = (0, _system.styled)(_Datatable.default)`
103
103
  }
104
104
 
105
105
  tr.MuiTableRow-root:not(.MuiTableRow-footer):hover {
106
- background: #f5f5f5;
106
+ background: var(--backgrounds-bg-highlight, #eff6ff);
107
107
  }
108
108
  tr.MuiTableRow-root:last-of-type td:first-of-type {
109
109
  border-bottom-left-radius: 8px;
@@ -97,8 +97,10 @@ const InvoiceTable = _react.default.memo(props => {
97
97
  refreshDeps: [search]
98
98
  });
99
99
  const columns = [{
100
- label: t("payment.customer.invoice.invoiceNumber"),
101
- name: "number",
100
+ label: t("common.amount"),
101
+ name: "total",
102
+ width: 60,
103
+ align: "right",
102
104
  options: {
103
105
  customBodyRenderLite: (_, index) => {
104
106
  const invoice = data?.list[index];
@@ -107,13 +109,15 @@ const InvoiceTable = _react.default.memo(props => {
107
109
  href: link.url,
108
110
  target: link.external ? "_blank" : target,
109
111
  rel: "noreferrer",
110
- children: invoice?.number
112
+ children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
113
+ children: [(0, _util.formatBNStr)(invoice.total, invoice.paymentCurrency.decimal), "\xA0", invoice.paymentCurrency.symbol]
114
+ })
111
115
  });
112
116
  }
113
117
  }
114
118
  }, {
115
- label: t("common.amount"),
116
- name: "total",
119
+ label: t("payment.customer.invoice.invoiceNumber"),
120
+ name: "number",
117
121
  options: {
118
122
  customBodyRenderLite: (_, index) => {
119
123
  const invoice = data?.list[index];
@@ -122,9 +126,7 @@ const InvoiceTable = _react.default.memo(props => {
122
126
  href: link.url,
123
127
  target: link.external ? "_blank" : target,
124
128
  rel: "noreferrer",
125
- children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
126
- children: [(0, _util.formatBNStr)(invoice.total, invoice.paymentCurrency.decimal), "\xA0", invoice.paymentCurrency.symbol]
127
- })
129
+ children: invoice?.number
128
130
  });
129
131
  }
130
132
  }
@@ -105,3 +105,4 @@ export declare function formatQuantityInventory(price: TPrice, quantity: string
105
105
  export declare function formatSubscriptionStatus(status: string): string;
106
106
  export declare function formatAmountPrecisionLimit(amount: string, locale?: string, precision?: number): string;
107
107
  export declare function getWordBreakStyle(value: any): 'break-word' | 'break-all';
108
+ export declare function isMobileSafari(): boolean;
package/lib/libs/util.js CHANGED
@@ -44,6 +44,7 @@ exports.getSubscriptionStatusColor = getSubscriptionStatusColor;
44
44
  exports.getTxLink = exports.getSubscriptionTimeSummary = void 0;
45
45
  exports.getWebhookStatusColor = getWebhookStatusColor;
46
46
  exports.getWordBreakStyle = getWordBreakStyle;
47
+ exports.isMobileSafari = isMobileSafari;
47
48
  exports.isPaymentKitMounted = void 0;
48
49
  exports.isValidCountry = isValidCountry;
49
50
  exports.lazyLoad = lazyLoad;
@@ -974,4 +975,11 @@ function getWordBreakStyle(value) {
974
975
  return "break-word";
975
976
  }
976
977
  return "break-all";
978
+ }
979
+ function isMobileSafari() {
980
+ const ua = navigator.userAgent.toLowerCase();
981
+ const isSafari = ua.indexOf("safari") > -1 && ua.indexOf("chrome") === -1;
982
+ const isMobile = ua.indexOf("mobile") > -1 || /iphone|ipad|ipod/.test(ua);
983
+ const isIOS = /iphone|ipad|ipod/.test(ua);
984
+ return isSafari && isMobile && isIOS;
977
985
  }
package/lib/locales/en.js CHANGED
@@ -94,7 +94,8 @@ module.exports = (0, _flat.default)({
94
94
  recoverFrom: "Recovered From",
95
95
  quantityLimitPerCheckout: "Exceed purchase limit",
96
96
  quantityNotEnough: "Exceed inventory",
97
- amountPrecisionLimit: "Amount decimal places must be less than or equal to {precision}"
97
+ amountPrecisionLimit: "Amount decimal places must be less than or equal to {precision}",
98
+ saveAsDefaultPriceSuccess: "Set default price successfully"
98
99
  },
99
100
  payment: {
100
101
  checkout: {
@@ -233,7 +234,7 @@ module.exports = (0, _flat.default)({
233
234
  recover: {
234
235
  button: "Renew",
235
236
  title: "Renew your subscription",
236
- description: "Your subscription will no longer be canceled, it will renew on {date}"
237
+ description: "Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue"
237
238
  },
238
239
  changePlan: {
239
240
  button: "Update",
package/lib/locales/zh.js CHANGED
@@ -94,7 +94,8 @@ module.exports = (0, _flat.default)({
94
94
  recoverFrom: "\u6062\u590D\u81EA",
95
95
  quantityLimitPerCheckout: "\u8D85\u51FA\u8D2D\u4E70\u9650\u5236",
96
96
  quantityNotEnough: "\u5E93\u5B58\u4E0D\u8DB3",
97
- amountPrecisionLimit: "\u91D1\u989D\u5C0F\u6570\u4F4D\u6570\u5FC5\u987B\u5728 {precision} \u4F4D\u4EE5\u5185"
97
+ amountPrecisionLimit: "\u91D1\u989D\u5C0F\u6570\u4F4D\u6570\u5FC5\u987B\u5728 {precision} \u4F4D\u4EE5\u5185",
98
+ saveAsDefaultPriceSuccess: "\u8BBE\u7F6E\u9ED8\u8BA4\u4EF7\u683C\u6210\u529F"
98
99
  },
99
100
  payment: {
100
101
  checkout: {
@@ -233,7 +234,7 @@ module.exports = (0, _flat.default)({
233
234
  recover: {
234
235
  button: "\u7EED\u8BA2",
235
236
  title: "\u7EED\u8BA2\u60A8\u7684\u8BA2\u9605",
236
- description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u518D\u88AB\u53D6\u6D88\uFF0C\u5C06\u5728{date}\u7EED\u8BA2"
237
+ description: "\u60A8\u7684\u8BA2\u9605\u5C06\u4E0D\u4F1A\u88AB\u53D6\u6D88\uFF0C\u5E76\u5C06\u5728{date}\u81EA\u52A8\u7EED\u8BA2\uFF0C\u8BF7\u786E\u8BA4\u662F\u5426\u7EE7\u7EED"
237
238
  },
238
239
  changePlan: {
239
240
  button: "\u66F4\u65B0",
@@ -18,6 +18,7 @@ var _reactHookForm = require("react-hook-form");
18
18
  var _ufo = require("ufo");
19
19
  var _useBus = require("use-bus");
20
20
  var _isEmail = _interopRequireDefault(require("validator/es/lib/isEmail"));
21
+ var _lodash = require("lodash");
21
22
  var _confirm = _interopRequireDefault(require("../../components/confirm"));
22
23
  var _input = _interopRequireDefault(require("../../components/input"));
23
24
  var _payment = require("../../contexts/payment");
@@ -28,6 +29,7 @@ var _address = _interopRequireDefault(require("./address"));
28
29
  var _currency = _interopRequireDefault(require("./currency"));
29
30
  var _phone = _interopRequireDefault(require("./phone"));
30
31
  var _stripe = _interopRequireDefault(require("./stripe"));
32
+ var _mobile = require("../../hooks/mobile");
31
33
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
32
34
  const phoneUtil = _googleLibphonenumber.PhoneNumberUtil.getInstance();
33
35
  const waitForCheckoutComplete = async sessionId => {
@@ -66,6 +68,9 @@ function PaymentForm({
66
68
  const {
67
69
  t
68
70
  } = (0, _context.useLocaleContext)();
71
+ const {
72
+ isMobile
73
+ } = (0, _mobile.useMobile)();
69
74
  const {
70
75
  session,
71
76
  connect,
@@ -76,8 +81,12 @@ function PaymentForm({
76
81
  control,
77
82
  getValues,
78
83
  setValue,
79
- handleSubmit
84
+ handleSubmit,
85
+ formState: {
86
+ errors
87
+ }
80
88
  } = (0, _reactHookForm.useFormContext)();
89
+ const errorRef = (0, _react.useRef)(null);
81
90
  const quantityInventoryStatus = (0, _react.useMemo)(() => {
82
91
  let status = true;
83
92
  for (const item of checkoutSession.line_items) {
@@ -190,6 +199,13 @@ function PaymentForm({
190
199
  });
191
200
  }
192
201
  };
202
+ (0, _react.useEffect)(() => {
203
+ if (errorRef.current && !(0, _lodash.isEmpty)(errors) && isMobile) {
204
+ errorRef.current.scrollIntoView({
205
+ behavior: "smooth"
206
+ });
207
+ }
208
+ }, [errors, isMobile]);
193
209
  const onUserLoggedIn = async () => {
194
210
  const {
195
211
  data: profile
@@ -318,6 +334,11 @@ function PaymentForm({
318
334
  });
319
335
  };
320
336
  const onAction = () => {
337
+ if (errorRef.current && !(0, _lodash.isEmpty)(errors) && isMobile) {
338
+ errorRef.current.scrollIntoView({
339
+ behavior: "smooth"
340
+ });
341
+ }
321
342
  if (session?.user) {
322
343
  if (hasDidWallet(session.user)) {
323
344
  handleSubmit(onFormSubmit, onFormError)();
@@ -412,7 +433,9 @@ function PaymentForm({
412
433
  }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
413
434
  direction: "column",
414
435
  className: "cko-payment-form",
436
+ id: "cko-payment-form",
415
437
  spacing: 0,
438
+ ref: !(0, _lodash.isEmpty)(errors) ? errorRef : void 0,
416
439
  sx: {
417
440
  flex: 1,
418
441
  overflow: "auto"
@@ -477,16 +500,19 @@ function PaymentForm({
477
500
  in: true,
478
501
  children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
479
502
  className: "cko-payment-submit",
480
- children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_lab.LoadingButton, {
481
- variant: "contained",
482
- color: "primary",
483
- size: "large",
484
- className: "cko-submit-button",
485
- onClick: onAction,
486
- fullWidth: true,
487
- disabled: state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus || !payable,
488
- loading: state.submitting || state.paying,
489
- children: state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
503
+ children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
504
+ className: "cko-payment-submit-btn",
505
+ children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_lab.LoadingButton, {
506
+ variant: "contained",
507
+ color: "primary",
508
+ size: "large",
509
+ className: "cko-submit-button",
510
+ onClick: onAction,
511
+ fullWidth: true,
512
+ disabled: state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus || !payable,
513
+ loading: state.submitting || state.paying,
514
+ children: state.submitting || state.paying ? t("payment.checkout.processing") : buttonText
515
+ })
490
516
  }), ["subscription", "setup"].includes(checkoutSession.mode) && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
491
517
  sx: {
492
518
  mt: 2.5,
@@ -81,6 +81,24 @@ function PaymentInner({
81
81
  })
82
82
  }
83
83
  });
84
+ (0, _react.useEffect)(() => {
85
+ if (!(0, _util2.isMobileSafari)()) {
86
+ return () => {};
87
+ }
88
+ let scrollTop = 0;
89
+ const focusinHandler = () => {
90
+ scrollTop = window.scrollY;
91
+ };
92
+ const focusoutHandler = () => {
93
+ window.scrollTo(0, scrollTop);
94
+ };
95
+ document.body.addEventListener("focusin", focusinHandler);
96
+ document.body.addEventListener("focusout", focusoutHandler);
97
+ return () => {
98
+ document.body.removeEventListener("focusin", focusinHandler);
99
+ document.body.removeEventListener("focusout", focusoutHandler);
100
+ };
101
+ }, []);
84
102
  const currencyId = (0, _reactHookForm.useWatch)({
85
103
  control: methods.control,
86
104
  name: "payment_currency",
@@ -280,7 +298,11 @@ function Payment({
280
298
  setLivemode
281
299
  } = (0, _payment.usePaymentContext)();
282
300
  const [delay, setDelay] = (0, _react.useState)(false);
301
+ const {
302
+ isMobile
303
+ } = (0, _mobile.useMobile)();
283
304
  const hideSummaryCard = mode.endsWith("-minimal") || !showCheckoutSummary;
305
+ const isMobileSafariEnv = (0, _util2.isMobileSafari)();
284
306
  (0, _react.useEffect)(() => {
285
307
  setTimeout(() => {
286
308
  setDelay(true);
@@ -359,7 +381,7 @@ function Payment({
359
381
  flexDirection: "column",
360
382
  sx: {
361
383
  height: mode === "standalone" ? "100vh" : "auto",
362
- overflow: "hidden"
384
+ overflow: isMobileSafariEnv ? "visible" : "hidden"
363
385
  },
364
386
  children: [mode === "standalone" ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_Header.default, {
365
387
  meta: void 0,
@@ -375,7 +397,39 @@ function Payment({
375
397
  }) : null, /* @__PURE__ */(0, _jsxRuntime.jsxs)(Root, {
376
398
  mode,
377
399
  sx: {
378
- flex: 1
400
+ flex: 1,
401
+ overflow: {
402
+ xs: isMobileSafariEnv ? "visible" : "auto",
403
+ md: "hidden"
404
+ },
405
+ ...(isMobile && mode === "standalone" ? {
406
+ ".cko-payment-submit-btn": {
407
+ position: "fixed",
408
+ bottom: 20,
409
+ left: 0,
410
+ right: 0,
411
+ zIndex: 999,
412
+ background: "#fff",
413
+ padding: "10px",
414
+ textAlign: "center",
415
+ button: {
416
+ color: "#fff",
417
+ maxWidth: 542
418
+ }
419
+ },
420
+ ".cko-footer": {
421
+ position: "fixed",
422
+ bottom: 0,
423
+ left: 0,
424
+ right: 0,
425
+ zIndex: 999,
426
+ background: "#fff",
427
+ marginBottom: 0
428
+ },
429
+ ".cko-payment": {
430
+ paddingBottom: "100px"
431
+ }
432
+ } : {})
379
433
  },
380
434
  children: [goBack && /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.ArrowBackOutlined, {
381
435
  sx: {
@@ -541,7 +595,6 @@ const Root = exports.Root = (0, _system.styled)(_material.Box)`
541
595
  @media (max-width: ${({
542
596
  theme
543
597
  }) => theme.breakpoints.values.md}px) {
544
- background: ${props => props.mode === "standalone" ? "var(--backgrounds-bg-subtle, #F9FAFB)" : "transparent"};
545
598
  padding-top: 0;
546
599
  overflow: auto;
547
600
  &:before {
@@ -550,7 +603,8 @@ const Root = exports.Root = (0, _system.styled)(_material.Box)`
550
603
  .cko-container {
551
604
  flex-direction: column;
552
605
  align-items: center;
553
- gap: 32px;
606
+ justify-content: flex-start;
607
+ gap: 0;
554
608
  overflow: visible;
555
609
  min-width: 200px;
556
610
  }
@@ -563,6 +617,7 @@ const Root = exports.Root = (0, _system.styled)(_material.Box)`
563
617
  width: 100%;
564
618
  height: fit-content;
565
619
  flex: none;
620
+ border-top: 1px solid var(--stroke-border-base, #eff1f5);
566
621
  &:before {
567
622
  display: none;
568
623
  }
@@ -56,7 +56,7 @@ function ProductCard({
56
56
  sx: {
57
57
  fontWeight: 500,
58
58
  mb: 0.5,
59
- lineHeight: 1,
59
+ lineHeight: 1.2,
60
60
  fontSize: 16
61
61
  },
62
62
  color: "text.primary",
@@ -67,7 +67,7 @@ function ProductCard({
67
67
  sx: {
68
68
  fontSize: "0.85rem",
69
69
  mb: 0.5,
70
- lineHeight: 1,
70
+ lineHeight: 1.2,
71
71
  textAlign: "left"
72
72
  },
73
73
  color: "text.lighter",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.14.28",
3
+ "version": "1.14.30",
4
4
  "description": "Reusable react components for payment kit v2",
5
5
  "keywords": [
6
6
  "react",
@@ -93,7 +93,7 @@
93
93
  "@babel/core": "^7.25.2",
94
94
  "@babel/preset-env": "^7.25.2",
95
95
  "@babel/preset-react": "^7.24.7",
96
- "@blocklet/payment-types": "1.14.28",
96
+ "@blocklet/payment-types": "1.14.30",
97
97
  "@storybook/addon-essentials": "^7.6.20",
98
98
  "@storybook/addon-interactions": "^7.6.20",
99
99
  "@storybook/addon-links": "^7.6.20",
@@ -123,5 +123,5 @@
123
123
  "vite-plugin-babel": "^1.2.0",
124
124
  "vite-plugin-node-polyfills": "^0.21.0"
125
125
  },
126
- "gitHead": "79178aa9251fb54006f6e0fe32a14619d1e8f06e"
126
+ "gitHead": "f98dee7bce684f81f4e060b942efaa9ad8730b55"
127
127
  }
@@ -16,7 +16,7 @@ export default function Livemode({ color, backgroundColor, sx }: Props) {
16
16
  sx={{
17
17
  ml: 2,
18
18
  height: 18,
19
- lineHeight: 1,
19
+ lineHeight: 1.2,
20
20
  textTransform: 'uppercase',
21
21
  fontSize: '0.8rem',
22
22
  fontWeight: 'bold',
@@ -93,7 +93,7 @@ const Wrapped = styled(Datatable)`
93
93
  }
94
94
 
95
95
  tr.MuiTableRow-root:not(.MuiTableRow-footer):hover {
96
- background: #f5f5f5;
96
+ background: var(--backgrounds-bg-highlight, #eff6ff);
97
97
  }
98
98
  tr.MuiTableRow-root:last-of-type td:first-of-type {
99
99
  border-bottom-left-radius: 8px;
@@ -124,39 +124,40 @@ const InvoiceTable = React.memo((props: Props & { onPay: (invoiceId: string) =>
124
124
 
125
125
  const columns = [
126
126
  {
127
- label: t('payment.customer.invoice.invoiceNumber'),
128
- name: 'number',
127
+ label: t('common.amount'),
128
+ name: 'total',
129
+ width: 60,
130
+ align: 'right',
129
131
  options: {
130
132
  customBodyRenderLite: (_: string, index: number) => {
131
133
  const invoice = data?.list[index] as TInvoiceExpanded;
132
134
  const link = getInvoiceLink(invoice, action);
133
135
  return (
134
136
  <a href={link.url} target={link.external ? '_blank' : target} rel="noreferrer">
135
- {invoice?.number}
137
+ <Typography>
138
+ {formatBNStr(invoice.total, invoice.paymentCurrency.decimal)}&nbsp;
139
+ {invoice.paymentCurrency.symbol}
140
+ </Typography>
136
141
  </a>
137
142
  );
138
143
  },
139
144
  },
140
145
  },
141
146
  {
142
- label: t('common.amount'),
143
- name: 'total',
147
+ label: t('payment.customer.invoice.invoiceNumber'),
148
+ name: 'number',
144
149
  options: {
145
150
  customBodyRenderLite: (_: string, index: number) => {
146
151
  const invoice = data?.list[index] as TInvoiceExpanded;
147
152
  const link = getInvoiceLink(invoice, action);
148
153
  return (
149
154
  <a href={link.url} target={link.external ? '_blank' : target} rel="noreferrer">
150
- <Typography>
151
- {formatBNStr(invoice.total, invoice.paymentCurrency.decimal)}&nbsp;
152
- {invoice.paymentCurrency.symbol}
153
- </Typography>
155
+ {invoice?.number}
154
156
  </a>
155
157
  );
156
158
  },
157
159
  },
158
160
  },
159
-
160
161
  {
161
162
  label: t('common.updatedAt'),
162
163
  name: 'name',
package/src/libs/util.ts CHANGED
@@ -1042,3 +1042,11 @@ export function getWordBreakStyle(value: any): 'break-word' | 'break-all' {
1042
1042
  }
1043
1043
  return 'break-all';
1044
1044
  }
1045
+
1046
+ export function isMobileSafari() {
1047
+ const ua = navigator.userAgent.toLowerCase();
1048
+ const isSafari = ua.indexOf('safari') > -1 && ua.indexOf('chrome') === -1;
1049
+ const isMobile = ua.indexOf('mobile') > -1 || /iphone|ipad|ipod/.test(ua);
1050
+ const isIOS = /iphone|ipad|ipod/.test(ua);
1051
+ return isSafari && isMobile && isIOS;
1052
+ }
@@ -90,6 +90,7 @@ export default flat({
90
90
  quantityLimitPerCheckout: 'Exceed purchase limit',
91
91
  quantityNotEnough: 'Exceed inventory',
92
92
  amountPrecisionLimit: 'Amount decimal places must be less than or equal to {precision}',
93
+ saveAsDefaultPriceSuccess: 'Set default price successfully',
93
94
  },
94
95
  payment: {
95
96
  checkout: {
@@ -234,7 +235,8 @@ export default flat({
234
235
  recover: {
235
236
  button: 'Renew',
236
237
  title: 'Renew your subscription',
237
- description: 'Your subscription will no longer be canceled, it will renew on {date}',
238
+ description:
239
+ 'Your subscription will not be canceled and will be automatically renewed on {date}, please confirm to continue',
238
240
  },
239
241
  changePlan: {
240
242
  button: 'Update',
@@ -90,6 +90,7 @@ export default flat({
90
90
  quantityLimitPerCheckout: '超出购买限制',
91
91
  quantityNotEnough: '库存不足',
92
92
  amountPrecisionLimit: '金额小数位数必须在 {precision} 位以内',
93
+ saveAsDefaultPriceSuccess: '设置默认价格成功',
93
94
  },
94
95
  payment: {
95
96
  checkout: {
@@ -228,7 +229,7 @@ export default flat({
228
229
  recover: {
229
230
  button: '续订',
230
231
  title: '续订您的订阅',
231
- description: '您的订阅将不再被取消,将在{date}续订',
232
+ description: '您的订阅将不会被取消,并将在{date}自动续订,请确认是否继续',
232
233
  },
233
234
  changePlan: {
234
235
  button: '更新',
@@ -11,16 +11,17 @@ import type {
11
11
  TPaymentMethodExpanded,
12
12
  } from '@blocklet/payment-types';
13
13
  import { LoadingButton } from '@mui/lab';
14
- import { Divider, Fade, FormLabel, Stack, Typography } from '@mui/material';
14
+ import { Box, Divider, Fade, FormLabel, Stack, Typography } from '@mui/material';
15
15
  import { useMemoizedFn, useSetState } from 'ahooks';
16
16
  import { PhoneNumberUtil } from 'google-libphonenumber';
17
17
  import pWaitFor from 'p-wait-for';
18
- import { useEffect, useMemo, useState } from 'react';
18
+ import { useEffect, useMemo, useRef, useState } from 'react';
19
19
  import { Controller, useFormContext, useWatch } from 'react-hook-form';
20
20
  import { joinURL } from 'ufo';
21
21
  import { dispatch } from 'use-bus';
22
22
  import isEmail from 'validator/es/lib/isEmail';
23
23
 
24
+ import { isEmpty } from 'lodash';
24
25
  import ConfirmDialog from '../../components/confirm';
25
26
  import FormInput from '../../components/input';
26
27
  import { usePaymentContext } from '../../contexts/payment';
@@ -39,6 +40,7 @@ import AddressForm from './address';
39
40
  import CurrencySelector from './currency';
40
41
  import PhoneInput from './phone';
41
42
  import StripeCheckout from './stripe';
43
+ import { useMobile } from '../../hooks/mobile';
42
44
 
43
45
  const phoneUtil = PhoneNumberUtil.getInstance();
44
46
 
@@ -97,9 +99,17 @@ export default function PaymentForm({
97
99
  }: PageData) {
98
100
  // const theme = useTheme();
99
101
  const { t } = useLocaleContext();
102
+ const { isMobile } = useMobile();
100
103
  const { session, connect, payable } = usePaymentContext();
101
104
  const subscription = useSubscription('events');
102
- const { control, getValues, setValue, handleSubmit } = useFormContext();
105
+ const {
106
+ control,
107
+ getValues,
108
+ setValue,
109
+ handleSubmit,
110
+ formState: { errors },
111
+ } = useFormContext();
112
+ const errorRef = useRef<HTMLDivElement | null>(null);
103
113
  const quantityInventoryStatus = useMemo(() => {
104
114
  let status = true;
105
115
  for (const item of checkoutSession.line_items) {
@@ -236,6 +246,12 @@ export default function PaymentForm({
236
246
  }
237
247
  };
238
248
 
249
+ useEffect(() => {
250
+ if (errorRef.current && !isEmpty(errors) && isMobile) {
251
+ errorRef.current.scrollIntoView({ behavior: 'smooth' });
252
+ }
253
+ }, [errors, isMobile]);
254
+
239
255
  const onUserLoggedIn = async () => {
240
256
  const { data: profile } = await api.get('/api/customers/me?fallback=1');
241
257
  if (profile) {
@@ -345,6 +361,9 @@ export default function PaymentForm({
345
361
  };
346
362
 
347
363
  const onAction = () => {
364
+ if (errorRef.current && !isEmpty(errors) && isMobile) {
365
+ errorRef.current.scrollIntoView({ behavior: 'smooth' });
366
+ }
348
367
  if (session?.user) {
349
368
  if (hasDidWallet(session.user)) {
350
369
  handleSubmit(onFormSubmit, onFormError)();
@@ -428,7 +447,13 @@ export default function PaymentForm({
428
447
  {/* <Typography sx={{ color: 'text.secondary', fontWeight: 600 }}>{t('payment.checkout.contact')}</Typography> */}
429
448
  {/* {isColumnLayout || mode !== 'standalone' ? null : <UserButtons />} */}
430
449
  </Stack>
431
- <Stack direction="column" className="cko-payment-form" spacing={0} sx={{ flex: 1, overflow: 'auto' }}>
450
+ <Stack
451
+ direction="column"
452
+ className="cko-payment-form"
453
+ id="cko-payment-form"
454
+ spacing={0}
455
+ ref={!isEmpty(errors) ? (errorRef as any) : undefined}
456
+ sx={{ flex: 1, overflow: 'auto' }}>
432
457
  <FormLabel className="base-label">{t('payment.checkout.customer.name')}</FormLabel>
433
458
  <FormInput
434
459
  name="customer_name"
@@ -481,17 +506,20 @@ export default function PaymentForm({
481
506
  <Divider sx={{ mt: 2.5, mb: 2.5 }} />
482
507
  <Fade in>
483
508
  <Stack className="cko-payment-submit">
484
- <LoadingButton
485
- variant="contained"
486
- color="primary"
487
- size="large"
488
- className="cko-submit-button"
489
- onClick={onAction}
490
- fullWidth
491
- disabled={state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus || !payable}
492
- loading={state.submitting || state.paying}>
493
- {state.submitting || state.paying ? t('payment.checkout.processing') : buttonText}
494
- </LoadingButton>
509
+ <Box className="cko-payment-submit-btn">
510
+ <LoadingButton
511
+ variant="contained"
512
+ color="primary"
513
+ size="large"
514
+ className="cko-submit-button"
515
+ onClick={onAction}
516
+ fullWidth
517
+ disabled={state.submitting || state.paying || state.stripePaying || !quantityInventoryStatus || !payable}
518
+ loading={state.submitting || state.paying}>
519
+ {state.submitting || state.paying ? t('payment.checkout.processing') : buttonText}
520
+ </LoadingButton>
521
+ </Box>
522
+
495
523
  {['subscription', 'setup'].includes(checkoutSession.mode) && (
496
524
  <Typography sx={{ mt: 2.5, color: 'text.lighter', fontSize: '0.9rem', lineHeight: '1.1rem' }}>
497
525
  {t('payment.checkout.confirm', { payee })}
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/indent */
1
2
  /* eslint-disable no-nested-ternary */
2
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
4
  import Toast from '@arcblock/ux/lib/Toast';
@@ -21,7 +22,7 @@ import type { LiteralUnion } from 'type-fest';
21
22
 
22
23
  import { usePaymentContext } from '../contexts/payment';
23
24
  import api from '../libs/api';
24
- import { findCurrency, formatError, getStatementDescriptor, isValidCountry } from '../libs/util';
25
+ import { findCurrency, formatError, getStatementDescriptor, isMobileSafari, isValidCountry } from '../libs/util';
25
26
  import { CheckoutCallbacks, CheckoutContext, CheckoutFormData } from '../types';
26
27
  import PaymentError from './error';
27
28
  import CheckoutFooter from './footer';
@@ -86,6 +87,25 @@ function PaymentInner({
86
87
  },
87
88
  });
88
89
 
90
+ useEffect(() => {
91
+ if (!isMobileSafari()) {
92
+ return () => {};
93
+ }
94
+ let scrollTop = 0;
95
+ const focusinHandler = () => {
96
+ scrollTop = window.scrollY;
97
+ };
98
+ const focusoutHandler = () => {
99
+ window.scrollTo(0, scrollTop);
100
+ };
101
+ document.body.addEventListener('focusin', focusinHandler);
102
+ document.body.addEventListener('focusout', focusoutHandler);
103
+ return () => {
104
+ document.body.removeEventListener('focusin', focusinHandler);
105
+ document.body.removeEventListener('focusout', focusoutHandler);
106
+ };
107
+ }, []);
108
+
89
109
  const currencyId = useWatch({ control: methods.control, name: 'payment_currency', defaultValue: defaultCurrencyId });
90
110
  const currency =
91
111
  (findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) ||
@@ -260,7 +280,9 @@ export default function Payment({
260
280
  const { t } = useLocaleContext();
261
281
  const { refresh, livemode, setLivemode } = usePaymentContext();
262
282
  const [delay, setDelay] = useState(false);
283
+ const { isMobile } = useMobile();
263
284
  const hideSummaryCard = mode.endsWith('-minimal') || !showCheckoutSummary;
285
+ const isMobileSafariEnv = isMobileSafari();
264
286
 
265
287
  useEffect(() => {
266
288
  setTimeout(() => {
@@ -342,11 +364,15 @@ export default function Payment({
342
364
  />
343
365
  );
344
366
  };
367
+
345
368
  return (
346
369
  <Stack
347
370
  display="flex"
348
371
  flexDirection="column"
349
- sx={{ height: mode === 'standalone' ? '100vh' : 'auto', overflow: 'hidden' }}>
372
+ sx={{
373
+ height: mode === 'standalone' ? '100vh' : 'auto',
374
+ overflow: isMobileSafariEnv ? 'visible' : 'hidden',
375
+ }}>
350
376
  {mode === 'standalone' ? (
351
377
  <Header
352
378
  meta={undefined}
@@ -359,7 +385,45 @@ export default function Payment({
359
385
  sx={{ borderBottom: '1px solid var(--stroke-border-base, #EFF1F5)' }}
360
386
  />
361
387
  ) : null}
362
- <Root mode={mode} sx={{ flex: 1 }}>
388
+ <Root
389
+ mode={mode}
390
+ sx={{
391
+ flex: 1,
392
+ overflow: {
393
+ xs: isMobileSafariEnv ? 'visible' : 'auto',
394
+ md: 'hidden',
395
+ },
396
+ ...(isMobile && mode === 'standalone'
397
+ ? {
398
+ '.cko-payment-submit-btn': {
399
+ position: 'fixed',
400
+ bottom: 20,
401
+ left: 0,
402
+ right: 0,
403
+ zIndex: 999,
404
+ background: '#fff',
405
+ padding: '10px',
406
+ textAlign: 'center',
407
+ button: {
408
+ color: '#fff',
409
+ maxWidth: 542,
410
+ },
411
+ },
412
+ '.cko-footer': {
413
+ position: 'fixed',
414
+ bottom: 0,
415
+ left: 0,
416
+ right: 0,
417
+ zIndex: 999,
418
+ background: '#fff',
419
+ marginBottom: 0,
420
+ },
421
+ '.cko-payment': {
422
+ paddingBottom: '100px',
423
+ },
424
+ }
425
+ : {}),
426
+ }}>
363
427
  {goBack && (
364
428
  <ArrowBackOutlined
365
429
  sx={{ mr: 0.5, color: 'text.secondary', alignSelf: 'flex-start', margin: '16px 0', cursor: 'pointer' }}
@@ -517,7 +581,6 @@ export const Root = styled(Box)<{ mode: LiteralUnion<'standalone' | 'inline' | '
517
581
  }
518
582
 
519
583
  @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
520
- background: ${(props) => (props.mode === 'standalone' ? 'var(--backgrounds-bg-subtle, #F9FAFB)' : 'transparent')};
521
584
  padding-top: 0;
522
585
  overflow: auto;
523
586
  &:before {
@@ -526,7 +589,8 @@ export const Root = styled(Box)<{ mode: LiteralUnion<'standalone' | 'inline' | '
526
589
  .cko-container {
527
590
  flex-direction: column;
528
591
  align-items: center;
529
- gap: 32px;
592
+ justify-content: flex-start;
593
+ gap: 0;
530
594
  overflow: visible;
531
595
  min-width: 200px;
532
596
  }
@@ -539,6 +603,7 @@ export const Root = styled(Box)<{ mode: LiteralUnion<'standalone' | 'inline' | '
539
603
  width: 100%;
540
604
  height: fit-content;
541
605
  flex: none;
606
+ border-top: 1px solid var(--stroke-border-base, #eff1f5);
542
607
  &:before {
543
608
  display: none;
544
609
  }
@@ -33,7 +33,7 @@ export default function ProductCard({ size, variant, name, logo, description, ex
33
33
  className="cko-ellipsis"
34
34
  variant="body1"
35
35
  title={name}
36
- sx={{ fontWeight: 500, mb: 0.5, lineHeight: 1, fontSize: 16 }}
36
+ sx={{ fontWeight: 500, mb: 0.5, lineHeight: 1.2, fontSize: 16 }}
37
37
  color="text.primary">
38
38
  {name}
39
39
  </Typography>
@@ -41,7 +41,7 @@ export default function ProductCard({ size, variant, name, logo, description, ex
41
41
  <Typography
42
42
  variant="body1"
43
43
  title={description}
44
- sx={{ fontSize: '0.85rem', mb: 0.5, lineHeight: 1, textAlign: 'left' }}
44
+ sx={{ fontSize: '0.85rem', mb: 0.5, lineHeight: 1.2, textAlign: 'left' }}
45
45
  color="text.lighter">
46
46
  {description}
47
47
  </Typography>