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,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-my-survey-sdk",
3
- "version": "2.1.7",
3
+ "version": "2.1.9",
4
4
  "description": "Xebo survey collection SDK for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -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
- {!!introPage.content && (
17
- <Text style={[styles.content, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
18
- {introPage.content}
19
- </Text>
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
- justifyContent: 'space-between',
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: 16,
63
+ marginTop: 12,
52
64
  },
53
65
  buttonText: {
54
66
  color: '#FFFFFF',
@@ -1,15 +1,13 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
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 // NPS Pro — space for follow-up card
53
- : SCREEN_HEIGHT * 0.38; // simple NPS — scale + button only
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
- openModal();
106
- } else {
107
- closeModal();
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
- if (!visible) return null;
200
+ // ─── Render ───────────────────────────────────────────────────────────────
261
201
 
262
202
  return (
263
- <Modal
264
- transparent
265
- visible={visible}
266
- animationType="none"
267
- statusBarTranslucent
268
- hardwareAccelerated
269
- onRequestClose={handleSkip}
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
- {/* Backdrop — no dismiss on tap */}
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
- </Animated.View>
309
- </KeyboardAvoidingView>
310
- </Modal>
249
+ </View>
250
+ </RNModal>
311
251
  );
312
252
  };
313
253
 
314
254
  const styles = StyleSheet.create({
315
- backdrop: {
316
- position: 'absolute',
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
- zIndex: 9999,
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,