docs(packages): Add comprehensive README for broadcast-client and survey-client
- Installation, quick start, API reference - React integration examples with hooks and providers - Offline support documentation - Type definitions and error handling
This commit is contained in:
parent
3842f65c81
commit
80df7c1c1e
227
packages/broadcast-client/README.md
Normal file
227
packages/broadcast-client/README.md
Normal file
@ -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<string> | 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 (
|
||||
<div>
|
||||
{messages.map(msg => (
|
||||
<Banner
|
||||
key={msg.id}
|
||||
title={msg.title}
|
||||
body={msg.body}
|
||||
onDismiss={() => markDismissed(msg.id)}
|
||||
onClick={() => markRead(msg.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Provider Pattern
|
||||
|
||||
```typescript
|
||||
// BroadcastProvider.tsx
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createBroadcastClient, BroadcastClient, InAppMessage } from '@bytelyst/broadcast-client';
|
||||
|
||||
const BroadcastContext = createContext<BroadcastContextType | null>(null);
|
||||
|
||||
export function BroadcastProvider({ children, config }: { children: React.ReactNode; config: BroadcastConfig }) {
|
||||
const [client] = useState(() => createBroadcastClient(config));
|
||||
const [messages, setMessages] = useState<InAppMessage[]>([]);
|
||||
|
||||
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 (
|
||||
<BroadcastContext.Provider value={value}>
|
||||
{children}
|
||||
</BroadcastContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
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<string, string>;
|
||||
};
|
||||
status: 'unread' | 'read' | 'dismissed';
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface BroadcastConfig {
|
||||
baseURL: string;
|
||||
productId: string;
|
||||
getAuthToken: () => Promise<string>;
|
||||
}
|
||||
```
|
||||
|
||||
## 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
|
||||
349
packages/survey-client/README.md
Normal file
349
packages/survey-client/README.md
Normal file
@ -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<string> | 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 (
|
||||
<SurveyModal
|
||||
survey={activeSurvey}
|
||||
currentQuestion={currentQuestion}
|
||||
progress={progress}
|
||||
onSubmit={submitAnswer}
|
||||
onComplete={completeSurvey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Complete Survey Flow Example
|
||||
|
||||
```typescript
|
||||
function SurveyFlow() {
|
||||
const client = useSurveyClient();
|
||||
const [survey, setSurvey] = useState<ActiveSurvey | null>(null);
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [answers, setAnswers] = useState<Record<string, SurveyAnswer>>({});
|
||||
|
||||
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 (
|
||||
<QuestionView
|
||||
question={question}
|
||||
onSubmit={(answer) => 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<string, unknown>;
|
||||
}
|
||||
|
||||
interface SurveyConfig {
|
||||
baseURL: string;
|
||||
productId: string;
|
||||
getAuthToken: () => Promise<string>;
|
||||
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
|
||||
Loading…
Reference in New Issue
Block a user