@frak-labs/core-sdk
Version:
Core SDK of the Frak wallet, low level library to interact directly with the frak ecosystem.
1 lines • 57.9 kB
Source Map (JSON)
{"version":3,"file":"src-DrO48QOo.cjs","names":["getClientId","getBackendUrl","Deferred","sdkConfigStore","Deferred","FrakRpcError","RpcErrorCodes","OpenPanel","getClientId","base64urlDecode","getSupportedCurrency"],"sources":["../src/constants.ts","../src/utils/i18n/detectPageLanguage.ts","../src/clients/ssoUrlListener.ts","../src/utils/browser/deepLinkWithFallback.ts","../src/utils/browser/inAppBrowser.ts","../src/utils/iframe/iframeHelper.ts","../src/clients/transports/iframeLifecycleManager.ts","../src/clients/createIFrameFrakClient.ts","../src/utils/compression/decompress.ts","../src/clients/setupClient.ts","../src/context/mergeAttribution.ts"],"sourcesContent":["/**\n * The backup key for client side backup if needed\n */\nexport const BACKUP_KEY = \"nexus-wallet-backup\";\n\n/**\n * Deep link scheme for Frak Wallet mobile app.\n *\n * Replaced at build time via tsdown/Vite `define`. Defaults to the prod scheme;\n * in-monorepo dev builds (listener at wallet-dev.frak.id) override this with\n * `frakwallet-dev://` so deep links open the dev wallet variant (id.frak.wallet.dev).\n * External integrators consuming the published NPM/CDN bundle always see the prod scheme.\n */\nexport const DEEP_LINK_SCHEME: string =\n process.env.DEEP_LINK_SCHEME ?? \"frakwallet://\";\n","import type { Language } from \"../../types/config\";\n\n/** Normalize a raw locale string (e.g. `\"fr-FR\"`) to a supported {@link Language}. */\nfunction normalize(raw: string | undefined | null): Language | undefined {\n const base = raw?.split(\"-\")[0]?.toLowerCase();\n return base === \"en\" || base === \"fr\" ? base : undefined;\n}\n\n/**\n * Best-effort detection of the page's intended content language for SDK copy.\n *\n * Precedence: the document's declared `<html lang>` (the page author's explicit\n * content language) → the browser UI language (`navigator.language`). Returns\n * `undefined` when neither resolves to a supported language, letting callers\n * apply their own fallback (e.g. `\"en\"`).\n *\n * The `<html lang>` check comes first so a page authored in French renders\n * French SDK copy even when the visitor's browser is set to another language.\n */\nexport function detectPageLanguage(): Language | undefined {\n const htmlLang =\n typeof document !== \"undefined\"\n ? normalize(document.documentElement.lang)\n : undefined;\n if (htmlLang) return htmlLang;\n\n return typeof navigator !== \"undefined\"\n ? normalize(navigator.language)\n : undefined;\n}\n","import type { RpcClient } from \"@frak-labs/frame-connector\";\nimport type { FrakLifecycleEvent } from \"../types\";\nimport type { IFrameRpcSchema } from \"../types/rpc\";\n\n/**\n * Listen for SSO redirect with compressed data in URL\n * Forwards compressed data to iframe via lifecycle event\n * Cleans URL immediately after detection\n *\n * Performance: One-shot URL check, no polling, no re-renders\n *\n * @param rpcClient - RPC client instance to send lifecycle events\n * @param waitForConnection - Promise that resolves when iframe is connected\n */\nexport function setupSsoUrlListener(\n rpcClient: RpcClient<IFrameRpcSchema, FrakLifecycleEvent>,\n waitForConnection: Promise<boolean>\n): void {\n if (typeof window === \"undefined\") {\n return;\n }\n\n // One-shot URL check - no need for MutationObserver or polling\n const url = new URL(window.location.href);\n const compressedSso = url.searchParams.get(\"sso\");\n\n // Early return if no SSO parameter\n if (!compressedSso) {\n return;\n }\n\n // Forward compressed data directly to iframe (no decompression on SDK side)\n // Iframe will decompress and process\n waitForConnection\n .then(() => {\n // Send lifecycle event with compressed string\n // This is a one-way notification, no response expected\n rpcClient.sendLifecycle({\n clientLifecycle: \"sso-redirect-complete\",\n data: { compressed: compressedSso },\n });\n\n console.log(\n \"[SSO URL Listener] Forwarded compressed SSO data to iframe\"\n );\n })\n .catch((error) => {\n console.error(\n \"[SSO URL Listener] Failed to forward SSO data:\",\n error\n );\n });\n\n // Clean URL immediately to prevent exposure in browser history\n // Use replaceState to avoid navigation/re-render\n url.searchParams.delete(\"sso\");\n window.history.replaceState({}, \"\", url.toString());\n\n console.log(\"[SSO URL Listener] SSO parameter detected and URL cleaned\");\n}\n","import { DEEP_LINK_SCHEME } from \"../../constants\";\n\n/**\n * Options for deep link with fallback\n */\nexport type DeepLinkFallbackOptions = {\n /** Timeout in ms before triggering fallback (default: 2500ms) */\n timeout?: number;\n /** Callback invoked when fallback is triggered (app not installed) */\n onFallback?: () => void;\n};\n\n/**\n * Check if running on a Chromium-based Android browser.\n *\n * On Chrome Android, custom scheme deep links (e.g. frakwallet://) trigger\n * a confirmation bar (\"Continue to Frak Wallet?\"). Using intent:// URLs\n * instead bypasses this for Chromium browsers while keeping custom scheme\n * fallback for non-Chromium browsers (e.g. Firefox) where it works fine.\n */\nexport function isChromiumAndroid(): boolean {\n const ua = navigator.userAgent;\n return /Android/i.test(ua) && /Chrome\\/\\d+/i.test(ua);\n}\n\n/**\n * Convert a Frak deep link to an Android intent:// URL.\n *\n * Intent URLs let Chromium browsers open the app directly without\n * showing the \"Continue to app?\" confirmation bar.\n *\n * Note: We intentionally omit the `package` parameter. Including it\n * causes Chrome to redirect to the Play Store when the app is not\n * installed, which breaks the visibility-based fallback detection.\n * Without `package`, Chrome simply does nothing when the app is\n * missing, allowing the fallback mechanism to fire correctly.\n *\n * The scheme is derived from `DEEP_LINK_SCHEME` so the dev variant\n * (`frakwallet-dev://`) routes to the dev app shell, not prod.\n *\n * Format: intent://path#Intent;scheme=<scheme>;end\n */\nconst DEEP_LINK_SCHEME_NAME = /* @__PURE__ */ DEEP_LINK_SCHEME.replace(\n \"://\",\n \"\"\n);\n\nexport function toAndroidIntentUrl(deepLink: string): string {\n // Extract everything after the scheme (e.g. \"frakwallet://\" or \"frakwallet-dev://\")\n const path = deepLink.slice(DEEP_LINK_SCHEME.length);\n return `intent://${path}#Intent;scheme=${DEEP_LINK_SCHEME_NAME};end`;\n}\n\n/**\n * Trigger a deep link with visibility-based fallback detection.\n *\n * Uses the Page Visibility API to detect if the app opened (page goes hidden).\n * If the page remains visible after the timeout, assumes app is not installed\n * and invokes the onFallback callback.\n *\n * On Chromium Android, converts custom scheme to intent:// URL to avoid\n * the \"Continue to app?\" confirmation bar.\n *\n * @param deepLink - The deep link URL to trigger (e.g., \"frakwallet://wallet\")\n * @param options - Optional configuration (timeout, onFallback callback)\n */\nexport function triggerDeepLinkWithFallback(\n deepLink: string,\n options?: DeepLinkFallbackOptions\n): void {\n const timeout = options?.timeout ?? 2500;\n\n // Track if the app opened (page went to background)\n let appOpened = false;\n\n const onVisibilityChange = () => {\n if (document.hidden) {\n appOpened = true;\n }\n };\n\n // Start listening for visibility changes\n document.addEventListener(\"visibilitychange\", onVisibilityChange);\n\n // On Chromium Android, use intent:// to avoid confirmation bar\n const url =\n isChromiumAndroid() && isFrakDeepLink(deepLink)\n ? toAndroidIntentUrl(deepLink)\n : deepLink;\n\n // Trigger the deep link\n window.location.href = url;\n\n // Check after timeout if app opened\n setTimeout(() => {\n // Clean up listener\n document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n\n if (!appOpened) {\n // App didn't open - trigger fallback callback\n options?.onFallback?.();\n }\n }, timeout);\n}\n\n/**\n * Check if a URL is a Frak deep link\n */\nexport function isFrakDeepLink(url: string): boolean {\n return url.startsWith(DEEP_LINK_SCHEME);\n}\n","/**\n * Check if the current device runs iOS (including iPadOS 13+).\n */\nfunction checkIsIOS(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const ua = navigator.userAgent;\n // Standard iOS devices\n if (/iPhone|iPad|iPod/i.test(ua)) return true;\n // iPadOS 13+ reports as Macintosh — detect via touch support\n if (/Macintosh/i.test(ua) && navigator.maxTouchPoints > 1) return true;\n return false;\n}\n\n/**\n * Whether the current device runs iOS (including iPadOS 13+).\n */\nexport const isIOS: boolean = /* @__PURE__ */ checkIsIOS();\n\n/**\n * Check if the current device is a mobile device (iOS, iPadOS, Android,\n * webOS, BlackBerry, IEMobile, Opera Mini). Reuses {@link isIOS} so the\n * iPadOS-13+ Macintosh heuristic stays in one place.\n */\nexport function isMobile(): boolean {\n if (typeof navigator === \"undefined\") return false;\n if (isIOS) return true;\n return /Android|webOS|BlackBerry|IEMobile|Opera Mini/i.test(\n navigator.userAgent\n );\n}\n\n/**\n * Check if the current browser is a social media in-app browser\n * (Instagram, Facebook WebView).\n */\nfunction checkInAppBrowser(): boolean {\n if (typeof navigator === \"undefined\") return false;\n const ua = navigator.userAgent.toLowerCase();\n return (\n ua.includes(\"instagram\") ||\n ua.includes(\"fban\") ||\n ua.includes(\"fbav\") ||\n ua.includes(\"facebook\")\n );\n}\n\n/**\n * Whether the current browser is a social media in-app browser\n * (Instagram, Facebook).\n */\nexport const isInAppBrowser: boolean = /* @__PURE__ */ checkInAppBrowser();\n\n/**\n * Redirect to external browser from in-app WebView.\n *\n * - **iOS**: Uses `x-safari-https://` scheme — server-side 302 redirects\n * to custom URL schemes are silently swallowed by WKWebView.\n * Direct `window.location.href` assignment works (confirmed iOS 17+).\n *\n * - **Android**: Uses backend `/common/social` endpoint which returns a PDF\n * Content-Type response, forcing the WebView to hand off to the default browser.\n *\n * @param targetUrl - The URL to open in the external browser\n */\nexport function redirectToExternalBrowser(targetUrl: string): void {\n if (isIOS && targetUrl.startsWith(\"https://\")) {\n window.location.href = `x-safari-https://${targetUrl.slice(8)}`;\n } else if (isIOS && targetUrl.startsWith(\"http://\")) {\n window.location.href = `x-safari-http://${targetUrl.slice(7)}`;\n } else {\n window.location.href = `${process.env.BACKEND_URL}/common/social?u=${encodeURIComponent(targetUrl)}`;\n }\n}\n","import { getBackendUrl } from \"../../config/backendUrl\";\nimport { getClientId } from \"../../config/clientId\";\nimport type { FrakWalletSdkConfig, ListenerPreloadOption } from \"../../types\";\n\n/**\n * Base props for the iframe\n * @ignore\n */\nexport const baseIframeProps = {\n id: \"frak-wallet\",\n name: \"frak-wallet\",\n title: \"Frak Wallet\",\n allow: \"publickey-credentials-get *; clipboard-write; web-share *\",\n style: {\n width: \"0\",\n height: \"0\",\n border: \"0\",\n position: \"absolute\",\n zIndex: 2000001,\n top: \"-1000px\",\n left: \"-1000px\",\n colorScheme: \"auto\",\n },\n};\n\n/**\n * Create the Frak iframe\n * @param args\n * @param args.walletBaseUrl - Use `config.walletUrl` instead. Will be removed in future versions.\n * @param args.config - The configuration object containing iframe options, including the replacement for `walletBaseUrl`.\n */\nexport function createIframe({\n walletBaseUrl,\n config,\n}: {\n walletBaseUrl?: string;\n config?: FrakWalletSdkConfig;\n}): Promise<HTMLIFrameElement | undefined> {\n // Check if the iframe is already created\n const alreadyCreatedIFrame = document.querySelector(\"#frak-wallet\");\n\n // If the iframe is already created, remove it\n if (alreadyCreatedIFrame) {\n alreadyCreatedIFrame.remove();\n }\n\n const iframe = document.createElement(\"iframe\");\n\n // Set the base iframe props\n iframe.id = baseIframeProps.id;\n iframe.name = baseIframeProps.name;\n iframe.allow = baseIframeProps.allow;\n iframe.style.zIndex = baseIframeProps.style.zIndex.toString();\n\n changeIframeVisibility({ iframe, isVisible: false });\n\n // Set src BEFORE appending to DOM to avoid about:blank load event\n const walletUrl =\n config?.walletUrl ?? walletBaseUrl ?? \"https://wallet.frak.id\";\n const clientId = getClientId();\n\n // Preconnect to the wallet + backend origins so the handshake doesn't pay\n // for a cold DNS/TLS round-trip on partner sites that didn't warm them.\n preconnect(walletUrl);\n preconnect(getBackendUrl(walletUrl));\n\n iframe.src = buildListenerUrl({\n walletUrl,\n clientId,\n preload: config?.preload,\n });\n\n return new Promise((resolve) => {\n iframe.addEventListener(\"load\", () => resolve(iframe));\n document.body.appendChild(iframe);\n });\n}\n\n/**\n * Build the listener iframe URL.\n *\n * Query params:\n * - `clientId` — anonymous SDK client identifier used for funnel joining.\n *\n * Hash params (consumed by `apps/listener/app/bootstrap.ts#setupPreloadHints`):\n * - `preload=modal,sharing` — idle-warms the matching Ring 1 + Ring 2 chunks.\n * Skipped entirely when no preload hints are provided so the listener\n * doesn't pay for warm-ups that nobody asked for.\n */\nfunction buildListenerUrl({\n walletUrl,\n clientId,\n preload,\n}: {\n walletUrl: string;\n clientId: string;\n preload?: ListenerPreloadOption[];\n}): string {\n const base = `${walletUrl}/listener?clientId=${encodeURIComponent(clientId)}`;\n if (!preload || preload.length === 0) return base;\n return `${base}#preload=${preload.join(\",\")}`;\n}\n\n/**\n * Change the visibility of the given iframe\n * @ignore\n */\nexport function changeIframeVisibility({\n iframe,\n isVisible,\n}: {\n iframe: HTMLIFrameElement;\n isVisible: boolean;\n}) {\n if (!isVisible) {\n iframe.style.width = \"0\";\n iframe.style.height = \"0\";\n iframe.style.border = \"0\";\n iframe.style.position = \"fixed\";\n iframe.style.top = \"-1000px\";\n iframe.style.left = \"-1000px\";\n return;\n }\n\n iframe.style.position = \"fixed\";\n iframe.style.top = \"0\";\n iframe.style.left = \"0\";\n iframe.style.width = \"100%\";\n iframe.style.height = \"100%\";\n iframe.style.pointerEvents = \"auto\";\n}\n\n/**\n * Find an iframe within window.opener by pathname\n *\n * When a popup is opened via window.open from an iframe, window.opener points to\n * the parent window, not the iframe itself. This utility searches through all frames\n * in window.opener to find an iframe matching the specified pathname.\n *\n * @param pathname - The pathname to search for (default: \"/listener\")\n * @returns The matching iframe window, or null if not found\n *\n * @example\n * ```typescript\n * // Find the default /listener iframe\n * const listenerIframe = findIframeInOpener();\n *\n * // Find a custom iframe\n * const customIframe = findIframeInOpener(\"/my-custom-iframe\");\n * ```\n */\nexport function findIframeInOpener(pathname = \"/listener\"): Window | null {\n if (!window.opener) return null;\n\n const frameCheck = (frame: Window) => {\n try {\n return (\n frame.location.origin === window.location.origin &&\n frame.location.pathname === pathname\n );\n } catch {\n // Cross-origin frame, skip\n return false;\n }\n };\n\n // Check if the openner window is the right one\n if (frameCheck(window.opener)) return window.opener;\n\n // Search through frames in window.opener\n try {\n const frames = window.opener.frames;\n for (let i = 0; i < frames.length; i++) {\n if (frameCheck(frames[i])) {\n return frames[i];\n }\n }\n return null;\n } catch (error) {\n console.error(\n `[findIframeInOpener] Error finding iframe with pathname ${pathname}:`,\n error\n );\n return null;\n }\n}\n\n/**\n * Inject a `<link rel=\"preconnect\">` for the given origin. Idempotent — browsers\n * de-duplicate preconnects per origin automatically, but we also guard on a data\n * attribute to avoid spamming the <head> in SPA re-inits.\n */\nfunction preconnect(url: string): void {\n if (typeof document === \"undefined\") return;\n try {\n const origin = new URL(url).origin;\n const selector = `link[rel=\"preconnect\"][data-frak-preconnect=\"${origin}\"]`;\n if (document.head.querySelector(selector)) return;\n const link = document.createElement(\"link\");\n link.rel = \"preconnect\";\n link.href = origin;\n link.crossOrigin = \"\";\n link.dataset.frakPreconnect = origin;\n document.head.appendChild(link);\n } catch {\n // Invalid URL — nothing to preconnect.\n }\n}\n","import { Deferred } from \"@frak-labs/frame-connector\";\nimport { BACKUP_KEY } from \"../../constants\";\nimport type { FrakLifecycleEvent } from \"../../types\";\nimport {\n isFrakDeepLink,\n triggerDeepLinkWithFallback,\n} from \"../../utils/browser/deepLinkWithFallback\";\nimport { isInAppBrowser, isIOS } from \"../../utils/browser/inAppBrowser\";\nimport { changeIframeVisibility } from \"../../utils/iframe/iframeHelper\";\n\n// iOS in-app browsers (Instagram, Facebook) silently swallow server-side 302\n// redirects to custom URL schemes (x-safari-https://) in WKWebView; direct\n// window.location.href assignment works.\nconst isIOSInAppBrowser = isIOS && isInAppBrowser;\n\n/** @ignore */\nexport type IframeLifecycleManager = {\n isConnected: Promise<boolean>;\n handleEvent: (messageEvent: FrakLifecycleEvent) => void;\n};\n\n/**\n * Handle backup storage\n */\nfunction handleBackup(backup: string | undefined): void {\n if (backup) {\n localStorage.setItem(BACKUP_KEY, backup);\n } else {\n localStorage.removeItem(BACKUP_KEY);\n }\n}\n\n/**\n * Compute final redirect URL with parameter substitution\n */\nfunction computeRedirectUrl(\n baseRedirectUrl: string,\n mergeToken?: string\n): string {\n try {\n const redirectUrl = new URL(baseRedirectUrl);\n if (!redirectUrl.searchParams.has(\"u\")) {\n return baseRedirectUrl;\n }\n\n // Append merge token to the page URL so it survives\n // the backend /common/social redirect chain\n const finalPageUrl = appendMergeToken(window.location.href, mergeToken);\n\n redirectUrl.searchParams.delete(\"u\");\n redirectUrl.searchParams.append(\"u\", finalPageUrl);\n\n return redirectUrl.toString();\n } catch {\n return baseRedirectUrl;\n }\n}\n\n/**\n * Redirect current page to Safari via x-safari-https:// scheme.\n * Used on iOS in-app browsers where backend 302 → custom scheme fails.\n */\nfunction redirectToSafari(mergeToken?: string) {\n const url = new URL(window.location.href);\n if (mergeToken) {\n url.searchParams.set(\"fmt\", mergeToken);\n }\n const scheme =\n url.protocol === \"http:\" ? \"x-safari-http\" : \"x-safari-https\";\n window.location.href = `${scheme}://${url.host}${url.pathname}${url.search}${url.hash}`;\n}\n\n/**\n * Check if this is a social/in-app-browser escape redirect (contains /common/social)\n */\nfunction isSocialRedirect(url: string): boolean {\n return url.includes(\"/common/social\");\n}\n\n/**\n * Append merge token to a URL as the `fmt` query parameter.\n */\nfunction appendMergeToken(urlString: string, mergeToken?: string): string {\n if (!mergeToken) return urlString;\n try {\n const url = new URL(urlString);\n url.searchParams.set(\"fmt\", mergeToken);\n return url.toString();\n } catch {\n const sep = urlString.includes(\"?\") ? \"&\" : \"?\";\n return `${urlString}${sep}fmt=${encodeURIComponent(mergeToken)}`;\n }\n}\n\n/**\n * Handle redirect with deep link fallback\n */\nfunction handleRedirect(\n iframe: HTMLIFrameElement,\n baseRedirectUrl: string,\n targetOrigin: string,\n mergeToken?: string,\n openInNewTab?: boolean\n): void {\n // If requested, open in a new tab instead of navigating the current page.\n // This preserves the merchant page while triggering universal links.\n // Requires the iframe postMessage to include user activation delegation.\n if (openInNewTab) {\n const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken);\n window.open(finalUrl, \"_blank\");\n return;\n }\n\n if (isFrakDeepLink(baseRedirectUrl)) {\n const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken);\n triggerDeepLinkWithFallback(finalUrl, {\n onFallback: () => {\n iframe.contentWindow?.postMessage(\n {\n clientLifecycle: \"deep-link-failed\",\n data: { originalUrl: finalUrl },\n },\n targetOrigin\n );\n },\n });\n } else if (isIOSInAppBrowser && isSocialRedirect(baseRedirectUrl)) {\n // iOS WKWebView silently swallows 302 redirects to custom URL\n // schemes — bypass the server redirect entirely\n redirectToSafari(mergeToken);\n } else {\n const finalUrl = computeRedirectUrl(baseRedirectUrl, mergeToken);\n window.location.href = finalUrl;\n }\n}\n\n/**\n * Create a new iframe lifecycle handler\n * @param args\n * @param args.iframe - The iframe element used for wallet communication\n * @param args.targetOrigin - The wallet URL origin for postMessage security\n * @ignore\n */\nexport function createIFrameLifecycleManager({\n iframe,\n targetOrigin,\n}: {\n iframe: HTMLIFrameElement;\n targetOrigin: string;\n}): IframeLifecycleManager {\n // Create the isConnected listener\n const isConnectedDeferred = new Deferred<boolean>();\n\n // Build the handler itself\n const handler = (messageEvent: FrakLifecycleEvent) => {\n if (!(\"iframeLifecycle\" in messageEvent)) return;\n\n const { iframeLifecycle: event, data } = messageEvent;\n\n switch (event) {\n // Resolve the isConnected promise\n case \"connected\":\n isConnectedDeferred.resolve(true);\n break;\n // Perform a frak backup\n case \"do-backup\":\n handleBackup(data.backup);\n break;\n // Remove frak backup\n case \"remove-backup\":\n localStorage.removeItem(BACKUP_KEY);\n break;\n // Change iframe visibility\n case \"show\":\n case \"hide\":\n changeIframeVisibility({ iframe, isVisible: event === \"show\" });\n break;\n // Redirect handling\n case \"redirect\":\n handleRedirect(\n iframe,\n data.baseRedirectUrl,\n targetOrigin,\n data.mergeToken,\n data.openInNewTab\n );\n break;\n }\n };\n\n return {\n handleEvent: handler,\n isConnected: isConnectedDeferred.promise,\n };\n}\n","import {\n createRpcClient,\n Deferred,\n FrakRpcError,\n type RpcClient,\n RpcErrorCodes,\n} from \"@frak-labs/frame-connector\";\nimport { OpenPanel } from \"@openpanel/web\";\nimport { getClientId } from \"../config/clientId\";\nimport { sdkConfigStore } from \"../config/sdkConfigStore\";\nimport { BACKUP_KEY } from \"../constants\";\nimport type { FrakLifecycleEvent } from \"../types\";\nimport type { FrakClient } from \"../types/client\";\nimport type { FrakWalletSdkConfig } from \"../types/config\";\nimport type { SdkResolvedConfig } from \"../types/resolvedConfig\";\nimport type { IFrameRpcSchema } from \"../types/rpc\";\nimport { clearAllCache } from \"../utils/cache\";\nimport { detectPageLanguage } from \"../utils/i18n/detectPageLanguage\";\nimport { setupSsoUrlListener } from \"./ssoUrlListener\";\nimport {\n createIFrameLifecycleManager,\n type IframeLifecycleManager,\n} from \"./transports/iframeLifecycleManager\";\n\ntype SdkRpcClient = RpcClient<IFrameRpcSchema, FrakLifecycleEvent>;\ntype MerchantConfigResult = Awaited<ReturnType<typeof sdkConfigStore.resolve>>;\n\n/**\n * Create a new iframe Frak client\n * @param args\n * @param args.config - The configuration to use for the Frak Wallet SDK.\n * When `config.domain` is set, it is used to resolve the correct merchant config in tunneled/proxied environments (e.g. Shopify dev with Cloudflare tunnel).\n * @param args.iframe - The iframe to use for the communication\n * @returns The created Frak Client\n *\n * @example\n * const frakConfig: FrakWalletSdkConfig = {\n * metadata: {\n * name: \"My app title\",\n * },\n * }\n * const iframe = await createIframe({ config: frakConfig });\n * const client = createIFrameFrakClient({ config: frakConfig, iframe });\n */\nexport function createIFrameFrakClient({\n config,\n iframe,\n}: {\n config: FrakWalletSdkConfig;\n iframe: HTMLIFrameElement;\n}): FrakClient {\n const frakWalletUrl = config?.walletUrl ?? \"https://wallet.frak.id\";\n\n // Precedence: explicit `metadata.lang` → page `<html lang>` → browser\n // language. Lets a page authored in a given language drive SDK copy even\n // when the visitor's browser is set to another language.\n const detectedLang = config.metadata.lang ?? detectPageLanguage();\n const targetDomain =\n config.domain ??\n (typeof window !== \"undefined\" ? window.location.hostname : \"\");\n sdkConfigStore.setCacheScope(targetDomain, detectedLang);\n sdkConfigStore.reset();\n\n // Skip fetch entirely if cache is fresh, otherwise fetch (SWR)\n const configPromise = sdkConfigStore.isCacheFresh\n ? undefined\n : sdkConfigStore.resolve(config.domain, config.walletUrl, detectedLang);\n\n // Create lifecycle manager\n const lifecycleManager = createIFrameLifecycleManager({\n iframe,\n targetOrigin: frakWalletUrl,\n });\n\n // Resolved after first resolved-config is sent to iframe (prevents RPC before context exists)\n const contextSent = new Deferred<void>();\n\n // Handshake timing: measured from client creation until the iframe\n // lifecycle manager resolves the `isConnected` promise.\n const handshakeStartedAt = Date.now();\n\n // Validate iframe\n if (!iframe.contentWindow) {\n throw new FrakRpcError(\n RpcErrorCodes.configError,\n \"The iframe does not have a content window\"\n );\n }\n\n // Create RPC client with middleware and lifecycle handlers\n const rpcClient = createRpcClient<IFrameRpcSchema, FrakLifecycleEvent>({\n emittingTransport: iframe.contentWindow,\n listeningTransport: window,\n targetOrigin: frakWalletUrl,\n middleware: [\n // Ensure we are connected and context is sent before sending request\n {\n async onRequest(_message, ctx) {\n const isConnected = await lifecycleManager.isConnected;\n if (!isConnected) {\n throw new FrakRpcError(\n RpcErrorCodes.clientNotConnected,\n \"The iframe provider isn't connected yet\"\n );\n }\n await contextSent.promise;\n return ctx;\n },\n },\n ],\n // Add lifecycle handlers to process iframe lifecycle events\n lifecycleHandlers: {\n iframeLifecycle: (event, _context) => {\n // Delegate to lifecycle manager (cast for type compatibility)\n lifecycleManager.handleEvent(event);\n },\n },\n });\n\n // Setup heartbeat\n const stopHeartbeat = setupHeartbeat(rpcClient, lifecycleManager);\n\n const destroy = async () => {\n stopHeartbeat();\n rpcClient.cleanup();\n iframe.remove();\n clearAllCache();\n sdkConfigStore.clearCache();\n sdkConfigStore.reset();\n };\n\n // Init open panel\n let openPanel: OpenPanel | undefined;\n if (\n process.env.OPEN_PANEL_API_URL &&\n process.env.OPEN_PANEL_SDK_CLIENT_ID\n ) {\n console.log(\"[Frak SDK] Initializing OpenPanel\");\n openPanel = new OpenPanel({\n apiUrl: process.env.OPEN_PANEL_API_URL,\n clientId: process.env.OPEN_PANEL_SDK_CLIENT_ID,\n trackScreenViews: true,\n trackOutgoingLinks: true,\n trackAttributes: false,\n // We use a filter to ensure we got the open panel instance initialized\n // A bit hacky, but this way we are sure that we got everything needed for the first event ever sent\n filter: ({ type, payload }) => {\n if (type !== \"track\") return true;\n if (!payload?.properties) return true;\n\n // Check if we we got the properties once initialized\n if (!(\"sdk_version\" in payload.properties)) {\n payload.properties = {\n ...payload.properties,\n sdk_version: process.env.SDK_VERSION,\n user_anonymous_client_id: getClientId(),\n };\n }\n\n return true;\n },\n });\n openPanel.setGlobalProperties({\n sdk_version: process.env.SDK_VERSION,\n user_anonymous_client_id: getClientId(),\n });\n openPanel.init();\n openPanel.track(\"sdk_initialized\", {\n sdk_version: process.env.SDK_VERSION,\n });\n\n // Race the connection against the heartbeat timeout so we can\n // distinguish \"connected\" from \"timeout\" cleanly without touching\n // the heartbeat plumbing. 30s matches `HEARTBEAT_TIMEOUT`.\n let settled = false;\n const timeoutHandle = setTimeout(() => {\n if (settled) return;\n settled = true;\n openPanel?.track(\"sdk_iframe_handshake_failed\", {\n reason: \"timeout\",\n });\n }, 30_000);\n lifecycleManager.isConnected\n .then(() => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n openPanel?.track(\"sdk_iframe_connected\", {\n handshake_duration_ms: Date.now() - handshakeStartedAt,\n });\n })\n .catch(() => {\n if (settled) return;\n settled = true;\n clearTimeout(timeoutHandle);\n openPanel?.track(\"sdk_iframe_handshake_failed\", {\n reason: \"unknown\",\n });\n });\n }\n\n // Perform the post connection setup\n const waitForSetup = postConnectionSetup({\n config,\n rpcClient,\n lifecycleManager,\n configPromise,\n contextSent,\n openPanel,\n })\n .then(() => {})\n .catch((err) => {\n contextSent.reject(err);\n throw err;\n });\n\n return {\n config,\n waitForConnection: lifecycleManager.isConnected,\n waitForSetup,\n request: rpcClient.request,\n listenerRequest: rpcClient.listen,\n destroy,\n openPanel,\n };\n}\n\n/**\n * Setup the heartbeat\n * @param rpcClient - RPC client to send lifecycle events\n * @param lifecycleManager - Lifecycle manager to track connection\n */\nfunction setupHeartbeat(\n rpcClient: SdkRpcClient,\n lifecycleManager: IframeLifecycleManager\n) {\n const HEARTBEAT_INTERVAL = 250; // Fallback discovery ping until we are connected\n const HEARTBEAT_TIMEOUT = 30_000; // 30 seconds timeout\n let heartbeatInterval: NodeJS.Timeout;\n let timeoutId: NodeJS.Timeout;\n\n const sendHeartbeat = () =>\n rpcClient.sendLifecycle({\n clientLifecycle: \"heartbeat\",\n });\n\n // Start sending heartbeats\n async function startHeartbeat() {\n sendHeartbeat(); // Send initial heartbeat\n heartbeatInterval = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);\n\n // Set up timeout\n timeoutId = setTimeout(() => {\n stopHeartbeat();\n console.log(\"Heartbeat timeout: connection failed\");\n }, HEARTBEAT_TIMEOUT);\n\n // Once connected, stop it\n await lifecycleManager.isConnected;\n\n // We are now connected, stop the heartbeat\n stopHeartbeat();\n }\n\n // Stop sending heartbeats\n function stopHeartbeat() {\n if (heartbeatInterval) {\n clearInterval(heartbeatInterval);\n }\n if (timeoutId) {\n clearTimeout(timeoutId);\n }\n }\n\n startHeartbeat();\n\n // Return cleanup function\n return stopHeartbeat;\n}\n\n/**\n * Perform the post connection setup\n * @param config - SDK configuration\n * @param rpcClient - RPC client to send lifecycle events\n * @param lifecycleManager - Lifecycle manager to track connection\n */\nasync function postConnectionSetup({\n config,\n rpcClient,\n lifecycleManager,\n configPromise,\n contextSent,\n openPanel,\n}: {\n config: FrakWalletSdkConfig;\n rpcClient: SdkRpcClient;\n lifecycleManager: IframeLifecycleManager;\n configPromise: Promise<MerchantConfigResult> | undefined;\n contextSent: Deferred<void>;\n openPanel: OpenPanel | undefined;\n}): Promise<void> {\n await lifecycleManager.isConnected;\n\n setupSsoUrlListener(rpcClient, lifecycleManager.isConnected);\n\n // Read and consume the pending merge token from URL (SSO identity merge)\n const url = new URL(window.location.href);\n const pendingMergeToken = url.searchParams.get(\"fmt\") ?? undefined;\n if (pendingMergeToken) {\n url.searchParams.delete(\"fmt\");\n window.history.replaceState({}, \"\", url.toString());\n }\n\n // Merge a raw backend response with SDK metadata and persist to store\n const mergeAndSetConfig = (merchantConfig: MerchantConfigResult) => {\n const merchantId =\n merchantConfig?.merchantId ?? config.metadata.merchantId ?? \"\";\n const domain = merchantConfig?.domain ?? \"\";\n const allowedDomains = merchantConfig?.allowedDomains ?? [];\n const raw = merchantConfig?.sdkConfig;\n\n // Per-field merge: backend wins over SDK static config.\n const mergedAttribution =\n raw?.attribution || config.attribution\n ? { ...config.attribution, ...raw?.attribution }\n : undefined;\n\n sdkConfigStore.setConfig(\n raw\n ? {\n isResolved: true,\n merchantId,\n domain,\n allowedDomains,\n hasRawSdkConfig: true,\n name: raw.name ?? config.metadata.name,\n logoUrl: raw.logoUrl ?? config.metadata.logoUrl,\n homepageLink:\n raw.homepageLink ?? config.metadata.homepageLink,\n lang: raw.lang ?? config.metadata.lang,\n currency: raw.currency ?? config.metadata.currency,\n hidden: raw.hidden,\n css: raw.css,\n translations: raw.translations,\n placements: raw.placements,\n components: raw.components,\n attribution: mergedAttribution,\n }\n : {\n isResolved: true,\n merchantId,\n domain,\n allowedDomains,\n name: config.metadata.name,\n logoUrl: config.metadata.logoUrl,\n homepageLink: config.metadata.homepageLink,\n lang: config.metadata.lang,\n currency: config.metadata.currency,\n attribution: mergedAttribution,\n }\n );\n };\n\n // Send the resolved-config lifecycle event to the iframe.\n // This is where we also update SDK-side OpenPanel global props with\n // `merchantId` + `domain` (first time they are known) so every\n // subsequent SDK event is merchant-attributed. We pass\n // `sdkAnonymousId` through so the listener can join SDK funnels.\n let mergeTokenConsumed = false;\n const sendLifecycleConfig = (resolved: SdkResolvedConfig) => {\n const token = mergeTokenConsumed ? undefined : pendingMergeToken;\n mergeTokenConsumed = true;\n\n const sdkConfig = resolved.hasRawSdkConfig\n ? {\n name: resolved.name,\n logoUrl: resolved.logoUrl,\n homepageLink: resolved.homepageLink,\n lang: resolved.lang,\n currency: resolved.currency,\n hidden: resolved.hidden,\n css: resolved.css,\n translations: resolved.translations,\n placements: resolved.placements,\n attribution: resolved.attribution,\n }\n : resolved.attribution\n ? { attribution: resolved.attribution }\n : undefined;\n\n const sdkAnonymousId = getClientId();\n\n if (openPanel) {\n const current = openPanel.global ?? {};\n openPanel.setGlobalProperties({\n ...current,\n merchant_id: resolved.merchantId,\n domain: resolved.domain ?? \"\",\n });\n }\n\n rpcClient.sendLifecycle({\n clientLifecycle: \"resolved-config\",\n data: {\n merchantId: resolved.merchantId,\n domain: resolved.domain ?? \"\",\n allowedDomains: resolved.allowedDomains ?? [],\n sourceUrl: window.location.href,\n ...(sdkAnonymousId && { sdkAnonymousId }),\n ...(token && { pendingMergeToken: token }),\n ...(sdkConfig && { sdkConfig }),\n },\n });\n };\n\n // SWR: if we have cached data, send it to the iframe immediately\n if (sdkConfigStore.isResolved) {\n sendLifecycleConfig(sdkConfigStore.getConfig());\n contextSent.resolve();\n }\n\n // If a fetch is running (stale/missing cache), wait for fresh data and update\n if (configPromise) {\n const merchantConfig = await configPromise;\n mergeAndSetConfig(merchantConfig);\n sendLifecycleConfig(sdkConfigStore.getConfig());\n contextSent.resolve();\n }\n\n // Push raw CSS if needed\n async function pushCss() {\n const cssLink = config.customizations?.css;\n if (!cssLink) return;\n rpcClient.sendLifecycle({\n clientLifecycle: \"modal-css\" as const,\n data: { cssLink },\n });\n }\n\n // Push i18n if needed\n async function pushI18n() {\n const i18n = config.customizations?.i18n;\n if (!i18n) return;\n rpcClient.sendLifecycle({\n clientLifecycle: \"modal-i18n\" as const,\n data: { i18n },\n });\n }\n\n // Push local backup if needed\n async function pushBackup() {\n if (typeof window === \"undefined\") return;\n const backup = window.localStorage.getItem(BACKUP_KEY);\n if (!backup) return;\n rpcClient.sendLifecycle({\n clientLifecycle: \"restore-backup\" as const,\n data: { backup },\n });\n }\n\n // Inspect each setup result — a failed CSS/i18n/backup push leaves the\n // partner UI in a broken-but-connected state (iframe reports\n // `sdk_iframe_connected`, user sees no modal styles / wrong locale).\n // Surface it as a distinct handshake reason so dashboards can\n // distinguish timeout vs. asset-push failures.\n const results = await Promise.allSettled([\n pushCss(),\n pushI18n(),\n pushBackup(),\n ]);\n const hasFailedAssetPush = results.some((r) => r.status === \"rejected\");\n if (hasFailedAssetPush) {\n openPanel?.track(\"sdk_iframe_handshake_failed\", {\n reason: \"asset_push\",\n });\n }\n}\n","import { jsonDecode } from \"@frak-labs/frame-connector\";\nimport { base64urlDecode } from \"./b64\";\n\n/**\n * Decompress json data\n * @param data\n * @ignore\n */\nexport function decompressJsonFromB64<T>(data: string): T | null {\n return jsonDecode<T>(base64urlDecode(data));\n}\n","import { createIFrameFrakClient } from \"../clients\";\nimport type { FrakClient, FrakWalletSdkConfig } from \"../types\";\nimport { createIframe, getSupportedCurrency } from \"../utils\";\n\n/**\n * Directly setup the Frak client with an iframe\n * Return when the FrakClient is ready (setup and communication estbalished with the wallet)\n *\n * @param config - The configuration to use for the Frak Wallet SDK\n * @returns a Promise with the Frak Client\n *\n * @example\n * const frakConfig: FrakWalletSdkConfig = {\n * metadata: {\n * name: \"My app title\",\n * },\n * }\n * const client = await setupClient({ config: frakConfig });\n */\nexport async function setupClient({\n config,\n}: {\n config: FrakWalletSdkConfig;\n}): Promise<FrakClient | undefined> {\n // Prepare the config\n const preparedConfig = prepareConfig(config);\n\n // Create our iframe\n const iframe = await createIframe({\n config: preparedConfig,\n });\n\n if (!iframe) {\n console.error(\"Failed to create iframe\");\n return;\n }\n\n // Create our client\n const client = createIFrameFrakClient({\n config: preparedConfig,\n iframe,\n });\n\n // Wait for the client to be all setup\n await client.waitForSetup;\n\n // Wait for the connection to be established\n const waitForConnection = await client.waitForConnection;\n if (!waitForConnection) {\n console.error(\"Failed to connect to client\");\n return;\n }\n\n return client;\n}\n\n/**\n * Prepare the config for the Frak Client\n * @param config - The configuration to use for the Frak Wallet SDK\n * @returns The prepared configuration with the supported currency\n */\nfunction prepareConfig(config: FrakWalletSdkConfig): FrakWalletSdkConfig {\n // Get the supported currency (e.g. \"eur\")\n const supportedCurrency = getSupportedCurrency(config.metadata?.currency);\n\n return {\n ...config,\n metadata: {\n ...config.metadata,\n currency: supportedCurrency,\n },\n };\n}\n","import type { AttributionDefaults, AttributionParams } from \"../types/tracking\";\n\n/**\n * Inputs for {@link mergeAttribution}.\n */\nexport type MergeAttributionInput = {\n /**\n * Per-call attribution override passed to actions like `displaySharingPage`.\n *\n * - `null` explicitly disables attribution (no UTM/ref/via params are added).\n * - `undefined` means \"no per-call override\" — defaults apply if present.\n * - An object (including `{}`) merges field-by-field with defaults.\n */\n perCall: AttributionParams | null | undefined;\n /**\n * Pre-merged merchant-level defaults (backend config > SDK static config).\n * `utm_content` is intentionally absent from this shape.\n */\n defaults?: AttributionDefaults;\n /**\n * Per-product `utm_content` override (from the currently selected\n * `SharingPageProduct`). Takes precedence over `perCall.utmContent`.\n */\n productUtmContent?: string;\n};\n\n/**\n * Merge the three attribution layers into a single {@link AttributionParams}\n * value suitable for `FrakContextManager.update`.\n *\n * Priority per field:\n * 1. `perCall` (wins)\n * 2. `defaults` (merchant-level, backend > SDK static, already pre-merged)\n * 3. Hardcoded fallbacks resolved later by `FrakContextManager`\n *\n * Special rules:\n * - `perCall === null` returns `undefined` (explicit disable: no UTM/ref/via).\n * - `perCall === undefined` (no opinion) yields at least `{}` so `FrakContextManager`\n * applies its hardcoded defaults (utm_source=frak, utm_medium=referral,\n * utm_campaign=<merchantId>, ref=<clientId>, via=frak).\n * - `utm_content` never comes from `defaults`; only `productUtmContent` or\n * `perCall.utmContent` can populate it.\n */\nexport function mergeAttribution({\n perCall,\n defaults,\n productUtmContent,\n}: MergeAttributionInput): AttributionParams | undefined {\n // Explicit disable\n if (perCall === null) return undefined;\n\n const hasPerCall = perCall !== undefined;\n const hasDefaults =\n defaults !== undefined && Object.keys(defaults).length > 0;\n const hasProductUtm =\n productUtmContent !== undefined && productUtmContent !== \"\";\n\n if (!hasPerCall && !hasDefaults && !hasProductUtm) return undefined;\n\n // Per-field merge: per-call wins over defaults.\n const merged: AttributionParams = {\n ...defaults,\n ...(perCall ?? {}),\n };\n\n // utm_content priority: productUtmContent > perCall.utmContent; never from defaults.\n const utmContent = productUtmContent ?? perCall?.utmContent;\n if (utmContent !== undefined && utmContent !== \"\") {\n merged.utmContent = utmContent;\n } else {\n delete merged.utmContent;\n }\n\n return merged;\n}\n"],"mappings":"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