react-native-my-survey-sdk 2.1.7 → 2.1.9
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,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
|
2
|
+
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
|
|
3
3
|
import { XeboSurveyPage } from '../models/XeboModels';
|
|
4
4
|
import { getTheme } from '../theme/XeboTheme';
|
|
5
5
|
|
|
@@ -13,11 +13,18 @@ export const XeboIntroView: React.FC<Props> = ({ introPage, onStart }) => {
|
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
15
|
<View style={styles.container}>
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
{/* Scrollable content area — button stays pinned below no matter how long the text */}
|
|
17
|
+
<ScrollView
|
|
18
|
+
style={styles.scroll}
|
|
19
|
+
contentContainerStyle={styles.scrollContent}
|
|
20
|
+
showsVerticalScrollIndicator={false}
|
|
21
|
+
>
|
|
22
|
+
{!!introPage.content && (
|
|
23
|
+
<Text style={[styles.content, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
24
|
+
{introPage.content}
|
|
25
|
+
</Text>
|
|
26
|
+
)}
|
|
27
|
+
</ScrollView>
|
|
21
28
|
|
|
22
29
|
<TouchableOpacity
|
|
23
30
|
style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}
|
|
@@ -38,17 +45,22 @@ const styles = StyleSheet.create({
|
|
|
38
45
|
paddingHorizontal: 20,
|
|
39
46
|
paddingTop: 16,
|
|
40
47
|
paddingBottom: 24,
|
|
41
|
-
|
|
48
|
+
},
|
|
49
|
+
scroll: {
|
|
50
|
+
flex: 1,
|
|
51
|
+
},
|
|
52
|
+
scrollContent: {
|
|
53
|
+
flexGrow: 1,
|
|
54
|
+
paddingBottom: 8,
|
|
42
55
|
},
|
|
43
56
|
content: {
|
|
44
57
|
fontSize: 16,
|
|
45
58
|
lineHeight: 24,
|
|
46
|
-
flexShrink: 1,
|
|
47
59
|
},
|
|
48
60
|
button: {
|
|
49
61
|
paddingVertical: 14,
|
|
50
62
|
alignItems: 'center',
|
|
51
|
-
marginTop:
|
|
63
|
+
marginTop: 12,
|
|
52
64
|
},
|
|
53
65
|
buttonText: {
|
|
54
66
|
color: '#FFFFFF',
|
|
@@ -1,15 +1,13 @@
|
|
|
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
|
-
KeyboardAvoidingView,
|
|
11
|
-
Modal,
|
|
12
9
|
} from 'react-native';
|
|
10
|
+
import RNModal from 'react-native-modal';
|
|
13
11
|
import { XeboSurveyManager, XEBO_EVENTS } from '../core/XeboSurveyManager';
|
|
14
12
|
import {
|
|
15
13
|
XeboSurvey,
|
|
@@ -31,10 +29,7 @@ import { XeboRatingView } from './XeboRatingView';
|
|
|
31
29
|
import { XeboMultiRatingView } from './XeboMultiRatingView';
|
|
32
30
|
|
|
33
31
|
// 'screen' includes the Android system navigation bar; 'window' excludes it.
|
|
34
|
-
// We need 'screen' so the backdrop covers the full physical display.
|
|
35
32
|
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
33
|
|
|
39
34
|
function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen): number {
|
|
40
35
|
if (screen === 'thankYou') return SCREEN_HEIGHT * 0.32;
|
|
@@ -44,13 +39,12 @@ function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen):
|
|
|
44
39
|
case XeboQuestionType.multiNps:
|
|
45
40
|
case XeboQuestionType.multiRating: {
|
|
46
41
|
const rowCount = question.options?.length ?? 3;
|
|
47
|
-
// base ~25% + ~15% per row, capped at 78%
|
|
48
42
|
return Math.min(SCREEN_HEIGHT * 0.78, SCREEN_HEIGHT * (0.25 + rowCount * 0.15));
|
|
49
43
|
}
|
|
50
44
|
case XeboQuestionType.nps:
|
|
51
45
|
return (question.conditionalFollowUps?.length ?? 0) > 0
|
|
52
|
-
? SCREEN_HEIGHT * 0.62
|
|
53
|
-
: SCREEN_HEIGHT * 0.38;
|
|
46
|
+
? SCREEN_HEIGHT * 0.62
|
|
47
|
+
: SCREEN_HEIGHT * 0.38;
|
|
54
48
|
case XeboQuestionType.rating:
|
|
55
49
|
return question.followUpQuestion ? SCREEN_HEIGHT * 0.56 : SCREEN_HEIGHT * 0.38;
|
|
56
50
|
case XeboQuestionType.singleTextBox:
|
|
@@ -70,7 +64,6 @@ function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen):
|
|
|
70
64
|
}
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
// Which screen we show inside the modal
|
|
74
67
|
type ModalScreen = 'intro' | 'question' | 'thankYou';
|
|
75
68
|
|
|
76
69
|
export const XeboSurveyModal: React.FC = () => {
|
|
@@ -88,10 +81,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
88
81
|
[survey, questionIndex, screen]
|
|
89
82
|
);
|
|
90
83
|
|
|
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
84
|
// ─── Subscribe to manager events ─────────────────────────────────────────
|
|
96
85
|
|
|
97
86
|
useEffect(() => {
|
|
@@ -102,10 +91,17 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
102
91
|
|
|
103
92
|
const onVisible = (v: boolean) => {
|
|
104
93
|
if (v) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
94
|
+
// Sync survey into React state before showing so first frame isn't blank
|
|
95
|
+
const s = XeboSurveyManager.survey;
|
|
96
|
+
setSurvey(s);
|
|
97
|
+
if (s?.introPage?.enabled) {
|
|
98
|
+
setScreen('intro');
|
|
99
|
+
} else {
|
|
100
|
+
setScreen('question');
|
|
101
|
+
setQuestionIndex(XeboSurveyManager.currentQuestionIndex);
|
|
102
|
+
}
|
|
108
103
|
}
|
|
104
|
+
setVisible(v);
|
|
109
105
|
};
|
|
110
106
|
|
|
111
107
|
const onQuestionChanged = (idx: number) => {
|
|
@@ -123,57 +119,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
123
119
|
};
|
|
124
120
|
}, []);
|
|
125
121
|
|
|
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
122
|
// ─── User actions ─────────────────────────────────────────────────────────
|
|
178
123
|
|
|
179
124
|
const handleSkip = () => {
|
|
@@ -190,8 +135,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
190
135
|
const answers = Array.isArray(answer) ? answer : [answer];
|
|
191
136
|
answers.forEach(a => XeboSurveyManager.recordAnswer(a));
|
|
192
137
|
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
138
|
const s = XeboSurveyManager.survey;
|
|
196
139
|
const newIdx = XeboSurveyManager.currentQuestionIndex;
|
|
197
140
|
if (s && s.questions[newIdx]?.id === 'thank_you_auto') {
|
|
@@ -203,7 +146,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
203
146
|
|
|
204
147
|
const renderQuestion = (question: XeboQuestion) => {
|
|
205
148
|
const questions = survey?.questions ?? [];
|
|
206
|
-
// The last "real" question before thank-you
|
|
207
149
|
const lastRealIndex = questions.findIndex(q => q.id === 'thank_you_auto') - 1;
|
|
208
150
|
const isLast = questionIndex === lastRealIndex;
|
|
209
151
|
|
|
@@ -232,8 +174,6 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
232
174
|
}
|
|
233
175
|
};
|
|
234
176
|
|
|
235
|
-
// ─── Content ──────────────────────────────────────────────────────────────
|
|
236
|
-
|
|
237
177
|
const renderContent = () => {
|
|
238
178
|
if (!survey) return null;
|
|
239
179
|
|
|
@@ -257,31 +197,32 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
257
197
|
return null;
|
|
258
198
|
};
|
|
259
199
|
|
|
260
|
-
|
|
200
|
+
// ─── Render ───────────────────────────────────────────────────────────────
|
|
261
201
|
|
|
262
202
|
return (
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
203
|
+
<RNModal
|
|
204
|
+
isVisible={visible}
|
|
205
|
+
onBackButtonPress={handleSkip}
|
|
206
|
+
backdropOpacity={0.5}
|
|
207
|
+
backdropColor="#000000"
|
|
208
|
+
style={styles.modal}
|
|
209
|
+
animationIn="slideInUp"
|
|
210
|
+
animationInTiming={350}
|
|
211
|
+
animationOut="slideOutDown"
|
|
212
|
+
animationOutTiming={300}
|
|
213
|
+
useNativeDriver={true}
|
|
214
|
+
useNativeDriverForBackdrop={true}
|
|
215
|
+
hideModalContentWhileAnimating={true}
|
|
216
|
+
coverScreen={true}
|
|
217
|
+
propagateSwipe={false}
|
|
218
|
+
avoidKeyboard={true}
|
|
219
|
+
onModalHide={() => setSurvey(null)}
|
|
270
220
|
>
|
|
271
|
-
|
|
272
|
-
<Animated.View style={[styles.backdrop, { opacity: backdropAnim }]} />
|
|
273
|
-
|
|
274
|
-
<KeyboardAvoidingView
|
|
275
|
-
style={styles.kvContainer}
|
|
276
|
-
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
277
|
-
keyboardVerticalOffset={0}
|
|
278
|
-
>
|
|
279
|
-
<Animated.View
|
|
221
|
+
<View
|
|
280
222
|
style={[
|
|
281
223
|
styles.sheet,
|
|
282
224
|
{
|
|
283
225
|
backgroundColor: theme.backgroundColor,
|
|
284
|
-
transform: [{ translateY: slideAnim }],
|
|
285
226
|
height: sheetHeight,
|
|
286
227
|
},
|
|
287
228
|
]}
|
|
@@ -305,36 +246,22 @@ export const XeboSurveyModal: React.FC = () => {
|
|
|
305
246
|
|
|
306
247
|
{/* Safe area bottom padding — home indicator on iOS, gesture nav on Android */}
|
|
307
248
|
<View style={{ height: bottomInset }} />
|
|
308
|
-
</
|
|
309
|
-
|
|
310
|
-
</Modal>
|
|
249
|
+
</View>
|
|
250
|
+
</RNModal>
|
|
311
251
|
);
|
|
312
252
|
};
|
|
313
253
|
|
|
314
254
|
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,
|
|
255
|
+
// react-native-modal wrapper: full screen, sheet aligned to bottom
|
|
256
|
+
modal: {
|
|
329
257
|
justifyContent: 'flex-end',
|
|
330
|
-
|
|
258
|
+
margin: 0,
|
|
331
259
|
},
|
|
332
260
|
sheet: {
|
|
333
261
|
borderTopLeftRadius: 16,
|
|
334
262
|
borderTopRightRadius: 16,
|
|
335
263
|
overflow: 'hidden',
|
|
336
264
|
elevation: 9999,
|
|
337
|
-
zIndex: 9999,
|
|
338
265
|
shadowColor: '#000',
|
|
339
266
|
shadowOffset: { width: 0, height: -4 },
|
|
340
267
|
shadowOpacity: 0.15,
|