UNPKG

@upbond/upbond-embed

Version:
1,281 lines (1,147 loc) 45.3 kB
/* eslint-disable no-console */ import { get, setAPIKey } from "@toruslabs/http-helpers"; import { BasePostMessageStream, JRPCRequest, ObjectMultiplex, setupMultiplex, Substream } from "@toruslabs/openlogin-jrpc"; import deepmerge from "lodash.merge"; import configuration from "./config"; import Consent from "./consent"; import { documentReady, handleStream, htmlToElement, runOnLoad } from "./embedUtils"; import UpbondInpageProvider from "./inpage-provider"; import generateIntegrity from "./integrity"; import { BUTTON_POSITION, BUTTON_POSITION_TYPE, EMBED_TRANSLATION_ITEM, IUpbondEmbedParams, LOGIN_PROVIDER, NetworkInterface, PAYMENT_PROVIDER_TYPE, PaymentParams, TorusCtorArgs, TorusLoginParams, TorusPublicKey, UnvalidatedJsonRpcRequest, UPBOND_BUILD_ENV, UserInfo, VerifierArgs, WALLET_OPENLOGIN_VERIFIER_MAP, WALLET_PATH, WhiteLabelParams, } from "./interfaces"; import log from "./loglevel"; import PopupHandler from "./PopupHandler"; import sendSiteMetadata from "./siteMetadata"; import { defaultLoginParam, defaultLoginParamProd, defaultLoginParamStg, FEATURES_CONFIRM_WINDOW, FEATURES_DEFAULT_WALLET_WINDOW, FEATURES_PROVIDER_CHANGE_WINDOW, getPreopenInstanceId, getUpbondWalletUrl, getUserLanguage, parseIdToken, searchToObject, validatePaymentProvider, } from "./utils"; const defaultVerifiers = { [LOGIN_PROVIDER.GOOGLE]: true, [LOGIN_PROVIDER.FACEBOOK]: true, [LOGIN_PROVIDER.REDDIT]: true, [LOGIN_PROVIDER.TWITCH]: true, [LOGIN_PROVIDER.DISCORD]: true, }; const iframeIntegrity = "sha384-RhqFseQpufEgNnJYPxNXx+EmyE55iWEWJwkgS7QX/pit6STKFZRf9K9kwmfpDIPw"; const expectedCacheControlHeader = "max-age=3600"; const UNSAFE_METHODS = [ "eth_sendTransaction", "eth_signTypedData", "eth_signTypedData_v3", "eth_signTypedData_v4", "personal_sign", "eth_getEncryptionPublicKey", "eth_decrypt", ]; // 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 upbondIframeHtml = document.createElement("link"); const { torusUrl } = await getUpbondWalletUrl("production", { check: false, hash: iframeIntegrity, version: "" }); upbondIframeHtml.href = `${torusUrl}/popup`; upbondIframeHtml.crossOrigin = "anonymous"; upbondIframeHtml.type = "text/html"; upbondIframeHtml.rel = "prefetch"; // if (upbondIframeHtml.relList && upbondIframeHtml.relList.supports) { // if (upbondIframeHtml.relList.supports("prefetch")) { // log.info("IFrame loaded"); // document.head.appendChild(upbondIframeHtml); // } // } } catch (error) { log.warn(error); } })(); export const ACCOUNT_TYPE = { NORMAL: "normal", THRESHOLD: "threshold", IMPORTED: "imported", }; class Upbond { buttonPosition: BUTTON_POSITION_TYPE = BUTTON_POSITION.BOTTOM_LEFT; buttonSize: number; torusUrl: string; upbondIframe: HTMLIFrameElement; skipDialog: boolean; styleLink: HTMLLinkElement; isLoggedIn: boolean; isInitialized: boolean; torusAlert: HTMLDivElement; apiKey: string; modalZIndex: number; alertZIndex: number; torusAlertContainer: HTMLDivElement; isIframeFullScreen: boolean; whiteLabel: WhiteLabelParams; requestedVerifier: string; currentVerifier: string; embedTranslations: EMBED_TRANSLATION_ITEM; ethereum: UpbondInpageProvider; provider: UpbondInpageProvider; communicationMux: ObjectMultiplex; isUsingDirect: boolean; isLoginCallback: () => void; dappRedirectUrl: string; paymentProviders = configuration.paymentProviders; selectedVerifier: string; buildEnv: (typeof UPBOND_BUILD_ENV)[keyof typeof UPBOND_BUILD_ENV]; widgetConfig: { showAfterLoggedIn: boolean; showBeforeLoggedIn: boolean }; consent: Consent | null; consentConfiguration: { enable: boolean; config: { publicKey: string; scope: string[]; origin: string; }; }; flowConfig: string; idToken: any; private loginHint = ""; private useWalletConnect: boolean; private isCustomLogin = false; constructor(opts: TorusCtorArgs = {}) { this.buttonPosition = opts.buttonPosition || "bottom-left"; this.buttonSize = opts.buttonSize || 56; this.torusUrl = ""; this.isLoggedIn = false; // ethereum.enable working this.isInitialized = false; // init done this.requestedVerifier = ""; this.currentVerifier = ""; this.apiKey = opts.apiKey || "torus-default"; setAPIKey(this.apiKey); this.modalZIndex = opts.modalZIndex || 999999; this.alertZIndex = this.modalZIndex + 1000; this.isIframeFullScreen = false; this.isUsingDirect = false; this.buildEnv = "production"; this.widgetConfig = { showAfterLoggedIn: true, showBeforeLoggedIn: false, }; this.idToken = ""; } async init({ buildEnv = UPBOND_BUILD_ENV.PRODUCTION, enableLogging = false, // deprecated: use loginConfig instead enabledVerifiers = defaultVerifiers, network = { host: "matic", chainId: 137, networkName: "Polygon", blockExplorer: "https://polygonscan.com/", ticker: "MATIC", tickerName: "MATIC", rpcUrl: "https://polygon-rpc.com", }, loginConfig = defaultLoginParam, widgetConfig = this.widgetConfig, // default widget config integrity = { check: false, hash: iframeIntegrity, version: "", }, whiteLabel, skipTKey = false, useWalletConnect = false, isUsingDirect = true, mfaLevel = "default", selectedVerifier, skipDialog = false, dappRedirectUri = window.location.origin, consentConfiguration = { enable: false, config: { publicKey: "", scope: [], origin: "", }, }, flowConfig = "normal", state = "", }: IUpbondEmbedParams = {}): Promise<void> { // Send WARNING for deprecated buildEnvs // Give message to use others instead let buildTempEnv = buildEnv; if (buildEnv === "v2_development") { console.log( `%c [UPBOND-EMBED] WARNING! This buildEnv is deprecating soon. Please use 'UPBOND_BUILD_ENV.DEVELOPMENT' instead to point wallet on DEVELOPMENT environment.`, "color: #FF0000" ); console.log(`More information, please visit https://github.com/upbond/embed`); buildTempEnv = "development"; } if (buildEnv === "v2_production") { console.log( `%c [UPBOND-EMBED] WARNING! This buildEnv is deprecating soon. Please use 'UPBOND_BUILD_ENV.PRODUCTION' instead to point wallet on PRODUCTION environment.`, "color: #FF0000" ); console.log(`More information, please visit https://github.com/upbond/embed`); buildTempEnv = "production"; } if (buildEnv.includes("v1")) { console.log( `%c [UPBOND-EMBED] WARNING! This buildEnv is deprecating soon. Please use 'UPBOND_BUILD_ENV.LOCAL|DEVELOPMENT|STAGING|PRODUCTION' instead to point wallet on each environment`, "color: #FF0000" ); console.log(`More information, please visit https://github.com/upbond/embed`); } if (state) this.idToken = state; buildEnv = buildTempEnv; log.info(`Using buildEnv: `, buildEnv); // Enable/Disable logging if (enableLogging) log.enableAll(); else log.disableAll(); // Check env staging / production, set LoginConfig let loginConfigTemp = loginConfig; if (JSON.stringify(loginConfig) === JSON.stringify(defaultLoginParam)) { // For development, using the defaultloginparam // For staging, using defaultLoginParamStg if (buildEnv.includes("staging")) loginConfigTemp = defaultLoginParamStg; // For production, using defaultLoginParamProd if (buildEnv.includes("production")) loginConfigTemp = defaultLoginParamProd; loginConfig = loginConfigTemp; } log.info(`Using login config: `, loginConfig); log.info(`Using network config: `, network); if (this.isInitialized) throw new Error("Already initialized"); const { torusUrl, logLevel } = await getUpbondWalletUrl(buildEnv, integrity); log.info(`Url Loaded: ${torusUrl} with log: ${logLevel}`); if (selectedVerifier) { try { const isAvailableOnLoginConfig = loginConfig[selectedVerifier]; if (isAvailableOnLoginConfig) { this.selectedVerifier = selectedVerifier; } else { throw new Error("Selected verifier is not exist on your loginConfig"); } } catch (error) { throw new Error("Selected verifier is not exist on your loginConfig"); } } this.buildEnv = buildEnv; this.skipDialog = skipDialog; this.dappRedirectUrl = dappRedirectUri; this.isUsingDirect = isUsingDirect; this.torusUrl = torusUrl; this.whiteLabel = whiteLabel; this.useWalletConnect = useWalletConnect; this.isCustomLogin = !!(loginConfig && Object.keys(loginConfig).length > 0) || !!(whiteLabel && Object.keys(whiteLabel).length > 0); log.setDefaultLevel(logLevel); this.consentConfiguration = consentConfiguration; this.flowConfig = flowConfig; const upbondIframeUrl = new URL(torusUrl); if (upbondIframeUrl.pathname.endsWith("/")) upbondIframeUrl.pathname += "popup"; else upbondIframeUrl.pathname += "/popup"; upbondIframeUrl.hash = `#isCustomLogin=${this.isCustomLogin}&isRedirect=${isUsingDirect}&dappRedirectUri=${encodeURIComponent( `${dappRedirectUri}` )}`; // Iframe code this.upbondIframe = htmlToElement<HTMLIFrameElement>( `<iframe id="upbondIframe" allow=${useWalletConnect ? "camera" : ""} class="upbondIframe" src="${upbondIframeUrl.href}" style="display: none; position: fixed; top: 0; right: 0; width: 100%; height: 100%; border: none; border-radius: 0; z-index: ${this.modalZIndex}" ></iframe>` ); this.torusAlertContainer = htmlToElement<HTMLDivElement>('<div id="torusAlertContainer"></div>'); this.torusAlertContainer.style.display = "none"; this.torusAlertContainer.style.setProperty("z-index", this.alertZIndex.toString()); const { defaultLanguage = getUserLanguage(), customTranslations = {} } = this.whiteLabel || {}; const mergedTranslations = deepmerge(configuration.translations, customTranslations); const languageTranslations = mergedTranslations[defaultLanguage] || configuration.translations[getUserLanguage()]; this.embedTranslations = languageTranslations.embed; const handleSetup = async () => { await documentReady(); return new Promise((resolve, reject) => { this.upbondIframe.onload = async () => { // only do this if iframe is not full screen await this._setupWeb3(); const initStream = this.communicationMux.getStream("init_stream") as Substream; initStream.on("data", (chunk) => { const { name, data, error } = chunk; if (name === "init_complete" && data.success) { // resolve promise this.isInitialized = true; this._displayIframe(); resolve(undefined); } else if (error) { reject(new Error(error)); } }); initStream.write({ name: "init_stream", data: { enabledVerifiers, loginConfig, whiteLabel: this.whiteLabel, buttonPosition: this.buttonPosition, buttonSize: this.buttonSize, apiKey: this.apiKey, skipTKey, network, widgetConfig: this.widgetConfig, mfaLevel, skipDialog, selectedVerifier, directConfig: { enabled: isUsingDirect, redirectUrl: dappRedirectUri, }, consentConfiguration: this.consentConfiguration, flowConfig, state, }, }); }; window.document.body.appendChild(this.upbondIframe); window.document.body.appendChild(this.torusAlertContainer); }); }; if (!widgetConfig) { log.info(`widgetConfig is not configured. Now using default widget configuration`); } else { this.widgetConfig = widgetConfig; } if (buildEnv === "production" && integrity.check) { // hacky solution to check for iframe integrity const fetchUrl = `${torusUrl}/popup`; const resp = await fetch(fetchUrl, { cache: "reload" }); if (resp.headers.get("Cache-Control") !== expectedCacheControlHeader) { throw new Error(`Unexpected Cache-Control headers, got ${resp.headers.get("Cache-Control")}`); } const response = await resp.text(); const calculatedIntegrity = generateIntegrity( { algorithms: ["sha384"], }, response ); if (calculatedIntegrity === integrity.hash) { await handleSetup(); } else { this.clearInit(); throw new Error("Integrity check failed"); } } else { try { await handleSetup(); if (this.consentConfiguration.enable && this.consentConfiguration.config.publicKey) { this.consent = new Consent({ publicKey: this.consentConfiguration.config.publicKey, scope: this.consentConfiguration.config.scope, consentStream: this.communicationMux, provider: this.provider, isLoggedIn: this.isLoggedIn, }); this.consent.init(); } else { this.consent = null; } } catch (error) { console.error(error, "@errorOnInit"); } } return undefined; } login({ verifier = "", login_hint: loginHint = "" }: TorusLoginParams = {}): Promise<string[]> { if (!this.isInitialized) throw new Error("Call init() first"); this.requestedVerifier = verifier; this.loginHint = loginHint; return this.ethereum.enable(); } requestAuthServiceAccessToken(): Promise<string> { return new Promise((resolve, reject) => { const stream = this.communicationMux.getStream("auth_service") as Substream; stream.write({ name: "access_token_request", }); stream.on("data", (ev) => { if (ev.name !== "error") { resolve(ev.data.authServiceAccessToken); } else { reject(ev.data.msg); } }); }); } logout(): Promise<void> { return new Promise((resolve, reject) => { if (!this.isInitialized) { reject(new Error("Please initialize first")); return; } const logOutStream = this.communicationMux.getStream("logout") as Substream; logOutStream.write({ name: "logOut" }); const statusStream = this.communicationMux.getStream("status") as Substream; const statusStreamHandler = (status) => { if (!status.loggedIn) { this.isLoggedIn = false; this.currentVerifier = ""; this.requestedVerifier = ""; resolve(); } else reject(new Error("Some Error Occured")); }; handleStream(statusStream, "data", statusStreamHandler); // Remove localstorage upbond_login used for caching localStorage.removeItem("upbond_login"); }); } async cleanUp(): Promise<void> { if (this.isLoggedIn) { await this.logout(); } this.clearInit(); } clearInit(): void { function isElement(element: unknown) { return element instanceof Element || element instanceof HTMLDocument; } if (isElement(this.styleLink) && window.document.body.contains(this.styleLink)) { this.styleLink.remove(); this.styleLink = undefined; } if (isElement(this.upbondIframe) && window.document.body.contains(this.upbondIframe)) { this.upbondIframe.remove(); this.upbondIframe = undefined; } if (isElement(this.torusAlertContainer) && window.document.body.contains(this.torusAlertContainer)) { this.torusAlert = undefined; this.torusAlertContainer.remove(); this.torusAlertContainer = undefined; } this.isInitialized = false; } hideWidget(): void { this._sendWidgetVisibilityStatus(false); this._displayIframe(); } showWidget(): void { this._sendWidgetVisibilityStatus(true); this._displayIframe(); } showMenu(): void { this._sendWidgetMenuVisibilityStatus(true); this._displayIframe(true); } hideMenu(): void { this._sendWidgetMenuVisibilityStatus(false); this._displayIframe(false); } setProvider({ host = "mainnet", chainId = null, networkName = "", ...rest }: NetworkInterface): Promise<void> { return new Promise((resolve, reject) => { const providerChangeStream = this.communicationMux.getStream("provider_change") as Substream; const handler = (chunk) => { const { err, success } = chunk.data; if (err) { reject(err); } else if (success) { resolve(); } else reject(new Error("some error occured")); }; handleStream(providerChangeStream, "data", handler); const preopenInstanceId = getPreopenInstanceId(); providerChangeStream.write({ name: "show_provider_change", data: { network: { host, chainId, networkName, ...rest, }, preopenInstanceId, override: false, }, }); }); } showWallet(path: WALLET_PATH, params: Record<string, string> = {}): void { const showWalletStream = this.communicationMux.getStream("show_wallet") as Substream; const finalPath = path ? `/${path}` : ""; showWalletStream.write({ name: "show_wallet", data: { path: finalPath } }); const showWalletHandler = (chunk) => { if (chunk.name === "show_wallet_instance") { // Let the error propogate up (hence, no try catch) const { instanceId } = chunk.data; const finalUrl = new URL(`${this.torusUrl}/wallet${finalPath}`); // Using URL constructor to prevent js injection and allow parameter validation.! finalUrl.searchParams.append("integrity", "true"); finalUrl.searchParams.append("instanceId", instanceId); Object.keys(params).forEach((x) => { finalUrl.searchParams.append(x, params[x]); }); finalUrl.hash = `#isCustomLogin=${this.isCustomLogin}`; log.info(`loaded: ${finalUrl}`); const walletWindow = new PopupHandler({ url: finalUrl, features: FEATURES_DEFAULT_WALLET_WINDOW }); walletWindow.open(); } }; handleStream(showWalletStream, "data", showWalletHandler); } showSignTransaction(path: WALLET_PATH, params: Record<string, string> = {}): void { const showWalletStream = this.communicationMux.getStream("show_wallet") as Substream; const finalPath = path ? `/${path}` : ""; showWalletStream.write({ name: "show_wallet", data: { path: finalPath } }); const showWalletHandler = (chunk) => { if (chunk.name === "show_wallet_instance") { // Let the error propogate up (hence, no try catch) const { instanceId } = chunk.data; const finalUrl = new URL(`${this.torusUrl}/confirm${finalPath}`); // Using URL constructor to prevent js injection and allow parameter validation.! finalUrl.searchParams.append("integrity", "true"); finalUrl.searchParams.append("instanceId", instanceId); Object.keys(params).forEach((x) => { finalUrl.searchParams.append(x, params[x]); }); finalUrl.hash = `#isCustomLogin=${this.isCustomLogin}`; log.info(`loaded: ${finalUrl}`); const walletWindow = new PopupHandler({ url: finalUrl, features: FEATURES_DEFAULT_WALLET_WINDOW }); walletWindow.open(); } }; handleStream(showWalletStream, "data", showWalletHandler); } async getPublicAddress({ verifier, verifierId, isExtended = false }: VerifierArgs): Promise<string | TorusPublicKey> { if (!configuration.supportedVerifierList.includes(verifier) || !WALLET_OPENLOGIN_VERIFIER_MAP[verifier]) throw new Error("Unsupported verifier"); const walletVerifier = verifier; const openloginVerifier = WALLET_OPENLOGIN_VERIFIER_MAP[verifier]; const url = new URL(`https://api.tor.us/lookup/torus`); url.searchParams.append("verifier", openloginVerifier); url.searchParams.append("verifierId", verifierId); url.searchParams.append("walletVerifier", walletVerifier); url.searchParams.append("network", "mainnet"); url.searchParams.append("isExtended", isExtended.toString()); return get( url.href, { headers: { "Content-Type": "application/json; charset=utf-8", }, }, { useAPIKey: true } ); } getUserInfo(message?: string): Promise<UserInfo> { return new Promise((resolve, reject) => { if (this.isLoggedIn) { const userInfoAccessStream = this.communicationMux.getStream("user_info_access") as Substream; userInfoAccessStream.write({ name: "user_info_access_request" }); const userInfoAccessHandler = (chunk) => { const { name, data: { approved, payload, rejected, newRequest }, } = chunk; if (name === "user_info_access_response") { if (approved) { resolve(payload); } else if (rejected) { reject(new Error("User rejected the request")); } else if (newRequest) { const userInfoStream = this.communicationMux.getStream("user_info") as Substream; const userInfoHandler = (handlerChunk) => { if (handlerChunk.name === "user_info_response") { if (handlerChunk.data.approved) { resolve(handlerChunk.data.payload); } else { reject(new Error("User rejected the request")); } } }; handleStream(userInfoStream, "data", userInfoHandler); const preopenInstanceId = getPreopenInstanceId(); this._handleWindow(preopenInstanceId, { target: "_blank", features: FEATURES_PROVIDER_CHANGE_WINDOW, }); userInfoStream.write({ name: "user_info_request", data: { message, preopenInstanceId } }); } } }; handleStream(userInfoAccessStream, "data", userInfoAccessHandler); } else reject(new Error("User has not logged in yet")); }); } getTkey(message?: string) { return new Promise((resolve, reject) => { if (this.isLoggedIn) { const tkeyAccessStream = this.communicationMux.getStream("tkey_access") as Substream; tkeyAccessStream.write({ name: "tkey_access_request" }); const tkeyAccessHandler = (chunk) => { const { name, data: { approved, payload, rejected, newRequest }, } = chunk; if (name === "tkey_access_response") { if (approved) { resolve(payload); } else if (rejected) { reject(new Error("User rejected the request")); } else if (newRequest) { const tkeyInfoStream = this.communicationMux.getStream("tkey") as Substream; const tkeyInfoHandler = (handlerChunk) => { if (handlerChunk.name === "tkey_response") { if (handlerChunk.data.approved) { resolve(handlerChunk.data.payload); } else { reject(new Error("User rejected the request")); } } }; handleStream(tkeyInfoStream, "data", tkeyInfoHandler); const preopenInstanceId = getPreopenInstanceId(); this._handleWindow(preopenInstanceId, { target: "_blank", features: FEATURES_PROVIDER_CHANGE_WINDOW, }); tkeyInfoStream.write({ name: "tkey_request", data: { message, preopenInstanceId } }); } } }; handleStream(tkeyAccessStream, "data", tkeyAccessHandler); } else reject(new Error("User has not logged in yet")); }); } getMpcProvider() { return new Promise((resolve, reject) => { const mpcProviderStream = this.communicationMux.getStream("mpc_provider_access") as Substream; mpcProviderStream.write({ name: "mpc_provider_request" }); const tkeyAccessHandler = (chunk) => { const { name, data: { approved, payload }, } = chunk; this._displayIframe(true); if (name === "mpc_provider_response") { if (approved) { resolve(payload); } else { reject(new Error("User rejected the request")); } } }; handleStream(mpcProviderStream, "data", tkeyAccessHandler); }); } sendTransaction(data: any) { return new Promise((resolve, reject) => { this._displayIframe(true); const stream = this.communicationMux.getStream("send_transaction_access") as Substream; stream.write({ name: "send_transaction_request", data }); const tkeyAccessHandler = (chunk) => { const { name, data: { approved, payload }, } = chunk; if (name === "send_transaction_response") { if (approved) { resolve(payload); } else { reject(new Error("User rejected the request")); } } }; handleStream(stream, "data", tkeyAccessHandler); }); } initiateTopup(provider: PAYMENT_PROVIDER_TYPE, params: PaymentParams): Promise<boolean> { return new Promise((resolve, reject) => { if (this.isInitialized) { const { errors, isValid } = validatePaymentProvider(provider, params); if (!isValid) { reject(new Error(JSON.stringify(errors))); return; } const topupStream = this.communicationMux.getStream("topup") as Substream; const topupHandler = (chunk) => { if (chunk.name === "topup_response") { if (chunk.data.success) { resolve(chunk.data.success); } else { reject(new Error(chunk.data.error)); } } }; handleStream(topupStream, "data", topupHandler); const preopenInstanceId = getPreopenInstanceId(); this._handleWindow(preopenInstanceId); topupStream.write({ name: "topup_request", data: { provider, params, preopenInstanceId } }); } else reject(new Error("Upbond is not initialized yet")); }); } async loginWithPrivateKey(loginParams: { privateKey: string; userInfo: Omit<UserInfo, "isNewUser"> }): Promise<void> { const { privateKey, userInfo } = loginParams; return new Promise((resolve, reject) => { if (this.isInitialized) { if (Buffer.from(privateKey, "hex").length !== 32) { reject(new Error("Invalid private key, Please provide a 32 byte valid secp25k1 private key")); return; } const loginPrivKeyStream = this.communicationMux.getStream("login_with_private_key") as Substream; const loginHandler = (chunk) => { if (chunk.name === "login_with_private_key_response") { if (chunk.data.success) { resolve(chunk.data.success); } else { reject(new Error(chunk.data.error)); } } }; handleStream(loginPrivKeyStream, "data", loginHandler); loginPrivKeyStream.write({ name: "login_with_private_key_request", data: { privateKey, userInfo } }); } else reject(new Error("Upbond is not initialized yet")); }); } async showWalletConnectScanner(): Promise<void> { if (!this.useWalletConnect) throw new Error("Set `useWalletConnect` as true in init function options to use wallet connect scanner"); return new Promise((resolve, reject) => { if (this.isLoggedIn) { const walletConnectStream = this.communicationMux.getStream("wallet_connect_stream") as Substream; const walletConnectHandler = (chunk) => { if (chunk.name === "wallet_connect_stream_res") { if (chunk.data.success) { resolve(chunk.data.success); } else { reject(new Error(chunk.data.error)); } this._displayIframe(); } }; handleStream(walletConnectStream, "data", walletConnectHandler); walletConnectStream.write({ name: "wallet_connect_stream_req" }); this._displayIframe(true); } else reject(new Error("User has not logged in yet")); }); } protected _handleWindow(preopenInstanceId: string, { url, target, features }: { url?: string; target?: string; features?: string } = {}): void { if (preopenInstanceId) { const windowStream = this.communicationMux.getStream("window") as Substream; const finalUrl = new URL(url || `${this.torusUrl}/redirect?preopenInstanceId=${preopenInstanceId}`); if (finalUrl.hash) finalUrl.hash += `&isCustomLogin=${this.isCustomLogin}`; else finalUrl.hash = `#isCustomLogin=${this.isCustomLogin}`; const handledWindow = new PopupHandler({ url: finalUrl, target, features }); handledWindow.open(); if (!handledWindow.window) { this._createPopupBlockAlert(preopenInstanceId, finalUrl.href); return; } windowStream.write({ name: "opened_window", data: { preopenInstanceId, }, }); const closeHandler = ({ preopenInstanceId: receivedId, close }) => { if (receivedId === preopenInstanceId && close) { handledWindow.close(); windowStream.removeListener("data", closeHandler); } }; windowStream.on("data", closeHandler); handledWindow.once("close", () => { windowStream.write({ data: { preopenInstanceId, closed: true, }, }); windowStream.removeListener("data", closeHandler); }); } } protected _setEmbedWhiteLabel(element: HTMLElement): void { // Set whitelabel const { theme } = this.whiteLabel || {}; if (theme) { const { isDark = false, colors = {} } = theme; if (isDark) element.classList.add("torus-dark"); if (colors.torusBrand1) element.style.setProperty("--torus-brand-1", colors.torusBrand1); if (colors.torusGray2) element.style.setProperty("--torus-gray-2", colors.torusGray2); } } protected _getLogoUrl(): string { let logoUrl = `${this.torusUrl}/images/torus_icon-blue.svg`; if (this.whiteLabel?.theme?.isDark) { logoUrl = this.whiteLabel?.logoLight || logoUrl; } else { logoUrl = this.whiteLabel?.logoDark || logoUrl; } return logoUrl; } protected _sendWidgetVisibilityStatus(status: boolean): void { const upbondButtonVisibilityStream = this.communicationMux.getStream("widget-visibility") as Substream; upbondButtonVisibilityStream.write({ data: status, }); } protected _sendWidgetMenuVisibilityStatus(status: boolean): void { const upbondButtonVisibilityStream = this.communicationMux.getStream("menu-visibility") as Substream; upbondButtonVisibilityStream.write({ data: status, }); } protected _displayIframe(isFull = false): void { const style: Partial<CSSStyleDeclaration> = {}; const size = this.buttonSize + 14; // 15px padding // set phase if (!isFull) { style.display = this.isLoggedIn ? "block" : !this.isLoggedIn ? "block" : "none"; style.height = `${size}px`; style.width = `${size}px`; switch (this.buttonPosition) { case BUTTON_POSITION.TOP_LEFT: style.top = "0px"; style.left = "0px"; style.right = "auto"; style.bottom = "auto"; break; case BUTTON_POSITION.TOP_RIGHT: style.top = "0px"; style.right = "0px"; style.left = "auto"; style.bottom = "auto"; break; case BUTTON_POSITION.BOTTOM_RIGHT: style.bottom = "0px"; style.right = "0px"; style.top = "auto"; style.left = "auto"; break; case BUTTON_POSITION.BOTTOM_LEFT: default: style.bottom = "0px"; style.left = "0px"; style.top = "auto"; style.right = "auto"; break; } } else { style.display = "block"; style.width = "100%"; style.height = "100%"; style.top = "0px"; style.right = "0px"; style.left = "0px"; style.bottom = "0px"; } Object.assign(this.upbondIframe.style, style); this.isIframeFullScreen = isFull; } protected async _setupWeb3(): Promise<void> { // setup background connection const metamaskStream = new BasePostMessageStream({ name: "embed_metamask", target: "iframe_metamask", targetWindow: this.upbondIframe.contentWindow, targetOrigin: new URL(this.torusUrl).origin, }); // Due to compatibility reasons, we should not set up multiplexing on window.metamaskstream // because the MetamaskInpageProvider also attempts to do so. // We create another LocalMessageDuplexStream for communication between dapp <> iframe const communicationStream = new BasePostMessageStream({ name: "embed_comm", target: "iframe_comm", targetWindow: this.upbondIframe.contentWindow, targetOrigin: new URL(this.torusUrl).origin, }); // Backward compatibility with Gotchi :) // window.metamaskStream = this.communicationStream // compose the inpage provider const inpageProvider = new UpbondInpageProvider(metamaskStream); // detect eth_requestAccounts and pipe to enable for now const detectAccountRequestPrototypeModifier = (m) => { const originalMethod = inpageProvider[m]; inpageProvider[m] = function providerFunc(method, ...args) { if (method && method === "eth_requestAccounts") { return inpageProvider.enable(); } return originalMethod.apply(this, [method, ...args]); }; }; detectAccountRequestPrototypeModifier("send"); detectAccountRequestPrototypeModifier("sendAsync"); inpageProvider.enable = () => { return new Promise((resolve, reject) => { // If user is already logged in, we assume they have given access to the website inpageProvider.sendAsync({ jsonrpc: "2.0", id: getPreopenInstanceId(), method: "eth_requestAccounts", params: [] }, (err, response) => { const { result: res } = (response as { result: unknown }) || {}; if (err) { setTimeout(() => { reject(err); }, 50); } else if (Array.isArray(res) && res.length > 0) { // If user is already rehydrated, resolve this // else wait for something to be written to status stream const handleLoginCb = () => { if (this.requestedVerifier !== "" && this.currentVerifier !== this.requestedVerifier) { const { requestedVerifier } = this; // eslint-disable-next-line promise/no-promise-in-callback this.logout() // eslint-disable-next-line promise/always-return .then((_) => { this.requestedVerifier = requestedVerifier; this._showLoginPopup(true, resolve, reject); }) .catch((error) => reject(error)); } else { resolve(res); } }; if (this.isLoggedIn) { handleLoginCb(); } else { this.isLoginCallback = handleLoginCb; } } else { // set up listener for login this._showLoginPopup(true, resolve, reject, this.skipDialog); } }); }); }; inpageProvider.tryPreopenHandle = (payload: UnvalidatedJsonRpcRequest | UnvalidatedJsonRpcRequest[], cb: (...args: unknown[]) => void) => { const _payload = payload; if (this.buildEnv.includes("v1")) { if (!Array.isArray(_payload) && UNSAFE_METHODS.includes(_payload.method)) { const preopenInstanceId = getPreopenInstanceId(); this._handleWindow(preopenInstanceId, { target: "_blank", features: FEATURES_CONFIRM_WINDOW, }); _payload.preopenInstanceId = preopenInstanceId; } } inpageProvider._rpcEngine.handle(_payload as JRPCRequest<unknown>[], cb); }; // Work around for web3@1.0 deleting the bound `sendAsync` but not the unbound // `sendAsync` method on the prototype, causing `this` reference issues with drizzle const proxiedInpageProvider = new Proxy(inpageProvider, { // straight up lie that we deleted the property so that it doesnt // throw an error in strict mode deleteProperty: () => true, }); this.ethereum = proxiedInpageProvider; const communicationMux = setupMultiplex(communicationStream); this.communicationMux = communicationMux; const windowStream = communicationMux.getStream("window") as Substream; windowStream.on("data", (chunk) => { if (chunk.name === "create_window") { // url is the url we need to open // we can pass the final url upfront so that it removes the step of redirecting to /redirect and waiting for finalUrl this._createPopupBlockAlert(chunk.data.preopenInstanceId, chunk.data.url); } }); // show torus widget if button clicked const widgetStream = communicationMux.getStream("widget") as Substream; widgetStream.on("data", async (chunk) => { const { data } = chunk; this._displayIframe(data); }); // Show torus button if wallet has been hydrated/detected const statusStream = communicationMux.getStream("status") as Substream; statusStream.on("data", (status) => { // login if (status.loggedIn && localStorage.getItem("upbond_login")) { this.isLoggedIn = status.loggedIn; this.currentVerifier = status.verifier; } // logout else this._displayIframe(); if (this.isLoginCallback) { this.isLoginCallback(); delete this.isLoginCallback; } }); this.provider = proxiedInpageProvider; if (this.provider.shouldSendMetadata) sendSiteMetadata(this.provider._rpcEngine); inpageProvider._initializeState(); const getCachedData = localStorage.getItem("upbond_login"); if (window.location.search || getCachedData || this.idToken) { let data; if (getCachedData) { data = JSON.parse(getCachedData) ? JSON.parse(getCachedData) : null; } if (this.idToken) { console.log("@masuk sini?"); const idTokenParsed = parseIdToken(this.idToken); console.log("@idTokenParsed", idTokenParsed); if (idTokenParsed?.wallet_address) { data = { loggedIn: true, rehydrate: true, selectedAddress: idTokenParsed?.wallet_address, verifier: "" }; localStorage.setItem("upbond_login", JSON.stringify(data)); } } if (window.location.search) { const { loggedIn, rehydrate, selectedAddress, verifier, state } = searchToObject<{ loggedIn: string; rehydrate: string; selectedAddress: string; verifier: string; state: string; }>(window.location.search); if (loggedIn !== undefined && rehydrate !== undefined && selectedAddress !== undefined && verifier !== undefined && state !== undefined) { data = { loggedIn, rehydrate, selectedAddress, verifier, state }; localStorage.setItem("upbond_login", JSON.stringify(data)); } } if (data) { const oauthStream = this.communicationMux.getStream("oauth") as Substream; const isLoggedIn = data.loggedIn === "true"; const isRehydrate = data.rehydrate === "true"; let state = ""; if (data.state) { state = data.state; } const { selectedAddress, verifier } = data; if (isLoggedIn) { this.isLoggedIn = true; this.currentVerifier = verifier; } else this._displayIframe(true); this._displayIframe(true); oauthStream.write({ selectedAddress }); statusStream.write({ loggedIn: isLoggedIn, rehydrate: isRehydrate, verifier, state }); await inpageProvider._initializeState(); if (data.selectedAddress && data.loggedIn && data.state) { const urlParams = new URLSearchParams(window.location.search); urlParams.delete("selectedAddress"); urlParams.delete("rehydrate"); urlParams.delete("loggedIn"); urlParams.delete("verifier"); urlParams.delete("state"); const newQueryParams = urlParams.toString(); const baseUrl = window.location.href.split("?")[0]; const newUrl = `${baseUrl}?${newQueryParams}`; window.history.replaceState(null, null, newUrl); } } else { const logOutStream = this.communicationMux.getStream("logout") as Substream; logOutStream.write({ name: "logOut" }); const statusStreamHandler = () => { this.isLoggedIn = false; this.currentVerifier = ""; this.requestedVerifier = ""; }; handleStream(statusStream, "data", statusStreamHandler); } } else { const logOutStream = this.communicationMux.getStream("logout") as Substream; logOutStream.write({ name: "logOut" }); const statusStreamHandler = () => { this.isLoggedIn = false; this.currentVerifier = ""; this.requestedVerifier = ""; }; handleStream(statusStream, "data", statusStreamHandler); } } protected _showLoginPopup(calledFromEmbed: boolean, resolve: (a: string[]) => void, reject: (err: Error) => void, skipDialog = false): void { const loginHandler = async (data) => { const { err, selectedAddress } = data; if (err) { log.error(err); if (reject) reject(err); } // returns an array (cause accounts expects it) else if (resolve) resolve([selectedAddress]); if (this.isIframeFullScreen) this._displayIframe(false); }; const oauthStream = this.communicationMux.getStream("oauth") as Substream; if (!this.requestedVerifier) { this._displayIframe(true); handleStream(oauthStream, "data", loginHandler); oauthStream.write({ name: "oauth_modal", data: { calledFromEmbed, skipDialog, isUsingDirect: this.isUsingDirect, verifier: this.currentVerifier, dappRedirectUrl: this.dappRedirectUrl, selectedVerifier: this.selectedVerifier, }, }); } else { handleStream(oauthStream, "data", loginHandler); const preopenInstanceId = getPreopenInstanceId(); this._handleWindow(preopenInstanceId); oauthStream.write({ name: "oauth", data: { calledFromEmbed, verifier: this.requestedVerifier, preopenInstanceId, login_hint: this.loginHint, skipDialog, selectedVerifier: this.selectedVerifier, }, }); } } protected _createPopupBlockAlert(preopenInstanceId: string, url: string): void { const logoUrl = this._getLogoUrl(); const torusAlert = htmlToElement<HTMLDivElement>( '<div id="torusAlert" class="torus-alert--v2">' + `<div id="torusAlert__logo"><img src="${logoUrl}" /></div>` + "<div>" + `<h1 id="torusAlert__title">${this.embedTranslations.actionRequired}</h1>` + `<p id="torusAlert__desc">${this.embedTranslations.pendingAction}</p>` + "</div>" + "</div>" ); const successAlert = htmlToElement(`<div><a id="torusAlert__btn">${this.embedTranslations.continue}</a></div>`); const btnContainer = htmlToElement('<div id="torusAlert__btn-container"></div>'); btnContainer.appendChild(successAlert); torusAlert.appendChild(btnContainer); const bindOnLoad = () => { successAlert.addEventListener("click", () => { this._handleWindow(preopenInstanceId, { url, target: "_blank", features: FEATURES_CONFIRM_WINDOW, }); torusAlert.remove(); if (this.torusAlertContainer.children.length === 0) this.torusAlertContainer.style.display = "none"; }); }; this._setEmbedWhiteLabel(torusAlert); const attachOnLoad = () => { this.torusAlertContainer.style.display = "block"; this.torusAlertContainer.appendChild(torusAlert); }; runOnLoad(attachOnLoad); runOnLoad(bindOnLoad); } } export default Upbond;