@originallyus/feedback-rn-sdk 4.0.0-beta.6 → 4.0.0-beta.7

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.

Potentially problematic release.


This version of @originallyus/feedback-rn-sdk might be problematic. Click here for more details.

@@ -1,248 +0,0 @@
1
- import {memo, useMemo, useRef} from 'react'
2
- import {View, Text, StyleSheet, TouchableOpacity, Animated, type TextStyle, type ViewStyle} from 'react-native'
3
- import * as Haptics from 'expo-haptics'
4
- import * as f from '@utils/common'
5
- import {COLOR_STAR_NORMAL, COLOR_STAR_SELECTED, ERROR_COLOR, TEXT_DARK} from '@utils/constants'
6
- import StarIcon from '@/assets/StarIcon'
7
- import ErrorIcon from '@/assets/ErrorIcon'
8
-
9
- let pressRatingTimestamp: number | null = null
10
-
11
- interface ButtonStarProps {
12
- disabled: boolean
13
- onRate: () => void
14
- active?: boolean
15
- normalStarColor: string
16
- selectedStarColor: string
17
- }
18
-
19
- /**
20
- * Props for the star rating component.
21
- *
22
- * Renders a title, question and a row of tappable stars with optional error text.
23
- */
24
- export interface RatingProps {
25
- /**
26
- * Main title shown above the question.
27
- *
28
- * @default 'Help us get better'
29
- */
30
- title?: string
31
- /**
32
- * Question or subtitle shown above the stars.
33
- *
34
- * @default 'How was your experience today?'
35
- */
36
- question?: string
37
- /**
38
- * Current star value (1..`rating_max`).
39
- */
40
- value: number
41
- /**
42
- * Called when a star is selected.
43
- *
44
- * @param rating Selected value.
45
- */
46
- onRate: (rating: number) => void
47
- /**
48
- * Minimum star value.
49
- *
50
- * @default 1
51
- */
52
- rating_min?: number
53
- /**
54
- * Maximum star value.
55
- *
56
- * @default 5
57
- */
58
- rating_max?: number
59
- /**
60
- * When true, disables all star presses.
61
- *
62
- * @default false
63
- */
64
- disabled?: boolean
65
- /**
66
- * Error message; when present, shown below the stars.
67
- */
68
- error?: string
69
- /**
70
- * Color for unselected stars.
71
- *
72
- * @default '#D0D0D0'
73
- */
74
- normalStarColor?: string
75
- /**
76
- * Color for selected stars.
77
- *
78
- * @default '#F7C926'
79
- */
80
- selectedStarColor?: string
81
- /**
82
- * Style for the title text.
83
- */
84
- titleStyle?: TextStyle
85
- /**
86
- * Style for the question text.
87
- */
88
- questionStyle?: TextStyle
89
- /**
90
- * Style for the error text.
91
- */
92
- errorStyle?: TextStyle
93
- /**
94
- * Style for the outer wrapper.
95
- */
96
- style?: ViewStyle
97
- }
98
-
99
- const ButtonStar = memo((props: ButtonStarProps) => {
100
- const {disabled, onRate, active = false, normalStarColor, selectedStarColor} = props
101
-
102
- const springAnimated = useRef(new Animated.Value(0)).current
103
-
104
- const onPressStar = () => {
105
- const nowTimestamp = Date.now()
106
- if (pressRatingTimestamp && nowTimestamp - pressRatingTimestamp < 200) {
107
- return
108
- }
109
- pressRatingTimestamp = nowTimestamp
110
-
111
- Animated.sequence([
112
- Animated.timing(springAnimated, {
113
- toValue: 1,
114
- duration: 80,
115
- useNativeDriver: true,
116
- }),
117
- Animated.timing(springAnimated, {
118
- toValue: 0,
119
- duration: 80,
120
- useNativeDriver: true,
121
- }),
122
- ]).start()
123
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
124
- onRate()
125
- }
126
-
127
- const scaleStar = useMemo(
128
- () =>
129
- springAnimated.interpolate({
130
- inputRange: [0, 1],
131
- outputRange: [1, 1.5],
132
- }),
133
- [springAnimated],
134
- )
135
-
136
- const starColor = active ? selectedStarColor : normalStarColor
137
- const size = f.isTablet ? 42 : 34
138
- const marginH = f.isTablet ? 12 : 8
139
-
140
- const starWrapStyle = useMemo(
141
- () => [
142
- styles.starWrap,
143
- {
144
- transform: [{scale: scaleStar}],
145
- marginHorizontal: marginH,
146
- },
147
- ],
148
- [scaleStar, marginH],
149
- )
150
-
151
- return (
152
- <Animated.View style={starWrapStyle}>
153
- <TouchableOpacity disabled={disabled} onPress={onPressStar} activeOpacity={0.8}>
154
- <StarIcon size={size} color={starColor} />
155
- </TouchableOpacity>
156
- </Animated.View>
157
- )
158
- })
159
-
160
- function Rating({
161
- title = 'Help us get better',
162
- question = 'How was your experience today?',
163
- value,
164
- onRate,
165
- rating_min = 1,
166
- rating_max = 5,
167
- disabled = false,
168
- error,
169
- normalStarColor = COLOR_STAR_NORMAL,
170
- selectedStarColor = COLOR_STAR_SELECTED,
171
- titleStyle,
172
- questionStyle,
173
- errorStyle,
174
- style,
175
- }: RatingProps) {
176
- const hasError = useMemo(() => error != null && error !== '', [error])
177
-
178
- const stars = useMemo(() => {
179
- const listStar = []
180
- for (let i = rating_min; i <= rating_max; i++) {
181
- listStar.push(
182
- <ButtonStar
183
- key={`ButtonStar-${i}`}
184
- normalStarColor={normalStarColor}
185
- selectedStarColor={selectedStarColor}
186
- disabled={disabled || value === i}
187
- onRate={() => onRate(i)}
188
- active={i <= value}
189
- />,
190
- )
191
- }
192
- return listStar
193
- }, [rating_min, rating_max, disabled, value, normalStarColor, selectedStarColor, onRate])
194
-
195
- const titleCombinedStyle = useMemo(() => [styles.title, {color: TEXT_DARK}, titleStyle], [titleStyle])
196
- const questionCombinedStyle = useMemo(() => [styles.question, {color: TEXT_DARK}, questionStyle], [questionStyle])
197
- const errorTextStyle = useMemo(() => [styles.error, {color: ERROR_COLOR}, errorStyle], [errorStyle])
198
-
199
- return (
200
- <View style={[styles.wrapper, style]}>
201
- {title != null && title !== '' && <Text style={titleCombinedStyle}>{title}</Text>}
202
- {question != null && question !== '' && <Text style={questionCombinedStyle}>{question}</Text>}
203
- <View style={styles.starsWrap}>{stars}</View>
204
- {hasError && (
205
- <View style={styles.errorWrap}>
206
- <ErrorIcon size={20} color={ERROR_COLOR} />
207
- <Text style={errorTextStyle}>{error}</Text>
208
- </View>
209
- )}
210
- </View>
211
- )
212
- }
213
-
214
- export default Rating
215
-
216
- const styles = StyleSheet.create({
217
- wrapper: {
218
- width: '100%',
219
- },
220
- title: {
221
- fontSize: 18,
222
- fontWeight: '700',
223
- marginBottom: 8,
224
- },
225
- question: {
226
- fontSize: 16,
227
- marginBottom: 16,
228
- },
229
- starsWrap: {
230
- flexDirection: 'row',
231
- justifyContent: 'center',
232
- alignItems: 'center',
233
- width: '100%',
234
- },
235
- errorWrap: {
236
- flexDirection: 'row',
237
- alignItems: 'center',
238
- marginTop: 8,
239
- gap: 6,
240
- },
241
- error: {
242
- fontSize: 14,
243
- lineHeight: 20,
244
- flex: 1,
245
- fontWeight: '400',
246
- },
247
- starWrap: {},
248
- })
@@ -1,421 +0,0 @@
1
- import ErrorIcon from '@/assets/ErrorIcon'
2
- import * as f from '@utils/common'
3
- import * as Haptics from 'expo-haptics'
4
- import {BG_DEFAULT, BG_SELECTED, BORDER_DEFAULT, BORDER_SELECTED, ERROR_COLOR, TEXT_DARK} from '@utils/constants'
5
- import {useEffect, useMemo, useRef} from 'react'
6
- import {Animated, StyleSheet, Text, TouchableOpacity, View, type TextStyle} from 'react-native'
7
-
8
- const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity)
9
-
10
- const DEFAULT_SELECTED_COLOR = BG_SELECTED
11
- const DEFAULT_SELECTED_BORDER_COLOR = BORDER_SELECTED
12
-
13
- const DEFAULT_ERROR_COLOR = ERROR_COLOR
14
-
15
- /**
16
- * Preset sizes for `RatingNumber` buttons.
17
- *
18
- * - `'small'` – compact height, small radius.
19
- * - `'medium'` – default size.
20
- * - `'large'` – larger height and radius.
21
- */
22
- export type RatingNumberSize = 'small' | 'medium' | 'large'
23
-
24
- const SIZE_PRESETS: Record<RatingNumberSize, {height: number; borderRadius: number; borderWidth: number; gap: number}> =
25
- {
26
- small: {height: 44, borderRadius: 6, borderWidth: 1, gap: 3},
27
- medium: {height: 48, borderRadius: 8, borderWidth: 1, gap: 4},
28
- large: {height: 60, borderRadius: 10, borderWidth: 1, gap: 5},
29
- }
30
-
31
- /**
32
- * Props for the numeric rating scale component.
33
- *
34
- * Renders a row of numbered buttons from `rating_min` to `rating_max`
35
- * with optional left/right scale labels and error text.
36
- */
37
- export interface RatingNumberProps {
38
- /**
39
- * Text color for the left and right scale labels.
40
- */
41
- scale_label_color: string
42
- /**
43
- * Optional text for the left label (e.g. "Strongly disagree").
44
- *
45
- * @default 'Strongly disagree'
46
- */
47
- scale_label_left?: string
48
- /**
49
- * Optional text for the right label (e.g. "Strongly agree").
50
- *
51
- * @default 'Strongly agree'
52
- */
53
- scale_label_right?: string
54
- /**
55
- * Font family for the scale labels.
56
- */
57
- scale_label_font?: string
58
- /**
59
- * Font size for the scale labels.
60
- */
61
- scale_label_fontsize?: number
62
- /**
63
- * Called when a number is pressed.
64
- *
65
- * @param rating Selected value.
66
- */
67
- handlePressRating: (rating: number) => void
68
- /**
69
- * Currently selected value.
70
- */
71
- numStar: number
72
- /**
73
- * Minimum value of the scale (inclusive).
74
- */
75
- rating_min: number
76
- /**
77
- * Maximum value of the scale (inclusive).
78
- */
79
- rating_max: number
80
- /**
81
- * App width, used for layout spacing.
82
- */
83
- appWidth: number
84
- /**
85
- * Preset for height, radius and gap between numbers.
86
- *
87
- * @default 'medium'
88
- */
89
- size?: RatingNumberSize
90
- /**
91
- * When true, all numbers from min up to and including the selected value are shown as active.
92
- * E.g. selecting 6 highlights 0–6. When false, only the selected number is active.
93
- *
94
- * @default true
95
- */
96
- activeRange?: boolean
97
- /**
98
- * Whether to remove gaps between buttons (segmented control style).
99
- *
100
- * @default true
101
- */
102
- compact?: boolean
103
- /**
104
- * Error message shown below the component.
105
- */
106
- error?: string
107
- /**
108
- * Border color when an error is present.
109
- *
110
- * @default '#D40C74'
111
- */
112
- errorBorderColor?: string
113
- /**
114
- * Font family for the number labels.
115
- */
116
- rating_number_font?: string
117
- /**
118
- * Font size for the number labels.
119
- */
120
- rating_number_fontsize?: number
121
- /**
122
- * Background color when a number is not selected.
123
- */
124
- rating_number_bg_color?: string
125
- /**
126
- * Border color when a number is not selected.
127
- */
128
- rating_number_border_color?: string
129
- /**
130
- * Background color when a number is selected.
131
- */
132
- rating_number_selected_bg_color?: string
133
- /**
134
- * Border color when a number is selected.
135
- */
136
- rating_number_selected_border_color?: string
137
- /**
138
- * Text color when a number is selected.
139
- */
140
- rating_number_selected_text_color?: string
141
- /**
142
- * Text color when a number is not selected.
143
- */
144
- rating_number_text_color?: string
145
- /**
146
- * Style override for the error text.
147
- */
148
- errorStyle?: TextStyle
149
-
150
- /**
151
- * Label for the component.
152
- */
153
- label?: string
154
- }
155
-
156
- interface ButtonNumberProps extends RatingNumberProps {
157
- handlePressRating: () => void
158
- number: number
159
- active: boolean
160
- isStart: boolean
161
- isEnd: boolean
162
- /** Last button in the active range (needs right border in compact mode). */
163
- isLastActive?: boolean
164
- /** First inactive button after active block (hide left border to avoid double line). */
165
- isFirstInactive?: boolean
166
- hasError?: boolean
167
- errorBorderColor?: string
168
- }
169
-
170
- function RatingNumber(props: RatingNumberProps) {
171
- const {
172
- scale_label_color,
173
- scale_label_left,
174
- scale_label_right,
175
- scale_label_font,
176
- scale_label_fontsize,
177
- handlePressRating,
178
- numStar,
179
- rating_min,
180
- rating_max,
181
- appWidth,
182
- label,
183
- } = props
184
-
185
- const activeRange = props.activeRange !== false
186
- const compact = props.compact !== false
187
- const sizePreset = props.size ?? 'medium'
188
- const preset = useMemo(() => SIZE_PRESETS[sizePreset], [sizePreset])
189
- const gap = useMemo(() => (compact ? 0 : preset.gap), [compact, preset])
190
- const hasError = useMemo(() => props.error != null && props.error !== '', [props.error])
191
- const errorBorderColor = useMemo(() => props.errorBorderColor ?? DEFAULT_ERROR_COLOR, [props.errorBorderColor])
192
-
193
- const renderRating = () => {
194
- const listStar = []
195
- for (let i = rating_min; i <= rating_max; i++) {
196
- const active = activeRange ? i <= numStar : numStar === i
197
- const nextActive = activeRange && i < rating_max && i + 1 <= numStar
198
- const prevActive = activeRange && i > rating_min && i - 1 <= numStar
199
- listStar.push(
200
- <ButtonNumber
201
- key={`rating-number-${i}`}
202
- {...props}
203
- hasError={hasError}
204
- errorBorderColor={errorBorderColor}
205
- handlePressRating={() => {
206
- Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
207
- handlePressRating(i)
208
- }}
209
- number={i}
210
- active={active}
211
- isStart={i === rating_min}
212
- isEnd={i === rating_max}
213
- isLastActive={active && !nextActive}
214
- isFirstInactive={!active && prevActive}
215
- />,
216
- )
217
- }
218
- return listStar
219
- }
220
-
221
- const rowStyle = useMemo(() => ({width: '100%' as const}), [])
222
-
223
- const containerStyle = useMemo(
224
- () => [styles.ratingContainer, !compact && {gap}, rowStyle],
225
- [compact, gap, rowStyle],
226
- )
227
-
228
- const labelContainerStyle = useMemo(
229
- () => [styles.ratingLabelContainer, rowStyle, {marginTop: f.w_p(appWidth, 12)}],
230
- [rowStyle, appWidth],
231
- )
232
-
233
- const leftLabelStyle = useMemo(
234
- () => ({
235
- color: scale_label_color,
236
- fontSize: scale_label_fontsize,
237
- fontFamily: scale_label_font,
238
- }),
239
- [scale_label_color, scale_label_fontsize, scale_label_font],
240
- )
241
-
242
- const rightLabelStyle = useMemo(
243
- () => ({
244
- color: scale_label_color,
245
- fontSize: scale_label_fontsize,
246
- fontFamily: scale_label_font,
247
- }),
248
- [scale_label_color, scale_label_fontsize, scale_label_font],
249
- )
250
-
251
- const errorTextStyle = useMemo(
252
- () => [styles.error, {color: errorBorderColor}, props.errorStyle],
253
- [errorBorderColor, props.errorStyle],
254
- )
255
-
256
- return (
257
- <View style={styles.wrapper}>
258
- <Text style={styles.question}>{label}</Text>
259
- <View style={containerStyle}>{renderRating()}</View>
260
- <View style={labelContainerStyle}>
261
- <Text style={leftLabelStyle}>{scale_label_left || 'Strongly disagree'}</Text>
262
- <Text style={rightLabelStyle}>{scale_label_right || 'Strongly agree'}</Text>
263
- </View>
264
- {hasError && (
265
- <View style={styles.errorWrap}>
266
- <ErrorIcon size={20} color={ERROR_COLOR} />
267
- <Text style={errorTextStyle}>{props.error}</Text>
268
- </View>
269
- )}
270
- </View>
271
- )
272
- }
273
-
274
- function ButtonNumber({
275
- handlePressRating,
276
- active,
277
- isStart,
278
- isEnd,
279
- isLastActive = false,
280
- isFirstInactive = false,
281
- number,
282
- size = 'medium',
283
- compact = true,
284
- hasError = false,
285
- errorBorderColor,
286
- rating_number_font,
287
- rating_number_fontsize,
288
- rating_number_bg_color,
289
- rating_number_border_color,
290
- rating_number_selected_bg_color,
291
- rating_number_selected_border_color,
292
- rating_number_selected_text_color,
293
- rating_number_text_color,
294
- }: ButtonNumberProps) {
295
- const springAnimated = useRef(new Animated.Value(0)).current
296
- const preset = useMemo(() => SIZE_PRESETS[size], [size])
297
-
298
- const h = preset.height
299
- const radius = preset.borderRadius
300
- const borderW = preset.borderWidth
301
- const selectedColor = rating_number_selected_bg_color ?? DEFAULT_SELECTED_COLOR
302
- const selectedBorderColor = rating_number_selected_border_color ?? DEFAULT_SELECTED_BORDER_COLOR
303
- const selectedTextColor = rating_number_selected_text_color ?? TEXT_DARK
304
- const inactiveBorderColor = useMemo(
305
- () => (hasError && errorBorderColor ? errorBorderColor : rating_number_border_color ?? BORDER_DEFAULT),
306
- [hasError, errorBorderColor, rating_number_border_color],
307
- )
308
-
309
- useEffect(() => {
310
- if (active) {
311
- Animated.timing(springAnimated, {
312
- toValue: 1,
313
- useNativeDriver: false,
314
- }).start()
315
- } else {
316
- Animated.timing(springAnimated, {
317
- toValue: 0,
318
- useNativeDriver: false,
319
- }).start()
320
- }
321
- }, [active, springAnimated])
322
-
323
- const backgroundColorAni = useMemo(
324
- () =>
325
- springAnimated.interpolate({
326
- inputRange: [0, 1],
327
- outputRange: [rating_number_bg_color ?? BG_DEFAULT, selectedColor],
328
- }),
329
- [springAnimated, rating_number_bg_color, selectedColor],
330
- )
331
-
332
- const compactStyle = useMemo(
333
- () =>
334
- compact
335
- ? {
336
- borderRightWidth: isEnd || isLastActive ? borderW : 0,
337
- borderLeftWidth: isFirstInactive ? 0 : undefined,
338
- borderTopLeftRadius: isStart ? radius : 0,
339
- borderBottomLeftRadius: isStart ? radius : 0,
340
- borderTopRightRadius: isEnd ? radius : 0,
341
- borderBottomRightRadius: isEnd ? radius : 0,
342
- }
343
- : {
344
- borderRadius: radius,
345
- },
346
- [compact, isEnd, isLastActive, isFirstInactive, borderW, isStart, radius],
347
- )
348
-
349
- const buttonStyle = useMemo(
350
- () => [
351
- styles.buttonNumberContainer,
352
- {
353
- flex: 1,
354
- height: h,
355
- borderWidth: borderW,
356
- borderColor: active ? selectedBorderColor : inactiveBorderColor,
357
- backgroundColor: backgroundColorAni,
358
- opacity: 1,
359
- ...compactStyle,
360
- },
361
- ],
362
- [h, borderW, active, selectedBorderColor, inactiveBorderColor, backgroundColorAni, compactStyle],
363
- )
364
-
365
- const labelStyle = useMemo(
366
- () => ({
367
- color: active ? selectedTextColor : rating_number_text_color,
368
- fontSize: rating_number_fontsize,
369
- fontFamily: rating_number_font,
370
- }),
371
- [active, selectedTextColor, rating_number_text_color, rating_number_fontsize, rating_number_font],
372
- )
373
-
374
- return (
375
- <AnimatedTouchable style={buttonStyle} onPress={handlePressRating} activeOpacity={1}>
376
- <Text style={labelStyle}>{number}</Text>
377
- </AnimatedTouchable>
378
- )
379
- }
380
-
381
- export default RatingNumber
382
-
383
- const styles = StyleSheet.create({
384
- wrapper: {
385
- flex: 1,
386
- width: '100%',
387
- },
388
- question: {
389
- fontSize: 16,
390
- fontWeight: '600',
391
- color: '#14181C',
392
- marginBottom: 8,
393
- },
394
- ratingContainer: {
395
- flexDirection: 'row',
396
- alignItems: 'center',
397
- width: '100%',
398
- },
399
- ratingLabelContainer: {
400
- flexDirection: 'row',
401
- justifyContent: 'space-between',
402
- alignItems: 'center',
403
- width: '100%',
404
- },
405
- errorWrap: {
406
- flexDirection: 'row',
407
- alignItems: 'center',
408
- marginTop: 8,
409
- gap: 6,
410
- },
411
- error: {
412
- fontSize: 14,
413
- lineHeight: 20,
414
- flex: 1,
415
- fontWeight: '400',
416
- },
417
- buttonNumberContainer: {
418
- justifyContent: 'center',
419
- alignItems: 'center',
420
- },
421
- })