@blocklet/payment-react 1.14.29 → 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.
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
+ }
@@ -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
  }
@@ -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
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blocklet/payment-react",
3
- "version": "1.14.29",
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.29",
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": "104f4189d9ef96c1e332ffa90824ebd3063c989d"
126
+ "gitHead": "f98dee7bce684f81f4e060b942efaa9ad8730b55"
127
127
  }
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
+ }
@@ -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
  }