@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
JavaScript
;
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