- Installation, quick start, API reference - React integration examples with hooks and providers - Offline support documentation - Type definitions and error handling
350 lines
7.9 KiB
Markdown
350 lines
7.9 KiB
Markdown
# @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
|