/** * Broadcast Client — Browser/React Native-safe broadcast messaging client * @module @bytelyst/broadcast-client */ // ============================================================================= // Types // ============================================================================= export interface Broadcast { id: string; productId: string; title: string; body: string; bodyMarkdown?: string; ctaText?: string; ctaUrl?: string; imageUrl?: string; channels: ('push' | 'in_app' | 'email')[]; status: 'draft' | 'scheduled' | 'sending' | 'sent' | 'paused'; scheduledAt?: string; sentAt?: string; variant?: 'control' | 'treatment'; experimentId?: string; parentBroadcastId?: string; metrics: BroadcastMetrics; createdAt: string; updatedAt: string; createdBy: string; } export interface BroadcastMetrics { targetedCount: number; sentCount: number; deliveredCount: number; openedCount: number; clickedCount: number; dismissedCount: number; convertedCount: number; } export interface InAppMessage { id: string; userId: string; productId: string; broadcastId: string; title: string; body: string; bodyMarkdown?: string; ctaText?: string; ctaUrl?: string; priority: 'low' | 'normal' | 'high' | 'urgent'; style: 'banner' | 'modal' | 'toast' | 'fullscreen'; dismissible: boolean; expiresAt?: string; status: 'unread' | 'read' | 'dismissed'; createdAt: string; updatedAt: string; } export interface BroadcastClientConfig { /** Platform service base URL */ baseUrl: string; /** Product ID */ productId: string; /** Auth token provider (async or sync) */ getAuthToken: (() => string) | (() => Promise); /** Platform identifier */ platform: 'web' | 'ios' | 'android' | 'macos' | 'windows'; /** App version */ appVersion: string; /** OS version */ osVersion: string; /** Optional country code */ countryCode?: string; /** Optional region code */ regionCode?: string; /** User segments (default: ['free']) */ userSegments?: string[]; } // ============================================================================= // Client Factory // ============================================================================= export interface BroadcastClient { /** List active in-app messages for current user */ listMessages(): Promise<{ messages: InAppMessage[] }>; /** Mark message as read */ markRead(messageId: string): Promise; /** Mark message as dismissed */ markDismissed(messageId: string): Promise; /** Track CTA click */ trackClick(messageId: string): Promise<{ redirectUrl?: string }>; /** Poll for new messages (use with setInterval) */ pollMessages(intervalMs?: number): () => void; } export function createBroadcastClient(config: BroadcastClientConfig): BroadcastClient { const headers = async () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${await Promise.resolve(config.getAuthToken())}`, 'x-product-id': config.productId, 'x-platform': config.platform, 'x-app-version': config.appVersion, 'x-os-version': config.osVersion, ...(config.countryCode && { 'x-country-code': config.countryCode }), ...(config.regionCode && { 'x-region-code': config.regionCode }), 'x-user-segments': (config.userSegments ?? ['free']).join(','), }); const request = async (path: string, options?: RequestInit): Promise => { const res = await fetch(`${config.baseUrl}${path}`, { ...options, headers: { ...(await headers()), ...(options?.headers || {}), }, }); if (!res.ok) { const err = await res.text(); throw new Error(`Broadcast API error: ${res.status} ${err}`); } return res.json() as Promise; }; let pollInterval: ReturnType | null = null; return { async listMessages() { return request<{ messages: InAppMessage[] }>('/broadcasts'); }, async markRead(messageId: string) { await request(`/broadcasts/${messageId}/read`, { method: 'POST' }); }, async markDismissed(messageId: string) { await request(`/broadcasts/${messageId}/dismiss`, { method: 'POST' }); }, async trackClick(messageId: string) { return request<{ redirectUrl?: string }>(`/broadcasts/${messageId}/click`, { method: 'POST', }); }, pollMessages(intervalMs = 60000) { if (pollInterval) clearInterval(pollInterval); pollInterval = setInterval(() => { this.listMessages().catch(() => {}); }, intervalMs); return () => { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } }; }, }; } // ============================================================================= // React Hook (optional) // ============================================================================= export function createUseBroadcast(client: BroadcastClient) { return function useBroadcast() { return { client }; }; } // ============================================================================= // Deep Link Router // ============================================================================= export { DeepLinkRouter, deepLinkRouter, DeepLinkScreens, createBroadcastDeepLink, type DeepLinkRoute, type DeepLinkHandler, } from './deep-link.js';