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
This commit is contained in:
parent
1832ef48a3
commit
c720f1c8de
21
packages/broadcast-client/package.json
Normal file
21
packages/broadcast-client/package.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
171
packages/broadcast-client/src/index.ts
Normal file
171
packages/broadcast-client/src/index.ts
Normal file
@ -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<string>);
|
||||||
|
/** 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<void>;
|
||||||
|
/** Mark message as dismissed */
|
||||||
|
markDismissed(messageId: string): Promise<void>;
|
||||||
|
/** 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 <T>(path: string, options?: RequestInit): Promise<T> => {
|
||||||
|
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<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let pollInterval: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
async listMessages() {
|
||||||
|
return request<{ messages: InAppMessage[] }>('/broadcasts');
|
||||||
|
},
|
||||||
|
|
||||||
|
async markRead(messageId: string) {
|
||||||
|
await request<void>(`/broadcasts/${messageId}/read`, { method: 'POST' });
|
||||||
|
},
|
||||||
|
|
||||||
|
async markDismissed(messageId: string) {
|
||||||
|
await request<void>(`/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 };
|
||||||
|
};
|
||||||
|
}
|
||||||
10
packages/broadcast-client/tsconfig.json
Normal file
10
packages/broadcast-client/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user