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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-my-survey-sdk",
3
- "version": "2.1.9",
3
+ "version": "2.2.0",
4
4
  "description": "Xebo survey collection SDK for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -44,7 +44,7 @@ const styles = StyleSheet.create({
44
44
  flex: 1,
45
45
  paddingHorizontal: 20,
46
46
  paddingTop: 16,
47
- paddingBottom: 24,
47
+ paddingBottom: 8,
48
48
  },
49
49
  scroll: {
50
50
  flex: 1,
@@ -18,9 +18,9 @@ interface Props {
18
18
  }
19
19
 
20
20
  // NPS zone colors — matches iOS SDK defaults
21
- const DETRACTOR_COLOR = '#F24236'; // UIColor(red:0.95,green:0.26,blue:0.21)
22
- const PASSIVE_COLOR = '#FFC208'; // UIColor(red:1.00,green:0.76,blue:0.03)
23
- const PROMOTER_COLOR = '#2ECC70'; // UIColor(red:0.18,green:0.80,blue:0.44)
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 (no separate questionId — embed text in NPS answer)
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
- <ScrollView
126
- style={styles.scroll}
127
- contentContainerStyle={styles.container}
128
- showsVerticalScrollIndicator={false}
129
- keyboardShouldPersistTaps="handled"
130
- >
131
- <Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
132
- {question.title}
133
- </Text>
134
- {!!question.subtitle && (
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
- {/* 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
- })}
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
- {/* 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
- ) : (
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
- </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>
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
- </ScrollView>
252
+ </View>
255
253
  );
256
254
  };
257
255
 
258
256
  const styles = StyleSheet.create({
259
- scroll: { flex: 1 },
260
- container: {
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: 24,
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: 20,
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: 4 },
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
- <ScrollView
142
- style={styles.scroll}
143
- contentContainerStyle={styles.container}
144
- showsVerticalScrollIndicator={false}
145
- keyboardShouldPersistTaps="handled"
146
- >
147
- <Text style={[styles.title, { color: theme.textColor, fontFamily: theme.fontFamily }]}>
148
- {question.title}
149
- </Text>
150
- {!!question.subtitle && (
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
- {/* 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
- 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
- <TextInput
202
- style={[styles.followUpInput, { borderColor: '#E5E7EB', color: theme.textColor, borderRadius: theme.cornerRadius }]}
203
- placeholder="Your comment..."
204
- placeholderTextColor="#9CA3AF"
205
- value={followUpText}
206
- onChangeText={setFollowUpText}
207
- multiline
208
- numberOfLines={3}
209
- textAlignVertical="top"
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
- {!!error && <Text style={styles.errorText}>{error}</Text>}
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
- {/* Spacer pushes button to bottom */}
217
- <View style={{ flex: 1 }} />
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
- </ScrollView>
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
- container: { paddingHorizontal: 20, paddingTop: 16, paddingBottom: 24, flexGrow: 1 },
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: 4 },
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.52;
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
- <KeyboardAvoidingView
66
- style={styles.kvContainer}
67
- behavior={Platform.OS === 'ios' ? 'padding' : undefined}
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.container}
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
- </KeyboardAvoidingView>
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
- kvContainer: {
128
+ outer: {
133
129
  flex: 1,
130
+ paddingHorizontal: 20,
131
+ paddingBottom: 16,
134
132
  },
135
133
  scroll: {
136
134
  flex: 1,
137
135
  },
138
- container: {
139
- paddingHorizontal: 20,
136
+ scrollContent: {
140
137
  paddingTop: 16,
141
- paddingBottom: 24,
142
- flexGrow: 1,
138
+ paddingBottom: 8,
143
139
  },
144
140
  title: {
145
141
  fontSize: 18,