UNPKG

@web3auth/ws-embed

Version:

Embed script

481 lines (470 loc) 18.8 kB
import _objectSpread from '@babel/runtime/helpers/objectSpread2'; import _defineProperty from '@babel/runtime/helpers/defineProperty'; import { randomId, CONFIRMATION_STRATEGY, SETUP_COMPLETE, COMMUNICATION_JRPC_METHODS, getPopupFeatures, FEATURES_CONFIRM_WINDOW, FEATURES_PROVIDER_CHANGE_WINDOW, PROVIDER_JRPC_METHODS } from '@toruslabs/base-controllers'; import { THEME_MODES, getRpcPromiseCallback, BasePostMessageStream } from '@web3auth/auth'; import CommunicationProvider from './communicationProvider.js'; import { SOLANA_CHAIN_IDS, SOLANA_METHOD_TYPES, EVM_METHOD_TYPES } from './constants.js'; import { htmlToElement } from './embedUtils.js'; import TorusInPageProvider from './inPageProvider.js'; import { WS_EMBED_BUILD_ENV, BUTTON_POSITION } from './interfaces.js'; import log from './loglevel.js'; import getSiteMetadata from './siteMetadata.js'; // don't use destructuring for process.env cause it messes up webpack env plugin const version = "5.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, "isPluginMode", void 0); _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, accountAbstractionConfig, enableKeyExport = false }) { if (this.isInitialized) throw new Error("Already initialized"); 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(); 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, whiteLabel, confirmationStrategy, accountAbstractionConfig, enableKeyExport }, 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 { if (!params.authConnection && !isAlreadyFullScreen) { this.communicationProvider.displayIframe({ isFull: true }); } // If user is already logged in, we assume they have given access to the website const res = await new Promise((resolve, reject) => { // 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; this.provider.rpcRequest({ method, params: [params.authConnection, params.login_hint] }, getRpcPromiseCallback(resolve, reject)); }); 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 loginWithSessionId(params) { if (!this.isInitialized) throw new Error("Call init() first"); if (!params.sessionId) throw new Error("sessionId is required"); try { const res = await this.communicationProvider.request({ method: COMMUNICATION_JRPC_METHODS.LOGIN_WITH_SESSION_ID, params: [params.sessionId, params.sessionNamespace || ""] }); log.info("check: res", res); if (res.success) { this.isPluginMode = true; 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; } } 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.isPluginMode) { 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 BasePostMessageStream({ 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 BasePostMessageStream({ 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 = (payload, cb) => { 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; } inPageProvider.rpcEngine.handle(_payload, cb); }; communicationProvider.tryWindowHandle = (payload, cb) => { 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; } communicationProvider.rpcEngine.handle(_payload, cb); }; // 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"); } } export { PROVIDER_UNSAFE_METHODS, WsEmbed as default, version };