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

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,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
+ }
@@ -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);
@@ -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);
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.4",
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.4"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/qrcode": "1.5.5",