From c720f1c8de81248ee40303961971b4652266770c Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Tue, 3 Mar 2026 07:34:39 -0800 Subject: [PATCH] feat(packages): Phase 3.1 - Create @bytelyst/broadcast-client package - package.json: ESM module config - src/index.ts: Broadcast client factory with types, hooks - tsconfig.json: TypeScript configuration --- packages/broadcast-client/package.json | 21 +++ packages/broadcast-client/src/index.ts | 171 ++++++++++++++++++++++++ packages/broadcast-client/tsconfig.json | 10 ++ 3 files changed, 202 insertions(+) create mode 100644 packages/broadcast-client/package.json create mode 100644 packages/broadcast-client/src/index.ts create mode 100644 packages/broadcast-client/tsconfig.json diff --git a/packages/broadcast-client/package.json b/packages/broadcast-client/package.json new file mode 100644 index 00000000..0c8f5b6c --- /dev/null +++ b/packages/broadcast-client/package.json @@ -0,0 +1,21 @@ +{ + "name": "@bytelyst/broadcast-client", + "version": "0.1.0", + "type": "module", + "description": "Browser/React Native-safe broadcast messaging client for platform-service", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "vitest run" + } +} diff --git a/packages/broadcast-client/src/index.ts b/packages/broadcast-client/src/index.ts new file mode 100644 index 00000000..ddee7ead --- /dev/null +++ b/packages/broadcast-client/src/index.ts @@ -0,0 +1,171 @@ +/** + * 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 }; + }; +} diff --git a/packages/broadcast-client/tsconfig.json b/packages/broadcast-client/tsconfig.json new file mode 100644 index 00000000..3686f563 --- /dev/null +++ b/packages/broadcast-client/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}