react-native-my-survey-sdk 2.0.6 → 2.0.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/babel.config.js +4 -0
- package/package.json +18 -10
- package/src/components/XeboDropdownView.tsx +197 -0
- package/src/components/XeboIntroView.tsx +58 -0
- package/src/components/XeboMultiNPSView.tsx +166 -0
- package/src/components/XeboMultiRatingView.tsx +165 -0
- package/src/components/XeboMultipleChoiceView.tsx +184 -0
- package/src/components/XeboNPSView.tsx +292 -0
- package/src/components/XeboRatingView.tsx +262 -0
- package/src/components/XeboSingleChoiceView.tsx +169 -0
- package/src/components/XeboSurveyModal.tsx +356 -0
- package/src/components/XeboTextBoxView.tsx +183 -0
- package/src/components/XeboThankYouView.tsx +100 -0
- package/src/core/XeboNetworkService.ts +375 -0
- package/src/core/XeboOfflineQueue.ts +33 -0
- package/src/core/XeboSurveyManager.ts +296 -0
- package/src/index.ts +23 -0
- package/src/models/XeboAPIModels.ts +80 -0
- package/src/models/XeboModels.ts +124 -0
- package/src/theme/XeboTheme.ts +33 -0
- package/tsconfig.json +17 -0
- package/ReadMe.md +0 -1
- package/index.js +0 -4
- package/src/SurveyWebView.js +0 -16
- package/src/useFaqSurvey.js +0 -28
- package/src/useSurveyAlert.js +0 -25
- package/src/useSurveyTimer.js +0 -39
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TouchableOpacity,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Animated,
|
|
8
|
+
Dimensions,
|
|
9
|
+
Platform,
|
|
10
|
+
KeyboardAvoidingView,
|
|
11
|
+
Modal,
|
|
12
|
+
TouchableWithoutFeedback,
|
|
13
|
+
SafeAreaView,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import { XeboSurveyManager, XEBO_EVENTS } from '../core/XeboSurveyManager';
|
|
16
|
+
import {
|
|
17
|
+
XeboSurvey,
|
|
18
|
+
XeboAnswer,
|
|
19
|
+
XeboQuestion,
|
|
20
|
+
XeboQuestionType,
|
|
21
|
+
} from '../models/XeboModels';
|
|
22
|
+
import { getTheme } from '../theme/XeboTheme';
|
|
23
|
+
|
|
24
|
+
import { XeboIntroView } from './XeboIntroView';
|
|
25
|
+
import { XeboThankYouView } from './XeboThankYouView';
|
|
26
|
+
import { XeboSingleChoiceView } from './XeboSingleChoiceView';
|
|
27
|
+
import { XeboMultipleChoiceView } from './XeboMultipleChoiceView';
|
|
28
|
+
import { XeboDropdownView } from './XeboDropdownView';
|
|
29
|
+
import { XeboTextBoxView } from './XeboTextBoxView';
|
|
30
|
+
import { XeboNPSView } from './XeboNPSView';
|
|
31
|
+
import { XeboMultiNPSView } from './XeboMultiNPSView';
|
|
32
|
+
import { XeboRatingView } from './XeboRatingView';
|
|
33
|
+
import { XeboMultiRatingView } from './XeboMultiRatingView';
|
|
34
|
+
|
|
35
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
36
|
+
const MODAL_MAX_HEIGHT = SCREEN_HEIGHT * 0.78; // used as initial off-screen translateY value
|
|
37
|
+
|
|
38
|
+
function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen): number {
|
|
39
|
+
if (screen === 'thankYou') return SCREEN_HEIGHT * 0.36;
|
|
40
|
+
if (screen === 'intro') return SCREEN_HEIGHT * 0.42;
|
|
41
|
+
if (!question) return SCREEN_HEIGHT * 0.48;
|
|
42
|
+
switch (question.type) {
|
|
43
|
+
case XeboQuestionType.multiNps:
|
|
44
|
+
case XeboQuestionType.multiRating: {
|
|
45
|
+
const rowCount = question.options?.length ?? 3;
|
|
46
|
+
// base ~25% + ~15% per row, capped at 78%
|
|
47
|
+
return Math.min(SCREEN_HEIGHT * 0.78, SCREEN_HEIGHT * (0.25 + rowCount * 0.15));
|
|
48
|
+
}
|
|
49
|
+
case XeboQuestionType.nps:
|
|
50
|
+
return (question.conditionalFollowUps?.length ?? 0) > 0
|
|
51
|
+
? SCREEN_HEIGHT * 0.62 // NPS Pro — space for follow-up card
|
|
52
|
+
: SCREEN_HEIGHT * 0.46; // simple NPS
|
|
53
|
+
case XeboQuestionType.rating:
|
|
54
|
+
return question.followUpQuestion ? SCREEN_HEIGHT * 0.56 : SCREEN_HEIGHT * 0.46;
|
|
55
|
+
case XeboQuestionType.singleTextBox:
|
|
56
|
+
return SCREEN_HEIGHT * 0.44;
|
|
57
|
+
case XeboQuestionType.multipleTextBox: {
|
|
58
|
+
const fieldCount = question.options?.length ?? 1;
|
|
59
|
+
return Math.min(SCREEN_HEIGHT * 0.70, SCREEN_HEIGHT * (0.28 + fieldCount * 0.10));
|
|
60
|
+
}
|
|
61
|
+
case XeboQuestionType.singleChoice:
|
|
62
|
+
case XeboQuestionType.multipleChoice:
|
|
63
|
+
case XeboQuestionType.dropdown: {
|
|
64
|
+
const optCount = question.options?.length ?? 0;
|
|
65
|
+
return Math.min(SCREEN_HEIGHT * 0.72, SCREEN_HEIGHT * (0.30 + optCount * 0.058));
|
|
66
|
+
}
|
|
67
|
+
default:
|
|
68
|
+
return SCREEN_HEIGHT * 0.48;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Which screen we show inside the modal
|
|
73
|
+
type ModalScreen = 'intro' | 'question' | 'thankYou';
|
|
74
|
+
|
|
75
|
+
export const XeboSurveyModal: React.FC = () => {
|
|
76
|
+
const theme = getTheme();
|
|
77
|
+
|
|
78
|
+
const [visible, setVisible] = useState(false);
|
|
79
|
+
const [survey, setSurvey] = useState<XeboSurvey | null>(null);
|
|
80
|
+
const [screen, setScreen] = useState<ModalScreen>('intro');
|
|
81
|
+
const [questionIndex, setQuestionIndex] = useState(0);
|
|
82
|
+
|
|
83
|
+
const sheetHeight = useMemo(
|
|
84
|
+
() => computeSheetHeight(survey?.questions[questionIndex] ?? null, screen),
|
|
85
|
+
[survey, questionIndex, screen]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Spring slide-up animation
|
|
89
|
+
const slideAnim = useRef(new Animated.Value(MODAL_MAX_HEIGHT)).current;
|
|
90
|
+
const backdropAnim = useRef(new Animated.Value(0)).current;
|
|
91
|
+
|
|
92
|
+
// ─── Subscribe to manager events ─────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const onLoaded = (s: XeboSurvey) => {
|
|
96
|
+
setSurvey(s);
|
|
97
|
+
setQuestionIndex(0);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const onVisible = (v: boolean) => {
|
|
101
|
+
if (v) {
|
|
102
|
+
openModal();
|
|
103
|
+
} else {
|
|
104
|
+
closeModal();
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onQuestionChanged = (idx: number) => {
|
|
109
|
+
setQuestionIndex(idx);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
XeboSurveyManager.on(XEBO_EVENTS.SURVEY_LOADED, onLoaded);
|
|
113
|
+
XeboSurveyManager.on(XEBO_EVENTS.SURVEY_VISIBLE, onVisible);
|
|
114
|
+
XeboSurveyManager.on(XEBO_EVENTS.QUESTION_CHANGED, onQuestionChanged);
|
|
115
|
+
|
|
116
|
+
return () => {
|
|
117
|
+
XeboSurveyManager.off(XEBO_EVENTS.SURVEY_LOADED, onLoaded);
|
|
118
|
+
XeboSurveyManager.off(XEBO_EVENTS.SURVEY_VISIBLE, onVisible);
|
|
119
|
+
XeboSurveyManager.off(XEBO_EVENTS.QUESTION_CHANGED, onQuestionChanged);
|
|
120
|
+
};
|
|
121
|
+
}, []);
|
|
122
|
+
|
|
123
|
+
// ─── Animations ───────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
const openModal = useCallback(() => {
|
|
126
|
+
// Sync survey into React state NOW so the modal never renders blank on first frame
|
|
127
|
+
const s = XeboSurveyManager.survey;
|
|
128
|
+
setSurvey(s);
|
|
129
|
+
setVisible(true);
|
|
130
|
+
slideAnim.setValue(MODAL_MAX_HEIGHT);
|
|
131
|
+
backdropAnim.setValue(0);
|
|
132
|
+
|
|
133
|
+
// Determine initial screen
|
|
134
|
+
if (s?.introPage?.enabled) {
|
|
135
|
+
setScreen('intro');
|
|
136
|
+
} else {
|
|
137
|
+
setScreen('question');
|
|
138
|
+
setQuestionIndex(XeboSurveyManager.currentQuestionIndex);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Animated.parallel([
|
|
142
|
+
Animated.spring(slideAnim, {
|
|
143
|
+
toValue: 0,
|
|
144
|
+
tension: 180,
|
|
145
|
+
friction: 20,
|
|
146
|
+
useNativeDriver: true,
|
|
147
|
+
}),
|
|
148
|
+
Animated.timing(backdropAnim, {
|
|
149
|
+
toValue: 1,
|
|
150
|
+
duration: 200,
|
|
151
|
+
useNativeDriver: true,
|
|
152
|
+
}),
|
|
153
|
+
]).start();
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
const closeModal = useCallback(() => {
|
|
157
|
+
Animated.parallel([
|
|
158
|
+
Animated.timing(slideAnim, {
|
|
159
|
+
toValue: MODAL_MAX_HEIGHT,
|
|
160
|
+
duration: 300,
|
|
161
|
+
useNativeDriver: true,
|
|
162
|
+
}),
|
|
163
|
+
Animated.timing(backdropAnim, {
|
|
164
|
+
toValue: 0,
|
|
165
|
+
duration: 300,
|
|
166
|
+
useNativeDriver: true,
|
|
167
|
+
}),
|
|
168
|
+
]).start(() => {
|
|
169
|
+
setVisible(false);
|
|
170
|
+
setSurvey(null);
|
|
171
|
+
});
|
|
172
|
+
}, []);
|
|
173
|
+
|
|
174
|
+
// ─── User actions ─────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const handleSkip = () => {
|
|
177
|
+
XeboSurveyManager.dismissSurvey();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const handleIntroStart = () => {
|
|
181
|
+
setScreen('question');
|
|
182
|
+
setQuestionIndex(0);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const handleAnswer = useCallback((answer: XeboAnswer) => {
|
|
186
|
+
XeboSurveyManager.recordAnswer(answer);
|
|
187
|
+
XeboSurveyManager.nextQuestion();
|
|
188
|
+
// questionIndex will be updated via QUESTION_CHANGED event
|
|
189
|
+
// But also check if we're now at the thank-you question
|
|
190
|
+
const s = XeboSurveyManager.survey;
|
|
191
|
+
const newIdx = XeboSurveyManager.currentQuestionIndex;
|
|
192
|
+
if (s && s.questions[newIdx]?.id === 'thank_you_auto') {
|
|
193
|
+
setScreen('thankYou');
|
|
194
|
+
}
|
|
195
|
+
}, []);
|
|
196
|
+
|
|
197
|
+
// ─── Question renderer ────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
const renderQuestion = (question: XeboQuestion) => {
|
|
200
|
+
const questions = survey?.questions ?? [];
|
|
201
|
+
// The last "real" question before thank-you
|
|
202
|
+
const lastRealIndex = questions.findIndex(q => q.id === 'thank_you_auto') - 1;
|
|
203
|
+
const isLast = questionIndex === lastRealIndex;
|
|
204
|
+
|
|
205
|
+
const commonProps = { question, isLastQuestion: isLast, onAnswer: handleAnswer };
|
|
206
|
+
|
|
207
|
+
switch (question.type) {
|
|
208
|
+
case XeboQuestionType.singleChoice:
|
|
209
|
+
return <XeboSingleChoiceView {...commonProps} />;
|
|
210
|
+
case XeboQuestionType.multipleChoice:
|
|
211
|
+
return <XeboMultipleChoiceView {...commonProps} />;
|
|
212
|
+
case XeboQuestionType.dropdown:
|
|
213
|
+
return <XeboDropdownView {...commonProps} />;
|
|
214
|
+
case XeboQuestionType.singleTextBox:
|
|
215
|
+
case XeboQuestionType.multipleTextBox:
|
|
216
|
+
return <XeboTextBoxView {...commonProps} />;
|
|
217
|
+
case XeboQuestionType.nps:
|
|
218
|
+
return <XeboNPSView {...commonProps} />;
|
|
219
|
+
case XeboQuestionType.multiNps:
|
|
220
|
+
return <XeboMultiNPSView {...commonProps} />;
|
|
221
|
+
case XeboQuestionType.rating:
|
|
222
|
+
return <XeboRatingView {...commonProps} />;
|
|
223
|
+
case XeboQuestionType.multiRating:
|
|
224
|
+
return <XeboMultiRatingView {...commonProps} />;
|
|
225
|
+
default:
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// ─── Content ──────────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
const renderContent = () => {
|
|
233
|
+
if (!survey) return null;
|
|
234
|
+
|
|
235
|
+
if (screen === 'intro' && survey.introPage) {
|
|
236
|
+
return <XeboIntroView introPage={survey.introPage} onStart={handleIntroStart} />;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (screen === 'thankYou') {
|
|
240
|
+
const tyQuestion = survey.questions.find(q => q.id === 'thank_you_auto');
|
|
241
|
+
if (tyQuestion) {
|
|
242
|
+
return <XeboThankYouView question={tyQuestion} onDismiss={handleSkip} />;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (screen === 'question') {
|
|
247
|
+
const question = survey.questions[questionIndex];
|
|
248
|
+
if (!question) return null;
|
|
249
|
+
return renderQuestion(question);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (!visible) return null;
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<Modal
|
|
259
|
+
transparent
|
|
260
|
+
visible={visible}
|
|
261
|
+
animationType="none"
|
|
262
|
+
statusBarTranslucent
|
|
263
|
+
onRequestClose={handleSkip}
|
|
264
|
+
>
|
|
265
|
+
{/* Backdrop — no dismiss on tap */}
|
|
266
|
+
<Animated.View style={[styles.backdrop, { opacity: backdropAnim }]} />
|
|
267
|
+
|
|
268
|
+
<KeyboardAvoidingView
|
|
269
|
+
style={styles.kvContainer}
|
|
270
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
271
|
+
keyboardVerticalOffset={0}
|
|
272
|
+
>
|
|
273
|
+
<Animated.View
|
|
274
|
+
style={[
|
|
275
|
+
styles.sheet,
|
|
276
|
+
{
|
|
277
|
+
backgroundColor: theme.backgroundColor,
|
|
278
|
+
transform: [{ translateY: slideAnim }],
|
|
279
|
+
height: sheetHeight,
|
|
280
|
+
},
|
|
281
|
+
]}
|
|
282
|
+
>
|
|
283
|
+
{/* Handle bar */}
|
|
284
|
+
<View style={styles.handleWrap}>
|
|
285
|
+
<View style={styles.handle} />
|
|
286
|
+
</View>
|
|
287
|
+
|
|
288
|
+
{/* Skip button — right aligned */}
|
|
289
|
+
{screen !== 'thankYou' && (
|
|
290
|
+
<View style={styles.skipRow}>
|
|
291
|
+
<TouchableOpacity onPress={handleSkip} style={styles.skipButton} activeOpacity={0.7}>
|
|
292
|
+
<Text style={[styles.skipText, { color: theme.textColor }]}>Skip</Text>
|
|
293
|
+
</TouchableOpacity>
|
|
294
|
+
</View>
|
|
295
|
+
)}
|
|
296
|
+
|
|
297
|
+
{/* Main content */}
|
|
298
|
+
<View style={styles.content}>{renderContent()}</View>
|
|
299
|
+
|
|
300
|
+
{/* Safe area bottom padding */}
|
|
301
|
+
<SafeAreaView edges={['bottom'] as any} />
|
|
302
|
+
</Animated.View>
|
|
303
|
+
</KeyboardAvoidingView>
|
|
304
|
+
</Modal>
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const styles = StyleSheet.create({
|
|
309
|
+
backdrop: {
|
|
310
|
+
...StyleSheet.absoluteFillObject,
|
|
311
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
312
|
+
},
|
|
313
|
+
kvContainer: {
|
|
314
|
+
flex: 1,
|
|
315
|
+
justifyContent: 'flex-end',
|
|
316
|
+
},
|
|
317
|
+
sheet: {
|
|
318
|
+
borderTopLeftRadius: 16,
|
|
319
|
+
borderTopRightRadius: 16,
|
|
320
|
+
overflow: 'hidden',
|
|
321
|
+
elevation: 24,
|
|
322
|
+
shadowColor: '#000',
|
|
323
|
+
shadowOffset: { width: 0, height: -4 },
|
|
324
|
+
shadowOpacity: 0.15,
|
|
325
|
+
shadowRadius: 12,
|
|
326
|
+
},
|
|
327
|
+
handleWrap: {
|
|
328
|
+
alignItems: 'center',
|
|
329
|
+
paddingTop: 10,
|
|
330
|
+
paddingBottom: 4,
|
|
331
|
+
},
|
|
332
|
+
handle: {
|
|
333
|
+
width: 36,
|
|
334
|
+
height: 4,
|
|
335
|
+
borderRadius: 2,
|
|
336
|
+
backgroundColor: '#D1D5DB',
|
|
337
|
+
},
|
|
338
|
+
skipRow: {
|
|
339
|
+
flexDirection: 'row',
|
|
340
|
+
justifyContent: 'flex-end',
|
|
341
|
+
paddingHorizontal: 16,
|
|
342
|
+
paddingBottom: 4,
|
|
343
|
+
},
|
|
344
|
+
skipButton: {
|
|
345
|
+
paddingHorizontal: 12,
|
|
346
|
+
paddingVertical: 6,
|
|
347
|
+
},
|
|
348
|
+
skipText: {
|
|
349
|
+
fontSize: 14,
|
|
350
|
+
fontWeight: '500',
|
|
351
|
+
opacity: 0.6,
|
|
352
|
+
},
|
|
353
|
+
content: {
|
|
354
|
+
flex: 1,
|
|
355
|
+
},
|
|
356
|
+
});
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput,
|
|
6
|
+
TouchableOpacity,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
ScrollView,
|
|
9
|
+
Animated,
|
|
10
|
+
KeyboardAvoidingView,
|
|
11
|
+
Platform,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { XeboQuestion, XeboAnswer } from '../models/XeboModels';
|
|
14
|
+
import { XeboQuestionType } from '../models/XeboModels';
|
|
15
|
+
import { getTheme } from '../theme/XeboTheme';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
question: XeboQuestion;
|
|
19
|
+
isLastQuestion: boolean;
|
|
20
|
+
onAnswer: (answer: XeboAnswer) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const XeboTextBoxView: React.FC<Props> = ({ question, isLastQuestion, onAnswer }) => {
|
|
24
|
+
const theme = getTheme();
|
|
25
|
+
const isMulti = question.type === XeboQuestionType.multipleTextBox;
|
|
26
|
+
const fields = question.options && question.options.length > 0 ? question.options : [{ id: 'default', text: '', value: '' }];
|
|
27
|
+
|
|
28
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
29
|
+
const [error, setError] = useState('');
|
|
30
|
+
const shakeAnim = useRef(new Animated.Value(0)).current;
|
|
31
|
+
|
|
32
|
+
const shake = () => {
|
|
33
|
+
Animated.sequence([
|
|
34
|
+
Animated.timing(shakeAnim, { toValue: 8, duration: 60, useNativeDriver: true }),
|
|
35
|
+
Animated.timing(shakeAnim, { toValue: -8, duration: 60, useNativeDriver: true }),
|
|
36
|
+
Animated.timing(shakeAnim, { toValue: 6, duration: 60, useNativeDriver: true }),
|
|
37
|
+
Animated.timing(shakeAnim, { toValue: 0, duration: 60, useNativeDriver: true }),
|
|
38
|
+
]).start();
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleChange = (fieldId: string, text: string) => {
|
|
42
|
+
setValues(prev => ({ ...prev, [fieldId]: text }));
|
|
43
|
+
setError('');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleSubmit = () => {
|
|
47
|
+
if (question.isRequired) {
|
|
48
|
+
const empty = fields.some(f => !(values[f.id] ?? '').trim());
|
|
49
|
+
if (empty) {
|
|
50
|
+
setError('Please fill in all fields.');
|
|
51
|
+
shake();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onAnswer({
|
|
57
|
+
questionId: question.id,
|
|
58
|
+
value: fields
|
|
59
|
+
.filter(f => (values[f.id] ?? '').trim())
|
|
60
|
+
.map(f => ({ rowId: f.id, text: values[f.id] ?? '' })),
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<KeyboardAvoidingView
|
|
66
|
+
style={styles.kvContainer}
|
|
67
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
68
|
+
>
|
|
69
|
+
<ScrollView
|
|
70
|
+
style={styles.scroll}
|
|
71
|
+
contentContainerStyle={styles.container}
|
|
72
|
+
keyboardShouldPersistTaps="handled"
|
|
73
|
+
showsVerticalScrollIndicator={false}
|
|
74
|
+
>
|
|
75
|
+
<Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
76
|
+
{question.title}
|
|
77
|
+
</Text>
|
|
78
|
+
{!!question.subtitle && (
|
|
79
|
+
<Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
80
|
+
{question.subtitle}
|
|
81
|
+
</Text>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
{fields.map((field, idx) => (
|
|
85
|
+
<View key={field.id} style={styles.fieldWrap}>
|
|
86
|
+
{isMulti && !!field.text && (
|
|
87
|
+
<Text style={[styles.fieldLabel, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
88
|
+
{field.text}
|
|
89
|
+
</Text>
|
|
90
|
+
)}
|
|
91
|
+
<TextInput
|
|
92
|
+
style={[
|
|
93
|
+
styles.input,
|
|
94
|
+
{
|
|
95
|
+
borderColor: '#E5E7EB',
|
|
96
|
+
color: theme.textColor,
|
|
97
|
+
fontFamily: theme.fontFamily,
|
|
98
|
+
borderRadius: theme.cornerRadius,
|
|
99
|
+
},
|
|
100
|
+
]}
|
|
101
|
+
placeholder={isMulti ? '' : (question.placeholder || 'Type your answer here...')}
|
|
102
|
+
placeholderTextColor="#9CA3AF"
|
|
103
|
+
value={values[field.id] ?? ''}
|
|
104
|
+
onChangeText={text => handleChange(field.id, text)}
|
|
105
|
+
multiline={!isMulti}
|
|
106
|
+
numberOfLines={isMulti ? 1 : 4}
|
|
107
|
+
textAlignVertical={isMulti ? 'center' : 'top'}
|
|
108
|
+
returnKeyType={idx === fields.length - 1 ? 'done' : 'next'}
|
|
109
|
+
/>
|
|
110
|
+
</View>
|
|
111
|
+
))}
|
|
112
|
+
|
|
113
|
+
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
|
114
|
+
|
|
115
|
+
<Animated.View style={{ transform: [{ translateX: shakeAnim }] }}>
|
|
116
|
+
<TouchableOpacity
|
|
117
|
+
style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}
|
|
118
|
+
onPress={handleSubmit}
|
|
119
|
+
activeOpacity={0.8}
|
|
120
|
+
>
|
|
121
|
+
<Text style={[styles.buttonText, { fontFamily: theme.fontFamily }]}>
|
|
122
|
+
{isLastQuestion ? 'Submit' : 'Next'}
|
|
123
|
+
</Text>
|
|
124
|
+
</TouchableOpacity>
|
|
125
|
+
</Animated.View>
|
|
126
|
+
</ScrollView>
|
|
127
|
+
</KeyboardAvoidingView>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const styles = StyleSheet.create({
|
|
132
|
+
kvContainer: {
|
|
133
|
+
flex: 1,
|
|
134
|
+
},
|
|
135
|
+
scroll: {
|
|
136
|
+
flex: 1,
|
|
137
|
+
},
|
|
138
|
+
container: {
|
|
139
|
+
paddingHorizontal: 20,
|
|
140
|
+
paddingTop: 16,
|
|
141
|
+
paddingBottom: 24,
|
|
142
|
+
flexGrow: 1,
|
|
143
|
+
},
|
|
144
|
+
title: {
|
|
145
|
+
fontSize: 18,
|
|
146
|
+
fontWeight: '700',
|
|
147
|
+
marginBottom: 6,
|
|
148
|
+
},
|
|
149
|
+
subtitle: {
|
|
150
|
+
fontSize: 14,
|
|
151
|
+
opacity: 0.6,
|
|
152
|
+
marginBottom: 16,
|
|
153
|
+
},
|
|
154
|
+
fieldWrap: {
|
|
155
|
+
marginBottom: 14,
|
|
156
|
+
},
|
|
157
|
+
fieldLabel: {
|
|
158
|
+
fontSize: 14,
|
|
159
|
+
fontWeight: '500',
|
|
160
|
+
marginBottom: 6,
|
|
161
|
+
},
|
|
162
|
+
input: {
|
|
163
|
+
borderWidth: 1.5,
|
|
164
|
+
padding: 12,
|
|
165
|
+
fontSize: 15,
|
|
166
|
+
minHeight: 48,
|
|
167
|
+
},
|
|
168
|
+
errorText: {
|
|
169
|
+
color: '#EF4444',
|
|
170
|
+
fontSize: 13,
|
|
171
|
+
marginBottom: 8,
|
|
172
|
+
},
|
|
173
|
+
button: {
|
|
174
|
+
paddingVertical: 14,
|
|
175
|
+
alignItems: 'center',
|
|
176
|
+
marginTop: 8,
|
|
177
|
+
},
|
|
178
|
+
buttonText: {
|
|
179
|
+
color: '#FFFFFF',
|
|
180
|
+
fontSize: 16,
|
|
181
|
+
fontWeight: '600',
|
|
182
|
+
},
|
|
183
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Animated,
|
|
7
|
+
Linking,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import { XeboQuestion } from '../models/XeboModels';
|
|
10
|
+
import { getTheme } from '../theme/XeboTheme';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
question: XeboQuestion;
|
|
14
|
+
onDismiss: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const XeboThankYouView: React.FC<Props> = ({ question, onDismiss }) => {
|
|
18
|
+
const theme = getTheme();
|
|
19
|
+
const fadeAnim = useRef(new Animated.Value(0)).current;
|
|
20
|
+
const scaleAnim = useRef(new Animated.Value(0.8)).current;
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
Animated.parallel([
|
|
24
|
+
Animated.timing(fadeAnim, { toValue: 1, duration: 400, useNativeDriver: true }),
|
|
25
|
+
Animated.spring(scaleAnim, { toValue: 1, tension: 60, friction: 7, useNativeDriver: true }),
|
|
26
|
+
]).start();
|
|
27
|
+
|
|
28
|
+
if (question.redirectURL) {
|
|
29
|
+
const timer = setTimeout(async () => {
|
|
30
|
+
try {
|
|
31
|
+
await Linking.openURL(question.redirectURL!);
|
|
32
|
+
} catch {
|
|
33
|
+
// ignore
|
|
34
|
+
}
|
|
35
|
+
onDismiss();
|
|
36
|
+
}, 2500);
|
|
37
|
+
return () => clearTimeout(timer);
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Animated.View
|
|
43
|
+
style={[
|
|
44
|
+
styles.container,
|
|
45
|
+
{ opacity: fadeAnim, transform: [{ scale: scaleAnim }] },
|
|
46
|
+
]}
|
|
47
|
+
>
|
|
48
|
+
{/* Green checkmark circle */}
|
|
49
|
+
<View style={styles.iconCircle}>
|
|
50
|
+
<Text style={styles.checkmark}>✓</Text>
|
|
51
|
+
</View>
|
|
52
|
+
|
|
53
|
+
<Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
54
|
+
{question.title}
|
|
55
|
+
</Text>
|
|
56
|
+
|
|
57
|
+
{!!question.subtitle && (
|
|
58
|
+
<Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
59
|
+
{question.subtitle}
|
|
60
|
+
</Text>
|
|
61
|
+
)}
|
|
62
|
+
</Animated.View>
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const styles = StyleSheet.create({
|
|
67
|
+
container: {
|
|
68
|
+
flex: 1,
|
|
69
|
+
paddingHorizontal: 20,
|
|
70
|
+
paddingVertical: 40,
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
justifyContent: 'center',
|
|
73
|
+
},
|
|
74
|
+
iconCircle: {
|
|
75
|
+
width: 80,
|
|
76
|
+
height: 80,
|
|
77
|
+
borderRadius: 40,
|
|
78
|
+
backgroundColor: '#22C55E',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
justifyContent: 'center',
|
|
81
|
+
marginBottom: 24,
|
|
82
|
+
},
|
|
83
|
+
checkmark: {
|
|
84
|
+
color: '#FFFFFF',
|
|
85
|
+
fontSize: 40,
|
|
86
|
+
fontWeight: '700',
|
|
87
|
+
},
|
|
88
|
+
title: {
|
|
89
|
+
fontSize: 22,
|
|
90
|
+
fontWeight: '700',
|
|
91
|
+
textAlign: 'center',
|
|
92
|
+
marginBottom: 12,
|
|
93
|
+
},
|
|
94
|
+
subtitle: {
|
|
95
|
+
fontSize: 15,
|
|
96
|
+
textAlign: 'center',
|
|
97
|
+
opacity: 0.7,
|
|
98
|
+
lineHeight: 22,
|
|
99
|
+
},
|
|
100
|
+
});
|