react-native-my-survey-sdk 2.2.21 → 2.2.22

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.21",
3
+ "version": "2.2.22",
4
4
  "description": "Xebo survey collection SDK for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -8,6 +8,7 @@ import {
8
8
  StyleSheet,
9
9
  Dimensions,
10
10
  Platform,
11
+ useWindowDimensions,
11
12
  } from 'react-native';
12
13
  import RNModal from 'react-native-modal';
13
14
  import { XeboSurveyManager, XEBO_EVENTS } from '../core/XeboSurveyManager';
@@ -30,47 +31,37 @@ import { XeboMultiNPSView } from './XeboMultiNPSView';
30
31
  import { XeboRatingView } from './XeboRatingView';
31
32
  import { XeboMultiRatingView } from './XeboMultiRatingView';
32
33
 
33
- // 'window' excludes the Android navigation bar; 'screen' includes it.
34
- // Using window height so sheets are sized relative to the usable area.
35
- const SCREEN_HEIGHT = Dimensions.get('window').height;
36
-
37
- // Android navigation bar height = difference between screen and window.
38
- // Used as bottomInset so content isn't hidden behind the nav bar.
39
- const _androidNavBar = Platform.OS === 'android'
40
- ? Math.max(0, Dimensions.get('screen').height - Dimensions.get('window').height)
41
- : 0;
42
-
43
- function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen): number {
44
- if (screen === 'thankYou') return SCREEN_HEIGHT * 0.30;
45
- if (screen === 'intro') return SCREEN_HEIGHT * 0.35;
46
- if (!question) return SCREEN_HEIGHT * 0.42;
34
+ function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen, sh: number): number {
35
+ if (screen === 'thankYou') return sh * 0.30;
36
+ if (screen === 'intro') return sh * 0.38;
37
+ if (!question) return sh * 0.45;
47
38
  switch (question.type) {
48
39
  case XeboQuestionType.multiNps:
49
40
  case XeboQuestionType.multiRating: {
50
41
  const rowCount = question.options?.length ?? 3;
51
- return Math.min(SCREEN_HEIGHT * 0.70, SCREEN_HEIGHT * (0.22 + rowCount * 0.13));
42
+ return Math.min(sh * 0.75, sh * (0.25 + rowCount * 0.13));
52
43
  }
53
44
  case XeboQuestionType.nps:
54
45
  return (question.conditionalFollowUps?.length ?? 0) > 0
55
- ? SCREEN_HEIGHT * 0.55
56
- : SCREEN_HEIGHT * 0.34;
46
+ ? sh * 0.60
47
+ : sh * 0.38;
57
48
  case XeboQuestionType.rating:
58
- return question.followUpQuestion ? SCREEN_HEIGHT * 0.50 : SCREEN_HEIGHT * 0.34;
49
+ return question.followUpQuestion ? sh * 0.55 : sh * 0.38;
59
50
  case XeboQuestionType.singleTextBox:
60
- return SCREEN_HEIGHT * 0.35;
51
+ return sh * 0.38;
61
52
  case XeboQuestionType.multipleTextBox: {
62
53
  const fieldCount = question.options?.length ?? 1;
63
- return Math.min(SCREEN_HEIGHT * 0.65, SCREEN_HEIGHT * (0.26 + fieldCount * 0.10));
54
+ return Math.min(sh * 0.70, sh * (0.28 + fieldCount * 0.10));
64
55
  }
65
56
  case XeboQuestionType.dropdown:
66
- return SCREEN_HEIGHT * 0.55;
57
+ return sh * 0.62;
67
58
  case XeboQuestionType.singleChoice:
68
59
  case XeboQuestionType.multipleChoice: {
69
60
  const optCount = question.options?.length ?? 0;
70
- return Math.min(SCREEN_HEIGHT * 0.72, SCREEN_HEIGHT * (0.24 + optCount * 0.075));
61
+ return Math.min(sh * 0.75, sh * (0.26 + optCount * 0.08));
71
62
  }
72
63
  default:
73
- return SCREEN_HEIGHT * 0.42;
64
+ return sh * 0.45;
74
65
  }
75
66
  }
76
67
 
@@ -78,14 +69,16 @@ type ModalScreen = 'intro' | 'question' | 'thankYou';
78
69
 
79
70
  export const XeboSurveyModal: React.FC = () => {
80
71
  const theme = getTheme();
81
- // Bottom inset: iOS home indicator (34) or Android gesture nav zone (min 34)
82
- const bottomInset = Platform.OS === 'ios' ? 34 : Math.max(34, _androidNavBar);
72
+ // Always get the live window dimensions fixes stale height on Android first open
73
+ const { height: windowHeight } = useWindowDimensions();
74
+ const screenDims = Dimensions.get('screen');
75
+ const androidNavBar = Platform.OS === 'android'
76
+ ? Math.max(0, screenDims.height - windowHeight)
77
+ : 0;
78
+ const bottomInset = Platform.OS === 'ios' ? 34 : Math.max(34, androidNavBar);
83
79
 
84
80
  // Slides the sheet above the keyboard when any TextInput is focused.
85
- // Using translateY keeps the layout stable — no height re-calculation needed.
86
81
  const shiftAnim = useRef(new Animated.Value(0)).current;
87
- // Ref so the keyboard listener can always read the latest sheetHeight without
88
- // being re-registered every time the question changes.
89
82
  const sheetHeightRef = useRef(0);
90
83
 
91
84
  const [visible, setVisible] = useState(false);
@@ -94,10 +87,9 @@ export const XeboSurveyModal: React.FC = () => {
94
87
  const [questionIndex, setQuestionIndex] = useState(0);
95
88
 
96
89
  const sheetHeight = useMemo(
97
- () => computeSheetHeight(survey?.questions[questionIndex] ?? null, screen) + (Platform.OS === 'android' ? 18 : 0),
98
- [survey, questionIndex, screen]
90
+ () => computeSheetHeight(survey?.questions[questionIndex] ?? null, screen, windowHeight),
91
+ [survey, questionIndex, screen, windowHeight]
99
92
  );
100
- // Keep ref in sync so keyboard listener always has the latest height
101
93
  sheetHeightRef.current = sheetHeight;
102
94
 
103
95
  // ─── Keyboard: slide sheet above keyboard when any TextInput focuses ──────
@@ -108,8 +100,7 @@ export const XeboSurveyModal: React.FC = () => {
108
100
 
109
101
  const onShow = (e: { endCoordinates: { height: number } }) => {
110
102
  const kh = e.endCoordinates.height;
111
- // Cap only to prevent sheet going above screen top edge
112
- const maxShift = Math.max(0, SCREEN_HEIGHT - sheetHeightRef.current);
103
+ const maxShift = Math.max(0, windowHeight - sheetHeightRef.current);
113
104
  Animated.timing(shiftAnim, {
114
105
  toValue: -Math.min(kh, maxShift),
115
106
  duration: Platform.OS === 'ios' ? 250 : 0,
@@ -7,6 +7,9 @@ import {
7
7
  XeboAnswer,
8
8
  XeboEnvironment,
9
9
  XeboUserData,
10
+ XeboQualityFlags,
11
+ XeboDeviceInfo,
12
+ XeboGeolocation,
10
13
  } from '../models/XeboModels';
11
14
  import {
12
15
  APICollectorResponse,
@@ -334,6 +337,9 @@ export interface SubmitMeta {
334
337
  osVersion?: string;
335
338
  startTime?: string;
336
339
  endTime?: string;
340
+ qualityFlags?: XeboQualityFlags;
341
+ deviceInfo?: XeboDeviceInfo;
342
+ geolocation?: XeboGeolocation;
337
343
  }
338
344
 
339
345
  export async function submitResponse(
@@ -374,6 +380,9 @@ export async function submitResponse(
374
380
  text: v.text || null,
375
381
  })),
376
382
  })),
383
+ ...(meta?.qualityFlags && { qualityFlags: meta.qualityFlags }),
384
+ ...(meta?.deviceInfo && { deviceInfo: meta.deviceInfo }),
385
+ ...(meta?.geolocation && { geolocation: meta.geolocation }),
377
386
  },
378
387
  ],
379
388
  };
@@ -39,6 +39,9 @@ import {
39
39
  XeboEnvironment,
40
40
  XeboQueuedResponse,
41
41
  XeboUserData,
42
+ XeboQualityFlags,
43
+ XeboDeviceInfo,
44
+ XeboGeolocation,
42
45
  } from '../models/XeboModels';
43
46
  import {
44
47
  resolveZoneAndEnv,
@@ -85,6 +88,12 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
85
88
  resolvedCollectorMongoId = '';
86
89
  surveyVisible = false;
87
90
 
91
+ // Timing / quality
92
+ private surveyStartEpoch = 0;
93
+ private questionStartEpoch = 0;
94
+ private timePerQues: Record<string, number> = {};
95
+ private backTrackCount = 0;
96
+
88
97
  // Network monitoring unsubscribe handle
89
98
  private netInfoUnsubscribe: (() => void) | null = null;
90
99
 
@@ -169,6 +178,10 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
169
178
  this.currentAnswers = [];
170
179
  this.currentQuestionIndex = 0;
171
180
  this.surveyStartTime = new Date().toISOString();
181
+ this.surveyStartEpoch = Date.now();
182
+ this.questionStartEpoch = Date.now();
183
+ this.timePerQues = {};
184
+ this.backTrackCount = 0;
172
185
  this.emit(XEBO_EVENTS.SURVEY_LOADED, this.survey);
173
186
  this._setVisible(true);
174
187
  return;
@@ -186,6 +199,10 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
186
199
  this.currentAnswers = [];
187
200
  this.currentQuestionIndex = 0;
188
201
  this.surveyStartTime = new Date().toISOString();
202
+ this.surveyStartEpoch = Date.now();
203
+ this.questionStartEpoch = Date.now();
204
+ this.timePerQues = {};
205
+ this.backTrackCount = 0;
189
206
 
190
207
  this.emit(XEBO_EVENTS.SURVEY_LOADED, survey);
191
208
  this._setVisible(true);
@@ -209,9 +226,17 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
209
226
 
210
227
  nextQuestion(): void {
211
228
  if (!this.survey) return;
229
+
230
+ // Record time spent on the current question before advancing
231
+ const currentQ = this.survey.questions[this.currentQuestionIndex];
232
+ if (currentQ && currentQ.id !== 'thank_you_auto') {
233
+ this.timePerQues[currentQ.id] = (Date.now() - this.questionStartEpoch) / 1000;
234
+ }
235
+
212
236
  const next = this.currentQuestionIndex + 1;
213
237
  if (next < this.survey.questions.length) {
214
238
  this.currentQuestionIndex = next;
239
+ this.questionStartEpoch = Date.now();
215
240
  this.emit(XEBO_EVENTS.QUESTION_CHANGED, this.currentQuestionIndex);
216
241
 
217
242
  // If this is the thank-you question, submit in the background
@@ -224,6 +249,46 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
224
249
 
225
250
  // ─── Submission ────────────────────────────────────────────────────────────
226
251
 
252
+ private _buildQualityFlags(): XeboQualityFlags {
253
+ const endEpoch = Date.now();
254
+ const totalMinutes = (endEpoch - this.surveyStartEpoch) / 60000;
255
+ const now = new Date();
256
+ const curTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
257
+ return {
258
+ data: {
259
+ timePerQues: Object.fromEntries(
260
+ Object.entries(this.timePerQues).map(([qId, secs]) => [qId, { timeOnQues: secs }])
261
+ ),
262
+ survey: {
263
+ totalTimeDuration: totalMinutes,
264
+ netTimeDuration: totalMinutes,
265
+ curTime,
266
+ startTime: this.surveyStartEpoch,
267
+ backTrackCount: this.backTrackCount,
268
+ },
269
+ },
270
+ straightLining: { questionList: [] },
271
+ };
272
+ }
273
+
274
+ private _buildDeviceInfo(): XeboDeviceInfo {
275
+ return {
276
+ type: 'mobile',
277
+ os: Platform.OS,
278
+ browser: 'app',
279
+ };
280
+ }
281
+
282
+ private _buildGeolocation(): XeboGeolocation {
283
+ return {
284
+ type: 'point',
285
+ coordinates: [null, null],
286
+ country: this.userData?.country ?? null,
287
+ city: this.userData?.city ?? null,
288
+ region: this.userData?.region ?? null,
289
+ };
290
+ }
291
+
227
292
  private async _submitCurrentSession(): Promise<void> {
228
293
  if (!this.survey) return;
229
294
 
@@ -243,6 +308,9 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
243
308
  osVersion: this.osVersion,
244
309
  startTime: this.surveyStartTime,
245
310
  endTime,
311
+ qualityFlags: this._buildQualityFlags(),
312
+ deviceInfo: this._buildDeviceInfo(),
313
+ geolocation: this._buildGeolocation(),
246
314
  },
247
315
  );
248
316
 
@@ -277,6 +345,9 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
277
345
  deviceModel: this.deviceModel,
278
346
  platform: Platform.OS,
279
347
  osVersion: this.osVersion,
348
+ qualityFlags: this._buildQualityFlags(),
349
+ deviceInfo: this._buildDeviceInfo(),
350
+ geolocation: this._buildGeolocation(),
280
351
  };
281
352
 
282
353
  await XeboOfflineQueue.enqueue(queued);
@@ -308,6 +379,9 @@ class XeboSurveyManagerClass extends SimpleEventEmitter {
308
379
  osVersion: queued.osVersion,
309
380
  startTime: queued.startTime,
310
381
  endTime: queued.endTime,
382
+ qualityFlags: queued.qualityFlags,
383
+ deviceInfo: queued.deviceInfo,
384
+ geolocation: queued.geolocation,
311
385
  },
312
386
  );
313
387
  if (!result.ok) throw new Error(`HTTP ${result.status}`);
@@ -102,6 +102,35 @@ export interface XeboUserData {
102
102
  uniqueId?: string;
103
103
  country?: string;
104
104
  city?: string;
105
+ region?: string;
106
+ }
107
+
108
+ export interface XeboQualityFlags {
109
+ data: {
110
+ timePerQues: Record<string, { timeOnQues: number }>;
111
+ survey: {
112
+ totalTimeDuration: number;
113
+ netTimeDuration: number;
114
+ curTime: string;
115
+ startTime: number;
116
+ backTrackCount: number;
117
+ };
118
+ };
119
+ straightLining: { questionList: string[] };
120
+ }
121
+
122
+ export interface XeboDeviceInfo {
123
+ type: string;
124
+ os: string;
125
+ browser: string;
126
+ }
127
+
128
+ export interface XeboGeolocation {
129
+ type: string;
130
+ coordinates: [number | null, number | null];
131
+ country: string | null;
132
+ city: string | null;
133
+ region: string | null;
105
134
  }
106
135
 
107
136
  // ─── Offline Queue ────────────────────────────────────────────────────────────
@@ -119,6 +148,9 @@ export interface XeboQueuedResponse {
119
148
  deviceModel?: string;
120
149
  platform?: string;
121
150
  osVersion?: string;
151
+ qualityFlags?: XeboQualityFlags;
152
+ deviceInfo?: XeboDeviceInfo;
153
+ geolocation?: XeboGeolocation;
122
154
  }
123
155
 
124
156
  // ─── Config ───────────────────────────────────────────────────────────────────