@coin-voyage/paykit 2.4.4-beta.0 → 2.4.5-beta.0

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.
@@ -3,7 +3,7 @@ import { getFiatPaymentData } from "@coin-voyage/shared/payment";
3
3
  import { PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
4
4
  import { loadStripeOnramp } from "@stripe/crypto/pure";
5
5
  import { useQuery } from "@tanstack/react-query";
6
- import { useCallback, useEffect, useMemo, useRef } from "react";
6
+ import { memo, useCallback, useEffect, useMemo, useRef } from "react";
7
7
  import { AlertIcon } from "../../../assets/icons";
8
8
  import useLocales from "../../../hooks/useLocales";
9
9
  import styled from "../../../styles/styled";
@@ -44,14 +44,17 @@ function CardPaymentContent({ paymentData, isLoading, error, onRetry, }) {
44
44
  }
45
45
  function ExpiredCardPayment() {
46
46
  const locales = useLocales();
47
- const { paymentState, setRoute } = usePayContext();
47
+ const { paymentState, setOpen } = usePayContext();
48
48
  const isDeposit = paymentState.payOrder?.mode === PayOrderMode.DEPOSIT;
49
- return (_jsx(StatusCard, { warning: true, title: locales.payWithTokenScreen_expired_h1, body: locales.payWithTokenScreen_expired_p, actionLabel: isDeposit ? locales.refresh : locales.selectTokenScreen_selectAnotherMethod, onAction: () => {
49
+ const body = isDeposit
50
+ ? locales.payWithTokenScreen_expired_p
51
+ : "This checkout has expired. Please restart checkout from the merchant.";
52
+ return (_jsx(StatusCard, { warning: true, title: locales.payWithTokenScreen_expired_h1, body: body, actionLabel: isDeposit ? locales.refresh : locales.close, onAction: () => {
50
53
  if (isDeposit) {
51
54
  void paymentState.copyDepositPayOrder();
52
55
  return;
53
56
  }
54
- setRoute(ROUTE.SELECT_METHOD);
57
+ setOpen(false);
55
58
  } }));
56
59
  }
57
60
  function StatusCard({ title, body, actionLabel, onAction, loading = false, warning = false, }) {
@@ -100,7 +103,7 @@ function StripeOnrampCheckout({ paymentData }) {
100
103
  }
101
104
  return (_jsxs(OnrampShell, { children: [isLoading ? (_jsx(OnrampOverlay, { children: _jsx(Spinner, {}) })) : null, stripeOnramp ? (_jsx(OnrampSession, { stripeOnramp: stripeOnramp, clientSecret: paymentData.client_secret, theme: theme, onUiLoaded: triggerResize, onSessionUpdate: handleSessionUpdate }, paymentData.client_secret)) : null] }));
102
105
  }
103
- function OnrampSession({ stripeOnramp, clientSecret, theme, onUiLoaded, onSessionUpdate, }) {
106
+ const OnrampSession = memo(function OnrampSession({ stripeOnramp, clientSecret, theme, onUiLoaded, onSessionUpdate, }) {
104
107
  const mountNodeRef = useRef(null);
105
108
  const onUiLoadedRef = useLatestRef(onUiLoaded);
106
109
  const onSessionUpdateRef = useLatestRef(onSessionUpdate);
@@ -130,7 +133,7 @@ function OnrampSession({ stripeOnramp, clientSecret, theme, onUiLoaded, onSessio
130
133
  };
131
134
  }, [clientSecret, onSessionUpdateRef, onUiLoadedRef, stripeOnramp, theme]);
132
135
  return _jsx(OnrampMount, { ref: mountNodeRef });
133
- }
136
+ });
134
137
  function useCardPaymentData() {
135
138
  const { paymentState } = usePayContext();
136
139
  const { payOrder, payWithCard } = paymentState;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { PayOrderMode } from "@coin-voyage/shared/types";
2
+ import { PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
3
3
  import { useCallback, useEffect, useMemo } from "react";
4
4
  import { AlertIcon } from "../../../assets/icons";
5
5
  import { useCountdown } from "../../../hooks/useCountdown";
@@ -17,61 +17,60 @@ import TokenChainLogo from "../../ui/TokenChainLogo";
17
17
  import { CopyableInfo } from "./copyable-info";
18
18
  import { LogoRow, QRWrap } from "./styles";
19
19
  export default function PayToAddress() {
20
- return (_jsxs(PageContent, { children: [_jsx(PayToAddressView, {}), _jsx(PoweredByFooter, {})] }));
20
+ return (_jsxs(PageContent, { children: [_jsx(PayToAddressContent, {}), _jsx(PoweredByFooter, {})] }));
21
21
  }
22
- function PayToAddressView() {
22
+ function PayToAddressContent() {
23
23
  const { paymentState, triggerResize } = usePayContext();
24
- const { payToAddressChainId: payToAddressChain, payToAddressCurrency } = paymentState;
25
- const { data, isLoading, isError } = useDepositAddressQuery({
26
- enabled: payToAddressCurrency != undefined,
24
+ const { payToAddressChainId, payToAddressCurrency, payOrder } = paymentState;
25
+ const depositAddressQuery = useDepositAddressQuery({
26
+ enabled: payToAddressCurrency != null,
27
27
  });
28
- if (isError) {
29
- return payToAddressChain ? _jsx(DepositFailed, {}) : null;
28
+ if (depositAddressQuery.isError) {
29
+ return payToAddressChainId ? _jsx(DepositAddressUnavailable, {}) : null;
30
30
  }
31
- if (isLoading || !data) {
31
+ if (depositAddressQuery.isLoading || !depositAddressQuery.data) {
32
32
  return _jsx(DepositAddressLoading, {});
33
33
  }
34
- return (_jsx(DepositAddressInfo, { details: data, triggerResize: triggerResize, isDeposit: paymentState.payOrder?.mode === PayOrderMode.DEPOSIT }));
34
+ return (_jsx(DepositAddressDetails, { details: depositAddressQuery.data, isDeposit: payOrder?.mode === PayOrderMode.DEPOSIT, isPayOrderExpired: payOrder?.status === PayOrderStatus.EXPIRED, onResizeNeeded: triggerResize, onRefreshDepositOrder: paymentState.copyDepositPayOrder }));
35
35
  }
36
36
  function DepositAddressLoading() {
37
37
  const locales = useLocales();
38
38
  return (_jsxs(ModalContent, { "$center": true, style: { paddingTop: 32, paddingBottom: 32 }, children: [_jsx(Spinner, {}), _jsx(ModalBody, { style: { marginTop: 12 }, children: locales.payToAddressScreen_generatingDepositAddress })] }));
39
39
  }
40
- function DepositAddressInfo({ details, triggerResize, isDeposit, }) {
41
- const locales = useLocales();
40
+ function DepositAddressDetails({ details, isDeposit, isPayOrderExpired, onResizeNeeded, onRefreshDepositOrder, }) {
42
41
  const [remainingS, totalS] = useCountdown(details.expirationS);
43
- const isExpired = details.expirationS != null && remainingS === 0;
44
- useEffect(triggerResize, [details, isExpired, triggerResize]);
45
- const logoElement = useMemo(() => (_jsx(TokenChainLogo, { chainId: details.chainId, src: details.logoURI, alt: details.ticker }, `${details.ticker}-${details.chainId}`)), [details.chainId, details.ticker, details.logoURI]);
46
- return (_jsxs(ModalContent, { children: [_jsx(DepositAddressView, { isExpired: isExpired, isDeposit: isDeposit, depositAddress: details.depositAddress, logoElement: logoElement, localesRefreshLabel: locales.refresh }), _jsx("div", { style: { height: 8 } }), _jsx(CopyableInfo, { details: details, remainingS: remainingS, totalS: totalS })] }));
42
+ const isExpired = isPayOrderExpired || (details.expirationS != null && remainingS === 0);
43
+ const effectiveRemainingS = isExpired ? 0 : remainingS;
44
+ useEffect(() => {
45
+ onResizeNeeded();
46
+ }, [details, isExpired, onResizeNeeded]);
47
+ const tokenChainLogo = useMemo(() => (_jsx(TokenChainLogo, { chainId: details.chainId, chainStyle: { borderRadius: 9999, bottom: 6, right: 4, height: "20px", width: "20px" }, src: details.logoURI, alt: details.ticker })), [details.chainId, details.ticker, details.logoURI]);
48
+ return (_jsxs(ModalContent, { children: [_jsx(DepositAddressPrimaryContent, { depositAddress: details.depositAddress, isExpired: isExpired, isDeposit: isDeposit, tokenChainLogo: tokenChainLogo, onRefreshDepositOrder: onRefreshDepositOrder }), _jsx("div", { style: { height: 8 } }), _jsx(CopyableInfo, { details: details, remainingS: effectiveRemainingS, totalS: totalS })] }));
47
49
  }
48
- function DepositAddressView({ isExpired, isDeposit, depositAddress, logoElement, localesRefreshLabel, }) {
49
- const locales = useLocales();
50
- const { paymentState } = usePayContext();
51
- if (isExpired) {
52
- if (!isDeposit) {
53
- return (_jsxs(ModalContent, { "$center": true, style: { paddingTop: 32, paddingBottom: 32 }, children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.payWithTokenScreen_expired_h1] }), _jsx(ModalBody, { children: locales.payWithTokenScreen_expired_p })] }));
54
- }
55
- return (_jsx(LogoRow, { children: _jsx(Button, { onClick: () => {
56
- // Create a new deposit pay order and trigger a refetch of the payment details query
57
- paymentState.copyDepositPayOrder();
58
- }, style: { width: 128 }, children: localesRefreshLabel }) }));
50
+ function DepositAddressPrimaryContent({ depositAddress, isExpired, isDeposit, tokenChainLogo, onRefreshDepositOrder, }) {
51
+ if (!isExpired) {
52
+ return (_jsx(QRWrap, { children: _jsx(CustomQRCode, { value: depositAddress, image: tokenChainLogo }) }));
59
53
  }
60
- return (_jsx(QRWrap, { children: _jsx(CustomQRCode, { value: depositAddress, image: logoElement }) }));
54
+ if (isDeposit) {
55
+ return _jsx(RefreshExpiredDepositAddress, { onRefresh: onRefreshDepositOrder });
56
+ }
57
+ return _jsx(ExpiredPaymentAddressMessage, {});
58
+ }
59
+ function RefreshExpiredDepositAddress({ onRefresh }) {
60
+ const locales = useLocales();
61
+ return (_jsx(LogoRow, { children: _jsx(Button, { onClick: onRefresh, style: { width: 128 }, children: locales.refresh }) }));
62
+ }
63
+ function ExpiredPaymentAddressMessage() {
64
+ const locales = useLocales();
65
+ return (_jsxs(ModalContent, { "$center": true, style: { paddingTop: 32, paddingBottom: 32 }, children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.payWithTokenScreen_expired_h1] }), _jsx(ModalBody, { children: locales.payWithTokenScreen_expired_p })] }));
61
66
  }
62
- function DepositFailed() {
67
+ function DepositAddressUnavailable() {
63
68
  const locales = useLocales();
64
69
  const { setRoute, paymentState } = usePayContext();
65
- const { setPayToAddressChainId: setPayToAddressChain, setPayToAddressCurrency } = paymentState;
66
70
  const onSelectAnotherMethod = useCallback(() => {
67
- setPayToAddressChain(undefined);
68
- setPayToAddressCurrency(undefined);
71
+ paymentState.setPayToAddressChainId(undefined);
72
+ paymentState.setPayToAddressCurrency(undefined);
69
73
  setRoute(ROUTE.ADDRESS_CHAIN_SELECT);
70
- }, [setRoute, setPayToAddressChain, setPayToAddressCurrency]);
71
- return (_jsxs(ModalContent, { "$center": true, style: {
72
- marginLeft: 24,
73
- marginRight: 24,
74
- paddingTop: 16,
75
- paddingBottom: 16,
76
- }, children: [_jsx(ModalH1, { children: locales.selectPayToAddressWaitingScreen_unavailable_h1 }), _jsx(ModalBody, { children: locales.selectPayToAddressWaitingScreen_unavailable_p }), _jsx(SelectAnotherMethod, { buttonText: locales.selectTokenScreen_selectAnotherMethod, onSelectAnotherMethod: onSelectAnotherMethod })] }));
74
+ }, [paymentState, setRoute]);
75
+ return (_jsxs(ModalContent, { "$center": true, style: { marginLeft: 24, marginRight: 24, paddingTop: 16, paddingBottom: 16 }, children: [_jsx(ModalH1, { children: locales.selectPayToAddressWaitingScreen_unavailable_h1 }), _jsx(ModalBody, { children: locales.selectPayToAddressWaitingScreen_unavailable_p }), _jsx(SelectAnotherMethod, { buttonText: locales.selectTokenScreen_selectAnotherMethod, onSelectAnotherMethod: onSelectAnotherMethod })] }));
77
76
  }
@@ -1,4 +1,4 @@
1
- import type { ChainId, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderCreationErrorEvent, PayOrderMetadata, PayOrderRefundedEvent } from "@coin-voyage/shared/types";
1
+ import type { ChainId, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderCreationErrorEvent, PayOrderExecutingEvent, PayOrderMetadata, PayOrderRefundedEvent, PayOrderStartedEvent } from "@coin-voyage/shared/types";
2
2
  import type { CustomTheme, Mode, PayModalOptions, Theme } from "../../types";
3
3
  type DepositPayButtonParams = {
4
4
  /**
@@ -34,8 +34,17 @@ type PayButtonCommonProps = PayButtonPaymentProps & {
34
34
  intent?: string;
35
35
  /** Called when invalid properties are used in order to create a deposit payOrder */
36
36
  onPaymentCreationError?: (event: PayOrderCreationErrorEvent) => void;
37
- /** Called when user sends payment and transaction is seen on chain */
37
+ /** Called when payment details are available and the order is awaiting payment. */
38
+ onAwaitingPayment?: (event: PayOrderStartedEvent) => void;
39
+ /** Called when the payment is detected and awaiting confirmation. */
40
+ onConfirmingPayment?: (event: PayOrderConfirmingEvent) => void;
41
+ /**
42
+ * Called when the payment is detected and awaiting confirmation.
43
+ * @deprecated Use `onConfirmingPayment` instead.
44
+ */
38
45
  onPaymentStarted?: (event: PayOrderConfirmingEvent) => void;
46
+ /** Called when the payment is confirmed and the order is executing. */
47
+ onExecutingPayment?: (event: PayOrderExecutingEvent) => void;
39
48
  /** Called when destination transfer or call completes successfully */
40
49
  onPaymentCompleted?: (event: PayOrderCompletedEvent) => void;
41
50
  /** Called when destination call reverts and funds are refunded */
@@ -23,7 +23,9 @@ function PayButtonCustom(props) {
23
23
  usePayModalCallbacks(props.onOpen, props.onClose);
24
24
  const { order, show, hide } = usePayButtonController(props);
25
25
  usePaymentLifecycle(order, {
26
- onPaymentStarted: props.onPaymentStarted,
26
+ onAwaitingPayment: props.onAwaitingPayment,
27
+ onConfirmingPayment: props.onConfirmingPayment ?? props.onPaymentStarted,
28
+ onExecutingPayment: props.onExecutingPayment,
27
29
  onPaymentCompleted: props.onPaymentCompleted,
28
30
  onPaymentBounced: props.onPaymentBounced,
29
31
  }, {
@@ -1,9 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { AnimatePresence, motion } from "framer-motion";
3
2
  import { isWalletConnectConnector } from "../../utils";
4
3
  import usePayContext from "../contexts/pay";
5
4
  import Alert from "../ui/Alert";
6
- import { contentVariants } from "../ui/Modal";
7
5
  import ConnectWithInjector from "./ConnectWithInjector";
8
6
  import ConnectWithQRCode from "./ConnectWithQRCode";
9
7
  export default function ConnectUsing() {
@@ -14,5 +12,5 @@ export default function ConnectUsing() {
14
12
  }
15
13
  // If cannot be scanned, display injector flow, which if extension is not installed will show CTA to install it
16
14
  const isQrCode = isWalletConnectConnector(wallet?.id) && wallet?.getWalletDeeplink;
17
- return (_jsx(AnimatePresence, { children: isQrCode ? (_jsx(motion.div, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: _jsx(ConnectWithQRCode, {}) }, "QRCODE")) : (_jsx(motion.div, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: _jsx(ConnectWithInjector, {}) })) }));
15
+ return isQrCode ? _jsx(ConnectWithQRCode, {}) : _jsx(ConnectWithInjector, {});
18
16
  }
@@ -100,7 +100,6 @@ export default function ConnectWithInjector({ forceState }) {
100
100
  },
101
101
  onSuccess(_, variables) {
102
102
  if (variables?.connector) {
103
- setStatus(states.CONNECTED);
104
103
  setRoute(ROUTE.WALLET_TOKEN_SELECT);
105
104
  }
106
105
  },
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { getChainTypeName } from "@coin-voyage/shared/chain";
3
+ import { PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
3
4
  import { useCallback, useMemo } from "react";
4
5
  import { routeConfig } from "../../config/route-config";
5
6
  import useLocales from "../../hooks/useLocales";
@@ -17,7 +18,9 @@ export function PayModal({ onExited }) {
17
18
  CHAIN_TYPE: getChainTypeName(connectorChainType),
18
19
  });
19
20
  const config = routeConfig[route];
20
- const showBackButton = config?.showBackButton !== false;
21
+ const isExpiredPayment = paymentState.payOrder?.status === PayOrderStatus.EXPIRED &&
22
+ paymentState.payOrder.mode === PayOrderMode.SALE;
23
+ const showBackButton = config?.showBackButton !== false && !isExpiredPayment;
21
24
  const showInfoButton = config?.showInfoButton !== false;
22
25
  const depth = config?.depth ?? 1;
23
26
  const headingCtx = {
@@ -32,7 +32,14 @@ export function useModalTransition({ open, positionInside, onClose, resizeDepend
32
32
  // laid out yet. ResizeObserver or a subsequent contentRef attachment will re-measure
33
33
  // once the node reports a real size.
34
34
  if (w > 0 && h > 0) {
35
- setDimensions({ width: `${w}px`, height: `${h}px` });
35
+ const nextDimensions = { width: `${w}px`, height: `${h}px` };
36
+ setDimensions((currentDimensions) => {
37
+ if (currentDimensions.width === nextDimensions.width &&
38
+ currentDimensions.height === nextDimensions.height) {
39
+ return currentDimensions;
40
+ }
41
+ return nextDimensions;
42
+ });
36
43
  }
37
44
  }, []);
38
45
  // Re-measure when callers update `resizeDependency` (used by pages to force a re-measure
@@ -1,6 +1,8 @@
1
1
  import type { ChainId } from "@coin-voyage/shared/types";
2
- interface TokenChainLogoProps extends React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {
2
+ import type { CSSProperties, DetailedHTMLProps, ImgHTMLAttributes } from "react";
3
+ interface TokenChainLogoProps extends DetailedHTMLProps<ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement> {
3
4
  chainId: ChainId;
5
+ chainStyle?: CSSProperties;
4
6
  }
5
- export default function TokenChainLogo({ chainId, ...props }: TokenChainLogoProps): import("react/jsx-runtime").JSX.Element;
7
+ export default function TokenChainLogo({ chainId, chainStyle, style, ...props }: TokenChainLogoProps): import("react/jsx-runtime").JSX.Element;
6
8
  export {};
@@ -1,6 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { getChainLogo } from "@coin-voyage/shared/chain";
3
+ import { cloneElement } from "react";
3
4
  import { ChainContainer, TokenChainContainer } from "./styles";
4
- export default function TokenChainLogo({ chainId, ...props }) {
5
- return (_jsxs(TokenChainContainer, { children: [_jsx("img", { ...props, style: { borderRadius: "22.5%" } }), _jsx(ChainContainer, { children: getChainLogo(chainId) })] }));
5
+ export default function TokenChainLogo({ chainId, chainStyle, style, ...props }) {
6
+ const chainLogo = getChainLogo(chainId);
7
+ return (_jsxs(TokenChainContainer, { children: [_jsx("img", { ...props, style: {
8
+ ...style,
9
+ borderRadius: style?.borderRadius ?? "22.5%",
10
+ } }), _jsx(ChainContainer, { style: chainStyle, children: chainLogo && cloneElement(chainLogo) })] }));
6
11
  }
@@ -1,6 +1,7 @@
1
- import styled from "../../../styles/styled";
2
1
  import { motion } from "framer-motion";
2
+ import styled from "../../../styles/styled";
3
3
  export const TokenChainContainer = styled(motion.div) `
4
+ position: relative;
4
5
  width: 100%;
5
6
  height: 100%;
6
7
  `;
@@ -12,4 +13,10 @@ export const ChainContainer = styled(motion.div) `
12
13
  overflow: hidden;
13
14
  bottom: 0px;
14
15
  right: 0px;
16
+
17
+ svg {
18
+ display: block;
19
+ width: 100%;
20
+ height: 100%;
21
+ }
15
22
  `;
@@ -1,23 +1,25 @@
1
- import { useEffect, useState } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
2
  export function useCountdown(expirationS) {
3
- const [nowS, setNowS] = useState(0);
4
- const [initialRemaining] = useState(() => {
5
- if (!expirationS)
6
- return 0;
7
- return Math.max(0, expirationS - Math.floor(Date.now() / 1000));
8
- });
3
+ const [nowS, setNowS] = useState(getNowS);
4
+ const initialRemaining = useMemo(() => computeRemaining(expirationS, getNowS()), [expirationS]);
5
+ const remainingS = computeRemaining(expirationS, nowS);
9
6
  useEffect(() => {
10
- if (!expirationS)
7
+ if (expirationS == null)
11
8
  return;
12
- const tick = () => {
13
- setNowS(Math.floor(Date.now() / 1000));
14
- };
9
+ const tick = () => setNowS(getNowS());
15
10
  tick();
11
+ if (expirationS <= getNowS())
12
+ return;
16
13
  const id = setInterval(tick, 1000);
17
14
  return () => clearInterval(id);
18
15
  }, [expirationS]);
19
- if (!expirationS || nowS === 0)
20
- return [0, 0];
21
- const remainingS = Math.max(0, expirationS - nowS);
22
16
  return [remainingS, initialRemaining];
23
17
  }
18
+ function getNowS() {
19
+ return Math.floor(Date.now() / 1000);
20
+ }
21
+ function computeRemaining(expirationS, nowS) {
22
+ if (expirationS == null)
23
+ return 0;
24
+ return Math.max(0, expirationS - nowS);
25
+ }
@@ -0,0 +1,9 @@
1
+ import { PayOrderEvent } from "@coin-voyage/shared/types";
2
+ interface UseOrderStatusWSProps {
3
+ orderId?: string;
4
+ enabled?: boolean;
5
+ onEvent?: (eventData: PayOrderEvent) => void;
6
+ onError?: (error: unknown) => void;
7
+ }
8
+ export declare function useOrderStatusWS({ orderId, enabled, onEvent, onError, }: UseOrderStatusWSProps): void;
9
+ export {};
@@ -0,0 +1,49 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useBackendApi } from "../components/contexts/api";
3
+ export function useOrderStatusWS({ orderId, enabled = true, onEvent, onError, }) {
4
+ const api = useBackendApi();
5
+ const socketRef = useRef(null);
6
+ const onEventRef = useRef(null);
7
+ const onErrorRef = useRef(null);
8
+ useEffect(() => {
9
+ onEventRef.current = onEvent;
10
+ }, [onEvent]);
11
+ useEffect(() => {
12
+ onErrorRef.current = onError;
13
+ }, [onError]);
14
+ useEffect(() => {
15
+ if (!enabled)
16
+ return;
17
+ if (socketRef.current)
18
+ return;
19
+ const socket = api.subscribeOrderStatus();
20
+ socketRef.current = socket;
21
+ socket.onOpen(() => {
22
+ if (orderId) {
23
+ socket.subscribe(orderId);
24
+ }
25
+ else {
26
+ socket.subscribeOrg();
27
+ }
28
+ });
29
+ socket.onMessage((msg) => {
30
+ if (msg.type === "event") {
31
+ onEventRef.current?.(msg.data);
32
+ }
33
+ });
34
+ socket.onError((e) => {
35
+ onErrorRef.current?.(e);
36
+ });
37
+ socket.onClose(() => {
38
+ if (socketRef.current === socket) {
39
+ socketRef.current = null;
40
+ }
41
+ });
42
+ return () => {
43
+ if (socketRef.current === socket) {
44
+ socketRef.current = null;
45
+ }
46
+ socket.close();
47
+ };
48
+ }, [api, enabled, orderId]);
49
+ }
@@ -1,9 +1,5 @@
1
+ import type { Option } from "../components/ui/OptionsList/types";
1
2
  export declare function usePayToAddressTokens(): {
2
- options: {
3
- id: string;
4
- title: string;
5
- icons: import("react/jsx-runtime").JSX.Element[];
6
- onClick: () => void;
7
- }[];
3
+ options: Option[];
8
4
  isLoading: boolean;
9
5
  };
@@ -15,6 +15,7 @@ export function usePayToAddressTokens() {
15
15
  icons: token.logoURI
16
16
  ? [_jsx(SquircleIcon, { icon: token.logoURI, alt: token.name }, token.address ?? token.ticker)]
17
17
  : [],
18
+ iconShape: "circle",
18
19
  onClick: () => {
19
20
  const currency = tokenToCurrency(token);
20
21
  setPayToAddressCurrency(currency);
@@ -1,6 +1,8 @@
1
- import { PayOrder, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderRefundedEvent } from "@coin-voyage/shared/types";
1
+ import { PayOrder, PayOrderCompletedEvent, PayOrderConfirmingEvent, PayOrderExecutingEvent, PayOrderRefundedEvent, PayOrderStartedEvent } from "@coin-voyage/shared/types";
2
2
  type PaymentLifecycleHandlers = {
3
- onPaymentStarted: ((event: PayOrderConfirmingEvent) => void) | undefined;
3
+ onAwaitingPayment: ((event: PayOrderStartedEvent) => void) | undefined;
4
+ onConfirmingPayment: ((event: PayOrderConfirmingEvent) => void) | undefined;
5
+ onExecutingPayment: ((event: PayOrderExecutingEvent) => void) | undefined;
4
6
  onPaymentCompleted: ((event: PayOrderCompletedEvent) => void) | undefined;
5
7
  onPaymentBounced: ((event: PayOrderRefundedEvent) => void) | undefined;
6
8
  };
@@ -2,11 +2,13 @@ import { PayOrderMode, PayOrderStatus, } from "@coin-voyage/shared/types";
2
2
  import { useEffect, useRef } from "react";
3
3
  const COMPLETED_STATES = [PayOrderStatus.COMPLETED, PayOrderStatus.REFUNDED];
4
4
  const STARTED_STATES = [
5
- ...COMPLETED_STATES,
5
+ PayOrderStatus.AWAITING_PAYMENT,
6
6
  PayOrderStatus.PARTIAL_PAYMENT,
7
7
  PayOrderStatus.AWAITING_CONFIRMATION,
8
8
  PayOrderStatus.OPTIMISTIC_CONFIRMED,
9
9
  PayOrderStatus.EXECUTING_ORDER,
10
+ PayOrderStatus.COMPLETED,
11
+ PayOrderStatus.REFUNDED,
10
12
  ];
11
13
  /**
12
14
  * Handles payment lifecycle events of an order, such as started, completed, and bounced.
@@ -16,58 +18,92 @@ const STARTED_STATES = [
16
18
  * @returns
17
19
  */
18
20
  export function usePaymentLifecycle(order, handlers, options = {}) {
19
- const sentStart = useRef(false);
21
+ const sentAwaitingPayment = useRef(false);
22
+ const sentConfirmingPayment = useRef(false);
23
+ const sentExecutingPayment = useRef(false);
20
24
  const sentComplete = useRef(false);
25
+ const sentBounced = useRef(false);
21
26
  const currentOrderId = useRef(undefined);
22
- const { onPaymentStarted, onPaymentCompleted, onPaymentBounced } = handlers;
27
+ const { onAwaitingPayment, onConfirmingPayment, onExecutingPayment, onPaymentCompleted, onPaymentBounced } = handlers;
23
28
  const { optimisticConfirmation = true } = options;
24
29
  const orderId = order?.id;
25
30
  const orderStatus = order?.status;
26
31
  const orderMetadata = order?.metadata;
27
32
  const payment = order?.payment;
28
- const allowOptimisticCompletion = optimisticConfirmation && order?.mode === PayOrderMode.SALE;
33
+ const allowOptimisticFinalized = optimisticConfirmation && order?.mode === PayOrderMode.SALE;
29
34
  const isStarted = !!orderStatus && STARTED_STATES.includes(orderStatus);
30
- const isFinalized = !!orderStatus &&
31
- (COMPLETED_STATES.includes(orderStatus) ||
32
- (allowOptimisticCompletion &&
33
- (orderStatus === PayOrderStatus.OPTIMISTIC_CONFIRMED || orderStatus === PayOrderStatus.EXECUTING_ORDER)));
35
+ const isFinalized = (!!orderStatus && COMPLETED_STATES.includes(orderStatus)) ||
36
+ (allowOptimisticFinalized &&
37
+ (orderStatus === PayOrderStatus.OPTIMISTIC_CONFIRMED || orderStatus === PayOrderStatus.EXECUTING_ORDER));
34
38
  useEffect(() => {
35
39
  if (!orderId)
36
40
  return;
37
41
  if (currentOrderId.current === orderId)
38
42
  return;
39
43
  currentOrderId.current = orderId;
40
- sentStart.current = false;
44
+ sentAwaitingPayment.current = false;
45
+ sentConfirmingPayment.current = false;
46
+ sentExecutingPayment.current = false;
41
47
  sentComplete.current = false;
48
+ sentBounced.current = false;
42
49
  }, [orderId]);
43
50
  useEffect(() => {
44
- if (sentStart.current || !orderId || !payment || !orderStatus || !isStarted)
51
+ if (sentAwaitingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.AWAITING_PAYMENT)
45
52
  return;
46
- sentStart.current = true;
47
- onPaymentStarted?.({
53
+ sentAwaitingPayment.current = true;
54
+ onAwaitingPayment?.({
55
+ type: "payorder_started",
56
+ payorder_id: orderId,
57
+ status: orderStatus,
58
+ metadata: orderMetadata,
59
+ payment_data: payment,
60
+ });
61
+ }, [onAwaitingPayment, orderId, orderMetadata, orderStatus, payment]);
62
+ useEffect(() => {
63
+ if (sentConfirmingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.AWAITING_CONFIRMATION) {
64
+ return;
65
+ }
66
+ sentConfirmingPayment.current = true;
67
+ onConfirmingPayment?.({
48
68
  type: "payorder_confirming",
49
69
  payorder_id: orderId,
50
70
  status: orderStatus,
51
71
  metadata: orderMetadata,
52
72
  payment_data: payment,
73
+ source_tx_hash: payment.source_tx_hash ?? "",
53
74
  });
54
- }, [isStarted, onPaymentStarted, orderId, orderMetadata, orderStatus, payment]);
75
+ }, [onConfirmingPayment, orderId, orderMetadata, orderStatus, payment]);
55
76
  useEffect(() => {
56
- if (sentComplete.current || !orderId || !payment || !orderStatus || !isFinalized)
77
+ if (sentExecutingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.EXECUTING_ORDER)
57
78
  return;
58
- sentComplete.current = true;
59
- if (orderStatus === PayOrderStatus.REFUNDED) {
60
- onPaymentBounced?.({
61
- type: "payorder_refunded",
62
- payorder_id: orderId,
63
- status: orderStatus,
64
- metadata: orderMetadata,
65
- payment_data: payment,
66
- refund_address: payment.refund_address ?? "",
67
- refund_tx_hash: payment.refund_tx_hash ?? "",
68
- });
79
+ sentExecutingPayment.current = true;
80
+ onExecutingPayment?.({
81
+ type: "payorder_executing",
82
+ payorder_id: orderId,
83
+ status: orderStatus,
84
+ metadata: orderMetadata,
85
+ payment_data: payment,
86
+ source_tx_hash: payment.source_tx_hash ?? "",
87
+ });
88
+ }, [onExecutingPayment, orderId, orderMetadata, orderStatus, payment]);
89
+ useEffect(() => {
90
+ if (sentBounced.current || !orderId || !payment || orderStatus !== PayOrderStatus.REFUNDED)
69
91
  return;
70
- }
92
+ sentBounced.current = true;
93
+ onPaymentBounced?.({
94
+ type: "payorder_refunded",
95
+ payorder_id: orderId,
96
+ status: orderStatus,
97
+ metadata: orderMetadata,
98
+ payment_data: payment,
99
+ refund_address: payment.refund_address ?? "",
100
+ refund_tx_hash: payment.refund_tx_hash ?? "",
101
+ });
102
+ }, [onPaymentBounced, orderId, orderMetadata, orderStatus, payment]);
103
+ useEffect(() => {
104
+ if (sentComplete.current || !orderId || !payment || orderStatus !== PayOrderStatus.COMPLETED)
105
+ return;
106
+ sentComplete.current = true;
71
107
  onPaymentCompleted?.({
72
108
  type: "payorder_completed",
73
109
  payorder_id: orderId,
@@ -77,6 +113,6 @@ export function usePaymentLifecycle(order, handlers, options = {}) {
77
113
  source_tx_hash: payment.source_tx_hash ?? "",
78
114
  destination_tx_hash: payment.destination_tx_hash ?? "",
79
115
  });
80
- }, [isFinalized, onPaymentBounced, onPaymentCompleted, orderId, orderMetadata, orderStatus, payment]);
116
+ }, [onPaymentCompleted, orderId, orderMetadata, orderStatus, payment]);
81
117
  return { isStarted, isFinalized, order };
82
118
  }
@@ -33,7 +33,7 @@ export function useTokenOptions(disabled) {
33
33
  disabled: isDisabled,
34
34
  iconShape: "squircle",
35
35
  icons: [
36
- _jsx(TokenChainLogo, { src: quote.image_uri, alt: `${quote.ticker} logo`, chainId: quote.chain_id }, `${quote.ticker}-${quote.chain_id}`),
36
+ _jsx(TokenChainLogo, { src: quote.image_uri, alt: `${quote.ticker} logo`, chainId: quote.chain_id, style: { borderRadius: 9999 }, chainStyle: { bottom: -1, right: -1 } }, `${quote.ticker}-${quote.chain_id}`),
37
37
  ],
38
38
  onClick: () => {
39
39
  setSelectedCurrencyOption(quote);
@@ -1,5 +1,5 @@
1
1
  import { ApiClient as ApiClientInternal, APIEnvironment } from "@coin-voyage/shared/api";
2
- export declare function ApiClient({ apiKey, environment, }: {
2
+ export declare function ApiClient({ apiKey, environment }: {
3
3
  apiKey: string;
4
4
  environment?: APIEnvironment;
5
5
  }): ApiClientInternal;
@@ -1,7 +1,7 @@
1
1
  import { ApiClient as ApiClientInternal } from "@coin-voyage/shared/api";
2
2
  import { paykitVersion } from "../../utils/version";
3
3
  import { v4 as uuidv4 } from "uuid";
4
- export function ApiClient({ apiKey, environment = "production", }) {
4
+ export function ApiClient({ apiKey, environment = "production" }) {
5
5
  return new ApiClientInternal({
6
6
  apiKey,
7
7
  version: paykitVersion,
@@ -2,13 +2,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useInWagmiContext } from "@coin-voyage/crypto/evm";
3
3
  import { useAccount, useConnectCallback } from "@coin-voyage/crypto/hooks";
4
4
  import { getDepositAddress, getPaymentStep } from "@coin-voyage/shared/payment";
5
- import { PaymentMethod, PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
5
+ import { PaymentMethod, PayOrderStatus } from "@coin-voyage/shared/types";
6
6
  import { Buffer } from "buffer";
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
8
8
  import { ThemeProvider as StyledThemeProvider } from "styled-components";
9
9
  import { PayContext } from "../components/contexts/pay/index";
10
10
  import { PayModal } from "../components/pay-modal/index";
11
11
  import { useThemeFont } from "../hooks/useGoogleFont";
12
+ import { useOrderStatusWS } from "../hooks/useOrderStatusWS";
12
13
  import { usePaymentState } from "../hooks/usePaymentState";
13
14
  import defaultTheme from "../styles/defaultTheme";
14
15
  import { ROUTE } from "../types/routes";
@@ -167,13 +168,17 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
167
168
  removeInitStateParam();
168
169
  }
169
170
  }, [setOpen, setPayId, setConnectorChainType]);
170
- useEffect(() => {
171
- const intervalMs = getPollingIntervalMs(payOrder?.status, payOrder?.mode);
172
- if (!intervalMs)
171
+ const onOrderStatusEvent = useCallback((event) => {
172
+ if (!("payorder_id" in event) || event.payorder_id !== payOrder?.id)
173
173
  return;
174
- const timeoutId = setTimeout(refreshOrder, intervalMs);
175
- return () => clearTimeout(timeoutId);
176
- }, [payOrder?.mode, payOrder?.status, refreshOrder]);
174
+ void refreshOrder();
175
+ }, [payOrder?.id, refreshOrder]);
176
+ useOrderStatusWS({
177
+ orderId: payOrder?.id,
178
+ enabled: shouldSubscribeOrderStatus(payOrder?.status),
179
+ onEvent: onOrderStatusEvent,
180
+ onError: log,
181
+ });
177
182
  useEffect(() => {
178
183
  if (isFinalPayOrderStatus(payOrder?.status)) {
179
184
  setRoute(ROUTE.CONFIRMATION);
@@ -259,17 +264,11 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
259
264
  // === Helper functions ===
260
265
  const NON_FINAL_PAY_ORDER_STATUSES = [PayOrderStatus.PENDING, PayOrderStatus.AWAITING_PAYMENT, PayOrderStatus.EXPIRED];
261
266
  const isFinalPayOrderStatus = (status) => !!status && !NON_FINAL_PAY_ORDER_STATUSES.includes(status);
262
- const getPollingIntervalMs = (status, mode) => {
263
- if (!status)
264
- return null;
265
- if (status === PayOrderStatus.AWAITING_PAYMENT) {
266
- return 5000;
267
- }
268
- if ([PayOrderStatus.AWAITING_CONFIRMATION, PayOrderStatus.OPTIMISTIC_CONFIRMED].includes(status)) {
269
- return 2500;
270
- }
271
- if (status === PayOrderStatus.EXECUTING_ORDER) {
272
- return mode === PayOrderMode.DEPOSIT ? 1000 : 2500;
273
- }
274
- return null;
275
- };
267
+ const ORDER_STATUS_SUBSCRIPTION_STATUSES = [
268
+ PayOrderStatus.PENDING,
269
+ PayOrderStatus.AWAITING_PAYMENT,
270
+ PayOrderStatus.AWAITING_CONFIRMATION,
271
+ PayOrderStatus.OPTIMISTIC_CONFIRMED,
272
+ PayOrderStatus.EXECUTING_ORDER,
273
+ ];
274
+ const shouldSubscribeOrderStatus = (status) => !!status && ORDER_STATUS_SUBSCRIPTION_STATUSES.includes(status);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coin-voyage/paykit",
3
3
  "description": "Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.",
4
- "version": "2.4.4-beta.0",
4
+ "version": "2.4.5-beta.0",
5
5
  "private": false,
6
6
  "sideEffects": false,
7
7
  "author": "Lars <lars@coinvoyage.io>",
@@ -60,8 +60,8 @@
60
60
  "@stripe/crypto": "0.0.4",
61
61
  "styled-components": "^5.3.11",
62
62
  "uuid": "13.0.0",
63
- "@coin-voyage/shared": "2.4.4-beta.1",
64
- "@coin-voyage/crypto": "2.4.3"
63
+ "@coin-voyage/crypto": "2.4.4",
64
+ "@coin-voyage/shared": "2.4.5-beta.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/qrcode": "1.5.5",