react-native-my-survey-sdk 2.2.13 → 2.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-my-survey-sdk",
3
- "version": "2.2.13",
3
+ "version": "2.2.15",
4
4
  "description": "Xebo survey collection SDK for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -15,13 +15,17 @@
15
15
  "react-native": ">=0.71.0",
16
16
  "@react-native-async-storage/async-storage": ">=1.0.0",
17
17
  "@react-native-community/netinfo": ">=11.0.0",
18
+ "react-native-device-info": ">=10.0.0",
18
19
  "react-native-modal": ">=13.0.0",
19
20
  "react-native-reanimated": ">=3.0.0"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@babel/runtime": "^7.29.2",
24
+ "@react-native-community/netinfo": "^11.4.1",
23
25
  "@types/react": "^18.2.0",
24
26
  "@types/react-native": "^0.73.0",
27
+ "react-native": "^0.73.0",
28
+ "react-native-device-info": "^10.13.2",
25
29
  "typescript": "^5.3.0"
26
30
  }
27
31
  }
@@ -63,10 +63,12 @@ function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen):
63
63
  const fieldCount = question.options?.length ?? 1;
64
64
  return Math.min(SCREEN_HEIGHT * 0.72, SCREEN_HEIGHT * (0.30 + fieldCount * 0.12));
65
65
  }
66
+ case XeboQuestionType.dropdown:
67
+ // Must always fit: title + trigger + 200px open list + button + padding + header overhead
68
+ // Minimum ~0.62 regardless of whether options are parsed yet
69
+ return SCREEN_HEIGHT * 0.62;
66
70
  case XeboQuestionType.singleChoice:
67
- case XeboQuestionType.multipleChoice:
68
- case XeboQuestionType.dropdown: {
69
- // each option row ≈ 58dp; base includes title + button; 0.085 per option is accurate
71
+ case XeboQuestionType.multipleChoice: {
70
72
  const optCount = question.options?.length ?? 0;
71
73
  return Math.min(SCREEN_HEIGHT * 0.80, SCREEN_HEIGHT * (0.28 + optCount * 0.085));
72
74
  }
@@ -289,33 +291,8 @@ export const XeboSurveyModal: React.FC = () => {
289
291
  <View style={styles.handle} />
290
292
  </View>
291
293
 
292
- {/* Header row: progress bar + Skip / X close */}
294
+ {/* Header row: Skip / X close */}
293
295
  <View style={styles.headerRow}>
294
- <View style={styles.progressArea}>
295
- {screen === 'question' && survey && (() => {
296
- const realQs = survey.questions.filter((q: XeboQuestion) => q.id !== 'thank_you_auto');
297
- const total = realQs.length;
298
- if (total === 0) return null;
299
- return (
300
- <View style={styles.progressContainer}>
301
- <View style={[styles.progressTrack, { backgroundColor: '#E5E7EB' }]}>
302
- <View
303
- style={[
304
- styles.progressFill,
305
- {
306
- backgroundColor: theme.primaryColor,
307
- width: `${((questionIndex + 1) / total) * 100}%` as any,
308
- },
309
- ]}
310
- />
311
- </View>
312
- <Text style={[styles.progressText, { color: theme.textColor }]}>
313
- {questionIndex + 1} / {total}
314
- </Text>
315
- </View>
316
- );
317
- })()}
318
- </View>
319
296
  {screen === 'thankYou' ? (
320
297
  <TouchableOpacity onPress={handleSkip} style={styles.closeButton} activeOpacity={0.7}>
321
298
  <Text style={[styles.closeText, { color: theme.textColor }]}>✕</Text>
@@ -367,34 +344,9 @@ const styles = StyleSheet.create({
367
344
  },
368
345
  headerRow: {
369
346
  flexDirection: 'row',
370
- alignItems: 'center',
347
+ justifyContent: 'flex-end',
371
348
  paddingHorizontal: 16,
372
349
  paddingBottom: 4,
373
- minHeight: 36,
374
- },
375
- progressArea: {
376
- flex: 1,
377
- },
378
- progressContainer: {
379
- flexDirection: 'row',
380
- alignItems: 'center',
381
- gap: 8,
382
- },
383
- progressTrack: {
384
- flex: 1,
385
- height: 4,
386
- borderRadius: 2,
387
- overflow: 'hidden',
388
- },
389
- progressFill: {
390
- height: 4,
391
- borderRadius: 2,
392
- },
393
- progressText: {
394
- fontSize: 12,
395
- opacity: 0.6,
396
- minWidth: 36,
397
- textAlign: 'right',
398
350
  },
399
351
  skipButton: {
400
352
  paddingHorizontal: 12,
@@ -6,6 +6,7 @@ import {
6
6
  XeboConditionalFollowUp,
7
7
  XeboAnswer,
8
8
  XeboEnvironment,
9
+ XeboUserData,
9
10
  } from '../models/XeboModels';
10
11
  import {
11
12
  APICollectorResponse,
@@ -322,20 +323,59 @@ export interface SubmitResult {
322
323
  responseJson: unknown;
323
324
  }
324
325
 
326
+ export interface SubmitMeta {
327
+ userData?: XeboUserData;
328
+ deviceModel?: string;
329
+ platform?: string;
330
+ osVersion?: string;
331
+ startTime?: string;
332
+ endTime?: string;
333
+ }
334
+
325
335
  export async function submitResponse(
326
336
  feedbackBaseURL: string,
327
337
  surveyId: string,
328
338
  collectorUUID: string,
329
339
  apiKey: string,
330
- answers: XeboAnswer[]
340
+ answers: XeboAnswer[],
341
+ meta?: SubmitMeta,
331
342
  ): Promise<SubmitResult> {
332
343
  const url = `${feedbackBaseURL}/v3/survey-participation/${surveyId}/response/create-response`;
333
344
 
334
345
  // Filter out auto thank-you answer
335
346
  const filteredAnswers = answers.filter(a => a.questionId !== 'thank_you_auto');
336
347
 
337
- const payload = {
348
+ // Calculate time spent in seconds
349
+ let timeSpentSeconds: number | undefined;
350
+ if (meta?.startTime && meta?.endTime) {
351
+ timeSpentSeconds = Math.round(
352
+ (new Date(meta.endTime).getTime() - new Date(meta.startTime).getTime()) / 1000,
353
+ );
354
+ }
355
+
356
+ const payload: Record<string, unknown> = {
338
357
  collector_id: collectorUUID,
358
+ mode: 'React Native SDK',
359
+ ...(meta?.userData && {
360
+ respondent: {
361
+ name: meta.userData.name,
362
+ email: meta.userData.email,
363
+ phone: meta.userData.phone,
364
+ unique_id: meta.userData.uniqueId,
365
+ country: meta.userData.country,
366
+ city: meta.userData.city,
367
+ },
368
+ }),
369
+ device_info: {
370
+ device: meta?.deviceModel ?? 'Unknown',
371
+ platform: meta?.platform ?? 'mobile',
372
+ os_version: meta?.osVersion ?? '',
373
+ },
374
+ timing: {
375
+ start_time: meta?.startTime,
376
+ end_time: meta?.endTime,
377
+ time_spent_seconds: timeSpentSeconds,
378
+ },
339
379
  responses: [
340
380
  {
341
381
  response_status: 'complete',
@@ -1,4 +1,6 @@
1
1
  import NetInfo from '@react-native-community/netinfo';
2
+ import DeviceInfo from 'react-native-device-info';
3
+ import { Platform } from 'react-native';
2
4
 
3
5
  // Minimal inline event emitter — no external 'events' package needed
4
6
  type Listener = (...args: any[]) => void;
@@ -28,6 +30,7 @@ const uuidv4 = () =>
28
30
  const r = (Math.random() * 16) | 0;
29
31
  return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
30
32
  });
33
+
31
34
  import {
32
35
  XeboSurvey,
33
36
  XeboAnswer,
@@ -35,6 +38,7 @@ import {
35
38
  XeboThemeConfig,
36
39
  XeboEnvironment,
37
40
  XeboQueuedResponse,
41
+ XeboUserData,
38
42
  } from '../models/XeboModels';
39
43
  import {
40
44
  resolveZoneAndEnv,
@@ -67,6 +71,11 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
67
71
  private baseURL = '';
68
72
  private feedbackBaseURL = '';
69
73
 
74
+ // User / eData
75
+ private userData: XeboUserData | null = null;
76
+ private deviceModel = '';
77
+ private osVersion = '';
78
+
70
79
  // State
71
80
  survey: XeboSurvey | null = null;
72
81
  currentAnswers: XeboAnswer[] = [];
@@ -88,8 +97,12 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
88
97
  const { resolvedZone, environment } = resolveZoneAndEnv(config.zone, config.environment);
89
98
  this.resolvedZone = resolvedZone;
90
99
  this.environment = environment;
91
- this.baseURL = buildBaseURL(resolvedZone, environment);
92
- this.feedbackBaseURL = buildFeedbackBaseURL(resolvedZone, environment);
100
+ this.baseURL = buildBaseURL(this.resolvedZone, this.environment);
101
+ this.feedbackBaseURL = buildFeedbackBaseURL(this.resolvedZone, this.environment);
102
+
103
+ // Capture device info once at configure time
104
+ this.deviceModel = `${DeviceInfo.getModel()} (${DeviceInfo.getSystemName()})`;
105
+ this.osVersion = `${Platform.OS} ${DeviceInfo.getSystemVersion()}`;
93
106
 
94
107
  this._startNetworkMonitoring();
95
108
  // Pre-fetch survey in background so it's ready instantly when button is tapped
@@ -100,6 +113,16 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
100
113
  configureTheme(config);
101
114
  }
102
115
 
116
+ // ─── User identity ─────────────────────────────────────────────────────────
117
+
118
+ setUser(data: XeboUserData): void {
119
+ this.userData = data;
120
+ }
121
+
122
+ clearUser(): void {
123
+ this.userData = null;
124
+ }
125
+
103
126
  // ─── Network monitoring ────────────────────────────────────────────────────
104
127
 
105
128
  private _startNetworkMonitoring(): void {
@@ -109,7 +132,7 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
109
132
 
110
133
  let wasOffline = false;
111
134
 
112
- this.netInfoUnsubscribe = NetInfo.addEventListener(state => {
135
+ this.netInfoUnsubscribe = NetInfo.addEventListener((state: { isConnected: boolean | null }) => {
113
136
  const isConnected = state.isConnected ?? false;
114
137
  if (wasOffline && isConnected) {
115
138
  console.log('[Xebo] Network restored — flushing offline queue');
@@ -212,7 +235,15 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
212
235
  this.survey.id,
213
236
  this.resolvedCollectorUUID,
214
237
  this.apiKey,
215
- this.currentAnswers
238
+ this.currentAnswers,
239
+ {
240
+ userData: this.userData ?? undefined,
241
+ deviceModel: this.deviceModel,
242
+ platform: Platform.OS,
243
+ osVersion: this.osVersion,
244
+ startTime: this.surveyStartTime,
245
+ endTime,
246
+ },
216
247
  );
217
248
 
218
249
  if (!result.ok) {
@@ -242,6 +273,10 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
242
273
  startTime: this.surveyStartTime,
243
274
  endTime,
244
275
  answers: [...this.currentAnswers],
276
+ userData: this.userData ?? undefined,
277
+ deviceModel: this.deviceModel,
278
+ platform: Platform.OS,
279
+ osVersion: this.osVersion,
245
280
  };
246
281
 
247
282
  await XeboOfflineQueue.enqueue(queued);
@@ -265,7 +300,15 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
265
300
  queued.surveyId,
266
301
  queued.collectorUUID,
267
302
  this.apiKey,
268
- queued.answers
303
+ queued.answers,
304
+ {
305
+ userData: queued.userData,
306
+ deviceModel: queued.deviceModel,
307
+ platform: queued.platform,
308
+ osVersion: queued.osVersion,
309
+ startTime: queued.startTime,
310
+ endTime: queued.endTime,
311
+ },
269
312
  );
270
313
  if (!result.ok) throw new Error(`HTTP ${result.status}`);
271
314
  console.log('[Xebo Offline] Flushed response:', queued.responseId);
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export type {
18
18
  XeboNPSLabels,
19
19
  XeboConditionalFollowUp,
20
20
  XeboSurveyPage,
21
+ XeboUserData,
21
22
  } from './models/XeboModels';
22
23
 
23
24
  export { XeboQuestionType } from './models/XeboModels';
@@ -93,6 +93,17 @@ export interface XeboAnswer {
93
93
  value: XeboAnswerValue[];
94
94
  }
95
95
 
96
+ // ─── User / eData ─────────────────────────────────────────────────────────────
97
+
98
+ export interface XeboUserData {
99
+ name?: string;
100
+ email?: string;
101
+ phone?: string;
102
+ uniqueId?: string;
103
+ country?: string;
104
+ city?: string;
105
+ }
106
+
96
107
  // ─── Offline Queue ────────────────────────────────────────────────────────────
97
108
 
98
109
  export interface XeboQueuedResponse {
@@ -104,6 +115,10 @@ export interface XeboQueuedResponse {
104
115
  startTime: string;
105
116
  endTime: string;
106
117
  answers: XeboAnswer[];
118
+ userData?: XeboUserData;
119
+ deviceModel?: string;
120
+ platform?: string;
121
+ osVersion?: string;
107
122
  }
108
123
 
109
124
  // ─── Config ───────────────────────────────────────────────────────────────────