# @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