react-native-my-survey-sdk 2.1.7 → 2.1.8
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 +1 -1
- package/src/components/XeboSurveyModal.tsx +37 -105
package/package.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useMemo,
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
StyleSheet,
|
|
7
|
-
Animated,
|
|
8
7
|
Dimensions,
|
|
9
8
|
Platform,
|
|
10
9
|
KeyboardAvoidingView,
|
|
11
|
-
Modal,
|
|
12
10
|
} from 'react-native';
|
|
11
|
+
import RNModal from 'react-native-modal';
|
|
13
12
|
import { XeboSurveyManager, XEBO_EVENTS } from '../core/XeboSurveyManager';
|
|
14
13
|
import {
|
|
15
14
|
XeboSurvey,
|
|
@@ -31,10 +30,7 @@ import { XeboRatingView } from './XeboRatingView';
|
|
|
31
30
|
import { XeboMultiRatingView } from './XeboMultiRatingView';
|
|
32
31
|
|
|
33
32
|
// 'screen' includes the Android system navigation bar; 'window' excludes it.
|
|
34
|
-
// We need 'screen' so the backdrop covers the full physical display.
|
|
35
33
|
const SCREEN_HEIGHT = Dimensions.get('screen').height;
|
|
36
|
-
const SCREEN_WIDTH = Dimensions.get('screen').width;
|
|
37
|
-
const MODAL_MAX_HEIGHT = SCREEN_HEIGHT * 0.78; // used as initial off-screen translateY value
|
|
38
34
|
|
|
39
35
|
function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen): number {
|
|
40
36
|
if (screen === 'thankYou') return SCREEN_HEIGHT * 0.32;
|
|
@@ -44,13 +40,12 @@ function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen):
|
|
|
44
40
|
case XeboQuestionType.multiNps:
|
|
45
41
|
case XeboQuestionType.multiRating: {
|
|
46
42
|
const rowCount = question.options?.length ?? 3;
|
|
47
|
-
// base ~25% + ~15% per row, capped at 78%
|
|
48
43
|
return Math.min(SCREEN_HEIGHT * 0.78, SCREEN_HEIGHT * (0.25 + rowCount * 0.15));
|
|
49
44
|
}
|
|
50
45
|
case XeboQuestionType.nps:
|
|
51
46
|
return (question.conditionalFollowUps?.length ?? 0) > 0
|
|
52
|
-
? SCREEN_HEIGHT * 0.62
|
|
53
|
-
: SCREEN_HEIGHT * 0.38;
|
|
47
|
+
? SCREEN_HEIGHT * 0.62
|
|
48
|
+
: SCREEN_HEIGHT * 0.38;
|
|
54
49
|
case XeboQuestionType.rating:
|
|
55
50
|
return question.followUpQuestion ? SCREEN_HEIGHT * 0.56 : SCREEN_HEIGHT * 0.38;
|
|
56
51
|
case XeboQuestionType.singleTextBox:
|
|
@@ -70,7 +65,6 @@ function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen):
|
|
|
70
65
|
}
|
|
71
66
|
}
|
|
72
67
|
|
|
73
|
-
// Which screen we show inside the modal
|
|
74
68
|
type ModalScreen = 'intro' | 'question' | 'thankYou';
|
|
75
69
|
|
|
76
70
|
export const XeboSurveyModal: React.FC = () => {
|
|
@@ -88,10 +82,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
88
82
|
[survey, questionIndex, screen]
|
|
89
83
|
);
|
|
90
84
|
|
|
91
|
-
// Spring slide-up animation
|
|
92
|
-
const slideAnim = useRef(new Animated.Value(MODAL_MAX_HEIGHT)).current;
|
|
93
|
-
const backdropAnim = useRef(new Animated.Value(0)).current;
|
|
94
|
-
|
|
95
85
|
// ─── Subscribe to manager events ─────────────────────────────────────────
|
|
96
86
|
|
|
97
87
|
useEffect(() => {
|
|
@@ -102,10 +92,17 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
102
92
|
|
|
103
93
|
const onVisible = (v: boolean) => {
|
|
104
94
|
if (v) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
// Sync survey into React state before showing so first frame isn't blank
|
|
96
|
+
const s = XeboSurveyManager.survey;
|
|
97
|
+
setSurvey(s);
|
|
98
|
+
if (s?.introPage?.enabled) {
|
|
99
|
+
setScreen('intro');
|
|
100
|
+
} else {
|
|
101
|
+
setScreen('question');
|
|
102
|
+
setQuestionIndex(XeboSurveyManager.currentQuestionIndex);
|
|
103
|
+
}
|
|
108
104
|
}
|
|
105
|
+
setVisible(v);
|
|
109
106
|
};
|
|
110
107
|
|
|
111
108
|
const onQuestionChanged = (idx: number) => {
|
|
@@ -123,57 +120,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
123
120
|
};
|
|
124
121
|
}, []);
|
|
125
122
|
|
|
126
|
-
// ─── Animations ───────────────────────────────────────────────────────────
|
|
127
|
-
|
|
128
|
-
const openModal = useCallback(() => {
|
|
129
|
-
// Sync survey into React state NOW so the modal never renders blank on first frame
|
|
130
|
-
const s = XeboSurveyManager.survey;
|
|
131
|
-
setSurvey(s);
|
|
132
|
-
setVisible(true);
|
|
133
|
-
slideAnim.setValue(MODAL_MAX_HEIGHT);
|
|
134
|
-
backdropAnim.setValue(0);
|
|
135
|
-
|
|
136
|
-
// Determine initial screen
|
|
137
|
-
if (s?.introPage?.enabled) {
|
|
138
|
-
setScreen('intro');
|
|
139
|
-
} else {
|
|
140
|
-
setScreen('question');
|
|
141
|
-
setQuestionIndex(XeboSurveyManager.currentQuestionIndex);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
Animated.parallel([
|
|
145
|
-
Animated.spring(slideAnim, {
|
|
146
|
-
toValue: 0,
|
|
147
|
-
tension: 180,
|
|
148
|
-
friction: 20,
|
|
149
|
-
useNativeDriver: true,
|
|
150
|
-
}),
|
|
151
|
-
Animated.timing(backdropAnim, {
|
|
152
|
-
toValue: 1,
|
|
153
|
-
duration: 200,
|
|
154
|
-
useNativeDriver: true,
|
|
155
|
-
}),
|
|
156
|
-
]).start();
|
|
157
|
-
}, []);
|
|
158
|
-
|
|
159
|
-
const closeModal = useCallback(() => {
|
|
160
|
-
Animated.parallel([
|
|
161
|
-
Animated.timing(slideAnim, {
|
|
162
|
-
toValue: MODAL_MAX_HEIGHT,
|
|
163
|
-
duration: 300,
|
|
164
|
-
useNativeDriver: true,
|
|
165
|
-
}),
|
|
166
|
-
Animated.timing(backdropAnim, {
|
|
167
|
-
toValue: 0,
|
|
168
|
-
duration: 300,
|
|
169
|
-
useNativeDriver: true,
|
|
170
|
-
}),
|
|
171
|
-
]).start(() => {
|
|
172
|
-
setVisible(false);
|
|
173
|
-
setSurvey(null);
|
|
174
|
-
});
|
|
175
|
-
}, []);
|
|
176
|
-
|
|
177
123
|
// ─── User actions ─────────────────────────────────────────────────────────
|
|
178
124
|
|
|
179
125
|
const handleSkip = () => {
|
|
@@ -190,8 +136,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
190
136
|
const answers = Array.isArray(answer) ? answer : [answer];
|
|
191
137
|
answers.forEach(a => XeboSurveyManager.recordAnswer(a));
|
|
192
138
|
XeboSurveyManager.nextQuestion();
|
|
193
|
-
// questionIndex will be updated via QUESTION_CHANGED event
|
|
194
|
-
// But also check if we're now at the thank-you question
|
|
195
139
|
const s = XeboSurveyManager.survey;
|
|
196
140
|
const newIdx = XeboSurveyManager.currentQuestionIndex;
|
|
197
141
|
if (s && s.questions[newIdx]?.id === 'thank_you_auto') {
|
|
@@ -203,7 +147,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
203
147
|
|
|
204
148
|
const renderQuestion = (question: XeboQuestion) => {
|
|
205
149
|
const questions = survey?.questions ?? [];
|
|
206
|
-
// The last "real" question before thank-you
|
|
207
150
|
const lastRealIndex = questions.findIndex(q => q.id === 'thank_you_auto') - 1;
|
|
208
151
|
const isLast = questionIndex === lastRealIndex;
|
|
209
152
|
|
|
@@ -232,8 +175,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
232
175
|
}
|
|
233
176
|
};
|
|
234
177
|
|
|
235
|
-
// ─── Content ──────────────────────────────────────────────────────────────
|
|
236
|
-
|
|
237
178
|
const renderContent = () => {
|
|
238
179
|
if (!survey) return null;
|
|
239
180
|
|
|
@@ -257,31 +198,35 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
257
198
|
return null;
|
|
258
199
|
};
|
|
259
200
|
|
|
260
|
-
|
|
201
|
+
// ─── Render ───────────────────────────────────────────────────────────────
|
|
261
202
|
|
|
262
203
|
return (
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
204
|
+
<RNModal
|
|
205
|
+
isVisible={visible}
|
|
206
|
+
onBackButtonPress={handleSkip}
|
|
207
|
+
backdropOpacity={0.5}
|
|
208
|
+
backdropColor="#000000"
|
|
209
|
+
style={styles.modal}
|
|
210
|
+
animationIn="slideInUp"
|
|
211
|
+
animationInTiming={350}
|
|
212
|
+
animationOut="slideOutDown"
|
|
213
|
+
animationOutTiming={300}
|
|
214
|
+
useNativeDriver={true}
|
|
215
|
+
useNativeDriverForBackdrop={true}
|
|
216
|
+
hideModalContentWhileAnimating={true}
|
|
217
|
+
coverScreen={true}
|
|
218
|
+
propagateSwipe={false}
|
|
219
|
+
onModalHide={() => setSurvey(null)}
|
|
270
220
|
>
|
|
271
|
-
{/* Backdrop — no dismiss on tap */}
|
|
272
|
-
<Animated.View style={[styles.backdrop, { opacity: backdropAnim }]} />
|
|
273
|
-
|
|
274
221
|
<KeyboardAvoidingView
|
|
275
|
-
style={styles.kvContainer}
|
|
276
222
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
277
223
|
keyboardVerticalOffset={0}
|
|
278
224
|
>
|
|
279
|
-
<
|
|
225
|
+
<View
|
|
280
226
|
style={[
|
|
281
227
|
styles.sheet,
|
|
282
228
|
{
|
|
283
229
|
backgroundColor: theme.backgroundColor,
|
|
284
|
-
transform: [{ translateY: slideAnim }],
|
|
285
230
|
height: sheetHeight,
|
|
286
231
|
},
|
|
287
232
|
]}
|
|
@@ -305,36 +250,23 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
305
250
|
|
|
306
251
|
{/* Safe area bottom padding — home indicator on iOS, gesture nav on Android */}
|
|
307
252
|
<View style={{ height: bottomInset }} />
|
|
308
|
-
</
|
|
253
|
+
</View>
|
|
309
254
|
</KeyboardAvoidingView>
|
|
310
|
-
</
|
|
255
|
+
</RNModal>
|
|
311
256
|
);
|
|
312
257
|
};
|
|
313
258
|
|
|
314
259
|
const styles = StyleSheet.create({
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
top: 0,
|
|
318
|
-
left: 0,
|
|
319
|
-
// Explicit screen dimensions instead of absoluteFillObject so the backdrop
|
|
320
|
-
// covers the Android system navigation bar area that window-relative
|
|
321
|
-
// positioning would miss, preventing the host app's tab bar from bleeding through.
|
|
322
|
-
width: SCREEN_WIDTH,
|
|
323
|
-
height: SCREEN_HEIGHT,
|
|
324
|
-
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
325
|
-
zIndex: 9998,
|
|
326
|
-
},
|
|
327
|
-
kvContainer: {
|
|
328
|
-
flex: 1,
|
|
260
|
+
// react-native-modal wrapper: full screen, sheet aligned to bottom
|
|
261
|
+
modal: {
|
|
329
262
|
justifyContent: 'flex-end',
|
|
330
|
-
|
|
263
|
+
margin: 0,
|
|
331
264
|
},
|
|
332
265
|
sheet: {
|
|
333
266
|
borderTopLeftRadius: 16,
|
|
334
267
|
borderTopRightRadius: 16,
|
|
335
268
|
overflow: 'hidden',
|
|
336
269
|
elevation: 9999,
|
|
337
|
-
zIndex: 9999,
|
|
338
270
|
shadowColor: '#000',
|
|
339
271
|
shadowOffset: { width: 0, height: -4 },
|
|
340
272
|
shadowOpacity: 0.15,
|