@coin-voyage/paykit 2.4.2 → 2.4.3-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.
@@ -19,7 +19,7 @@ export default function Confirmation() {
19
19
  onSuccess();
20
20
  }
21
21
  }, [uiState, onSuccess]);
22
- return (_jsx(PageContent, { "$center": true, children: _jsxs(ModalContent, { "$center": true, style: { paddingBottom: 0 }, children: [_jsx(AnimationContainer, { children: _jsxs(InsetContainer, { children: [_jsx(Spinner, { "$status": uiState === "loading" }), _jsx(SuccessIcon, { "$status": uiState === "success" }), _jsx(ErrorIcon, { "$status": uiState === "error" }), _jsx(WarningIcon, { "$status": uiState === "warning" })] }) }), _jsx(ModalH1, { children: title }), txURL && (_jsxs(Link, { href: txURL, target: "_blank", rel: "noopener noreferrer", children: [_jsx(ExternalLinkIcon, { width: 14, height: 14, fillOpacity: 0.9 }), "view transaction"] })), _jsx(PoweredByFooter, {})] }) }));
22
+ return (_jsx(PageContent, { "$center": true, children: _jsxs(ModalContent, { "$center": true, style: { paddingBottom: 0 }, children: [_jsx(AnimationContainer, { children: _jsxs(InsetContainer, { children: [_jsx(Spinner, { "$status": uiState === "loading" }), _jsx(SuccessIcon, { "$status": uiState === "success" }), _jsx(ErrorIcon, { "$status": uiState === "error" }), _jsx(WarningIcon, { "$status": uiState === "warning" })] }) }), _jsx(ModalH1, { children: title }), txURL && (_jsxs(Link, { href: txURL, target: "_blank", rel: "noopener noreferrer", className: "text-sm", children: [_jsx(ExternalLinkIcon, { width: 14, height: 14, fillOpacity: 0.9 }), "View Transaction"] })), _jsx(PoweredByFooter, {})] }) }));
23
23
  }
24
24
  function getConfirmationState(payOrder, isDeposit, locales, optimisticConfirmation) {
25
25
  if (!payOrder) {
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useAccount } from "@coin-voyage/crypto/hooks";
3
- import { getChainLogo, getChainTypeByChainId } from "@coin-voyage/shared/chain";
4
- import { ChainType, PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
3
+ import { getChainLogo } from "@coin-voyage/shared/chain";
4
+ import { PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
5
5
  import { assert } from "@coin-voyage/shared/utils";
6
6
  import { AnimatePresence, motion } from "framer-motion";
7
7
  import { useCallback, useEffect, useMemo, useState } from "react";
8
- import { useChainId, useSwitchChain } from "wagmi";
9
8
  import { AlertIcon, RetryIconCircle } from "../../../assets/icons";
10
9
  import useLocales from "../../../hooks/useLocales";
11
10
  import styled from "../../../styles/styled";
@@ -19,7 +18,6 @@ import Tooltip from "../../ui/Tooltip";
19
18
  var PayState;
20
19
  (function (PayState) {
21
20
  PayState["RequestingPayment"] = "Requesting Payment";
22
- PayState["SwitchingChain"] = "Switching Chain";
23
21
  PayState["RequestCancelled"] = "Payment Cancelled";
24
22
  PayState["RequestSuccessful"] = "Payment Successful";
25
23
  })(PayState || (PayState = {}));
@@ -29,30 +27,9 @@ export default function PayWithToken() {
29
27
  const { account } = useAccount();
30
28
  const [payState, setPayState] = useState(PayState.RequestingPayment);
31
29
  const locales = useLocales();
32
- const walletChainId = useChainId();
33
- const { switchChainAsync } = useSwitchChain();
34
30
  assert(payOrder !== undefined, "Pay order must be defined");
35
31
  const isExpired = payOrder.status === PayOrderStatus.EXPIRED;
36
32
  const isDeposit = payOrder.mode === PayOrderMode.DEPOSIT;
37
- const chainType = getChainTypeByChainId(paymentState.selectedCurrencyOption?.chain_id);
38
- const trySwitchingChain = useCallback(async (option, forceSwitch = false) => {
39
- if (walletChainId === option.chain_id && !forceSwitch)
40
- return true;
41
- try {
42
- const switched = await switchChainAsync({ chainId: option.chain_id });
43
- return switched?.id === option.chain_id;
44
- }
45
- catch (e) {
46
- console.error("Failed to switch chain", e);
47
- return false;
48
- }
49
- }, [walletChainId, switchChainAsync]);
50
- const ensureCorrectChain = useCallback(async (token) => {
51
- if (chainType !== ChainType.EVM)
52
- return true;
53
- setPayState(PayState.SwitchingChain);
54
- return trySwitchingChain(token);
55
- }, [chainType, trySwitchingChain]);
56
33
  const executePayment = useCallback(async (token) => {
57
34
  const txHash = await payFromWallet(token);
58
35
  if (!txHash)
@@ -66,10 +43,6 @@ export default function PayWithToken() {
66
43
  const handleTransfer = useCallback(async (token) => {
67
44
  if (isRestricted)
68
45
  return;
69
- if (!(await ensureCorrectChain(token))) {
70
- setPayState(PayState.RequestCancelled);
71
- return;
72
- }
73
46
  setPayState(PayState.RequestingPayment);
74
47
  try {
75
48
  await executePayment(token);
@@ -77,17 +50,10 @@ export default function PayWithToken() {
77
50
  setTimeout(() => setRoute(ROUTE.CONFIRMATION), 200);
78
51
  }
79
52
  catch (e) {
80
- if (e?.name === "ConnectorChainMismatchError") {
81
- log("Chain mismatch detected, retrying");
82
- if (await trySwitchingChain(token, true)) {
83
- await executePayment(token);
84
- return;
85
- }
86
- }
87
53
  setPayState(PayState.RequestCancelled);
88
54
  log("Failed to pay with token", e);
89
55
  }
90
- }, [isRestricted, ensureCorrectChain, executePayment, trySwitchingChain, setPayState, setRoute, log]);
56
+ }, [isRestricted, executePayment, setPayState, setRoute, log]);
91
57
  useEffect(() => {
92
58
  if (!selectedCurrencyOption)
93
59
  return;
@@ -109,7 +75,7 @@ export default function PayWithToken() {
109
75
  handleTransfer(selectedCurrencyOption);
110
76
  }
111
77
  };
112
- return (_jsxs(PageContent, { children: [_jsx(LoadingContainer, { children: _jsxs(AnimationContainer, { "$shake": payState === PayState.RequestCancelled, "$circle": true, children: [_jsx(AnimatePresence, { children: payState === PayState.RequestCancelled || (isExpired && isDeposit) ? (_jsx(RetryButton, { "aria-label": "Retry", initial: { opacity: 0, scale: 0.8 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.8 }, whileTap: { scale: 0.9 }, transition: { duration: 0.1 }, onClick: onRetry, children: _jsx(RetryIconContainer, { children: _jsx(Tooltip, { open: payState === PayState.RequestCancelled, message: locales.tryAgainQuestion, xOffset: -6, children: _jsx(RetryIconCircle, {}) }) }) })) : (_jsx(ChainLogoContainer, { children: getChainLogo(selectedCurrencyOption?.chain_id) }, "ChainLogoContainer")) }), _jsx(AnimatePresence, { children: _jsx(CircleSpinner, { logo: _jsx("img", { src: selectedCurrencyOption?.image_uri, alt: selectedCurrencyOption?.ticker }, selectedCurrencyOption?.image_uri), loading: payState === PayState.RequestingPayment && !isExpired, unavailable: false }, "CircleSpinner") })] }) }), !isExpired ? (_jsxs(ModalContent, { children: [payState === PayState.RequestCancelled && (_jsxs(_Fragment, { children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.injectionScreen_rejected_h1] }), _jsx(ModalBody, { children: locales.injectionScreen_rejected_p })] })), payState === PayState.SwitchingChain && _jsx(ModalH1, { children: locales.switchNetworkScreen_heading }), payState === PayState.RequestingPayment && (_jsxs(_Fragment, { children: [_jsx(ModalH1, { children: locales.requesting_payment_h1 }), _jsx(ModalBody, { children: locales.requesting_payment_p })] })), payState === PayState.RequestSuccessful && _jsx(ModalH1, { children: locales.injectionScreen_connected_h1 })] })) : (_jsxs(ModalContent, { children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.payWithTokenScreen_expired_h1] }), _jsx(ModalBody, { children: locales.payWithTokenScreen_expired_p })] }))] }));
78
+ return (_jsxs(PageContent, { children: [_jsx(LoadingContainer, { children: _jsxs(AnimationContainer, { "$shake": payState === PayState.RequestCancelled, "$circle": true, children: [_jsx(AnimatePresence, { children: payState === PayState.RequestCancelled || (isExpired && isDeposit) ? (_jsx(RetryButton, { "aria-label": "Retry", initial: { opacity: 0, scale: 0.8 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.8 }, whileTap: { scale: 0.9 }, transition: { duration: 0.1 }, onClick: onRetry, children: _jsx(RetryIconContainer, { children: _jsx(Tooltip, { open: payState === PayState.RequestCancelled, message: locales.tryAgainQuestion, xOffset: -6, children: _jsx(RetryIconCircle, {}) }) }) })) : (_jsx(ChainLogoContainer, { children: getChainLogo(selectedCurrencyOption?.chain_id) }, "ChainLogoContainer")) }), _jsx(AnimatePresence, { children: _jsx(CircleSpinner, { logo: _jsx("img", { src: selectedCurrencyOption?.image_uri, alt: selectedCurrencyOption?.ticker }, selectedCurrencyOption?.image_uri), loading: payState === PayState.RequestingPayment && !isExpired, unavailable: false }, "CircleSpinner") })] }) }), !isExpired ? (_jsxs(ModalContent, { children: [payState === PayState.RequestCancelled && (_jsxs(_Fragment, { children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.injectionScreen_rejected_h1] }), _jsx(ModalBody, { children: locales.injectionScreen_rejected_p })] })), payState === PayState.RequestingPayment && (_jsxs(_Fragment, { children: [_jsx(ModalH1, { children: locales.requesting_payment_h1 }), _jsx(ModalBody, { children: locales.requesting_payment_p })] })), payState === PayState.RequestSuccessful && _jsx(ModalH1, { children: locales.injectionScreen_connected_h1 })] })) : (_jsxs(ModalContent, { children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.payWithTokenScreen_expired_h1] }), _jsx(ModalBody, { children: locales.payWithTokenScreen_expired_p })] }))] }));
113
79
  }
114
80
  const LoadingContainer = styled(motion.div) `
115
81
  display: flex;
@@ -1,4 +1,4 @@
1
- import type { ChainType, PayOrder } from "@coin-voyage/shared/types";
1
+ import { type ChainType, type PayOrder } from "@coin-voyage/shared/types";
2
2
  import type { CurrencyAndQuoteID } from "../types/state";
3
3
  interface PayFromWalletParams {
4
4
  senderAddr: string | undefined;
@@ -1,8 +1,56 @@
1
1
  import { usePrepareTransaction } from "@coin-voyage/crypto/hooks";
2
- import { getDepositAddress, getWalletPaymentData } from "@coin-voyage/shared/payment";
2
+ import { PaymentRail, StepKind, } from "@coin-voyage/shared/types";
3
3
  import { assert } from "@coin-voyage/shared/utils";
4
4
  import { useBackendApi } from "../components/contexts/api";
5
5
  import { fetchPaymentDetails } from "../lib/api/payment-details";
6
+ async function executeWalletStep({ actions, paymentData, senderAddr, step, }) {
7
+ if (step.kind === StepKind.KIND_TRANSACTION) {
8
+ assert(Boolean(step.data?.crypto), "Transaction step is missing executable wallet data");
9
+ return actions.execute({
10
+ from: senderAddr,
11
+ paymentData: step.data?.crypto,
12
+ });
13
+ }
14
+ assert(step.kind === StepKind.KIND_DEPOSIT, "Unsupported wallet payment step");
15
+ assert(Boolean(step.deposit_address), "Deposit step is missing a deposit address");
16
+ return actions.execute({
17
+ amount: BigInt(paymentData.src.currency_amount.raw_amount),
18
+ from: senderAddr,
19
+ to: step.deposit_address,
20
+ chainId: paymentData.src.chain_id,
21
+ token: paymentData.src.address
22
+ ? { address: paymentData.src.address, decimals: paymentData.src.decimals }
23
+ : undefined,
24
+ });
25
+ }
26
+ async function executeWalletPayment({ actions, paymentData, senderAddr, log, }) {
27
+ let lastTxHash;
28
+ let sourceTxHash;
29
+ for (const [index, step] of paymentData.steps.entries()) {
30
+ if (step.rail !== PaymentRail.CRYPTO) {
31
+ throw new Error(`Unsupported payment rail for wallet execution: ${step.rail}`);
32
+ }
33
+ log(`[PAY-WITH-TOKEN] Executing step ${index + 1}/${paymentData.steps.length}: ${step.kind}`);
34
+ const txHash = await executeWalletStep({
35
+ actions,
36
+ paymentData,
37
+ senderAddr,
38
+ log,
39
+ step,
40
+ });
41
+ if (!txHash) {
42
+ return undefined;
43
+ }
44
+ if (!sourceTxHash && step.kind === StepKind.KIND_DEPOSIT) {
45
+ sourceTxHash = txHash;
46
+ }
47
+ lastTxHash = txHash;
48
+ log(`[PAY-WITH-TOKEN] Step ${index + 1}/${paymentData.steps.length} hash: ${txHash}`);
49
+ }
50
+ const txHash = sourceTxHash ?? lastTxHash;
51
+ assert(Boolean(txHash), "Payment execution did not produce a transaction hash");
52
+ return txHash;
53
+ }
6
54
  export function usePayFromWallet({ senderAddr, payOrder, setPayOrder, chainType, log }) {
7
55
  const actions = usePrepareTransaction(chainType);
8
56
  const api = useBackendApi();
@@ -25,24 +73,15 @@ export function usePayFromWallet({ senderAddr, payOrder, setPayOrder, chainType,
25
73
  const paymentData = paymentDetails.data;
26
74
  log(`[PAY-WITH-TOKEN] Final Quote for Order: ${JSON.stringify(paymentDetails)}, params: ${JSON.stringify(params)}`);
27
75
  try {
28
- const walletPaymentData = getWalletPaymentData(paymentData);
29
- const depositAddress = getDepositAddress(paymentData);
30
- assert(Boolean(walletPaymentData || depositAddress), "Payment step is missing executable wallet data");
31
- const txHash = walletPaymentData
32
- ? await actions.execute({
33
- from: senderAddr,
34
- chainId: paymentData.src.chain_id,
35
- paymentData: walletPaymentData,
36
- })
37
- : await actions.execute({
38
- amount: BigInt(paymentData.src.currency_amount.raw_amount),
39
- from: senderAddr,
40
- to: depositAddress,
41
- chainId: paymentData.src.chain_id,
42
- token: paymentData.src.address
43
- ? { address: paymentData.src.address, decimals: paymentData.src.decimals }
44
- : undefined,
45
- });
76
+ const txHash = await executeWalletPayment({
77
+ actions,
78
+ paymentData,
79
+ senderAddr,
80
+ log,
81
+ });
82
+ if (!txHash) {
83
+ return undefined;
84
+ }
46
85
  const nextPaymentData = {
47
86
  ...paymentData,
48
87
  source_tx_hash: txHash,
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { getDepositAddress, getFiatPaymentStep, getWalletPaymentStep } from "@coin-voyage/shared/payment";
3
2
  import { useInWagmiContext } from "@coin-voyage/crypto/evm";
4
3
  import { useAccount, useConnectCallback } from "@coin-voyage/crypto/hooks";
4
+ import { getDepositAddress, getPaymentStep } from "@coin-voyage/shared/payment";
5
5
  import { PaymentMethod, PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
6
6
  import { Buffer } from "buffer";
7
7
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
@@ -35,6 +35,7 @@ export const PayKitProvider = ({ apiKey, environment = "production", ...props })
35
35
  };
36
36
  function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, options, onConnect, onConnectValidation, onDisconnect, debugMode = false, children, }) {
37
37
  const { account } = useAccount();
38
+ const { isConnected, chainType } = account;
38
39
  const [allowedWallets, setAllowedWallets] = useState(onConnectValidation ? [] : null);
39
40
  useConnectCallback({
40
41
  onConnect,
@@ -160,7 +161,7 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
160
161
  }
161
162
  }, [setOpen, setPayId, setConnectorChainType]);
162
163
  useEffect(() => {
163
- const intervalMs = getPollingIntervalMs(payOrder);
164
+ const intervalMs = getPollingIntervalMs(payOrder?.status, payOrder?.mode);
164
165
  if (!intervalMs)
165
166
  return;
166
167
  const timeoutId = setTimeout(refreshOrder, intervalMs);
@@ -174,7 +175,6 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
174
175
  const showModal = useCallback((modalOptions) => {
175
176
  setModalOptions(modalOptions);
176
177
  setOpen(true);
177
- const { isConnected, chainType } = account;
178
178
  const { payOrder, paymentMethod } = paymentState;
179
179
  const status = payOrder?.status;
180
180
  const payment = payOrder?.payment;
@@ -186,18 +186,18 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
186
186
  return;
187
187
  }
188
188
  if (status === PayOrderStatus.AWAITING_PAYMENT && payment) {
189
- const fiatStep = getFiatPaymentStep(payment);
190
- const walletStep = getWalletPaymentStep(payment);
191
- const depositAddress = getDepositAddress(payment);
192
- if (paymentMethod === PaymentMethod.CARD && fiatStep) {
189
+ if (paymentMethod === PaymentMethod.CARD) {
193
190
  setRoute(ROUTE.CARD_PAYMENT);
194
191
  return;
195
192
  }
196
- if (paymentMethod === PaymentMethod.WALLET && (walletStep || depositAddress)) {
193
+ const step = getPaymentStep(payment);
194
+ const isTransactionStep = step?.kind === "transaction";
195
+ if (paymentMethod === PaymentMethod.WALLET || isTransactionStep) {
197
196
  paymentState.setSelectedCurrencyOption(payment.src);
198
197
  setRoute(ROUTE.WALLET_PAYMENT);
199
198
  return;
200
199
  }
200
+ const depositAddress = getDepositAddress(payment);
201
201
  if (paymentMethod === PaymentMethod.DEPOSIT_ADDRESS && depositAddress) {
202
202
  paymentState.setPayToAddressChainId(payment.src.chain_id);
203
203
  paymentState.setPayToAddressCurrency(payment.src);
@@ -206,7 +206,7 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
206
206
  }
207
207
  }
208
208
  setRoute(isConnected ? ROUTE.WALLET_TOKEN_SELECT : ROUTE.SELECT_METHOD);
209
- }, [account.isConnected, account.chainType, paymentState, setOpen]);
209
+ }, [isConnected, chainType, paymentState, setOpen]);
210
210
  const triggerResize = useCallback(() => onResize((prev) => prev + 1), []);
211
211
  const displayError = useCallback((message, code) => {
212
212
  setErrorMessage(message);
@@ -252,17 +252,17 @@ function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, op
252
252
  // === Helper functions ===
253
253
  const NON_FINAL_PAY_ORDER_STATUSES = [PayOrderStatus.PENDING, PayOrderStatus.AWAITING_PAYMENT, PayOrderStatus.EXPIRED];
254
254
  const isFinalPayOrderStatus = (status) => !!status && !NON_FINAL_PAY_ORDER_STATUSES.includes(status);
255
- const getPollingIntervalMs = (payOrder) => {
256
- if (!payOrder?.status)
255
+ const getPollingIntervalMs = (status, mode) => {
256
+ if (!status)
257
257
  return null;
258
- if (payOrder.status === PayOrderStatus.AWAITING_PAYMENT) {
258
+ if (status === PayOrderStatus.AWAITING_PAYMENT) {
259
259
  return 5000;
260
260
  }
261
- if ([PayOrderStatus.AWAITING_CONFIRMATION, PayOrderStatus.OPTIMISTIC_CONFIRMED].includes(payOrder.status)) {
261
+ if ([PayOrderStatus.AWAITING_CONFIRMATION, PayOrderStatus.OPTIMISTIC_CONFIRMED].includes(status)) {
262
262
  return 2500;
263
263
  }
264
- if (payOrder.status === PayOrderStatus.EXECUTING_ORDER) {
265
- return payOrder.mode === PayOrderMode.DEPOSIT ? 1000 : 2500;
264
+ if (status === PayOrderStatus.EXECUTING_ORDER) {
265
+ return mode === PayOrderMode.DEPOSIT ? 1000 : 2500;
266
266
  }
267
267
  return null;
268
268
  };
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.2",
4
+ "version": "2.4.3-beta.0",
5
5
  "private": false,
6
6
  "sideEffects": false,
7
7
  "author": "Lars <lars@coinvoyage.io>",
@@ -63,8 +63,8 @@
63
63
  "@stripe/crypto": "0.0.4",
64
64
  "styled-components": "^5.3.11",
65
65
  "uuid": "13.0.0",
66
- "@coin-voyage/crypto": "2.4.0",
67
- "@coin-voyage/shared": "2.4.2"
66
+ "@coin-voyage/shared": "2.4.3-beta.0",
67
+ "@coin-voyage/crypto": "2.4.3-beta.0"
68
68
  },
69
69
  "devDependencies": {
70
70
  "@types/qrcode": "1.5.5",