diff --git a/package.json b/package.json index 7a8b3d0..71f36b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@authsignal/browser", - "version": "0.3.2", + "version": "0.3.3", "type": "module", "main": "dist/index.js", "module": "dist/index.js", diff --git a/src/authsignal.ts b/src/authsignal.ts index c324ef4..6433474 100644 --- a/src/authsignal.ts +++ b/src/authsignal.ts @@ -6,9 +6,11 @@ import { AuthsignalWindowMessage, AuthsignalWindowMessageData, LaunchOptions, + PopupLaunchOptions, TokenPayload, + WindowLaunchOptions, } from "./types"; -import {PopupHandler} from "./popup-handler"; +import {PopupHandler, WindowHandler} from "./handlers"; import {Passkey} from "./passkey"; const DEFAULT_COOKIE_NAME = "__as_aid"; @@ -57,41 +59,85 @@ export class Authsignal { launch(url: string, options?: {mode?: "redirect"} & LaunchOptions): undefined; launch(url: string, options?: {mode: "popup"} & LaunchOptions): Promise; + launch(url: string, options?: {mode: "window"} & LaunchOptions): Promise; launch(url: string, options?: LaunchOptions) { - if (!options?.mode || options.mode === "redirect") { - window.location.href = url; - } else { - const {popupOptions} = options; + switch (options?.mode) { + case "window": + return this.launchWithWindow(url, options); + case "popup": + return this.launchWithPopup(url, options); + case "redirect": + default: + this.launchWithRedirect(url); + } + } - const Popup = new PopupHandler({width: popupOptions?.width}); + private launchWithRedirect(url: string) { + window.location.href = url; + } - const popupUrl = `${url}&mode=popup`; + private launchWithPopup(url: string, options: PopupLaunchOptions) { + const {popupOptions} = options; - Popup.show({url: popupUrl}); + const popupHandler = new PopupHandler({width: popupOptions?.width}); - return new Promise((resolve) => { - const onMessage = (event: MessageEvent) => { - let data: AuthsignalWindowMessageData | null = null; + const popupUrl = `${url}&mode=popup`; - try { - data = JSON.parse(event.data) as AuthsignalWindowMessageData; - } catch { - // Ignore if the event data is not valid JSON - } + popupHandler.show({url: popupUrl}); - if (data?.event === AuthsignalWindowMessage.AUTHSIGNAL_CLOSE_POPUP) { - this._token = data.token; + return new Promise((resolve) => { + const onMessage = (event: MessageEvent) => { + let data: AuthsignalWindowMessageData | null = null; - Popup.close(); - } - }; + try { + data = JSON.parse(event.data) as AuthsignalWindowMessageData; + } catch { + // Ignore if the event data is not valid JSON + } - Popup.on("hide", () => { - resolve({token: this._token}); - }); + if (data?.event === AuthsignalWindowMessage.AUTHSIGNAL_CLOSE_POPUP) { + this._token = data.token; - window.addEventListener("message", onMessage, false); + popupHandler.close(); + } + }; + + popupHandler.on("hide", () => { + resolve({token: this._token}); }); - } + + window.addEventListener("message", onMessage, false); + }); + } + + private launchWithWindow(url: string, options: WindowLaunchOptions) { + const {windowOptions} = options; + + const windowHandler = new WindowHandler(); + + const windowUrl = `${url}&mode=popup`; + + windowHandler.show({url: windowUrl, width: windowOptions?.width, height: windowOptions?.height}); + + return new Promise((resolve) => { + const onMessage = (event: MessageEvent) => { + let data: AuthsignalWindowMessageData | null = null; + + try { + data = JSON.parse(event.data) as AuthsignalWindowMessageData; + } catch { + // Ignore if the event data is not valid JSON + } + + if (data?.event === AuthsignalWindowMessage.AUTHSIGNAL_CLOSE_POPUP) { + this._token = data.token; + + windowHandler.close(); + resolve({token: this._token}); + } + }; + + window.addEventListener("message", onMessage, false); + }); } } diff --git a/src/handlers/index.ts b/src/handlers/index.ts new file mode 100644 index 0000000..853d68d --- /dev/null +++ b/src/handlers/index.ts @@ -0,0 +1,2 @@ +export {WindowHandler} from "./window-handler"; +export {PopupHandler} from "./popup-handler"; diff --git a/src/popup-handler.ts b/src/handlers/popup-handler.ts similarity index 99% rename from src/popup-handler.ts rename to src/handlers/popup-handler.ts index abdb827..ba77a75 100644 --- a/src/popup-handler.ts +++ b/src/handlers/popup-handler.ts @@ -20,7 +20,7 @@ type PopupHandlerOptions = { width?: string; }; -class PopupHandler { +export class PopupHandler { private popup: A11yDialog | null = null; constructor({width}: PopupHandlerOptions) { @@ -174,5 +174,3 @@ function resizeIframe(event: MessageEvent) { iframeEl.style.height = event.data.height + "px"; } } - -export {PopupHandler}; diff --git a/src/handlers/window-handler.ts b/src/handlers/window-handler.ts new file mode 100644 index 0000000..415bfa9 --- /dev/null +++ b/src/handlers/window-handler.ts @@ -0,0 +1,54 @@ +type WindowShowInput = { + url: string; + width?: number; + height?: number; +}; + +const DEFAULT_WIDTH = 400; +const DEFAULT_HEIGHT = 500; + +export class WindowHandler { + private windowRef: Window | null = null; + + show({url, width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT}: WindowShowInput) { + const windowRef = openWindow({url, width, height, win: window}); + + if (!windowRef) { + throw new Error("Window is not initialized"); + } + + this.windowRef = windowRef; + + return windowRef; + } + + close() { + if (!this.windowRef) { + throw new Error("Window is not initialized"); + } + + this.windowRef.close(); + } +} + +type OpenWindowInput = { + url: string; + width: number; + height: number; + win: Window; +}; + +function openWindow({url, width, height, win}: OpenWindowInput): Window | null { + if (!win.top) { + return null; + } + + const y = win.top.outerHeight / 2 + win.top.screenY - height / 2; + const x = win.top.outerWidth / 2 + win.top.screenX - width / 2; + + return window.open( + url, + "", + `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${width}, height=${height}, top=${y}, left=${x}` + ); +} diff --git a/src/types.ts b/src/types.ts index 15da520..d6e377a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,14 +5,14 @@ type BaseLaunchOptions = { * will trigger a full page redirect. * If no value is supplied, mode defaults to `redirect`. */ - mode?: "popup" | "redirect"; + mode?: "popup" | "redirect" | "window"; }; -type RedirectLaunchOptions = BaseLaunchOptions & { +export type RedirectLaunchOptions = BaseLaunchOptions & { mode: "redirect"; }; -type PopupLaunchOptions = BaseLaunchOptions & { +export type PopupLaunchOptions = BaseLaunchOptions & { mode: "popup"; popupOptions?: { /** Any valid CSS value for the `width` property. */ @@ -24,7 +24,15 @@ type PopupLaunchOptions = BaseLaunchOptions & { }; }; -export type LaunchOptions = RedirectLaunchOptions | PopupLaunchOptions; +export type WindowLaunchOptions = BaseLaunchOptions & { + mode: "window"; + windowOptions?: { + width?: number; + height?: number; + }; +}; + +export type LaunchOptions = RedirectLaunchOptions | PopupLaunchOptions | WindowLaunchOptions; export type AuthsignalOptions = { /**