UNPKG

@frak-labs/core-sdk

Version:

Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.

212 lines (194 loc) 6.65 kB
import { Deferred } from "@frak-labs/frame-connector"; import { BACKUP_KEY } from "../../constants"; import type { FrakLifecycleEvent } from "../../types"; import { isFrakDeepLink, triggerDeepLinkWithFallback, } from "../../utils/browser/deepLinkWithFallback"; import { changeIframeVisibility } from "../../utils/iframe/iframeHelper"; /** * Detect iOS in-app browsers (Instagram, Facebook) where server-side * 302 redirects to custom URL schemes (x-safari-https://) are silently * swallowed by WKWebView. Direct window.location.href assignment works. */ const isIOSInAppBrowser = (() => { if (typeof navigator === "undefined") return false; const ua = navigator.userAgent; // Standard iOS or iPadOS 13+ (reports as Macintosh with touch) const isIOS = /iPhone|iPad|iPod/i.test(ua) || (/Macintosh/i.test(ua) && navigator.maxTouchPoints > 1); if (!isIOS) return false; const lower = ua.toLowerCase(); return ( lower.includes("instagram") || lower.includes("fban") || lower.includes("fbav") || lower.includes("facebook") ); })(); /** @ignore */ export type IframeLifecycleManager = { isConnected: Promise<boolean>; handleEvent: (messageEvent: FrakLifecycleEvent) => void; }; /** * Handle backup storage */ function handleBackup(backup: string | undefined): void { if (backup) { localStorage.setItem(BACKUP_KEY, backup); } else { localStorage.removeItem(BACKUP_KEY); } } /** * Compute final redirect URL with parameter substitution */ function computeRedirectUrl( baseRedirectUrl: string, mergeToken?: string ): string { try { const redirectUrl = new URL(baseRedirectUrl); if (!redirectUrl.searchParams.has("u")) { return baseRedirectUrl; } // Append merge token to the page URL so it survives // the backend /common/social redirect chain const finalPageUrl = appendMergeToken(window.location.href, mergeToken); redirectUrl.searchParams.delete("u"); redirectUrl.searchParams.append("u", finalPageUrl); return redirectUrl.toString(); } catch { return baseRedirectUrl; } } /** * Redirect current page to Safari via x-safari-https:// scheme. * Used on iOS in-app browsers where backend 302 → custom scheme fails. */ function redirectToSafari(mergeToken?: string) { const url = new URL(window.location.href); if (mergeToken) { url.searchParams.set("fmt", mergeToken); } const scheme = url.protocol === "http:" ? "x-safari-http" : "x-safari-https"; window.location.href = `${scheme}://${url.host}${url.pathname}${url.search}${url.hash}`; } /** * Check if this is a social/in-app-browser escape redirect (contains /common/social) */ function isSocialRedirect(url: string): boolean { return url.includes("/common/social"); } /** * Append merge token to a URL as the `fmt` query parameter. */ function appendMergeToken(urlString: string, mergeToken?: string): string { if (!mergeToken) return urlString; try { const url = new URL(urlString); url.searchParams.set("fmt", mergeToken); return url.toString(); } catch { const sep = urlString.includes("?") ? "&" : "?"; return `${urlString}${sep}fmt=${encodeURIComponent(mergeToken)}`; } } /** * Handle redirect with deep link fallback */ function handleRedirect( iframe: HTMLIFrameElement, baseRedirectUrl: string, targetOrigin: string, mergeToken?: string, openInNewTab?: boolean ): void { // If requested, open in a new tab instead of navigating the current page. // This preserves the merchant page while triggering universal links. // Requires the iframe postMessage to include user activation delegation. if (openInNewTab) { const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken); window.open(finalUrl, "_blank"); return; } if (isFrakDeepLink(baseRedirectUrl)) { const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken); triggerDeepLinkWithFallback(finalUrl, { onFallback: () => { iframe.contentWindow?.postMessage( { clientLifecycle: "deep-link-failed", data: { originalUrl: finalUrl }, }, targetOrigin ); }, }); } else if (isIOSInAppBrowser && isSocialRedirect(baseRedirectUrl)) { // iOS WKWebView silently swallows 302 redirects to custom URL // schemes — bypass the server redirect entirely redirectToSafari(mergeToken); } else { const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken); window.location.href = finalUrl; } } /** * Create a new iframe lifecycle handler * @param args * @param args.iframe - The iframe element used for wallet communication * @param args.targetOrigin - The wallet URL origin for postMessage security * @ignore */ export function createIFrameLifecycleManager({ iframe, targetOrigin, }: { iframe: HTMLIFrameElement; targetOrigin: string; }): IframeLifecycleManager { // Create the isConnected listener const isConnectedDeferred = new Deferred<boolean>(); // Build the handler itself const handler = (messageEvent: FrakLifecycleEvent) => { if (!("iframeLifecycle" in messageEvent)) return; const { iframeLifecycle: event, data } = messageEvent; switch (event) { // Resolve the isConnected promise case "connected": isConnectedDeferred.resolve(true); break; // Perform a frak backup case "do-backup": handleBackup(data.backup); break; // Remove frak backup case "remove-backup": localStorage.removeItem(BACKUP_KEY); break; // Change iframe visibility case "show": case "hide": changeIframeVisibility({ iframe, isVisible: event === "show" }); break; // Redirect handling case "redirect": handleRedirect( iframe, data.baseRedirectUrl, targetOrigin, data.mergeToken, data.openInNewTab ); break; } }; return { handleEvent: handler, isConnected: isConnectedDeferred.promise, }; }