react-native-my-survey-sdk 2.1.9 → 2.2.0
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
|
@@ -18,9 +18,9 @@ interface Props {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// NPS zone colors — matches iOS SDK defaults
|
|
21
|
-
const DETRACTOR_COLOR = '#F24236';
|
|
22
|
-
const PASSIVE_COLOR = '#FFC208';
|
|
23
|
-
const PROMOTER_COLOR = '#2ECC70';
|
|
21
|
+
const DETRACTOR_COLOR = '#F24236';
|
|
22
|
+
const PASSIVE_COLOR = '#FFC208';
|
|
23
|
+
const PROMOTER_COLOR = '#2ECC70';
|
|
24
24
|
|
|
25
25
|
function npsColor(score: number, npsLabels?: XeboQuestion['npsLabels']): string {
|
|
26
26
|
if (score <= 6) return npsLabels?.detractorColor ?? DETRACTOR_COLOR;
|
|
@@ -96,14 +96,12 @@ export const XeboNPSView: React.FC<Props> = ({ question, isLastQuestion, onAnswe
|
|
|
96
96
|
];
|
|
97
97
|
|
|
98
98
|
if (followUpChoice) {
|
|
99
|
-
// Single-choice follow-up: row_id = selected option UUID
|
|
100
99
|
const selectedOption = activeFollowUp.followUpOptions.find(o => o.id === followUpChoice);
|
|
101
100
|
answers.push({
|
|
102
101
|
questionId: activeFollowUp.questionId,
|
|
103
102
|
value: [{ rowId: followUpChoice, text: selectedOption?.text ?? '' }],
|
|
104
103
|
});
|
|
105
104
|
} else if (followUpText.trim()) {
|
|
106
|
-
// Text follow-up: row_id = follow-up question UUID (placeholder, matches iOS behaviour)
|
|
107
105
|
answers.push({
|
|
108
106
|
questionId: activeFollowUp.questionId,
|
|
109
107
|
value: [{ rowId: activeFollowUp.questionId, text: followUpText }],
|
|
@@ -114,7 +112,7 @@ export const XeboNPSView: React.FC<Props> = ({ question, isLastQuestion, onAnswe
|
|
|
114
112
|
return;
|
|
115
113
|
}
|
|
116
114
|
|
|
117
|
-
// Simple text follow-up
|
|
115
|
+
// Simple text follow-up
|
|
118
116
|
onAnswer({
|
|
119
117
|
questionId: question.id,
|
|
120
118
|
value: [{ rowId, colId, text: hasTextFollowUp ? followUpText : undefined }],
|
|
@@ -122,85 +120,107 @@ export const XeboNPSView: React.FC<Props> = ({ question, isLastQuestion, onAnswe
|
|
|
122
120
|
};
|
|
123
121
|
|
|
124
122
|
return (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
136
|
-
{question.subtitle}
|
|
123
|
+
// Outer view fills modal content area; button pinned at bottom outside ScrollView
|
|
124
|
+
<View style={styles.outer}>
|
|
125
|
+
<ScrollView
|
|
126
|
+
style={styles.scroll}
|
|
127
|
+
contentContainerStyle={styles.scrollContent}
|
|
128
|
+
showsVerticalScrollIndicator={false}
|
|
129
|
+
keyboardShouldPersistTaps="handled"
|
|
130
|
+
>
|
|
131
|
+
<Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
132
|
+
{question.title}
|
|
137
133
|
</Text>
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<View style={styles.scaleRow}>
|
|
142
|
-
{Array.from({ length: 11 }, (_, i) => {
|
|
143
|
-
const selected = selectedScore === i;
|
|
144
|
-
const color = npsColor(i, question.npsLabels);
|
|
145
|
-
return (
|
|
146
|
-
<Animated.View key={i} style={[styles.scoreCell, { transform: [{ scale: scaleAnim[i] }] }]}>
|
|
147
|
-
<TouchableOpacity
|
|
148
|
-
style={[
|
|
149
|
-
styles.scoreButton,
|
|
150
|
-
{
|
|
151
|
-
backgroundColor: color,
|
|
152
|
-
borderColor: selected ? '#000000' : color,
|
|
153
|
-
borderWidth: selected ? 2.5 : 1.5,
|
|
154
|
-
},
|
|
155
|
-
]}
|
|
156
|
-
onPress={() => handleScorePress(i)}
|
|
157
|
-
activeOpacity={0.8}
|
|
158
|
-
>
|
|
159
|
-
<Text style={[
|
|
160
|
-
styles.scoreText,
|
|
161
|
-
{ color: isColorDark(color) ? '#FFFFFF' : selected ? '#111111' : '#444444' },
|
|
162
|
-
]}>
|
|
163
|
-
{i}
|
|
164
|
-
</Text>
|
|
165
|
-
</TouchableOpacity>
|
|
166
|
-
</Animated.View>
|
|
167
|
-
);
|
|
168
|
-
})}
|
|
169
|
-
</View>
|
|
170
|
-
|
|
171
|
-
{/* Lower / Upper labels */}
|
|
172
|
-
{(question.npsLabels?.lower || question.npsLabels?.upper) && (
|
|
173
|
-
<View style={styles.labelsRow}>
|
|
174
|
-
<Text style={[styles.label, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
175
|
-
{question.npsLabels?.lower ?? ''}
|
|
176
|
-
</Text>
|
|
177
|
-
<Text style={[styles.label, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
178
|
-
{question.npsLabels?.upper ?? ''}
|
|
134
|
+
{!!question.subtitle && (
|
|
135
|
+
<Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
136
|
+
{question.subtitle}
|
|
179
137
|
</Text>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
{/* NPS Scale — single row, always-visible category colors */}
|
|
141
|
+
<View style={styles.scaleRow}>
|
|
142
|
+
{Array.from({ length: 11 }, (_, i) => {
|
|
143
|
+
const selected = selectedScore === i;
|
|
144
|
+
const color = npsColor(i, question.npsLabels);
|
|
145
|
+
return (
|
|
146
|
+
<Animated.View key={i} style={[styles.scoreCell, { transform: [{ scale: scaleAnim[i] }] }]}>
|
|
147
|
+
<TouchableOpacity
|
|
148
|
+
style={[
|
|
149
|
+
styles.scoreButton,
|
|
150
|
+
{
|
|
151
|
+
backgroundColor: color,
|
|
152
|
+
borderColor: selected ? '#000000' : color,
|
|
153
|
+
borderWidth: selected ? 2.5 : 1.5,
|
|
154
|
+
},
|
|
155
|
+
]}
|
|
156
|
+
onPress={() => handleScorePress(i)}
|
|
157
|
+
activeOpacity={0.8}
|
|
158
|
+
>
|
|
159
|
+
<Text style={[
|
|
160
|
+
styles.scoreText,
|
|
161
|
+
{ color: isColorDark(color) ? '#FFFFFF' : selected ? '#111111' : '#444444' },
|
|
162
|
+
]}>
|
|
163
|
+
{i}
|
|
164
|
+
</Text>
|
|
165
|
+
</TouchableOpacity>
|
|
166
|
+
</Animated.View>
|
|
167
|
+
);
|
|
168
|
+
})}
|
|
180
169
|
</View>
|
|
181
|
-
)}
|
|
182
170
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
171
|
+
{/* Lower / Upper labels */}
|
|
172
|
+
{(question.npsLabels?.lower || question.npsLabels?.upper) && (
|
|
173
|
+
<View style={styles.labelsRow}>
|
|
174
|
+
<Text style={[styles.label, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
175
|
+
{question.npsLabels?.lower ?? ''}
|
|
176
|
+
</Text>
|
|
177
|
+
<Text style={[styles.label, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
178
|
+
{question.npsLabels?.upper ?? ''}
|
|
179
|
+
</Text>
|
|
180
|
+
</View>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
{/* Conditional follow-up (NPS Pro) */}
|
|
184
|
+
{selectedScore !== null && activeFollowUp && (
|
|
185
|
+
<View style={[styles.followUpCard, { borderColor: '#E5E7EB', borderRadius: theme.cornerRadius }]}>
|
|
186
|
+
<Text style={[styles.followUpTitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
187
|
+
{activeFollowUp.followUpQuestion}
|
|
188
|
+
</Text>
|
|
189
|
+
{activeFollowUp.followUpType === XeboQuestionType.singleChoice && activeFollowUp.followUpOptions.length > 0 ? (
|
|
190
|
+
activeFollowUp.followUpOptions.map(opt => (
|
|
191
|
+
<TouchableOpacity
|
|
192
|
+
key={opt.id}
|
|
193
|
+
style={[
|
|
194
|
+
styles.followUpOption,
|
|
195
|
+
followUpChoice === opt.id && { borderColor: theme.primaryColor },
|
|
196
|
+
]}
|
|
197
|
+
onPress={() => setFollowUpChoice(opt.id)}
|
|
198
|
+
activeOpacity={0.7}
|
|
199
|
+
>
|
|
200
|
+
<Text style={[styles.followUpOptionText, { color: theme.textColor }]}>{opt.text}</Text>
|
|
201
|
+
</TouchableOpacity>
|
|
202
|
+
))
|
|
203
|
+
) : (
|
|
204
|
+
<TextInput
|
|
205
|
+
style={[styles.followUpInput, { borderColor: '#E5E7EB', color: theme.textColor, borderRadius: theme.cornerRadius }]}
|
|
206
|
+
placeholder="Your comment..."
|
|
207
|
+
placeholderTextColor="#9CA3AF"
|
|
208
|
+
value={followUpText}
|
|
209
|
+
onChangeText={setFollowUpText}
|
|
210
|
+
multiline
|
|
211
|
+
numberOfLines={3}
|
|
212
|
+
textAlignVertical="top"
|
|
213
|
+
/>
|
|
214
|
+
)}
|
|
215
|
+
</View>
|
|
216
|
+
)}
|
|
217
|
+
|
|
218
|
+
{/* Simple text follow-up */}
|
|
219
|
+
{selectedScore !== null && hasTextFollowUp && (
|
|
220
|
+
<View style={[styles.followUpCard, { borderColor: '#E5E7EB', borderRadius: theme.cornerRadius }]}>
|
|
221
|
+
<Text style={[styles.followUpTitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
222
|
+
{question.followUpQuestion}
|
|
223
|
+
</Text>
|
|
204
224
|
<TextInput
|
|
205
225
|
style={[styles.followUpInput, { borderColor: '#E5E7EB', color: theme.textColor, borderRadius: theme.cornerRadius }]}
|
|
206
226
|
placeholder="Your comment..."
|
|
@@ -211,34 +231,12 @@ export const XeboNPSView: React.FC<Props> = ({ question, isLastQuestion, onAnswe
|
|
|
211
231
|
numberOfLines={3}
|
|
212
232
|
textAlignVertical="top"
|
|
213
233
|
/>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
{/* Simple text follow-up */}
|
|
219
|
-
{selectedScore !== null && hasTextFollowUp && (
|
|
220
|
-
<View style={[styles.followUpCard, { borderColor: '#E5E7EB', borderRadius: theme.cornerRadius }]}>
|
|
221
|
-
<Text style={[styles.followUpTitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
222
|
-
{question.followUpQuestion}
|
|
223
|
-
</Text>
|
|
224
|
-
<TextInput
|
|
225
|
-
style={[styles.followUpInput, { borderColor: '#E5E7EB', color: theme.textColor, borderRadius: theme.cornerRadius }]}
|
|
226
|
-
placeholder="Your comment..."
|
|
227
|
-
placeholderTextColor="#9CA3AF"
|
|
228
|
-
value={followUpText}
|
|
229
|
-
onChangeText={setFollowUpText}
|
|
230
|
-
multiline
|
|
231
|
-
numberOfLines={3}
|
|
232
|
-
textAlignVertical="top"
|
|
233
|
-
/>
|
|
234
|
-
</View>
|
|
235
|
-
)}
|
|
234
|
+
</View>
|
|
235
|
+
)}
|
|
236
|
+
</ScrollView>
|
|
236
237
|
|
|
238
|
+
{/* Error + button always visible at bottom, outside scroll */}
|
|
237
239
|
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
|
238
|
-
|
|
239
|
-
{/* Spacer pushes button to bottom */}
|
|
240
|
-
<View style={{ flex: 1 }} />
|
|
241
|
-
|
|
242
240
|
<Animated.View style={{ transform: [{ translateX: shakeAnim }] }}>
|
|
243
241
|
<TouchableOpacity
|
|
244
242
|
style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius, opacity: selectedScore === null ? 0.4 : 1 }]}
|
|
@@ -251,17 +249,20 @@ export const XeboNPSView: React.FC<Props> = ({ question, isLastQuestion, onAnswe
|
|
|
251
249
|
</Text>
|
|
252
250
|
</TouchableOpacity>
|
|
253
251
|
</Animated.View>
|
|
254
|
-
</
|
|
252
|
+
</View>
|
|
255
253
|
);
|
|
256
254
|
};
|
|
257
255
|
|
|
258
256
|
const styles = StyleSheet.create({
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
outer: {
|
|
258
|
+
flex: 1,
|
|
261
259
|
paddingHorizontal: 20,
|
|
260
|
+
paddingBottom: 16,
|
|
261
|
+
},
|
|
262
|
+
scroll: { flex: 1 },
|
|
263
|
+
scrollContent: {
|
|
262
264
|
paddingTop: 16,
|
|
263
|
-
paddingBottom:
|
|
264
|
-
flexGrow: 1,
|
|
265
|
+
paddingBottom: 8,
|
|
265
266
|
},
|
|
266
267
|
title: { fontSize: 18, fontWeight: '700', marginBottom: 6 },
|
|
267
268
|
subtitle: { fontSize: 14, opacity: 0.6, marginBottom: 16 },
|
|
@@ -283,7 +284,7 @@ const styles = StyleSheet.create({
|
|
|
283
284
|
labelsRow: {
|
|
284
285
|
flexDirection: 'row',
|
|
285
286
|
justifyContent: 'space-between',
|
|
286
|
-
marginBottom:
|
|
287
|
+
marginBottom: 16,
|
|
287
288
|
},
|
|
288
289
|
label: { fontSize: 12, opacity: 0.6 },
|
|
289
290
|
followUpCard: {
|
|
@@ -307,6 +308,6 @@ const styles = StyleSheet.create({
|
|
|
307
308
|
minHeight: 80,
|
|
308
309
|
},
|
|
309
310
|
errorText: { color: '#EF4444', fontSize: 13, marginBottom: 8 },
|
|
310
|
-
button: { paddingVertical: 14, alignItems: 'center', marginTop:
|
|
311
|
+
button: { paddingVertical: 14, alignItems: 'center', marginTop: 8 },
|
|
311
312
|
buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
|
|
312
313
|
});
|
|
@@ -27,14 +27,12 @@ function RatingIcon({
|
|
|
27
27
|
selected,
|
|
28
28
|
filled,
|
|
29
29
|
color,
|
|
30
|
-
scale,
|
|
31
30
|
}: {
|
|
32
31
|
style: string;
|
|
33
32
|
index: number;
|
|
34
33
|
selected: boolean;
|
|
35
34
|
filled: boolean;
|
|
36
35
|
color: string;
|
|
37
|
-
scale: number;
|
|
38
36
|
}) {
|
|
39
37
|
const activeColor = color || '#F59E0B';
|
|
40
38
|
const inactive = '#E5E7EB';
|
|
@@ -138,84 +136,83 @@ export const XeboRatingView: React.FC<Props> = ({ question, isLastQuestion, onAn
|
|
|
138
136
|
};
|
|
139
137
|
|
|
140
138
|
return (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
152
|
-
{question.subtitle}
|
|
139
|
+
// Outer view fills modal content area; button pinned at bottom outside ScrollView
|
|
140
|
+
<View style={styles.outer}>
|
|
141
|
+
<ScrollView
|
|
142
|
+
style={styles.scroll}
|
|
143
|
+
contentContainerStyle={styles.scrollContent}
|
|
144
|
+
showsVerticalScrollIndicator={false}
|
|
145
|
+
keyboardShouldPersistTaps="handled"
|
|
146
|
+
>
|
|
147
|
+
<Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
148
|
+
{question.title}
|
|
153
149
|
</Text>
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<View style={styles.iconsRow}>
|
|
158
|
-
{Array.from({ length: totalSteps }, (_, i) => {
|
|
159
|
-
const isFilled = isCumulative ? selected !== null && i <= selected : i === selected;
|
|
160
|
-
return (
|
|
161
|
-
<TouchableOpacity
|
|
162
|
-
key={i}
|
|
163
|
-
onPress={() => handleSelect(i)}
|
|
164
|
-
style={styles.iconButton}
|
|
165
|
-
activeOpacity={0.7}
|
|
166
|
-
>
|
|
167
|
-
<RatingIcon
|
|
168
|
-
style={ratingStyle}
|
|
169
|
-
index={i}
|
|
170
|
-
selected={i === selected}
|
|
171
|
-
filled={isFilled}
|
|
172
|
-
color={ratingColor}
|
|
173
|
-
scale={1}
|
|
174
|
-
/>
|
|
175
|
-
</TouchableOpacity>
|
|
176
|
-
);
|
|
177
|
-
})}
|
|
178
|
-
</View>
|
|
179
|
-
|
|
180
|
-
{/* Column labels */}
|
|
181
|
-
{question.showColLabels && question.columns && (
|
|
182
|
-
<View style={styles.colLabels}>
|
|
183
|
-
{question.columns.map((col, i) => (
|
|
184
|
-
<Text
|
|
185
|
-
key={col.id}
|
|
186
|
-
style={[styles.colLabel, { color: theme.textColor, fontFamily: theme.fontFamily }]}
|
|
187
|
-
numberOfLines={1}
|
|
188
|
-
>
|
|
189
|
-
{col.text}
|
|
190
|
-
</Text>
|
|
191
|
-
))}
|
|
192
|
-
</View>
|
|
193
|
-
)}
|
|
194
|
-
|
|
195
|
-
{/* Follow-up after rating */}
|
|
196
|
-
{selected !== null && !!question.followUpQuestion && (
|
|
197
|
-
<View style={[styles.followUpCard, { borderRadius: theme.cornerRadius }]}>
|
|
198
|
-
<Text style={[styles.followUpTitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
199
|
-
{question.followUpQuestion}
|
|
150
|
+
{!!question.subtitle && (
|
|
151
|
+
<Text style={[styles.subtitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
152
|
+
{question.subtitle}
|
|
200
153
|
</Text>
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
{/* Rating icons row */}
|
|
157
|
+
<View style={styles.iconsRow}>
|
|
158
|
+
{Array.from({ length: totalSteps }, (_, i) => {
|
|
159
|
+
const isFilled = isCumulative ? selected !== null && i <= selected : i === selected;
|
|
160
|
+
return (
|
|
161
|
+
<TouchableOpacity
|
|
162
|
+
key={i}
|
|
163
|
+
onPress={() => handleSelect(i)}
|
|
164
|
+
style={styles.iconButton}
|
|
165
|
+
activeOpacity={0.7}
|
|
166
|
+
>
|
|
167
|
+
<RatingIcon
|
|
168
|
+
style={ratingStyle}
|
|
169
|
+
index={i}
|
|
170
|
+
selected={i === selected}
|
|
171
|
+
filled={isFilled}
|
|
172
|
+
color={ratingColor}
|
|
173
|
+
/>
|
|
174
|
+
</TouchableOpacity>
|
|
175
|
+
);
|
|
176
|
+
})}
|
|
211
177
|
</View>
|
|
212
|
-
)}
|
|
213
178
|
|
|
214
|
-
|
|
179
|
+
{/* Column labels */}
|
|
180
|
+
{question.showColLabels && question.columns && (
|
|
181
|
+
<View style={styles.colLabels}>
|
|
182
|
+
{question.columns.map((col) => (
|
|
183
|
+
<Text
|
|
184
|
+
key={col.id}
|
|
185
|
+
style={[styles.colLabel, { color: theme.textColor, fontFamily: theme.fontFamily }]}
|
|
186
|
+
numberOfLines={1}
|
|
187
|
+
>
|
|
188
|
+
{col.text}
|
|
189
|
+
</Text>
|
|
190
|
+
))}
|
|
191
|
+
</View>
|
|
192
|
+
)}
|
|
215
193
|
|
|
216
|
-
|
|
217
|
-
|
|
194
|
+
{/* Follow-up after rating */}
|
|
195
|
+
{selected !== null && !!question.followUpQuestion && (
|
|
196
|
+
<View style={[styles.followUpCard, { borderRadius: theme.cornerRadius }]}>
|
|
197
|
+
<Text style={[styles.followUpTitle, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
|
|
198
|
+
{question.followUpQuestion}
|
|
199
|
+
</Text>
|
|
200
|
+
<TextInput
|
|
201
|
+
style={[styles.followUpInput, { borderColor: '#E5E7EB', color: theme.textColor, borderRadius: theme.cornerRadius }]}
|
|
202
|
+
placeholder="Your comment..."
|
|
203
|
+
placeholderTextColor="#9CA3AF"
|
|
204
|
+
value={followUpText}
|
|
205
|
+
onChangeText={setFollowUpText}
|
|
206
|
+
multiline
|
|
207
|
+
numberOfLines={3}
|
|
208
|
+
textAlignVertical="top"
|
|
209
|
+
/>
|
|
210
|
+
</View>
|
|
211
|
+
)}
|
|
212
|
+
</ScrollView>
|
|
218
213
|
|
|
214
|
+
{/* Error + button always visible at bottom, outside scroll */}
|
|
215
|
+
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
|
219
216
|
<Animated.View style={{ transform: [{ translateX: shakeAnim }] }}>
|
|
220
217
|
<TouchableOpacity
|
|
221
218
|
style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius, opacity: selected === null ? 0.4 : 1 }]}
|
|
@@ -228,13 +225,18 @@ export const XeboRatingView: React.FC<Props> = ({ question, isLastQuestion, onAn
|
|
|
228
225
|
</Text>
|
|
229
226
|
</TouchableOpacity>
|
|
230
227
|
</Animated.View>
|
|
231
|
-
</
|
|
228
|
+
</View>
|
|
232
229
|
);
|
|
233
230
|
};
|
|
234
231
|
|
|
235
232
|
const styles = StyleSheet.create({
|
|
233
|
+
outer: {
|
|
234
|
+
flex: 1,
|
|
235
|
+
paddingHorizontal: 20,
|
|
236
|
+
paddingBottom: 16,
|
|
237
|
+
},
|
|
236
238
|
scroll: { flex: 1 },
|
|
237
|
-
|
|
239
|
+
scrollContent: { paddingTop: 16, paddingBottom: 8 },
|
|
238
240
|
title: { fontSize: 18, fontWeight: '700', marginBottom: 6 },
|
|
239
241
|
subtitle: { fontSize: 14, opacity: 0.6, marginBottom: 16 },
|
|
240
242
|
iconsRow: { flexDirection: 'row', marginBottom: 8, flexWrap: 'nowrap', justifyContent: 'space-between' },
|
|
@@ -259,6 +261,6 @@ const styles = StyleSheet.create({
|
|
|
259
261
|
minHeight: 80,
|
|
260
262
|
},
|
|
261
263
|
errorText: { color: '#EF4444', fontSize: 13, marginBottom: 8 },
|
|
262
|
-
button: { paddingVertical: 14, alignItems: 'center', marginTop:
|
|
264
|
+
button: { paddingVertical: 14, alignItems: 'center', marginTop: 8 },
|
|
263
265
|
buttonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '600' },
|
|
264
266
|
});
|
|
@@ -33,7 +33,7 @@ const SCREEN_HEIGHT = Dimensions.get('screen').height;
|
|
|
33
33
|
|
|
34
34
|
function computeSheetHeight(question: XeboQuestion | null, screen: ModalScreen): number {
|
|
35
35
|
if (screen === 'thankYou') return SCREEN_HEIGHT * 0.32;
|
|
36
|
-
if (screen === 'intro') return SCREEN_HEIGHT * 0.
|
|
36
|
+
if (screen === 'intro') return SCREEN_HEIGHT * 0.42;
|
|
37
37
|
if (!question) return SCREEN_HEIGHT * 0.48;
|
|
38
38
|
switch (question.type) {
|
|
39
39
|
case XeboQuestionType.multiNps:
|
|
@@ -7,11 +7,8 @@ import {
|
|
|
7
7
|
StyleSheet,
|
|
8
8
|
ScrollView,
|
|
9
9
|
Animated,
|
|
10
|
-
KeyboardAvoidingView,
|
|
11
|
-
Platform,
|
|
12
10
|
} from 'react-native';
|
|
13
|
-
import { XeboQuestion, XeboAnswer } from '../models/XeboModels';
|
|
14
|
-
import { XeboQuestionType } from '../models/XeboModels';
|
|
11
|
+
import { XeboQuestion, XeboAnswer, XeboQuestionType } from '../models/XeboModels';
|
|
15
12
|
import { getTheme } from '../theme/XeboTheme';
|
|
16
13
|
|
|
17
14
|
interface Props {
|
|
@@ -62,13 +59,12 @@ export const XeboTextBoxView: React.FC<Props> = ({ question, isLastQuestion, onA
|
|
|
62
59
|
};
|
|
63
60
|
|
|
64
61
|
return (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
>
|
|
62
|
+
// Outer view fills modal content area; button pinned at bottom outside ScrollView.
|
|
63
|
+
// react-native-modal's avoidKeyboard=true moves the whole modal up — no KAV needed.
|
|
64
|
+
<View style={styles.outer}>
|
|
69
65
|
<ScrollView
|
|
70
66
|
style={styles.scroll}
|
|
71
|
-
contentContainerStyle={styles.
|
|
67
|
+
contentContainerStyle={styles.scrollContent}
|
|
72
68
|
keyboardShouldPersistTaps="handled"
|
|
73
69
|
showsVerticalScrollIndicator={false}
|
|
74
70
|
>
|
|
@@ -109,37 +105,37 @@ export const XeboTextBoxView: React.FC<Props> = ({ question, isLastQuestion, onA
|
|
|
109
105
|
/>
|
|
110
106
|
</View>
|
|
111
107
|
))}
|
|
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
108
|
</ScrollView>
|
|
127
|
-
|
|
109
|
+
|
|
110
|
+
{/* Error + button always visible at bottom, outside scroll */}
|
|
111
|
+
{!!error && <Text style={styles.errorText}>{error}</Text>}
|
|
112
|
+
<Animated.View style={{ transform: [{ translateX: shakeAnim }] }}>
|
|
113
|
+
<TouchableOpacity
|
|
114
|
+
style={[styles.button, { backgroundColor: theme.primaryColor, borderRadius: theme.cornerRadius }]}
|
|
115
|
+
onPress={handleSubmit}
|
|
116
|
+
activeOpacity={0.8}
|
|
117
|
+
>
|
|
118
|
+
<Text style={[styles.buttonText, { fontFamily: theme.fontFamily }]}>
|
|
119
|
+
{isLastQuestion ? 'Submit' : 'Next'}
|
|
120
|
+
</Text>
|
|
121
|
+
</TouchableOpacity>
|
|
122
|
+
</Animated.View>
|
|
123
|
+
</View>
|
|
128
124
|
);
|
|
129
125
|
};
|
|
130
126
|
|
|
131
127
|
const styles = StyleSheet.create({
|
|
132
|
-
|
|
128
|
+
outer: {
|
|
133
129
|
flex: 1,
|
|
130
|
+
paddingHorizontal: 20,
|
|
131
|
+
paddingBottom: 16,
|
|
134
132
|
},
|
|
135
133
|
scroll: {
|
|
136
134
|
flex: 1,
|
|
137
135
|
},
|
|
138
|
-
|
|
139
|
-
paddingHorizontal: 20,
|
|
136
|
+
scrollContent: {
|
|
140
137
|
paddingTop: 16,
|
|
141
|
-
paddingBottom:
|
|
142
|
-
flexGrow: 1,
|
|
138
|
+
paddingBottom: 8,
|
|
143
139
|
},
|
|
144
140
|
title: {
|
|
145
141
|
fontSize: 18,
|