diff --git a/packages/broadcast-client/README.md b/packages/broadcast-client/README.md new file mode 100644 index 00000000..3227c51f --- /dev/null +++ b/packages/broadcast-client/README.md @@ -0,0 +1,227 @@ +# @bytelyst/broadcast-client + +TypeScript client for the ByteLyst Broadcast & Messaging platform. Provides in-app message polling, read receipts, and push notification token management. + +## Installation + +```bash +npm install @bytelyst/broadcast-client +# or +pnpm add @bytelyst/broadcast-client +``` + +## Quick Start + +```typescript +import { createBroadcastClient } from '@bytelyst/broadcast-client'; + +const client = createBroadcastClient({ + baseURL: 'https://api.bytelyst.io/v1', + productId: 'lysnrai', + getAuthToken: async () => { + // Return your JWT token + return localStorage.getItem('token'); + } +}); + +// Start polling for messages (every 60 seconds) +client.startPolling(60000, (messages) => { + console.log('New messages:', messages); +}); +``` + +## API Reference + +### `createBroadcastClient(config)` + +Creates a new broadcast client instance. + +**Config:** +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `baseURL` | string | Yes | API base URL | +| `productId` | string | Yes | Product identifier | +| `getAuthToken` | () => Promise | Yes | Function to retrieve JWT token | + +### Methods + +#### `getMessages()` +Fetch active in-app messages for the current user. + +```typescript +const { data, error } = await client.getMessages(); +// Returns: { messages: InAppMessage[] } +``` + +#### `markRead(messageId: string)` +Mark a message as read. + +```typescript +await client.markRead('msg_123'); +``` + +#### `markDismissed(messageId: string)` +Dismiss a message. + +```typescript +await client.markDismissed('msg_123'); +``` + +#### `trackClick(messageId: string)` +Track when user clicks/taps a message CTA. + +```typescript +await client.trackClick('msg_123'); +``` + +#### `startPolling(intervalMs: number, callback: (messages) => void)` +Start polling for new messages. + +```typescript +client.startPolling(60000, (messages) => { + // Called every 60 seconds with current messages +}); +``` + +#### `stopPolling()` +Stop message polling. + +```typescript +client.stopPolling(); +``` + +#### `registerDeviceToken(token: string, platform: 'ios' | 'android' | 'web')` +Register push notification device token. + +```typescript +await client.registerDeviceToken('fcm_token_xyz', 'android'); +``` + +#### `unregisterDeviceToken(token: string)` +Unregister device token (e.g., on logout). + +```typescript +await client.unregisterDeviceToken('fcm_token_xyz'); +``` + +## React Integration + +### Hook Usage + +```typescript +import { useBroadcastClient } from './hooks/useBroadcastClient'; + +function App() { + const { messages, unreadCount, markRead, markDismissed } = useBroadcastClient({ + pollingInterval: 60000 + }); + + return ( +
+ {messages.map(msg => ( + markDismissed(msg.id)} + onClick={() => markRead(msg.id)} + /> + ))} +
+ ); +} +``` + +### Provider Pattern + +```typescript +// BroadcastProvider.tsx +import { createContext, useContext, useEffect, useState } from 'react'; +import { createBroadcastClient, BroadcastClient, InAppMessage } from '@bytelyst/broadcast-client'; + +const BroadcastContext = createContext(null); + +export function BroadcastProvider({ children, config }: { children: React.ReactNode; config: BroadcastConfig }) { + const [client] = useState(() => createBroadcastClient(config)); + const [messages, setMessages] = useState([]); + + useEffect(() => { + client.startPolling(60000, setMessages); + return () => client.stopPolling(); + }, [client]); + + const value = { + messages, + unreadCount: messages.filter(m => m.status === 'unread').length, + markRead: client.markRead.bind(client), + markDismissed: client.markDismissed.bind(client), + trackClick: client.trackClick.bind(client), + }; + + return ( + + {children} + + ); +} + +export const useBroadcast = () => { + const ctx = useContext(BroadcastContext); + if (!ctx) throw new Error('useBroadcast must be used within BroadcastProvider'); + return ctx; +}; +``` + +## Types + +```typescript +interface InAppMessage { + id: string; + broadcastId: string; + title: string; + body?: string; + style: 'banner' | 'modal' | 'fullscreen' | 'toast'; + priority: 'low' | 'normal' | 'high' | 'urgent'; + ctaText?: string; + ctaUrl?: string; + imageUrl?: string; + deepLink?: { + screen: string; + params: Record; + }; + status: 'unread' | 'read' | 'dismissed'; + createdAt: string; +} + +interface BroadcastConfig { + baseURL: string; + productId: string; + getAuthToken: () => Promise; +} +``` + +## Error Handling + +All methods return a result tuple `[data, error]`: + +```typescript +const [data, error] = await client.getMessages(); + +if (error) { + console.error('Failed to fetch messages:', error.message); + return; +} + +// Use data.messages +``` + +## Browser Support + +- Chrome 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ + +## License + +MIT © ByteLyst diff --git a/packages/survey-client/README.md b/packages/survey-client/README.md new file mode 100644 index 00000000..d86a83a9 --- /dev/null +++ b/packages/survey-client/README.md @@ -0,0 +1,349 @@ +# @bytelyst/survey-client + +TypeScript client for the ByteLyst Survey platform. Provides survey discovery, question flow management, response submission, and offline caching. + +## Installation + +```bash +npm install @bytelyst/survey-client +# or +pnpm add @bytelyst/survey-client +``` + +## Quick Start + +```typescript +import { createSurveyClient } from '@bytelyst/survey-client'; + +const client = createSurveyClient({ + baseURL: 'https://api.bytelyst.io/v1', + productId: 'lysnrai', + getAuthToken: async () => { + return localStorage.getItem('token'); + } +}); + +// Check for active survey +const [survey, error] = await client.getActiveSurvey(); +if (survey) { + console.log('Survey available:', survey.title); +} +``` + +## API Reference + +### `createSurveyClient(config)` + +Creates a new survey client instance. + +**Config:** +| Option | Type | Required | Description | +|--------|------|----------|-------------| +| `baseURL` | string | Yes | API base URL | +| `productId` | string | Yes | Product identifier | +| `getAuthToken` | () => Promise | Yes | Function to retrieve JWT token | +| `enableOfflineCache` | boolean | No | Enable offline response caching (default: true) | + +### Methods + +#### `getActiveSurvey()` +Check if there's an active survey for the current user. + +```typescript +const [survey, error] = await client.getActiveSurvey(); +// Returns: ActiveSurvey | null +``` + +#### `startSurvey(surveyId: string)` +Start a survey session. + +```typescript +const [state, error] = await client.startSurvey('srv_123'); +// Returns: { surveyStateId: string, currentQuestionIndex: number } +``` + +#### `submitAnswer(surveyId: string, questionId: string, answer: SurveyAnswer)` +Submit an answer for a specific question. + +```typescript +const [result, error] = await client.submitAnswer('srv_123', 'q1', { + type: 'rating', + value: { value: 8 } +}); +// Returns: { currentQuestionIndex: number, nextQuestionId: string, isComplete: boolean } +``` + +**Answer Types:** +```typescript +// Single choice +{ type: 'single_choice', value: { value: 'option_1' } } + +// Multiple choice +{ type: 'multiple_choice', value: { values: ['opt_1', 'opt_2'] } } + +// Rating/NPS/Scale +{ type: 'rating', value: { value: 8 } } + +// Text +{ type: 'text', value: { value: 'My feedback here' } } + +// Ranking +{ type: 'ranking', value: { order: ['opt_1', 'opt_2', 'opt_3'] } } +``` + +#### `completeSurvey(surveyId: string)` +Mark survey as complete and claim any incentive. + +```typescript +const [completion, error] = await client.completeSurvey('srv_123'); +// Returns: { success: boolean, incentiveClaimed: boolean, incentiveType?: string, incentiveAmount?: number } +``` + +#### `dismissSurvey(surveyId: string)` +Dismiss survey without completing. + +```typescript +await client.dismissSurvey('srv_123'); +``` + +#### `startPolling(intervalMs: number, callback: (survey) => void)` +Start polling for active surveys. + +```typescript +client.startPolling(60000, (survey) => { + if (survey) { + showSurveyModal(survey); + } +}); +``` + +#### `stopPolling()` +Stop survey polling. + +```typescript +client.stopPolling(); +``` + +#### `flushOfflineQueue()` +Manually flush any cached offline responses. + +```typescript +await client.flushOfflineQueue(); +``` + +## React Integration + +### Hook Usage + +```typescript +import { useSurvey } from './hooks/useSurvey'; + +function SurveyWidget() { + const { + activeSurvey, + currentQuestion, + submitAnswer, + completeSurvey, + progress + } = useSurvey({ + autoStart: true, + pollingInterval: 60000 + }); + + if (!activeSurvey) return null; + + return ( + + ); +} +``` + +### Complete Survey Flow Example + +```typescript +function SurveyFlow() { + const client = useSurveyClient(); + const [survey, setSurvey] = useState(null); + const [currentIndex, setCurrentIndex] = useState(0); + const [answers, setAnswers] = useState>({}); + + useEffect(() => { + loadSurvey(); + }, []); + + async function loadSurvey() { + const [activeSurvey] = await client.getActiveSurvey(); + if (activeSurvey) { + await client.startSurvey(activeSurvey.id); + setSurvey(activeSurvey); + } + } + + async function handleAnswer(questionId: string, answer: SurveyAnswer) { + const [result] = await client.submitAnswer(survey!.id, questionId, answer); + + setAnswers(prev => ({ ...prev, [questionId]: answer })); + setCurrentIndex(result.currentQuestionIndex); + + if (result.isComplete) { + const [completion] = await client.completeSurvey(survey!.id); + if (completion.incentiveClaimed) { + showIncentiveToast(completion.incentiveAmount, completion.incentiveType); + } + } + } + + if (!survey) return null; + + const question = survey.questions[currentIndex]; + const isLast = currentIndex === survey.questions.length - 1; + + return ( + handleAnswer(question.id, answer)} + onSkip={question.required ? undefined : () => handleSkip(question.id)} + /> + ); +} +``` + +## Offline Support + +The client automatically caches responses when offline: + +```typescript +const client = createSurveyClient({ + baseURL: 'https://api.bytelyst.io/v1', + productId: 'lysnrai', + getAuthToken: () => getToken(), + enableOfflineCache: true // Enabled by default +}); + +// Responses are queued when offline +// Flush queue manually or on reconnect +window.addEventListener('online', () => { + client.flushOfflineQueue(); +}); +``` + +## Types + +```typescript +interface ActiveSurvey { + id: string; + title: string; + description?: string; + questions: Question[]; + currentQuestionIndex: number; + incentive?: { + type: 'pro_days' | 'credits'; + amount: number; + }; +} + +interface Question { + id: string; + type: 'single_choice' | 'multiple_choice' | 'rating' | 'scale' | + 'nps' | 'text_short' | 'text_long' | 'ranking' | 'dropdown'; + text: string; + description?: string; + required: boolean; + options?: QuestionOption[]; + minValue?: number; + maxValue?: number; + maxLength?: number; + showIf?: ShowIfCondition; +} + +interface QuestionOption { + id: string; + text: string; + emoji?: string; +} + +interface SurveyAnswer { + type: string; + value: Record; +} + +interface SurveyConfig { + baseURL: string; + productId: string; + getAuthToken: () => Promise; + enableOfflineCache?: boolean; +} +``` + +## Question Type Reference + +| Type | Answer Format | UI Component | +|------|--------------|--------------| +| `single_choice` | `{ value: string }` | Radio group | +| `multiple_choice` | `{ values: string[] }` | Checkboxes | +| `rating` | `{ value: number }` | Star rating (1-5) | +| `scale` | `{ value: number }` | Numeric slider | +| `nps` | `{ value: number }` | 0-10 buttons | +| `text_short` | `{ value: string }` | Single line input | +| `text_long` | `{ value: string }` | Textarea | +| `ranking` | `{ order: string[] }` | Drag-to-sort | +| `dropdown` | `{ value: string }` | Select dropdown | + +## Conditional Logic + +Questions can be shown/hidden based on previous answers: + +```typescript +// Question only shows if q1 answer is NOT 9 or 10 +{ + id: 'q2', + type: 'text_long', + text: 'What could we improve?', + showIf: { + questionId: 'q1', + operator: 'not_equals', + value: ['9', '10'] + } +} +``` + +**Operators:** `equals`, `not_equals`, `greater_than`, `less_than`, `contains` + +## Error Handling + +```typescript +const [data, error] = await client.submitAnswer('srv_123', 'q1', answer); + +if (error) { + switch (error.code) { + case 'SURVEY_NOT_FOUND': + console.error('Survey expired or unavailable'); + break; + case 'ALREADY_COMPLETED': + console.error('User already completed this survey'); + break; + case 'VALIDATION_ERROR': + console.error('Invalid answer format'); + break; + default: + console.error('Survey error:', error.message); + } +} +``` + +## Browser Support + +- Chrome 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ + +## License + +MIT © ByteLyst