@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.
- package/dist/components/Pages/CardPayment/index.js +9 -6
- package/dist/components/Pages/PayToAddress/index.js +38 -39
- package/dist/components/pay-button/index.d.ts +11 -2
- package/dist/components/pay-button/index.js +3 -1
- package/dist/components/pay-modal/ConnectUsing.js +1 -3
- package/dist/components/pay-modal/ConnectWithInjector/index.js +0 -1
- package/dist/components/pay-modal/index.js +4 -1
- package/dist/components/ui/Modal/useModalTransition.js +8 -1
- package/dist/components/ui/TokenChainLogo/index.d.ts +4 -2
- package/dist/components/ui/TokenChainLogo/index.js +7 -2
- package/dist/components/ui/TokenChainLogo/styles.js +8 -1
- package/dist/hooks/useCountdown.js +16 -14
- package/dist/hooks/useOrderStatusWS.d.ts +9 -0
- package/dist/hooks/useOrderStatusWS.js +49 -0
- package/dist/hooks/usePayToAddressTokens.d.ts +2 -6
- package/dist/hooks/usePayToAddressTokens.js +1 -0
- package/dist/hooks/usePaymentLifecycle.d.ts +4 -2
- package/dist/hooks/usePaymentLifecycle.js +63 -27
- package/dist/hooks/useTokenOptions.js +1 -1
- package/dist/lib/api/index.d.ts +1 -1
- package/dist/lib/api/index.js +1 -1
- package/dist/providers/paykit-provider.js +20 -21
- package/package.json +3 -3
|
@@ -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,
|
|
47
|
+
const { paymentState, setOpen } = usePayContext();
|
|
48
48
|
const isDeposit = paymentState.payOrder?.mode === PayOrderMode.DEPOSIT;
|
|
49
|
-
|
|
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
|
-
|
|
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(
|
|
20
|
+
return (_jsxs(PageContent, { children: [_jsx(PayToAddressContent, {}), _jsx(PoweredByFooter, {})] }));
|
|
21
21
|
}
|
|
22
|
-
function
|
|
22
|
+
function PayToAddressContent() {
|
|
23
23
|
const { paymentState, triggerResize } = usePayContext();
|
|
24
|
-
const { payToAddressChainId
|
|
25
|
-
const
|
|
26
|
-
enabled: payToAddressCurrency !=
|
|
24
|
+
const { payToAddressChainId, payToAddressCurrency, payOrder } = paymentState;
|
|
25
|
+
const depositAddressQuery = useDepositAddressQuery({
|
|
26
|
+
enabled: payToAddressCurrency != null,
|
|
27
27
|
});
|
|
28
|
-
if (isError) {
|
|
29
|
-
return
|
|
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(
|
|
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
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
68
|
-
setPayToAddressCurrency(undefined);
|
|
71
|
+
paymentState.setPayToAddressChainId(undefined);
|
|
72
|
+
paymentState.setPayToAddressCurrency(undefined);
|
|
69
73
|
setRoute(ROUTE.ADDRESS_CHAIN_SELECT);
|
|
70
|
-
}, [
|
|
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
|
|
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
|
-
|
|
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
|
|
15
|
+
return isQrCode ? _jsx(ConnectWithQRCode, {}) : _jsx(ConnectWithInjector, {});
|
|
18
16
|
}
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
4
|
-
const
|
|
5
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
33
|
+
const allowOptimisticFinalized = optimisticConfirmation && order?.mode === PayOrderMode.SALE;
|
|
29
34
|
const isStarted = !!orderStatus && STARTED_STATES.includes(orderStatus);
|
|
30
|
-
const isFinalized = !!orderStatus &&
|
|
31
|
-
(
|
|
32
|
-
(
|
|
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
|
-
|
|
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 (
|
|
51
|
+
if (sentAwaitingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.AWAITING_PAYMENT)
|
|
45
52
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
}, [
|
|
75
|
+
}, [onConfirmingPayment, orderId, orderMetadata, orderStatus, payment]);
|
|
55
76
|
useEffect(() => {
|
|
56
|
-
if (
|
|
77
|
+
if (sentExecutingPayment.current || !orderId || !payment || orderStatus !== PayOrderStatus.EXECUTING_ORDER)
|
|
57
78
|
return;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
}, [
|
|
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);
|
package/dist/lib/api/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/api/index.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
if (!intervalMs)
|
|
171
|
+
const onOrderStatusEvent = useCallback((event) => {
|
|
172
|
+
if (!("payorder_id" in event) || event.payorder_id !== payOrder?.id)
|
|
173
173
|
return;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
"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/
|
|
64
|
-
"@coin-voyage/
|
|
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",
|