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