@coin-voyage/paykit 2.4.5-beta.2 → 2.4.5-beta.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.
@@ -1 +1,8 @@
1
+ import type { StripeOnramp } from "@stripe/crypto/types";
2
+ import { type ReactNode } from "react";
1
3
  export default function CardPayment(): import("react/jsx-runtime").JSX.Element;
4
+ export declare const CryptoElements: ({ stripeOnramp, children, }: {
5
+ stripeOnramp: StripeOnramp | Promise<StripeOnramp | null>;
6
+ children: ReactNode;
7
+ }) => import("react/jsx-runtime").JSX.Element;
8
+ export declare const useStripeOnrampContext: () => StripeOnramp | null | undefined;
@@ -1,9 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { getFiatPaymentData } from "@coin-voyage/shared/payment";
3
3
  import { PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
4
- import { loadStripeOnramp } from "@stripe/crypto/pure";
4
+ import { loadStripeOnramp } from "@stripe/crypto";
5
5
  import { useQuery } from "@tanstack/react-query";
6
- import { memo, useCallback, useEffect, useMemo, useRef } from "react";
6
+ import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { AlertIcon } from "../../../assets/icons";
8
8
  import useLocales from "../../../hooks/useLocales";
9
9
  import styled from "../../../styles/styled";
@@ -15,11 +15,7 @@ import { OrderHeader } from "../../ui/OrderHeader";
15
15
  import PoweredByFooter from "../../ui/PoweredByFooter";
16
16
  import { Spinner } from "../../ui/Spinner";
17
17
  export default function CardPayment() {
18
- const { triggerResize } = usePayContext();
19
18
  const { data, isLoading, error, refetch } = useCardPaymentData();
20
- useEffect(() => {
21
- triggerResize();
22
- }, [triggerResize, isLoading, data?.session_id, data?.client_secret, error]);
23
19
  return (_jsxs(PageContent, { children: [_jsx(OrderHeader, { minified: true }), _jsx(CardPaymentContent, { paymentData: data, isLoading: isLoading, error: error instanceof Error ? error : null, onRetry: () => {
24
20
  void refetch();
25
21
  } }), _jsx(PoweredByFooter, {})] }));
@@ -66,29 +62,14 @@ function StatusCard({ title, body, actionLabel, onAction, loading = false, warni
66
62
  }, children: [loading ? _jsx(Spinner, {}) : warning ? _jsx(AlertIcon, {}) : null, _jsx(ModalH1, { "$warning": warning, children: title }), _jsx(ModalBody, { children: body }), actionLabel && onAction ? (_jsx(ActionRow, { children: _jsx(Button, { onClick: onAction, children: actionLabel }) })) : null] }));
67
63
  }
68
64
  function StripeOnrampCheckout({ paymentData }) {
69
- const { paymentState, mode, triggerResize } = usePayContext();
70
- const { data: stripeOnramp, isLoading, error, refetch } = useStripeOnramp(paymentData.stripe_publishable_key);
71
- const refreshOrderRef = useLatestRef(paymentState.refreshOrder);
65
+ const { mode, triggerResize } = usePayContext();
66
+ const { data: stripeOnramp, isLoading, error, refetch } = useLoadStripeOnramp(paymentData.stripe_publishable_key);
72
67
  const refreshDebounceRef = useRef(null);
73
- const lastStatusRef = useRef(null);
74
- const theme = useMemo(() => {
75
- return mode === "dark" ? "dark" : "light";
68
+ const appearance = useMemo(() => {
69
+ return {
70
+ theme: mode === "dark" ? "dark" : "light",
71
+ };
76
72
  }, [mode]);
77
- const handleSessionUpdate = useCallback((status) => {
78
- if (status === lastStatusRef.current)
79
- return;
80
- lastStatusRef.current = status;
81
- const shouldRefresh = status === "fulfillment_complete" || status === "rejected";
82
- if (!shouldRefresh) {
83
- return;
84
- }
85
- if (refreshDebounceRef.current) {
86
- window.clearTimeout(refreshDebounceRef.current);
87
- }
88
- refreshDebounceRef.current = window.setTimeout(() => {
89
- void refreshOrderRef.current();
90
- }, 500);
91
- }, [refreshOrderRef]);
92
73
  useEffect(() => {
93
74
  return () => {
94
75
  if (refreshDebounceRef.current) {
@@ -96,44 +77,41 @@ function StripeOnrampCheckout({ paymentData }) {
96
77
  }
97
78
  };
98
79
  }, []);
80
+ useEffect(() => {
81
+ triggerResize();
82
+ }, [error, isLoading, stripeOnramp, triggerResize]);
99
83
  if (error) {
100
84
  return (_jsx(StatusCard, { warning: true, title: "Stripe unavailable", body: error.message, actionLabel: "Try again", onAction: () => {
101
85
  void refetch();
102
86
  } }));
103
87
  }
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] }));
88
+ return (_jsxs(OnrampShell, { children: [isLoading ? (_jsx(OnrampOverlay, { children: _jsx(Spinner, {}) })) : null, paymentData.client_secret && stripeOnramp ? (_jsx(CryptoElements, { stripeOnramp: stripeOnramp, children: _jsx(OnrampElement, { id: "onramp-element", clientSecret: paymentData.client_secret, appearance: appearance, onReady: triggerResize }) })) : (_jsx(OnrampOverlay, { children: _jsx(Spinner, {}) }))] }));
105
89
  }
106
- const OnrampSession = memo(function OnrampSession({ stripeOnramp, clientSecret, theme, onUiLoaded, onSessionUpdate, }) {
107
- const mountNodeRef = useRef(null);
108
- const onUiLoadedRef = useLatestRef(onUiLoaded);
109
- const onSessionUpdateRef = useLatestRef(onSessionUpdate);
90
+ function OnrampElement({ clientSecret, appearance, onReady, onChange, ...props }) {
91
+ const stripeOnramp = useStripeOnrampContext();
92
+ const onrampElementRef = useRef(null);
93
+ const [session, setSession] = useState();
94
+ const appearanceJSON = JSON.stringify(appearance);
110
95
  useEffect(() => {
111
- const mountNode = mountNodeRef.current;
112
- if (!mountNode) {
113
- return;
96
+ const containerRef = onrampElementRef.current;
97
+ if (containerRef) {
98
+ // NB: ideally we want to be able to hot swap/update onramp iframe
99
+ // This currently results a flash if one needs to mint a new session when they need to update fixed transaction details
100
+ containerRef.innerHTML = "";
101
+ if (clientSecret && stripeOnramp) {
102
+ setSession(stripeOnramp
103
+ .createSession({
104
+ clientSecret,
105
+ appearance: appearanceJSON ? JSON.parse(appearanceJSON) : {},
106
+ })
107
+ .mount(containerRef));
108
+ }
114
109
  }
115
- mountNode.replaceChildren();
116
- const session = stripeOnramp.createSession({
117
- clientSecret,
118
- appearance: theme ? { theme } : undefined,
119
- });
120
- const handleUiLoaded = () => {
121
- onUiLoadedRef.current?.();
122
- };
123
- const handleSessionUpdated = (event) => {
124
- onSessionUpdateRef.current?.(event.payload.session.status);
125
- };
126
- session.addEventListener("onramp_ui_loaded", handleUiLoaded);
127
- session.addEventListener("onramp_session_updated", handleSessionUpdated);
128
- session.mount(mountNode);
129
- return () => {
130
- session.removeEventListener("onramp_ui_loaded", handleUiLoaded);
131
- session.removeEventListener("onramp_session_updated", handleSessionUpdated);
132
- mountNode.replaceChildren();
133
- };
134
- }, [clientSecret, onSessionUpdateRef, onUiLoadedRef, stripeOnramp, theme]);
135
- return _jsx(OnrampMount, { ref: mountNodeRef });
136
- });
110
+ }, [appearanceJSON, clientSecret, stripeOnramp]);
111
+ useOnrampSessionListener("onramp_ui_loaded", session, onReady);
112
+ useOnrampSessionListener("onramp_session_updated", session, onChange);
113
+ return _jsx(OnrampMount, { ...props, ref: onrampElementRef });
114
+ }
137
115
  function useCardPaymentData() {
138
116
  const { paymentState } = usePayContext();
139
117
  const { payOrder, payWithCard } = paymentState;
@@ -163,41 +141,73 @@ function useCardPaymentData() {
163
141
  data: paymentData ?? query.data,
164
142
  };
165
143
  }
166
- function useStripeOnramp(publishableKey) {
144
+ const stripeOnrampPromises = new Map();
145
+ function getStripeOnramp(publishableKey) {
146
+ const existingPromise = stripeOnrampPromises.get(publishableKey);
147
+ if (existingPromise) {
148
+ return existingPromise;
149
+ }
150
+ const stripeOnrampPromise = loadStripeOnramp(publishableKey).catch((error) => {
151
+ stripeOnrampPromises.delete(publishableKey);
152
+ throw error;
153
+ });
154
+ stripeOnrampPromises.set(publishableKey, stripeOnrampPromise);
155
+ return stripeOnrampPromise;
156
+ }
157
+ function useLoadStripeOnramp(publishableKey) {
167
158
  return useQuery({
168
159
  queryKey: ["stripe-onramp", publishableKey],
169
160
  enabled: Boolean(publishableKey),
170
161
  staleTime: Infinity,
171
- retry: false,
162
+ retry: 1,
163
+ refetchOnWindowFocus: false,
172
164
  queryFn: async () => {
173
- if (!publishableKey) {
174
- throw new Error("Missing Stripe publishable key.");
165
+ const stripeOnramp = await getStripeOnramp(publishableKey);
166
+ if (!stripeOnramp) {
167
+ throw new Error("Stripe checkout can only be loaded in a browser.");
175
168
  }
176
- const instance = await getStripeOnrampPromise(publishableKey);
177
- if (!instance) {
178
- throw new Error("Stripe Onramp is only available in the browser.");
179
- }
180
- return instance;
169
+ return stripeOnramp;
181
170
  },
182
171
  });
183
172
  }
184
- const stripeOnrampPromiseCache = new Map();
185
- function getStripeOnrampPromise(publishableKey) {
186
- const cached = stripeOnrampPromiseCache.get(publishableKey);
187
- if (cached) {
188
- return cached;
189
- }
190
- const nextPromise = loadStripeOnramp(publishableKey);
191
- stripeOnrampPromiseCache.set(publishableKey, nextPromise);
192
- return nextPromise;
193
- }
194
- function useLatestRef(value) {
195
- const ref = useRef(value);
173
+ // ReactContext to simplify access of StripeOnramp object
174
+ const CryptoElementsContext = createContext(null);
175
+ CryptoElementsContext.displayName = "CryptoElementsContext";
176
+ export const CryptoElements = ({ stripeOnramp, children, }) => {
177
+ const [ctx, setContext] = useState(() => ({
178
+ onramp: null,
179
+ }));
196
180
  useEffect(() => {
197
- ref.current = value;
198
- }, [value]);
199
- return ref;
200
- }
181
+ let isMounted = true;
182
+ Promise.resolve(stripeOnramp).then((onramp) => {
183
+ if (onramp && isMounted) {
184
+ setContext((ctx) => (ctx.onramp === onramp ? ctx : { onramp }));
185
+ }
186
+ });
187
+ return () => {
188
+ isMounted = false;
189
+ };
190
+ }, [stripeOnramp]);
191
+ return _jsx(CryptoElementsContext.Provider, { value: ctx, children: children });
192
+ };
193
+ // React hook to get StripeOnramp from context
194
+ export const useStripeOnrampContext = () => {
195
+ const context = useContext(CryptoElementsContext);
196
+ return context?.onramp;
197
+ };
198
+ // React element to render Onramp UI
199
+ const useOnrampSessionListener = (type, session, callback) => {
200
+ useEffect(() => {
201
+ if (session && callback) {
202
+ const listener = (e) => callback(e.payload);
203
+ session.addEventListener(type, listener);
204
+ return () => {
205
+ session.removeEventListener(type, listener);
206
+ };
207
+ }
208
+ return () => { };
209
+ }, [session, callback, type]);
210
+ };
201
211
  const OnrampShell = styled.div `
202
212
  position: relative;
203
213
  min-height: 480px;
@@ -12,6 +12,7 @@ import { ModalContent, PageContent } from "../../ui/Modal/styles";
12
12
  import { ScrollArea } from "../../ui/ScrollArea";
13
13
  import { Spinner } from "../../ui/Spinner";
14
14
  import { Container, WalletIcon, WalletItem, WalletLabel, WalletList } from "./styles";
15
+ import { getConnectorId } from "@coin-voyage/crypto/utils";
15
16
  const MoreIcon = (_jsx("svg", { width: "60", height: "60", viewBox: "0 0 60 60", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M30 42V19M19 30.5H42", stroke: "var(--ck-body-color-muted)", strokeWidth: "3", strokeLinecap: "round" }) }));
16
17
  export default function MobileConnectors() {
17
18
  const { paymentState } = usePayContext();
@@ -29,7 +30,7 @@ export default function MobileConnectors() {
29
30
  // check if wallet is supports currentChainType
30
31
  if (connectorChainType && wallet.chainTypes && !wallet.chainTypes.includes(connectorChainType))
31
32
  return false;
32
- if (wallets.find((w) => w.connectors.find((c) => c.connector.id === walletId)))
33
+ if (wallets.find((w) => w.connectors.find((c) => getConnectorId(c.connector) === walletId)))
33
34
  return false;
34
35
  return true;
35
36
  }) ?? [];
@@ -3,7 +3,7 @@ import { useConfig as useBigmiConfig } from "@bigmi/react";
3
3
  import { Arbitrum, Base, Bitcoin, Ethereum, Solana, Sui } from "@coin-voyage/shared/chain";
4
4
  import { ChainType, PayOrderMode } from "@coin-voyage/shared/types";
5
5
  import { truncateAddress, truncateENSName } from "@coin-voyage/shared/utils";
6
- import { useWallets } from "@mysten/dapp-kit";
6
+ import { useWallets } from "@mysten/dapp-kit-react";
7
7
  import { useWallet } from "@solana/wallet-adapter-react";
8
8
  import { useCallback, useMemo } from "react";
9
9
  import { useConfig as useWagmiConfig } from "wagmi";
@@ -1,7 +1,6 @@
1
- import { useAccount } from "@coin-voyage/crypto/hooks";
1
+ import { useAccount, useSuiNSName } from "@coin-voyage/crypto/hooks";
2
2
  import { zPayOrder } from "@coin-voyage/shared/schemas";
3
3
  import { ChainType, PayOrderMode, } from "@coin-voyage/shared/types";
4
- import { useResolveSuiNSName } from "@mysten/dapp-kit";
5
4
  import { useCallback, useRef, useState } from "react";
6
5
  import { mainnet } from "viem/chains";
7
6
  import { useEnsName } from "wagmi";
@@ -21,7 +20,7 @@ export function usePaymentState({ payOrder, setPayOrder, setRoute, log, }) {
21
20
  selectedWallet,
22
21
  chainType: connectorChainType,
23
22
  });
24
- const { data: suiEnsName } = useResolveSuiNSName(senderAccount.address, {
23
+ const { data: suiEnsName } = useSuiNSName(senderAccount.address, {
25
24
  enabled: !!senderAccount.address && senderAccount.chainType === ChainType.SUI && senderAccount.isConnected,
26
25
  });
27
26
  const { data: evmEnsName } = useEnsName({
@@ -3,6 +3,7 @@ import { useInstalledWallets } from "@coin-voyage/crypto/hooks";
3
3
  import usePayContext from "../components/contexts/pay";
4
4
  import { walletConfigs } from "../lib/config/wallet";
5
5
  import { isInjectedConnector } from "../utils";
6
+ import { getConnectorId } from "@coin-voyage/crypto/utils";
6
7
  export const useWallets = () => {
7
8
  const { paymentState } = usePayContext();
8
9
  const installedWallets = useInstalledWallets(paymentState.connectorChainType);
@@ -54,8 +55,8 @@ export const useWallets = () => {
54
55
  self.find((w) => w.id === "io.metamask" || w.id === "io.metamask.mobile")))
55
56
  // order by isInstalled injected connectors first
56
57
  .sort((a, b) => {
57
- const AisInstalled = a.isInstalled && isInjectedConnector(a.connectors[0].connector.id);
58
- const BisInstalled = b.isInstalled && isInjectedConnector(b.connectors[0].connector.id);
58
+ const AisInstalled = a.isInstalled && isInjectedConnector(getConnectorId(a.connectors[0].connector));
59
+ const BisInstalled = b.isInstalled && isInjectedConnector(getConnectorId(b.connectors[0].connector));
59
60
  if (AisInstalled && !BisInstalled)
60
61
  return -1;
61
62
  if (!AisInstalled && BisInstalled)
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.5-beta.2",
4
+ "version": "2.4.5-beta.4",
5
5
  "private": false,
6
6
  "sideEffects": false,
7
7
  "author": "Lars <lars@coinvoyage.io>",
@@ -57,11 +57,11 @@
57
57
  "react-transition-state": "^1.1.4",
58
58
  "react-use-measure": "^2.1.1",
59
59
  "@stripe/stripe-js": "8.10.0",
60
- "@stripe/crypto": "0.0.4",
60
+ "@stripe/crypto": "1.0.2",
61
61
  "styled-components": "^5.3.11",
62
62
  "uuid": "13.0.0",
63
- "@coin-voyage/shared": "2.4.5-beta.0",
64
- "@coin-voyage/crypto": "2.4.4"
63
+ "@coin-voyage/crypto": "2.4.5-beta.1",
64
+ "@coin-voyage/shared": "2.4.5-beta.0"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/qrcode": "1.5.5",
@@ -70,8 +70,8 @@
70
70
  "typescript-plugin-styled-components": "^3.0.0"
71
71
  },
72
72
  "peerDependencies": {
73
- "@bigmi/react": "^0.6.1",
74
- "@mysten/dapp-kit": "^0.19.8",
73
+ "@bigmi/react": "^0.8.0",
74
+ "@mysten/dapp-kit-react": "^2.0.3",
75
75
  "@solana/wallet-adapter-react": "^0.15.39",
76
76
  "@tanstack/react-query": "^5.90.6",
77
77
  "react": "^18 || ^19",