UNPKG

@oko-wallet/oko-sdk-core

Version:
654 lines (632 loc) 21.5 kB
import '@oko-wallet/stdlib-js'; var RedirectUriSearchParamsKey; (function (RedirectUriSearchParamsKey) { RedirectUriSearchParamsKey["STATE"] = "state"; })(RedirectUriSearchParamsKey || (RedirectUriSearchParamsKey = {})); async function sendMsgToIframe(msg) { await this.waitUntilInitialized; const contentWindow = this.iframe.contentWindow; if (contentWindow === null) { throw new Error("iframe contentWindow is null"); } return new Promise((resolve) => { const channel = new MessageChannel(); channel.port1.onmessage = (event) => { const data = event.data; console.debug("[oko] reply recv", data); if (data.hasOwnProperty("payload")) { resolve(data); } else { console.error("[oko] unknown msg type"); resolve({ target: "oko_sdk", msg_type: "unknown_msg_type", payload: JSON.stringify(data), }); } }; contentWindow.postMessage(msg, this.sdkEndpoint, [channel.port2]); }); } const FIVE_MINS = 60 * 5 * 1000; async function openModal(msg) { await this.waitUntilInitialized; let timeoutId = null; const timeout = new Promise((_, reject) => { timeoutId = setTimeout(() => reject(new Error("Show modal timeout")), FIVE_MINS); }); try { this.iframe.style.display = "block"; const openModalAck = await Promise.race([ this.sendMsgToIframe(msg), timeout, ]); if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } if (openModalAck.msg_type !== "open_modal_ack") { return { success: false, err: { type: "invalid_ack_type", received: openModalAck.msg_type }, }; } return { success: true, data: openModalAck.payload }; } catch (error) { return { success: false, err: { type: "unknown_error", error } }; } finally { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } this.closeModal(); } } const OKO_ATTACHED_TARGET = "oko_attached"; const GOOGLE_CLIENT_ID = "421793224165-cpmbt6enqrj6ad6n4ujokham8qdmnnln.apps.googleusercontent.com"; const FIVE_MINS_MS$1 = 5 * 60 * 1000; async function signIn(type) { await this.waitUntilInitialized; let signInRes; try { switch (type) { case "google": { signInRes = await tryGoogleSignIn(this.sdkEndpoint, this.apiKey, this.sendMsgToIframe.bind(this)); break; } default: throw new Error(`not supported sign in type, type: ${type}`); } } catch (err) { throw new Error(`Sign in error, err: ${err}`); } if (!signInRes.payload.success) { throw new Error(`sign in fail, err: ${signInRes.payload.err}`); } const publicKey = await this.getPublicKey(); const email = await this.getEmail(); if (!!publicKey && !!email) { console.log("[oko] emit CORE__accountsChanged"); this.eventEmitter.emit({ type: "CORE__accountsChanged", email, publicKey, }); } } async function tryGoogleSignIn(sdkEndpoint, apiKey, sendMsgToIframe) { const clientId = GOOGLE_CLIENT_ID; const redirectUri = `${new URL(sdkEndpoint).origin}/google/callback`; console.debug("[oko] window host: %s", window.location.host); console.debug("[oko] redirectUri: %s", redirectUri); const nonce = Array.from(crypto.getRandomValues(new Uint8Array(8))) .map((b) => b.toString(16).padStart(2, "0")) .join(""); const nonceAckPromise = sendMsgToIframe({ target: OKO_ATTACHED_TARGET, msg_type: "set_oauth_nonce", payload: nonce, }); const oauthState = { apiKey, targetOrigin: window.location.origin, provider: "google", }; const oauthStateString = JSON.stringify(oauthState); console.debug("[oko] oauthStateString: %s", oauthStateString); const authUrl = new URL("https://accounts.google.com/o/oauth2/v2/auth"); authUrl.searchParams.set("client_id", clientId); authUrl.searchParams.set("redirect_uri", redirectUri); authUrl.searchParams.set("response_type", "token id_token"); authUrl.searchParams.set("scope", "openid email profile"); authUrl.searchParams.set("prompt", "login"); authUrl.searchParams.set("nonce", nonce); authUrl.searchParams.set(RedirectUriSearchParamsKey.STATE, oauthStateString); const popup = window.open(authUrl.toString(), "google_oauth", "width=1200,height=800"); if (!popup) { throw new Error("Failed to open new window for google oauth sign in"); } const ack = await nonceAckPromise; if (ack.msg_type !== "set_oauth_nonce_ack" || !ack.payload.success) { throw new Error("Failed to set nonce for google oauth sign in"); } return new Promise(async (resolve, reject) => { let timeout; function onMessage(event) { if (event.ports.length < 1) { return; } const port = event.ports[0]; const data = event.data; if (data.msg_type === "oauth_sign_in_update") { console.log("[oko] oauth_sign_in_update recv, %o", data); const msg = { target: "oko_attached", msg_type: "oauth_sign_in_update_ack", payload: null, }; port.postMessage(msg); if (data.payload.success) { resolve(data); } else { reject(new Error(data.payload.err.type)); } cleanup(); } } window.addEventListener("message", onMessage); timeout = window.setTimeout(() => { cleanup(); reject(new Error("Timeout: no response within 5 minutes")); closePopup$1(popup); }, FIVE_MINS_MS$1); function cleanup() { console.log("[oko] clean up oauth sign in listener"); window.clearTimeout(timeout); window.removeEventListener("message", onMessage); } }); } function closePopup$1(popup) { if (popup && !popup.closed) { popup.close(); } } async function signOut() { await this.waitUntilInitialized; await this.sendMsgToIframe({ target: OKO_ATTACHED_TARGET, msg_type: "sign_out", payload: null, }); this.eventEmitter.emit({ type: "CORE__accountsChanged", email: null, publicKey: null, }); } async function getPublicKey() { await this.waitUntilInitialized; const res = await this.sendMsgToIframe({ target: OKO_ATTACHED_TARGET, msg_type: "get_public_key", payload: null, }); if (res.msg_type === "get_public_key_ack" && res.payload.success) { return res.payload.data; } return null; } async function getEmail() { await this.waitUntilInitialized; const res = await this.sendMsgToIframe({ target: OKO_ATTACHED_TARGET, msg_type: "get_email", payload: null, }); if (res.msg_type === "get_email_ack" && res.payload.success) { return res.payload.data; } return null; } function closeModal() { this.iframe.style.display = "none"; } function on(handlerDef) { this.eventEmitter.on(handlerDef); } async function startEmailSignIn(email) { await this.waitUntilInitialized; const response = await this.sendMsgToIframe({ target: OKO_ATTACHED_TARGET, msg_type: "auth0_email_send_code", payload: { email, }, }); if (response.msg_type !== "auth0_email_send_code_ack") { throw new Error(`Unexpected response: ${response.msg_type}`); } const ack = response; if (!ack.payload.success) { throw new Error(ack.payload.err); } } const FIVE_MINS_MS = 5 * 60 * 1000; async function completeEmailSignIn(email, code) { await this.waitUntilInitialized; const result = await tryAuth0EmailSignIn(email, code, this.sdkEndpoint, this.apiKey, this.sendMsgToIframe.bind(this)); if (!result.payload.success) { throw new Error(result.payload.err.type); } const publicKey = await this.getPublicKey(); const userEmail = await this.getEmail(); if (publicKey && userEmail) { console.log("[oko] emit CORE__accountsChanged (auth0 email)"); this.eventEmitter.emit({ type: "CORE__accountsChanged", email: userEmail, publicKey, }); } } async function tryAuth0EmailSignIn(email, code, sdkEndpoint, apiKey, sendMsgToIframe) { const baseUrl = new URL(sdkEndpoint); const attachedOrigin = baseUrl.origin; const redirectUri = `${attachedOrigin}/auth0/callback`; console.debug("[oko] Auth0 email sign in"); console.debug("[oko] redirectUri: %s", redirectUri); const nonce = Array.from(crypto.getRandomValues(new Uint8Array(8))) .map((b) => b.toString(16).padStart(2, "0")) .join(""); const nonceAckPromise = sendMsgToIframe({ target: OKO_ATTACHED_TARGET, msg_type: "set_oauth_nonce", payload: nonce, }); const oauthState = { apiKey, targetOrigin: window.location.origin, email, provider: "auth0", }; const oauthStateString = JSON.stringify(oauthState); console.debug("[oko] oauthStateString: %s", oauthStateString); const popupUrl = new URL(`${attachedOrigin}/auth0/popup`); popupUrl.searchParams.set("email", email); popupUrl.searchParams.set("code", code); popupUrl.searchParams.set("nonce", nonce); popupUrl.searchParams.set("state", oauthStateString); const popup = window.open(popupUrl.toString(), "auth0_email_oauth", "width=1200,height=800"); if (!popup) { throw new Error("Failed to open new window for Auth0 email sign in"); } const ack = await nonceAckPromise; if (ack.msg_type !== "set_oauth_nonce_ack" || !ack.payload.success) { throw new Error("Failed to set nonce for Auth0 email sign in"); } return new Promise((resolve, reject) => { let timeout; function onMessage(event) { if (event.ports.length < 1) { return; } const port = event.ports[0]; const data = event.data; if (data.msg_type === "oauth_sign_in_update") { console.log("[oko] oauth_sign_in_update recv, %o", data); const msg = { target: "oko_attached", msg_type: "oauth_sign_in_update_ack", payload: null, }; port.postMessage(msg); if (data.payload.success) { resolve(data); } else { reject(new Error(data.payload.err.type)); } cleanup(); } } window.addEventListener("message", onMessage); timeout = window.setTimeout(() => { cleanup(); reject(new Error("Timeout: no response within 5 minutes")); closePopup(popup); }, FIVE_MINS_MS); function cleanup() { console.log("[oko] clean up oauth sign in listener"); window.clearTimeout(timeout); window.removeEventListener("message", onMessage); } }); } function closePopup(popup) { if (popup && !popup.closed) { popup.close(); } } const OKO_IFRAME_ID = "oko-attached"; function setUpIframeElement(url) { const oldEl = document.getElementById(OKO_IFRAME_ID); if (oldEl !== null) { console.warn("[oko] iframe already exists"); return { success: true, data: oldEl, }; } const bodyEls = document.getElementsByTagName("body"); if (bodyEls.length < 1 || bodyEls[0] === undefined) { console.error("body element not found"); return { success: false, err: "body element not found", }; } const bodyEl = bodyEls[0]; console.debug("[oko] setting up iframe"); const iframe = document.createElement("iframe"); if (document.readyState === "complete") { loadIframe(iframe, bodyEl, url); } else { window.addEventListener("load", () => loadIframe(iframe, bodyEl, url)); } return { success: true, data: iframe }; } function loadIframe(iframe, bodyEl, url) { console.log("[oko] loading iframe"); iframe.src = url.toString(); iframe.loading = "eager"; iframe.id = OKO_IFRAME_ID; iframe.style.position = "fixed"; iframe.style.top = "0"; iframe.style.left = "0"; iframe.style.width = "100vw"; iframe.style.height = "100vh"; iframe.style.border = "none"; iframe.style.display = "none"; iframe.style.backgroundColor = "transparent"; iframe.style.overflow = "hidden"; iframe.style.zIndex = "1000000"; bodyEl.appendChild(iframe); } function registerMsgListener(_okoWallet) { if (window.__oko_ev) { console.error("[oko] isn't it already initialized?"); } return new Promise((resolve) => { async function handler(event) { if (event.ports.length < 1) { return; } const port = event.ports[0]; const msg = event.data; if (msg.msg_type === "init") { const ack = { target: "oko_attached", msg_type: "init_ack", payload: { success: true, data: null }, }; port.postMessage(ack); window.removeEventListener("message", handler); resolve(msg.payload); } } window.addEventListener("message", handler); window.__oko_ev = handler; console.log("[oko] msg listener registered"); }); } async function lazyInit(okoWallet) { await waitUntilDocumentLoad(); const el = document.getElementById(OKO_IFRAME_ID); if (el === null) { return { success: false, err: "iframe not exists even after oko wallet initialization", }; } const checkURLRes = await checkURL(okoWallet.sdkEndpoint); if (!checkURLRes.success) { return checkURLRes; } const registerRes = await registerMsgListener(); if (registerRes.success) { const initResult = registerRes.data; const { email, public_key } = initResult; okoWallet.state = { email, publicKey: public_key }; if (email && public_key) { okoWallet.eventEmitter.emit({ type: "CORE__accountsChanged", email: email, publicKey: public_key, }); } return { success: true, data: okoWallet.state }; } else { return { success: false, err: "msg listener register fail", }; } } async function checkURL(url) { try { const response = await fetch(url, { mode: "no-cors" }); if (!response.ok) { return { success: true, data: url }; } else { return { success: false, err: `SDK endpoint, resp contains err, url: ${url}`, }; } } catch (err) { console.error("[oko] check url fail, url: %s", url); return { success: false, err: `check url fail, ${err.toString()}` }; } } async function waitUntilDocumentLoad() { return new Promise((resolve) => { if (document.readyState === "complete") { Promise.resolve().then(() => { resolve(0); }); } else { window.addEventListener("load", () => { Promise.resolve().then(() => { resolve(0); }); }); } }); } class EventEmitter3 { constructor() { this.listeners = {}; } on(handlerDef) { const { handler, type } = handlerDef; if (typeof handler !== "function") { throw new TypeError(`The "handler" argument must be of type function. \ Received ${handler === null ? "null" : typeof handler}`); } if (this.listeners[type] === undefined) { this.listeners[type] = []; } this.listeners[type].push(handler); } emit(event) { const { type, ...rest } = event; console.log("[oko] emit, type: %s", type); const handlers = this.listeners[type]; if (handlers === undefined) { return { success: false, err: { type: "handler_not_found", event_type: type, }, }; } for (let idx = 0; idx < handlers.length; idx += 1) { try { const handler = handlers[idx]; handler(rest); } catch (err) { return { success: false, err: { type: "handle_error", error: err.toString() }, }; } } return { success: true, data: void 0 }; } off(handlerDef) { const { type, handler } = handlerDef; const handlers = this.listeners[type]; if (handlers === undefined) { return; } const index = handlers.indexOf(handler); if (index === -1) { return; } handlers.splice(index, 1); if (handlers.length === 0) { delete this.listeners[type]; } } } const OkoWallet = function (apiKey, iframe, sdkEndpoint) { this.apiKey = apiKey; this.iframe = iframe; this.sdkEndpoint = sdkEndpoint; this.origin = window.location.origin; this.eventEmitter = new EventEmitter3(); this.state = { email: null, publicKey: null, }; this.waitUntilInitialized = lazyInit(this).then(); }; const SDK_ENDPOINT = `https://attached.oko.app`; function init(args) { try { console.log("[oko] init"); if (window === undefined) { console.error("[oko] oko wallet can only be initialized in the browser"); return { success: false, err: { type: "not_in_browser" }, }; } if (window.__oko_locked === true) { console.warn("oko wallet init is locked. Is init being executed concurrently?"); return { success: false, err: { type: "is_locked" } }; } else { window.__oko_locked = true; } console.log("[oko] sdk endpoint: %s", args.sdk_endpoint); if (window.__oko) { console.warn("[oko] already initialized"); return { success: true, data: window.__oko }; } const hostOrigin = new URL(window.location.toString()).origin; if (hostOrigin.length === 0) { return { success: false, err: { type: "host_origin_empty" }, }; } const sdkEndpoint = args.sdk_endpoint ?? SDK_ENDPOINT; let sdkEndpointURL; try { sdkEndpointURL = new URL(sdkEndpoint); sdkEndpointURL.searchParams.append("host_origin", hostOrigin); } catch (err) { return { success: false, err: { type: "sdk_endpoint_invalid_url" }, }; } console.log("[oko] resolved sdk endpoint: %s", sdkEndpoint); console.log("[oko] host origin: %s", hostOrigin); const iframeRes = setUpIframeElement(sdkEndpointURL); if (!iframeRes.success) { return { success: false, err: { type: "iframe_setup_fail", msg: iframeRes.err.toString() }, }; } const iframe = iframeRes.data; const okoWallet = new OkoWallet(args.api_key, iframe, sdkEndpoint); if (window.__oko) { console.warn("[oko] oko wallet has been initialized by another process"); return { success: true, data: window.__oko }; } else { window.__oko = okoWallet; return { success: true, data: okoWallet }; } } catch (err) { console.error("[oko] init fail", err); throw new Error("[oko] sdk init fail, unreachable"); } finally { if (window.__oko_locked === true) { window.__oko_locked = false; } } } OkoWallet.init = init; const ptype = OkoWallet.prototype; ptype.openModal = openModal; ptype.closeModal = closeModal; ptype.sendMsgToIframe = sendMsgToIframe; ptype.signIn = signIn; ptype.signOut = signOut; ptype.getPublicKey = getPublicKey; ptype.getEmail = getEmail; ptype.startEmailSignIn = startEmailSignIn; ptype.completeEmailSignIn = completeEmailSignIn; ptype.on = on; export { EventEmitter3, OKO_IFRAME_ID, OkoWallet, RedirectUriSearchParamsKey, setUpIframeElement }; //# sourceMappingURL=index.js.map