react-native-my-survey-sdk 2.2.20 → 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
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
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
|
-
?
|
|
56
|
-
:
|
|
46
|
+
? sh * 0.60
|
|
47
|
+
: sh * 0.38;
|
|
57
48
|
case XeboQuestionType.rating:
|
|
58
|
-
return question.followUpQuestion ?
|
|
49
|
+
return question.followUpQuestion ? sh * 0.55 : sh * 0.38;
|
|
59
50
|
case XeboQuestionType.singleTextBox:
|
|
60
|
-
return
|
|
51
|
+
return sh * 0.38;
|
|
61
52
|
case XeboQuestionType.multipleTextBox: {
|
|
62
53
|
const fieldCount = question.options?.length ?? 1;
|
|
63
|
-
return Math.min(
|
|
54
|
+
return Math.min(sh * 0.70, sh * (0.28 + fieldCount * 0.10));
|
|
64
55
|
}
|
|
65
56
|
case XeboQuestionType.dropdown:
|
|
66
|
-
return
|
|
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(
|
|
61
|
+
return Math.min(sh * 0.75, sh * (0.26 + optCount * 0.08));
|
|
71
62
|
}
|
|
72
63
|
default:
|
|
73
|
-
return
|
|
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
|
-
//
|
|
82
|
-
const
|
|
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),
|
|
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
|
-
|
|
112
|
-
const maxShift = Math.max(0, SCREEN_HEIGHT - sheetHeightRef.current - 24);
|
|
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(
|
|
@@ -355,7 +361,7 @@ export async function submitResponse(
|
|
|
355
361
|
if (meta.userData.name) customVariables.fName = meta.userData.name;
|
|
356
362
|
if (meta.userData.email) customVariables.email = meta.userData.email;
|
|
357
363
|
if (meta.userData.phone) customVariables.mobile = meta.userData.phone;
|
|
358
|
-
if (meta.userData.uniqueId) customVariables.unique_id
|
|
364
|
+
if (meta.userData.uniqueId) customVariables.unique_id = meta.userData.uniqueId;
|
|
359
365
|
if (meta.userData.country) customVariables.country = meta.userData.country;
|
|
360
366
|
if (meta.userData.city) customVariables.city = meta.userData.city;
|
|
361
367
|
}
|
|
@@ -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}`);
|
package/src/models/XeboModels.ts
CHANGED
|
@@ -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 ───────────────────────────────────────────────────────────────────
|