UNPKG

@web3auth/ws-embed

Version:

Embed script

611 lines (595 loc) 24.7 kB
import _defineProperty from '@babel/runtime/helpers/defineProperty'; import _objectSpread from '@babel/runtime/helpers/objectSpread2'; import { randomId, CONFIRMATION_STRATEGY, SETUP_COMPLETE, getPopupFeatures, FEATURES_DEFAULT_POPUP_WINDOW, COMMUNICATION_JRPC_METHODS, FEATURES_CONFIRM_WINDOW, FEATURES_PROVIDER_CHANGE_WINDOW, PROVIDER_JRPC_METHODS } from '@toruslabs/base-controllers'; import { THEME_MODES, PostMessageStream, serializeError } from '@web3auth/auth'; import deepmerge from 'deepmerge'; import CommunicationProvider from './communicationProvider.js'; import { SOLANA_CHAIN_IDS, SOLANA_METHOD_TYPES, EVM_METHOD_TYPES, AUTH_PROVIDERS } from './constants.js'; import { htmlToElement } from './embedUtils.js'; import TorusInPageProvider from './inPageProvider.js'; import { WS_EMBED_LOGIN_MODE, WS_EMBED_BUILD_ENV, BUTTON_POSITION } from './interfaces.js'; import log from './loglevel.js'; import getSiteMetadata from './siteMetadata.js'; import { fetchProjectConfig } from './utils.js'; const getCombinedConnectionId = (authConnectionId, groupedAuthConnectionId) => { if (groupedAuthConnectionId) { return `${groupedAuthConnectionId}_${authConnectionId}`; } return authConnectionId; }; const mergeAuthConnectionConfig = (dashboardAuthConnectionConfig, userAuthConnectionConfig) => { const authProviders = new Set(AUTH_PROVIDERS); const dashboardAuthConnectionConfigMap = new Map(); // Build map of dashboard config items dashboardAuthConnectionConfig.forEach(item => { const dashboardAuthConnectionConfigItem = item; dashboardAuthConnectionConfigMap.set(getCombinedConnectionId(item.authConnectionId, item.groupedAuthConnectionId), dashboardAuthConnectionConfigItem); }); // Process user config items - only merge with matching dashboard items // Throw error if user config item doesn't match any dashboard config Object.entries(userAuthConnectionConfig).forEach(([authConnection, userAuthConnectionConfigItem]) => { if (!authProviders.has(authConnection)) { throw new Error(`Invalid auth connection: ${authConnection}.`); } const key = getCombinedConnectionId(userAuthConnectionConfigItem.authConnectionId, userAuthConnectionConfigItem.groupedAuthConnectionId); const dashboardItem = dashboardAuthConnectionConfigMap.get(key); if (!dashboardItem) { throw new Error(`Invalid auth connection config, authConnection: ${userAuthConnectionConfigItem.authConnection}. Missing AuthConnectionConfig from the dashboard.`); } // override the individual auth config item from dashboard with user specified config userAuthConnectionConfig[authConnection] = _objectSpread(_objectSpread({}, dashboardItem), userAuthConnectionConfigItem); }); return userAuthConnectionConfig; }; const version = "6.1.0"; const addedVersion = `v${version.split(".")[0]}` ; const WALLET_URLS_DEFAULT = { [WS_EMBED_BUILD_ENV.TESTING]: { url: "https://develop-wallet.web3auth.io", logLevel: "debug" }, [WS_EMBED_BUILD_ENV.DEVELOPMENT]: { url: "http://localhost:4050", logLevel: "debug" }, [WS_EMBED_BUILD_ENV.STAGING]: { url: `https://staging-wallet.web3auth.io/${addedVersion}`, logLevel: "debug" }, [WS_EMBED_BUILD_ENV.PRODUCTION]: { url: `https://wallet.web3auth.io/${addedVersion}`, logLevel: "error" } }; // preload for iframe doesn't work https://bugs.chromium.org/p/chromium/issues/detail?id=593267 (async function preLoadIframe() { try { if (typeof document === "undefined") return; const torusIframeHtml = document.createElement("link"); const { url: torusUrl } = WALLET_URLS_DEFAULT.production; torusIframeHtml.href = `${torusUrl}/frame`; torusIframeHtml.crossOrigin = "anonymous"; torusIframeHtml.type = "text/html"; torusIframeHtml.rel = "prefetch"; if (torusIframeHtml.relList && torusIframeHtml.relList.supports) { if (torusIframeHtml.relList.supports("prefetch")) { document.head.appendChild(torusIframeHtml); } } } catch (error) { log.warn(error); } })(); new Set([COMMUNICATION_JRPC_METHODS.SET_PROVIDER]); const PROVIDER_UNSAFE_METHODS = new Set([SOLANA_METHOD_TYPES.SEND_TRANSACTION, SOLANA_METHOD_TYPES.SIGN_MESSAGE, SOLANA_METHOD_TYPES.SIGN_TRANSACTION, SOLANA_METHOD_TYPES.SIGN_ALL_TRANSACTIONS, EVM_METHOD_TYPES.ETH_SIGN, EVM_METHOD_TYPES.ETH_SIGN_TYPED_DATA_V4, EVM_METHOD_TYPES.PERSONAL_SIGN, EVM_METHOD_TYPES.ETH_TRANSACTION, EVM_METHOD_TYPES.ADD_CHAIN, EVM_METHOD_TYPES.SWITCH_CHAIN, PROVIDER_JRPC_METHODS.WALLET_SWITCH_CHAIN]); class WsEmbed { constructor({ modalZIndex = 99999, web3AuthClientId, web3AuthNetwork }) { _defineProperty(this, "isInitialized", void 0); _defineProperty(this, "modalZIndex", void 0); _defineProperty(this, "provider", void 0); _defineProperty(this, "communicationProvider", void 0); _defineProperty(this, "buttonPosition", void 0); _defineProperty(this, "web3AuthClientId", void 0); _defineProperty(this, "web3AuthNetwork", void 0); _defineProperty(this, "loginMode", WS_EMBED_LOGIN_MODE.EMBED); _defineProperty(this, "enableLegacyConfig", false); _defineProperty(this, "confirmationStrategy", void 0); _defineProperty(this, "buildEnv", void 0); _defineProperty(this, "embedNonce", randomId()); this.isInitialized = false; // init done this.modalZIndex = modalZIndex; this.web3AuthClientId = web3AuthClientId; this.web3AuthNetwork = web3AuthNetwork; } get isLoggedIn() { var _this$communicationPr; return ((_this$communicationPr = this.communicationProvider) === null || _this$communicationPr === void 0 ? void 0 : _this$communicationPr.isLoggedIn) || false; } get getBuildEnv() { return this.buildEnv; } async init({ buildEnv = WS_EMBED_BUILD_ENV.PRODUCTION, enableLogging = false, chains, chainId, walletUrls, whiteLabel, confirmationStrategy = CONFIRMATION_STRATEGY.DEFAULT, loginMethodConfig = {}, accountAbstractionConfig, enableKeyExport = false, loginMode = WS_EMBED_LOGIN_MODE.EMBED, enableLegacyConfig = false }) { if (this.isInitialized) throw new Error("Already initialized"); this.enableLegacyConfig = enableLegacyConfig; this.loginMode = loginMode; this.buildEnv = buildEnv; this.confirmationStrategy = confirmationStrategy; const { url: torusUrl, logLevel } = walletUrls && walletUrls[buildEnv] ? _objectSpread(_objectSpread({}, WALLET_URLS_DEFAULT[buildEnv]), walletUrls[buildEnv]) : WALLET_URLS_DEFAULT[buildEnv]; log.info(torusUrl, "url loaded"); log.setDefaultLevel(logLevel); if (enableLogging) log.enableAll();else log.disableAll(); // if loginMode is embed, get the project config and initialize the auth connection config and white label let authConnectionConfig, finalWhiteLabel = whiteLabel; if (this.loginMode === WS_EMBED_LOGIN_MODE.EMBED) { const projectConfig = await this.getProjectConfig(); authConnectionConfig = (projectConfig.embeddedWalletAuth || []).filter(item => !item.isDefault); loginMethodConfig = await this.initializeAuthConnectionConfig(projectConfig, loginMethodConfig); finalWhiteLabel = this.initializeWhiteLabel(projectConfig, whiteLabel); } const torusIframeUrl = new URL(torusUrl); if (torusIframeUrl.pathname.endsWith("/")) torusIframeUrl.pathname += "frame";else torusIframeUrl.pathname += "/frame"; const hashParams = new URLSearchParams(); hashParams.append("origin", window.location.origin); hashParams.append("nonce", this.embedNonce); torusIframeUrl.hash = hashParams.toString(); const colorScheme = this.getTheme((whiteLabel === null || whiteLabel === void 0 ? void 0 : whiteLabel.mode) || THEME_MODES.light); // sandbox="allow-popups allow-scripts allow-same-origin" // Iframe code const walletIframe = htmlToElement(`<iframe id="walletIframe-${this.embedNonce}" class="walletIframe-${this.embedNonce}" sandbox="allow-popups allow-scripts allow-same-origin allow-forms allow-modals allow-downloads" src="${torusIframeUrl.href}" style="display: none; position: fixed; top: 0; right: 0; width: 100%; height: 100%; border: none; border-radius: 0; z-index: ${this.modalZIndex.toString()}; color-scheme: ${colorScheme}" allow="clipboard-write" ></iframe>`); this.buttonPosition = (whiteLabel === null || whiteLabel === void 0 ? void 0 : whiteLabel.buttonPosition) || BUTTON_POSITION.BOTTOM_LEFT; const dappMetadata = await getSiteMetadata(); return new Promise((resolve, reject) => { try { window.document.body.appendChild(walletIframe); // Wait for iframe to send ready const handleMessage = async ev => { if (ev.origin !== torusIframeUrl.origin) return; const { message, nonce } = ev.data || {}; if (message === SETUP_COMPLETE && nonce === this.embedNonce) { window.removeEventListener("message", handleMessage); // send init params here walletIframe.contentWindow.postMessage({ chains, chainId, dappMetadata, enableLogging, web3AuthClientId: this.web3AuthClientId, web3AuthNetwork: this.web3AuthNetwork, authConnectionConfig, loginMethodConfig, whiteLabel: finalWhiteLabel, confirmationStrategy, accountAbstractionConfig, enableKeyExport, enableLegacyConfig }, torusIframeUrl.origin); await this.setupWeb3({ torusUrl, walletIframe }); this.isInitialized = true; resolve(); } }; window.addEventListener("message", handleMessage); } catch (error) { reject(error); } }); } async login(params = {}) { if (!this.isInitialized) throw new Error("Call init() first"); const isAlreadyFullScreen = this.communicationProvider.isIFrameFullScreen; try { // Pre-open popup window for auth connection to handle popup blocker // Similar to how transactions handle popup blocking let windowId; if (params.authConnection) { windowId = randomId(); await this.communicationProvider.handleWindow(windowId, { target: "_blank", features: getPopupFeatures(FEATURES_DEFAULT_POPUP_WINDOW), isAuthWindow: true, timeout: 30000 // auth flow may take longer }); } else if (!isAlreadyFullScreen) { // No authConnection specified, show login modal in iframe this.communicationProvider.displayIframe({ isFull: true }); } // We use this method because we want to update inPage provider state with account info const isSolana = Object.values(SOLANA_CHAIN_IDS).includes(this.provider.chainId); const method = isSolana ? SOLANA_METHOD_TYPES.SOLANA_REQUEST_ACCOUNTS : EVM_METHOD_TYPES.ETH_REQUEST_ACCOUNTS; const requestParams = params.authConnection ? [params.authConnection, params.login_hint, params.authConnectionId, params.groupedAuthConnectionId, windowId] : []; // If user is already logged in, we assume they have given access to the website const res = await this.provider.rpcRequest({ id: randomId(), method, params: requestParams }); log.info("check: res", res); if (Array.isArray(res) && res.length > 0) { if (this.confirmationStrategy === CONFIRMATION_STRATEGY.AUTO_APPROVE) log.warn("Confirmation strategy auto approve is not allowed on Embed Mode, using Default instead."); return res; } // This would never happen, but just in case throw new Error("Login failed"); } catch (error) { log.error("login failed", error); throw error; } finally { if (!isAlreadyFullScreen) this.communicationProvider.displayIframe({ isFull: false }); } } async connectWithSession(params) { if (!this.isInitialized) throw new Error("Call init() first"); if (!params.sessionId) throw new Error("sessionId is required"); if (!this.communicationProvider.hasAccessTokenProvider) throw new Error("accessTokenProvider is required. Call setAccessTokenProvider() first"); try { const res = await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.LOGIN_WITH_SESSION_ID, params: [`${params.sessionId}:${params.sessionNamespace || ""}`, params.idToken] }); log.info("check: res", res); if (res.success) { if (this.confirmationStrategy === CONFIRMATION_STRATEGY.POPUP) log.warn("Confirmation strategy popup is not allowed on Plugin Mode, using Default instead."); return res.success; } // This should never happen, but just in case throw new Error("Login failed"); } catch (error) { log.error("login with session id failed", error); throw error; } } setAccessTokenProvider(provider) { if (!this.isInitialized) throw new Error("Call init() first"); this.communicationProvider.setAccessTokenProvider(provider); } async logout() { if (!this.communicationProvider.isLoggedIn) throw new Error("Not logged in"); await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.LOGOUT, params: [] }); } async cleanUp() { var _this$communicationPr2; if ((_this$communicationPr2 = this.communicationProvider) !== null && _this$communicationPr2 !== void 0 && _this$communicationPr2.isLoggedIn) { await this.logout(); } this.clearInit(); } clearInit() { function isElement(element) { return element instanceof Element || element instanceof Document; } const walletIframe = window.document.getElementById(`walletIframe-${this.embedNonce}`); if (isElement(walletIframe)) { walletIframe.remove(); } this.isInitialized = false; } async getUserInfo() { const userInfoResponse = await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.USER_INFO, params: [] }); return userInfoResponse; } async showWalletConnectScanner(showWalletConnectParams) { const isShow = showWalletConnectParams ? showWalletConnectParams.show : true; this.communicationProvider.displayIframe({ isFull: isShow }); try { await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.SHOW_WALLET_CONNECT, params: showWalletConnectParams }); } catch { this.communicationProvider.displayIframe({ isFull: false }); } } async showWalletUi(showWalletUiParams) { const isShow = showWalletUiParams ? showWalletUiParams.show : true; this.communicationProvider.displayIframe({ isFull: isShow }); try { await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.SHOW_WALLET_UI, params: showWalletUiParams }); } catch { this.communicationProvider.displayIframe({ isFull: false }); } } async showFunding(showFundingParams) { const isShow = showFundingParams ? showFundingParams.show : true; this.communicationProvider.displayIframe({ isFull: isShow }); try { await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.SHOW_FUNDING, params: showFundingParams }); } catch { this.communicationProvider.displayIframe({ isFull: false }); } } async showCheckout(showCheckoutParams) { const isShow = showCheckoutParams ? showCheckoutParams.show : true; this.communicationProvider.displayIframe({ isFull: isShow }); try { await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.SHOW_CHECKOUT, params: showCheckoutParams }); } catch { this.communicationProvider.displayIframe({ isFull: false }); } } async showReceive(showReceiveParams) { const isShow = showReceiveParams ? showReceiveParams.show : true; this.communicationProvider.displayIframe({ isFull: isShow }); try { await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.SHOW_RECEIVE, params: showReceiveParams }); } catch { this.communicationProvider.displayIframe({ isFull: false }); } } async showSwap(shoSwapParams) { const isShow = shoSwapParams ? shoSwapParams.show : true; this.communicationProvider.displayIframe({ isFull: isShow }); try { await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.SHOW_SWAP, params: shoSwapParams }); } catch { this.communicationProvider.displayIframe({ isFull: false }); } } getConfirmationStrategyFinal(payload) { let confirmationStrategyFinal = this.confirmationStrategy; if (!Array.isArray(payload) && PROVIDER_UNSAFE_METHODS.has(payload.method) && this.confirmationStrategy !== CONFIRMATION_STRATEGY.MODAL && this.loginMode !== WS_EMBED_LOGIN_MODE.PLUGIN) { confirmationStrategyFinal = CONFIRMATION_STRATEGY.POPUP; } return confirmationStrategyFinal; } getTheme(theme) { if (theme === THEME_MODES.light) return "light"; if (theme === THEME_MODES.dark) return "dark"; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; } async setupWeb3(providerParams) { log.info("setupWeb3 running"); // setup background connection const providerStream = new PostMessageStream({ name: "embed_torus", target: "iframe_torus", targetWindow: providerParams.walletIframe.contentWindow, targetOrigin: new URL(providerParams.torusUrl).origin }); // We create another LocalMessageDuplexStream for communication between dapp <> iframe const communicationStream = new PostMessageStream({ name: "embed_communication", target: "iframe_communication", targetWindow: providerParams.walletIframe.contentWindow, targetOrigin: new URL(providerParams.torusUrl).origin }); // compose the inPage provider const inPageProvider = new TorusInPageProvider(providerStream, {}); const communicationProvider = new CommunicationProvider(communicationStream, {}, { buttonPosition: this.buttonPosition }); inPageProvider.tryWindowHandle = async payload => { const _payload = payload; const confirmationStrategyFinal = this.getConfirmationStrategyFinal(payload); const { allowWalletService } = communicationProvider; if (confirmationStrategyFinal === CONFIRMATION_STRATEGY.POPUP && allowWalletService) { const windowId = randomId(); communicationProvider.handleWindow(windowId, { target: "_blank", features: getPopupFeatures(FEATURES_CONFIRM_WINDOW), timeout: 500 }); // for inPageProvider methods sending windowId in request instead of params // as params might be positional. _payload.windowId = windowId; } const result = await inPageProvider.rpcEngine.handle(_payload); return result; }; communicationProvider.tryWindowHandle = async payload => { const _payload = payload; const confirmationStrategyFinal = this.getConfirmationStrategyFinal(payload); const { allowWalletService } = communicationProvider; if (confirmationStrategyFinal === CONFIRMATION_STRATEGY.POPUP && allowWalletService) { const windowId = randomId(); communicationProvider.handleWindow(windowId, { target: "_blank", features: getPopupFeatures(FEATURES_PROVIDER_CHANGE_WINDOW), // todo: are these features generic for all timeout: 500 }); // for communication methods sending window id in jrpc req params _payload.params.windowId = windowId; } const result = await communicationProvider.rpcEngine.handle(_payload); return result; }; // detect requestAccounts and pipe to enable for now const detectAccountRequestPrototypeModifier = m => { const originalMethod = inPageProvider[m]; // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore inPageProvider[m] = function providerFunc(request, cb) { const { method, params = [] } = request; if (method === EVM_METHOD_TYPES.ETH_REQUEST_ACCOUNTS || method === SOLANA_METHOD_TYPES.SOLANA_REQUEST_ACCOUNTS) { if (!cb) return self.login({ authConnection: params[0] }); self.login({ authConnection: params[0] }) // eslint-disable-next-line promise/no-callback-in-promise .then(res => cb(null, res)) // eslint-disable-next-line promise/no-callback-in-promise .catch(err => cb(err)); } return originalMethod.apply(this, [request, cb]); }; }; // Detects call to requestAccounts in request & sendAsync and passes to login detectAccountRequestPrototypeModifier("request"); detectAccountRequestPrototypeModifier("sendAsync"); detectAccountRequestPrototypeModifier("send"); const proxiedInPageProvider = new Proxy(inPageProvider, { // straight up lie that we deleted the property so that it doesn't // throw an error in strict mode deleteProperty: () => true }); const proxiedCommunicationProvider = new Proxy(communicationProvider, { // straight up lie that we deleted the property so that it doesn't // throw an error in strict mode deleteProperty: () => true }); this.provider = proxiedInPageProvider; this.communicationProvider = proxiedCommunicationProvider; log.info("test setupWeb3 "); await Promise.all([inPageProvider.initializeState(), communicationProvider.initializeState(_objectSpread(_objectSpread({}, providerParams), {}, { torusIframeId: providerParams.walletIframe.id }))]); log.debug("WsEmbed - injected provider"); } async getProjectConfig() { let projectConfig; try { projectConfig = await fetchProjectConfig({ clientId: this.web3AuthClientId, web3AuthNetwork: this.web3AuthNetwork, buildEnv: this.buildEnv }); } catch (e) { const error = await serializeError(e); log.error("Failed to fetch project configurations", error); throw new Error("failed to fetch project configurations"); } log.debug("projectConfig", projectConfig); return projectConfig; } async initializeAuthConnectionConfig(projectConfig, loginMethodConfig) { // merge project config with authConnectionConfig. if (loginMethodConfig && Object.keys(loginMethodConfig).length > 0) { loginMethodConfig = mergeAuthConnectionConfig(projectConfig.embeddedWalletAuth || [], loginMethodConfig); } return loginMethodConfig; } initializeWhiteLabel(projectConfig, whiteLabel = {}) { const { walletUi, whitelabel } = projectConfig; const { enablePortfolioWidget = false, enableTokenDisplay = true, enableNftDisplay = true, enableWalletConnect = true, enableBuyButton = true, enableSendButton = true, enableSwapButton = true, enableReceiveButton = true, enableShowAllTokensButton = true, enableDefiPositionsDisplay = true, portfolioWidgetPosition = BUTTON_POSITION.BOTTOM_LEFT, defaultPortfolio = "token" } = walletUi || {}; const projectConfigWhiteLabel = _objectSpread(_objectSpread({}, whitelabel), {}, { showWidgetButton: enablePortfolioWidget, hideNftDisplay: !enableNftDisplay, hideTokenDisplay: !enableTokenDisplay, hideTransfers: !enableSendButton, hideTopup: !enableBuyButton, hideReceive: !enableReceiveButton, hideSwap: !enableSwapButton, hideShowAllTokens: !enableShowAllTokensButton, hideWalletConnect: !enableWalletConnect, hideDefiPositionsDisplay: !enableDefiPositionsDisplay, buttonPosition: portfolioWidgetPosition, defaultPortfolio }); return deepmerge.all([projectConfigWhiteLabel, whiteLabel]); } } export { PROVIDER_UNSAFE_METHODS, WsEmbed as default, version };