UNPKG

accounts

Version:

Tempo Accounts SDK

231 lines (209 loc) 6.52 kB
import type { RpcRequest, RpcResponse } from 'ox' import type * as Store from './Store.js' /** Messenger interface for cross-frame communication. */ export type Messenger = { /** Tear down all listeners. */ destroy: () => void /** Subscribe to a topic. Returns an unsubscribe function. */ on: <const topic extends Topic>( topic: topic | Topic, listener: (payload: Payload<topic>, event: MessageEvent) => void, id?: string | undefined, ) => () => void /** Send a message on a topic. */ send: <const topic extends Topic>( topic: topic | Topic, payload: Payload<topic>, targetOrigin?: string | undefined, ) => Promise<{ id: string; topic: topic; payload: Payload<topic> }> } /** Options sent with the `ready` signal from the remote frame. */ export type ReadyOptions = { /** CSS `color-scheme` used by the remote embed (e.g. `'dark'`). */ colorScheme?: string | undefined /** Hostnames trusted by the remote embed to render in an iframe. */ trustedHosts?: readonly string[] | undefined } /** Bridge messenger that waits for a `ready` signal from the remote frame. */ export type Bridge = Messenger & { /** Signal readiness (called by the remote frame). */ ready: (options?: ReadyOptions | undefined) => void /** Promise that resolves when the remote frame signals ready. */ waitForReady: () => Promise<ReadyOptions> } /** Message schema for cross-frame communication. */ export type Schema = [ { topic: 'ready' payload: ReadyOptions }, { topic: 'rpc-requests' payload: { account: { address: string } | undefined chainId: number requests: readonly Store.QueuedRequest[] } }, { topic: 'rpc-response' payload: RpcResponse.RpcResponse & { _request: RpcRequest.RpcRequest } }, { topic: 'close' payload: undefined }, { topic: 'switch-mode' payload: { mode: 'popup' } }, { topic: 'sync' payload: { addresses?: readonly string[] | undefined; valid?: boolean | undefined } }, { topic: 'theme' payload: { accent?: string | undefined radius?: string | undefined scheme?: string | undefined } }, ] /** Union of all topic strings. */ export type Topic = Schema[number]['topic'] /** Payload for a given topic. */ export type Payload<topic extends Topic> = Extract<Schema[number], { topic: topic }>['payload'] /** Creates a messenger from a custom implementation. */ export function from(messenger: Messenger): Messenger { return messenger } /** * Creates a messenger backed by `window.postMessage` / `addEventListener('message')`. * Filters messages by `targetOrigin` when provided. */ export function fromWindow(w: Window, options: fromWindow.Options = {}): Messenger { const { targetOrigin } = options const listeners = new Map<string, (event: MessageEvent) => void>() return from({ destroy() { for (const listener of listeners.values()) w.removeEventListener('message', listener) listeners.clear() }, on(topic, listener, id) { function onMessage(event: MessageEvent) { if (event.data.topic !== topic) return if (id && event.data.id !== id) return if (targetOrigin && event.origin !== targetOrigin) return listener(event.data.payload as never, event) } w.addEventListener('message', onMessage) listeners.set(topic, onMessage) return () => { w.removeEventListener('message', onMessage) listeners.delete(topic) } }, async send(topic, payload, target) { const id = crypto.randomUUID() w.postMessage(normalizeValue({ id, payload, topic }), target ?? targetOrigin ?? '*') return { id, payload, topic } as never }, }) } export declare namespace fromWindow { type Options = { /** Only accept messages from this origin. Also used as the `targetOrigin` for `postMessage`. */ targetOrigin?: string | undefined } } /** * Bridges two window messengers. The bridge waits for a `ready` signal * before sending messages when `waitForReady` is `true`. */ export function bridge(parameters: bridge.Parameters): Bridge { const { from: from_, to, waitForReady = false } = parameters let pending = false const ready = withResolvers<ReadyOptions>() from_.on('ready', (payload) => ready.resolve(payload ?? {})) const messenger = from({ destroy() { from_.destroy() to.destroy() if (pending) ready.reject() }, on(topic, listener, id) { return from_.on(topic, listener, id) }, async send(topic, payload) { pending = true if (waitForReady) await ready.promise.finally(() => (pending = false)) return to.send(topic, payload) }, }) return { ...messenger, ready(options) { void messenger.send('ready', options ?? {}) }, waitForReady() { return ready.promise }, } } export declare namespace bridge { type Parameters = { /** Listens on this messenger. */ from: Messenger /** Sends to this messenger. */ to: Messenger /** Buffer sends until `ready` is received. */ waitForReady?: boolean | undefined } } /** Returns a no-op bridge for SSR environments. */ export function noop(): Bridge { return { destroy() {}, on() { return () => {} }, send() { return Promise.resolve(undefined as never) }, ready() {}, waitForReady() { return Promise.resolve({}) }, } } function withResolvers<type>() { let resolve: (value: type | PromiseLike<type>) => void = () => undefined let reject: (reason?: unknown) => void = () => undefined const promise = new Promise<type>((resolve_, reject_) => { resolve = resolve_ reject = reject_ }) return { promise, reject, resolve } } /** * Normalizes a value into a structured-clone compatible format. * * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone */ function normalizeValue<type>(value: type): type { if (Array.isArray(value)) return value.map(normalizeValue) as never if (typeof value === 'function') return undefined as never if (typeof value !== 'object' || value === null) return value if (Object.getPrototypeOf(value) !== Object.prototype) try { return structuredClone(value) } catch { return undefined as never } const normalized: Record<string, unknown> = {} for (const [k, v] of Object.entries(value)) normalized[k] = normalizeValue(v) return normalized as never }