UNPKG

@turnkey/react-wallet-kit

Version:

The easiest and most powerful way to integrate Turnkey's Embedded Wallets into your React applications.

241 lines (237 loc) 9.24 kB
'use strict'; var sdkTypes = require('@turnkey/sdk-types'); var react = require('react'); var core = require('@turnkey/core'); const GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"; const APPLE_AUTH_URL = "https://account.apple.com/auth/authorize"; const FACEBOOK_AUTH_URL = "https://www.facebook.com/v11.0/dialog/oauth"; const FACEBOOK_GRAPH_URL = "https://graph.facebook.com/v11.0/oauth/access_token"; const popupWidth = 500; const popupHeight = 600; const SESSION_WARNING_THRESHOLD_MS = 60 * 1000; // 1 minute in milliseconds const useDebouncedCallback = (fn, wait = 100) => { const timer = react.useRef(null); const fnRef = react.useRef(fn); fnRef.current = fn; return react.useCallback((...args) => { if (timer.current) clearTimeout(timer.current); timer.current = setTimeout(() => { fnRef.current(...args); timer.current = null; }, wait); }, [wait]); }; const isValidSession = session => { return session?.expiry !== undefined && session.expiry * 1000 > Date.now(); }; async function withTurnkeyErrorHandling(fn, callbacks, fallbackMessage = "An unknown error occurred", fallbackCode = sdkTypes.TurnkeyErrorCodes.UNKNOWN) { try { return await fn(); } catch (error) { if (error instanceof sdkTypes.TurnkeyError) { callbacks?.onError?.(error); throw error; } const tkError = new sdkTypes.TurnkeyError(fallbackMessage, fallbackCode, error); callbacks?.onError?.(tkError); throw tkError; } } // Helper function to parse Apple OAuth redirects function parseAppleOAuthRedirect(hash) { // Apple's format has unencoded parameters in the state portion const idTokenMatch = hash.match(/id_token=([^&]+)$/); const idToken = idTokenMatch ? idTokenMatch[1] : null; // Extract state parameters - state is at the beginning // It typically looks like: state=provider=apple&flow=redirect&publicKey=123...&openModal=true&code=...&id_token=... const stateEndIndex = hash.indexOf("&code="); if (stateEndIndex === -1) return { idToken, provider: null, flow: null, publicKey: null, openModal: null }; const stateContent = hash.substring(6, stateEndIndex); // Remove "state=" prefix const stateParams = new URLSearchParams(stateContent); return { idToken, provider: stateParams.get("provider"), flow: stateParams.get("flow"), publicKey: stateParams.get("publicKey"), openModal: stateParams.get("openModal") }; } // Helper function to parse Google OAuth redirects function parseGoogleOAuthRedirect(hash) { const hashParams = new URLSearchParams(hash); const idToken = hashParams.get("id_token"); const state = hashParams.get("state"); let provider = null; let flow = null; let publicKey = null; let openModal = null; if (state) { const stateParams = new URLSearchParams(state); provider = stateParams.get("provider"); flow = stateParams.get("flow"); publicKey = stateParams.get("publicKey"); openModal = stateParams.get("openModal"); } return { idToken, provider, flow, publicKey, openModal }; } // Main function to determine provider and parse accordingly function parseOAuthRedirect(hash) { // Check if this is an Apple redirect if (hash.startsWith("state=provider=apple")) { return parseAppleOAuthRedirect(hash); } else { return parseGoogleOAuthRedirect(hash); } } // Function to generate PKCE challenge pair for Facebook OAuth async function generateChallengePair() { const randomBytes = new Uint8Array(32); window.crypto.getRandomValues(randomBytes); const verifier = btoa(String.fromCharCode(...randomBytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); const encoder = new TextEncoder(); const data = encoder.encode(verifier); const digest = await window.crypto.subtle.digest("SHA-256", data); const base64Challenge = btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return { verifier, codeChallenge: base64Challenge }; } // Function to exchange Facebook authorization code for token async function exchangeCodeForToken(clientId, redirectUri, code, codeVerifier) { const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, code_verifier: codeVerifier, code: code, grant_type: "authorization_code" }); const response = await fetch(FACEBOOK_GRAPH_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: params.toString() }); if (!response.ok) { const errorData = await response.json(); throw new Error(`Facebook token exchange failed: ${JSON.stringify(errorData)}`); } return await response.json(); } async function handleFacebookPKCEFlow({ code, publicKey, openModal, clientId, redirectURI, callbacks, completeOauth, onPushPage }) { // Retrieve the verifier stored during OAuth initiation const verifier = sessionStorage.getItem("facebook_verifier"); if (!verifier) { throw new sdkTypes.TurnkeyError("Missing PKCE verifier for Facebook authentication", sdkTypes.TurnkeyErrorCodes.OAUTH_SIGNUP_ERROR); } try { // Exchange the code for a token const tokenData = await exchangeCodeForToken(clientId, redirectURI, code, verifier); // Clean up the verifier as it's no longer needed sessionStorage.removeItem("facebook_verifier"); // Handle different UI flows based on openModal parameter if (openModal === "true") { await onPushPage(tokenData.id_token); } else if (callbacks?.onOauthRedirect) { callbacks.onOauthRedirect({ idToken: tokenData.id_token, publicKey }); } else { await completeOauth({ oidcToken: tokenData.id_token, publicKey }); } // Clean up the URL after processing window.history.replaceState(null, document.title, window.location.pathname); return; } catch (error) { console.error("Error exchanging Facebook code for token:", error); throw new sdkTypes.TurnkeyError("Failed to complete Facebook authentication", sdkTypes.TurnkeyErrorCodes.OAUTH_SIGNUP_ERROR, error); } } // Custom hook to get the current screen size function useScreenSize() { const [width, setWidth] = react.useState(typeof window !== "undefined" ? window.innerWidth : 1024); react.useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, []); return { width, // I have no idea why but, Tailwind's responsive design breakpoints do not work. Throughout the modal components, you will see conditional styling using this `isMobile` variable. // This is fine since we only need to style for 2 screen sizes: mobile and desktop. If anyone can figure out why Tailwind's responsive design breakpoints do not work, please fix it and restyle the components accordingly, changing the `isMobile` to the Tailwind stuff when applicable. isMobile: width < 640 }; } function isWalletConnect(wallet) { return wallet.interfaceType == core.WalletInterfaceType.WalletConnect; } function useWalletProviderState(initialState = []) { const [walletProviders, setWalletProviders] = react.useState(initialState); const prevProvidersRef = react.useRef(initialState); function isSameWalletProvider(a, b) { if (a.length !== b.length) return false; const key = provider => { const name = provider.info.name; const namespace = provider.chainInfo.namespace; const interfaceType = provider.interfaceType; const connectedAddresses = [...provider.connectedAddresses].map(x => x.toLowerCase()).sort().join(","); return `${namespace}|${interfaceType}|${name}|${connectedAddresses}`; }; const A = a.map(key).sort(); const B = b.map(key).sort(); for (let i = 0; i < A.length; i++) if (A[i] !== B[i]) return false; return true; } const updateWalletProviders = react.useCallback(newProviders => { if (!isSameWalletProvider(prevProvidersRef.current, newProviders)) { prevProvidersRef.current = newProviders; setWalletProviders(newProviders); } // we do nothing if the wallet providers are the same }, []); return [walletProviders, updateWalletProviders]; } exports.APPLE_AUTH_URL = APPLE_AUTH_URL; exports.FACEBOOK_AUTH_URL = FACEBOOK_AUTH_URL; exports.FACEBOOK_GRAPH_URL = FACEBOOK_GRAPH_URL; exports.GOOGLE_AUTH_URL = GOOGLE_AUTH_URL; exports.SESSION_WARNING_THRESHOLD_MS = SESSION_WARNING_THRESHOLD_MS; exports.exchangeCodeForToken = exchangeCodeForToken; exports.generateChallengePair = generateChallengePair; exports.handleFacebookPKCEFlow = handleFacebookPKCEFlow; exports.isValidSession = isValidSession; exports.isWalletConnect = isWalletConnect; exports.parseOAuthRedirect = parseOAuthRedirect; exports.popupHeight = popupHeight; exports.popupWidth = popupWidth; exports.useDebouncedCallback = useDebouncedCallback; exports.useScreenSize = useScreenSize; exports.useWalletProviderState = useWalletProviderState; exports.withTurnkeyErrorHandling = withTurnkeyErrorHandling; //# sourceMappingURL=utils.js.map