export interface DeepLink { url: URL; path: string; query: URLSearchParams; } export function parseDeepLink(value: string, schemes: string[] = []): DeepLink | null { try { const url = new URL(value); if (schemes.length && !schemes.includes(url.protocol.replace(/:$/, ""))) return null; return { url, path: url.pathname || "/", query: url.searchParams }; } catch { return null; } } export interface DeepLinkSource { current?(): Promise; subscribe(listener: (url: string) => void): void | (() => void); } /** Normalize initial and live native links and ignore malformed/unapproved schemes. */ export function listenDeepLinks( source: DeepLinkSource, listener: (link: DeepLink) => void, schemes: string[] = [], ): () => void { let active = true; const emit = (value: string) => { const link = parseDeepLink(value, schemes); if (active && link) listener(link); }; void source.current?.().then((value) => value && emit(value)); const unsubscribe = source.subscribe(emit); return () => { active = false; unsubscribe?.(); }; } export interface PushRegistration { token: string; platform?: string; } export interface PushAdapter { permission(): Promise<"granted" | "denied" | "prompt" | "unavailable">; requestPermission?(): Promise<"granted" | "denied">; register(): Promise; subscribe?(listener: (notification: unknown) => void): () => void; } export class PushNotifications { constructor(private readonly adapter: PushAdapter) {} async register(): Promise { let permission = await this.adapter.permission(); if (permission === "prompt" && this.adapter.requestPermission) permission = await this.adapter.requestPermission(); if (permission !== "granted") throw new Error("WRN-MOBILE-PUSH-PERMISSION-DENIED"); const registration = await this.adapter.register(); if (!registration.token.trim()) throw new Error("WRN-MOBILE-PUSH-EMPTY-TOKEN"); return registration; } subscribe(listener: (notification: unknown) => void): () => void { return this.adapter.subscribe?.(listener) ?? (() => undefined); } } export interface SecureStorageAdapter { get(key: string): Promise; set(key: string, value: string): Promise; remove(key: string): Promise; } export class SecureStorage { constructor( private readonly adapter: SecureStorageAdapter, private readonly namespace = "wrnexus", ) {} #key(key: string): string { if (!/^[A-Za-z0-9._-]{1,128}$/.test(key)) throw new TypeError("WRN-MOBILE-STORAGE-KEY"); return `${this.namespace}:${key}`; } get(key: string): Promise { return this.adapter.get(this.#key(key)); } set(key: string, value: string): Promise { return this.adapter.set(this.#key(key), value); } remove(key: string): Promise { return this.adapter.remove(this.#key(key)); } } export interface OfflineTask { id: string; type: string; payload: T; createdAt: number; attempts: number; } export interface OfflineTaskStore { load(): Promise; save(tasks: OfflineTask[]): Promise; } export function memoryOfflineTaskStore(): OfflineTaskStore { let tasks: OfflineTask[] = []; return { async load() { return structuredClone(tasks); }, async save(next) { tasks = structuredClone(next); }, }; } export class OfflineQueue { readonly #handlers = new Map Promise>(); constructor(private readonly store: OfflineTaskStore = memoryOfflineTaskStore()) {} process(type: string, handler: (payload: T) => Promise): void { this.#handlers.set(type, handler as (payload: unknown) => Promise); } async add(type: string, payload: T): Promise> { const tasks = await this.store.load(); const task = { id: crypto.randomUUID(), type, payload, createdAt: Date.now(), attempts: 0 }; tasks.push(task); await this.store.save(tasks); return task; } async sync(limit = 20): Promise<{ completed: number; failed: number }> { const tasks = await this.store.load(); const remaining: OfflineTask[] = []; let completed = 0, failed = 0; for (const task of tasks) { if (completed + failed >= limit) { remaining.push(task); continue; } const handler = this.#handlers.get(task.type); if (!handler) { remaining.push(task); continue; } try { task.attempts++; await handler(task.payload); completed++; } catch { failed++; remaining.push(task); } } await this.store.save(remaining); return { completed, failed }; } async size(): Promise { return (await this.store.load()).length; } } export interface MobileEnvironment { platform: string; native: boolean; online: boolean; userAgent?: string; } export function mobileEnvironment(): MobileEnvironment { const capacitor = ( globalThis as typeof globalThis & { Capacitor?: { isNativePlatform?: () => boolean; getPlatform?: () => string }; } ).Capacitor; const native = capacitor?.isNativePlatform?.() === true; return { platform: capacitor?.getPlatform?.() ?? "web", native, online: typeof navigator === "undefined" ? true : navigator.onLine, userAgent: typeof navigator === "undefined" ? undefined : navigator.userAgent, }; }