@originallyus/feedback-rn-sdk 4.0.0-beta.2 → 4.0.0-beta.3

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": "@originallyus/feedback-rn-sdk",
3
- "version": "4.0.0-beta.2",
3
+ "version": "4.0.0-beta.3",
4
4
  "description": "A cross-platform Feedback component for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/index.d.ts",
@@ -13,7 +13,6 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "files": [
16
- "src",
17
16
  "lib",
18
17
  "android",
19
18
  "ios",
@@ -1,296 +0,0 @@
1
- import React, {useState, useEffect, useCallback} from 'react'
2
- import {View, Text, StyleSheet, ActivityIndicator, type StyleProp, type ViewStyle} from 'react-native'
3
- import {feedbackService} from '@service/feedbackService'
4
- import type {SubmitSelectionData, SubmitFormData} from '@service/feedbackService'
5
- import type {InitOptions} from './utils/constants'
6
- import {PRIMARY_COLOR, TEXT_DARK, BORDER_DEFAULT} from './utils/constants'
7
- import YesNoButtons from './component/YesNoButtons'
8
- import Rating from './component/Rating'
9
- import MultiSelectButtons from './component/MultiSelectButtons'
10
- import Textarea from './component/Textarea'
11
- import ButtonSubmit from './component/ButtonSubmit'
12
-
13
- export interface AIAContentUsefulnessProps {
14
- slug: string
15
- options: InitOptions
16
- style?: StyleProp<ViewStyle>
17
- initialVisible?: boolean
18
- onSuccess?: (response: any) => void
19
- onError?: (error: Error) => void
20
- }
21
-
22
- type Step = 'PROMPT' | 'LOADING' | 'FORM' | 'SUCCESS'
23
-
24
- const AIAContentUsefulness: React.FC<AIAContentUsefulnessProps> = ({
25
- slug,
26
- options,
27
- style,
28
- initialVisible = false,
29
- onSuccess,
30
- onError,
31
- }) => {
32
- const [visible, setVisible] = useState(initialVisible)
33
- const [step, setStep] = useState<Step>('PROMPT')
34
- const [formData, setFormData] = useState<any>(null)
35
- const [loading, setLoading] = useState(true)
36
- const [submitting, setSubmitting] = useState(false)
37
- const [containerWidth, setContainerWidth] = useState(0)
38
-
39
- // Form values
40
- const [rating, setRating] = useState(0)
41
- const [selectedOptions, setSelectedOptions] = useState<string[]>([])
42
- const [freeText, setFreeText] = useState('')
43
-
44
- const loadForm = useCallback(() => {
45
- setLoading(true)
46
- feedbackService.requestForm({...options, formSlug: slug}, res => {
47
- setLoading(false)
48
- if (res && !res.error) {
49
- setFormData(res)
50
- } else {
51
- onError?.(new Error(res?.error || 'Failed to load form'))
52
- }
53
- })
54
- }, [slug, options, onError])
55
-
56
- useEffect(() => {
57
- if (visible) {
58
- loadForm()
59
- }
60
- }, [visible, loadForm])
61
-
62
- useEffect(() => {
63
- const unsubscribe = feedbackService.onShow((triggerSlug, showOptions) => {
64
- if (triggerSlug === slug && !showOptions?.forceModal) {
65
- setVisible(true)
66
- return true // Handled
67
- }
68
- return false
69
- })
70
- return unsubscribe
71
- }, [slug])
72
-
73
- const onPromptSelect = async (value: 'yes' | 'no') => {
74
- setStep('LOADING')
75
- try {
76
- const selectionData: SubmitSelectionData = {
77
- slug,
78
- debug: options.debug || false,
79
- event_tag: options.eventTag || '',
80
- metadata: options.metadata || {},
81
- rating: value === 'yes' ? 2 : 1,
82
- request_id: formData?.id || '',
83
- }
84
- const res = await feedbackService.submitSelection(selectionData, options)
85
- if (res && !res.error) {
86
- // Hide itself and trigger modal for the rest of the flow
87
- setVisible(false)
88
- feedbackService.emitShow(slug, {forceModal: true, initialData: res})
89
- } else {
90
- setStep('PROMPT')
91
- onError?.(new Error(res?.error || 'Failed to submit selection'))
92
- }
93
- } catch (err) {
94
- setStep('PROMPT')
95
- onError?.(err as Error)
96
- }
97
- }
98
-
99
- const onSubmitDetails = async () => {
100
- setSubmitting(true)
101
- try {
102
- const submitData: SubmitFormData = {
103
- slug: formData.slug || slug,
104
- debug: options.debug || false,
105
- event_tag: options.eventTag || '',
106
- metadata: options.metadata || {},
107
- request_id: formData.id || '',
108
- rating: rating,
109
- selected_options: selectedOptions.join(','),
110
- free_text: freeText,
111
- }
112
- const res = await feedbackService.submitForm(submitData, options)
113
- if (res && !res.error) {
114
- setStep('SUCCESS')
115
- onSuccess?.(res)
116
- } else {
117
- onError?.(new Error(res?.error || 'Failed to submit feedback'))
118
- }
119
- } catch (err) {
120
- onError?.(err as Error)
121
- } finally {
122
- setSubmitting(false)
123
- }
124
- }
125
-
126
- const getThemeColor = (key: string, fallback: string) => {
127
- return formData?.theme?.[key] || fallback
128
- }
129
-
130
- const textAlign = formData?.alignment === 'left' ? 'left' : 'center'
131
- const align: any = formData?.alignment === 'left' ? 'flex-start' : 'center'
132
-
133
- if (!visible) return null
134
-
135
- if (loading) {
136
- return (
137
- <View style={[styles.container, style, styles.center]}>
138
- <ActivityIndicator color={PRIMARY_COLOR} />
139
- </View>
140
- )
141
- }
142
-
143
- if (!formData) return null
144
-
145
- const renderPrompt = () => (
146
- <YesNoButtons
147
- question={formData.question}
148
- value={null}
149
- yesLabel={formData.positive_feedback_button || 'Yes'}
150
- noLabel={formData.negative_feedback_button || 'No'}
151
- onSelect={onPromptSelect}
152
- appWidth={containerWidth || 300}
153
- selectedVariant="secondary"
154
- questionStyle={{color: getThemeColor('question_color', TEXT_DARK), textAlign}}
155
- />
156
- )
157
-
158
- const renderForm = () => (
159
- <View style={{alignItems: align}}>
160
- <Text style={[styles.title, {color: getThemeColor('title_color', TEXT_DARK), textAlign}]}>
161
- {formData.title}
162
- </Text>
163
- {formData.question && (
164
- <Text style={[styles.question, {color: getThemeColor('question_color', TEXT_DARK), textAlign}]}>
165
- {formData.question}
166
- </Text>
167
- )}
168
-
169
- {formData.show_rating_star === 1 && (
170
- <View style={styles.mv16}>
171
- <Rating
172
- value={rating}
173
- onRate={setRating}
174
- selectedStarColor={getThemeColor('selected_star_color', PRIMARY_COLOR)}
175
- />
176
- </View>
177
- )}
178
-
179
- {formData.options && formData.options.length > 0 && (
180
- <MultiSelectButtons
181
- question={formData.instruction || ''}
182
- options={formData.options.map((o: any) => ({
183
- label: o.text || o.title || '',
184
- value: o.value || o.slug || '',
185
- }))}
186
- value={selectedOptions}
187
- onSelect={setSelectedOptions}
188
- appWidth={containerWidth || 300}
189
- style={styles.mv16}
190
- />
191
- )}
192
-
193
- {(formData.type === 'comment' || formData.show_comment) && (
194
- <Textarea
195
- value={freeText}
196
- onChangeText={setFreeText}
197
- placeholder={formData.comment_placeholder}
198
- label={formData.secondary_question || 'Please specify'}
199
- appWidth={containerWidth || 300}
200
- />
201
- )}
202
-
203
- <ButtonSubmit
204
- title={formData.submit_button_text || 'Submit'}
205
- onPress={onSubmitDetails}
206
- loading={submitting}
207
- appWidth={containerWidth || 300}
208
- backgroundColor={getThemeColor('button_bg_color', PRIMARY_COLOR)}
209
- />
210
- </View>
211
- )
212
-
213
- const renderSuccess = () => (
214
- <View style={styles.center}>
215
- <Text style={[styles.successTitle, {textAlign}]}>{formData.success_title || 'Submitted'}</Text>
216
- {formData.success_message && (
217
- <Text style={[styles.successMsg, {textAlign}]}>{formData.success_message}</Text>
218
- )}
219
- </View>
220
- )
221
-
222
- if (!visible) return null
223
-
224
- return (
225
- <View
226
- style={[styles.container, style]}
227
- onLayout={event => {
228
- const {width} = event.nativeEvent.layout
229
- setContainerWidth(width)
230
- }}
231
- >
232
- <View style={styles.expandedContent}>
233
- {loading ? (
234
- <View style={styles.center}>
235
- <ActivityIndicator color={PRIMARY_COLOR} />
236
- </View>
237
- ) : !formData ? null : (
238
- <>
239
- {step === 'PROMPT' && renderPrompt()}
240
- {step === 'LOADING' && (
241
- <View style={styles.center}>
242
- <ActivityIndicator color={PRIMARY_COLOR} />
243
- </View>
244
- )}
245
- {step === 'FORM' && renderForm()}
246
- {step === 'SUCCESS' && renderSuccess()}
247
- </>
248
- )}
249
- </View>
250
- </View>
251
- )
252
- }
253
-
254
- const styles = StyleSheet.create({
255
- container: {
256
- padding: 16,
257
- backgroundColor: '#FFF',
258
- borderRadius: 8,
259
- borderWidth: 1,
260
- borderColor: BORDER_DEFAULT,
261
- marginVertical: 10,
262
- },
263
- expandedContent: {
264
- marginTop: 0,
265
- },
266
- center: {
267
- padding: 20,
268
- alignItems: 'center',
269
- justifyContent: 'center',
270
- },
271
- title: {
272
- fontSize: 18,
273
- fontWeight: '700',
274
- marginBottom: 8,
275
- },
276
- question: {
277
- fontSize: 14,
278
- marginBottom: 16,
279
- },
280
- successTitle: {
281
- fontSize: 18,
282
- fontWeight: '700',
283
- color: PRIMARY_COLOR,
284
- },
285
- successMsg: {
286
- fontSize: 14,
287
- marginTop: 8,
288
- color: TEXT_DARK,
289
- },
290
- mv16: {
291
- marginVertical: 16,
292
- width: '100%',
293
- },
294
- })
295
-
296
- export default AIAContentUsefulness
@@ -1,354 +0,0 @@
1
- import {feedbackService} from '@service/feedbackService'
2
- import {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react'
3
- import {Linking, Modal, Platform, StyleSheet, TouchableOpacity, View} from 'react-native'
4
- import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
5
- import Animated, {Easing, FadeIn, FadeOut, LinearTransition, SlideInDown, SlideOutDown} from 'react-native-reanimated'
6
- import {SafeAreaProvider, useSafeAreaInsets} from 'react-native-safe-area-context'
7
- import AIAFeedbackForm from '@/AIAFeedbackForm'
8
- import AIAFeedbackSplash from '@/AIAFeedbackSplash'
9
- import {feedbackStyles} from '@/AIAFeedbackStyles'
10
- import AIAFeedbackSuccess from '@/AIAFeedbackSuccess'
11
- import * as f from '@/utils/common'
12
- import {type InitOptions, OUS_VARS} from '@/utils/constants'
13
- import CloseIcon from '@/assets/CloseIcon'
14
-
15
- export interface AIAFeedbackProps extends InitOptions {
16
- onClose?: () => void
17
- onSubmit?: (res: any) => void
18
- }
19
-
20
- export interface AIAFeedbackRef {
21
- show: (slug?: string, internalOptions?: {forceModal?: boolean; initialData?: any}) => void
22
- hide: () => void
23
- }
24
-
25
- const AIAFeedback = forwardRef<AIAFeedbackRef, AIAFeedbackProps>((props, ref) => {
26
- const {debug, metadata, onClose, onSubmit} = props
27
- const insets = useSafeAreaInsets()
28
- const [visible, setVisible] = useState(false)
29
- const [loading, setLoading] = useState(false)
30
- const [formData, setFormData] = useState<any>(null)
31
- const [submissionError, setSubmissionError] = useState<string | null>(null)
32
- const [isSubmitting, setIsSubmitting] = useState(false)
33
- const [didSubmit, setDidSubmit] = useState(false)
34
- const [submitRes, setSubmitRes] = useState<any>(null)
35
- const [showSplash, setShowSplash] = useState(false)
36
- const [step, setStep] = useState<'PROMPT' | 'FORM'>('FORM')
37
-
38
- // Form internal state
39
- const [rating, setRating] = useState<number>(0)
40
- const [freeText, setFreeText] = useState('')
41
- const [selectedOptions, setSelectedOptions] = useState<string[]>([])
42
-
43
- const isSplash = showSplash && !didSubmit
44
- const intrusive = !isSplash && !!formData?.instrusive_style
45
- const isNPSFlow = formData?.type === 'nps' || formData?.type === 'effort' || formData?.type === 'ces'
46
- const showSecondary = (!isNPSFlow && rating > 0 && formData?.show_comment) || (isNPSFlow && formData?.show_comment)
47
- const shouldCenter = intrusive && (didSubmit || !showSecondary)
48
-
49
- const resetForm = useCallback(() => {
50
- setRating(0)
51
- setFreeText('')
52
- setSelectedOptions([])
53
- setDidSubmit(false)
54
- setSubmitRes(null)
55
- setStep('FORM')
56
- setSubmissionError(null)
57
- }, [])
58
-
59
- useEffect(() => {
60
- // Initialize device UUID if not already set
61
- if (!OUS_VARS.uuid) {
62
- f.getUniqueID().then(id => {
63
- if (id) OUS_VARS.uuid = id
64
- })
65
- }
66
- }, [])
67
-
68
- const hide = useCallback(() => {
69
- setVisible(false)
70
- resetForm()
71
- setShowSplash(false)
72
- onClose?.()
73
- }, [onClose, resetForm])
74
-
75
- const show = useCallback(
76
- (slug?: string, internalOptions?: {forceModal?: boolean; initialData?: any}) => {
77
- const targetSlug = slug || props.formSlug
78
- // Check if any inline component wants to handle this slug
79
- // If forceModal is true, we bypass this check
80
- if (!internalOptions?.forceModal) {
81
- const handled = feedbackService.emitShow(targetSlug || '')
82
- if (handled) return
83
- }
84
-
85
- const options: InitOptions = {
86
- ...props,
87
- formSlug: targetSlug,
88
- }
89
-
90
- setLoading(true)
91
- setFormData(null)
92
- setSubmissionError(null)
93
-
94
- if (internalOptions?.initialData) {
95
- const res = internalOptions.initialData
96
- if (debug) console.log('Feedback SDK: Showing with initial data', res)
97
- setLoading(false)
98
- setFormData(res)
99
- setStep('FORM')
100
- if (res.splash_screen_enabled === 1) {
101
- setShowSplash(true)
102
- }
103
- setVisible(true)
104
- return
105
- }
106
-
107
- if (debug) console.log('Feedback SDK: Requesting form', targetSlug)
108
- feedbackService.requestForm(options, (res: any) => {
109
- if (debug) console.log('Feedback SDK: Form response', res)
110
- setLoading(false)
111
- if (res && res.type) {
112
- if (res.type === 'external_link' && res.button_url) {
113
- Linking.openURL(res.button_url)
114
- setVisible(false)
115
- return
116
- }
117
- if (res.type === 'content_usefulness') {
118
- setStep('PROMPT')
119
- } else {
120
- setStep('FORM')
121
- }
122
- setFormData(res)
123
- if (res.splash_screen_enabled === 1) {
124
- setShowSplash(true)
125
- }
126
- setVisible(true)
127
- } else {
128
- console.warn('Feedback SDK: Failed to load form or invalid response', res)
129
- setVisible(false)
130
- }
131
- })
132
- },
133
- [props, debug],
134
- )
135
- useEffect(() => {
136
- const unsubscribe = feedbackService.onShow((s, opts) => {
137
- if (opts?.forceModal) {
138
- show(s, {forceModal: true, initialData: opts.initialData})
139
- return true
140
- }
141
- return false
142
- })
143
- return unsubscribe
144
- }, [show])
145
-
146
- useImperativeHandle(ref, () => ({
147
- show,
148
- hide,
149
- }))
150
-
151
- const handlePromptSelect = useCallback(
152
- async (value: 'yes' | 'no') => {
153
- setLoading(true)
154
- try {
155
- const selectionData = {
156
- slug: formData.slug,
157
- debug: !!debug,
158
- event_tag: formData.event_tag || '',
159
- metadata: metadata || {},
160
- rating: value === 'yes' ? 2 : 1,
161
- request_id: formData.id || '',
162
- }
163
- const res = await feedbackService.submitSelection(selectionData, props)
164
- setLoading(false)
165
- if (res && !res.error) {
166
- setFormData(res)
167
- if (res.type === 'success' || !res.type) {
168
- setSubmitRes(res)
169
- setDidSubmit(true)
170
- } else {
171
- setStep('FORM')
172
- if (value === 'yes') setRating(5)
173
- else setRating(1)
174
- }
175
- }
176
- } catch (error) {
177
- console.error('Feedback SDK: Prompt select failed', error)
178
- setLoading(false)
179
- }
180
- },
181
- [formData, debug, metadata, props],
182
- )
183
-
184
- const handleSubmit = useCallback(async () => {
185
- if (isSubmitting) return
186
- setIsSubmitting(true)
187
-
188
- const data = {
189
- slug: formData.slug,
190
- debug: !!debug,
191
- event_tag: formData.event_tag || '',
192
- metadata: metadata || {},
193
- request_id: formData.id,
194
- rating: rating,
195
- selected_options: selectedOptions.join(','),
196
- free_text: freeText,
197
- }
198
-
199
- try {
200
- setSubmissionError(null)
201
- const res = await feedbackService.submitForm(data, props)
202
- if (formData.type === 'external' && formData.url) {
203
- Linking.openURL(formData.url)
204
- }
205
- setSubmitRes(res)
206
- setDidSubmit(true)
207
- onSubmit?.(res)
208
- } catch (error: any) {
209
- console.log('Feedback SDK: Submit failed', error)
210
- setSubmissionError(error?.message || 'Submit failed. Please try again.')
211
- } finally {
212
- setIsSubmitting(false)
213
- }
214
- }, [formData, debug, metadata, rating, selectedOptions, freeText, isSubmitting, props, onSubmit])
215
-
216
- const getThemeColor = (key: string, fallback: string) => {
217
- return formData?.theme?.[key] || fallback
218
- }
219
-
220
- const isTabletNonIntrusive = f.isTablet && !intrusive && !isSplash && !loading
221
-
222
- const feedbackContent = (
223
- <Animated.View
224
- entering={FadeIn.duration(300)}
225
- exiting={FadeOut.duration(200)}
226
- style={[
227
- styles.overlay,
228
- intrusive && !f.isTablet && styles.intrusiveOverlay,
229
- isSplash && styles.splashOverlay,
230
- loading && styles.splashOverlay,
231
- f.isTablet && styles.tabletOverlay,
232
- isTabletNonIntrusive && styles.nonBlockingOverlay,
233
- ]}
234
- pointerEvents={isTabletNonIntrusive ? 'box-none' : 'auto'}
235
- >
236
- <>
237
- {!intrusive && !isSplash && !isTabletNonIntrusive && (
238
- <TouchableOpacity style={styles.dismissArea} activeOpacity={1} onPress={hide} />
239
- )}
240
-
241
- <Animated.View
242
- entering={isSplash ? FadeIn.duration(400) : SlideInDown.duration(400)}
243
- exiting={isSplash ? FadeOut.duration(300) : SlideOutDown.duration(300)}
244
- layout={LinearTransition.duration(400).easing(Easing.out(Easing.cubic))}
245
- style={[
246
- styles.modalContent,
247
- isSplash && styles.splashContent,
248
- isSplash && f.isTablet && styles.splashTabletContent,
249
- intrusive && !f.isTablet && styles.intrusiveContent,
250
- f.isTablet && !isSplash && (intrusive ? styles.tabletIntrusive : styles.tabletNonIntrusive),
251
- ]}
252
- >
253
- {isSplash ? (
254
- <View style={styles.splashInner}>
255
- <AIAFeedbackSplash
256
- formData={formData}
257
- getThemeColor={getThemeColor}
258
- onSkip={hide}
259
- onProceed={() => setShowSplash(false)}
260
- />
261
- </View>
262
- ) : (
263
- <View style={[styles.safeArea, isTabletNonIntrusive && styles.autoHeightReset]}>
264
- <View style={styles.header}>
265
- {Platform.OS === 'ios' && insets.top > 0 && intrusive && (
266
- <View style={{height: insets.top}} />
267
- )}
268
-
269
- <View style={styles.headerRow}>
270
- {!intrusive && !f.isTablet && <View style={styles.handle} />}
271
- {(intrusive || f.isTablet) && (
272
- <TouchableOpacity
273
- onPress={hide}
274
- style={[styles.closeBtn, intrusive && styles.intrusiveCloseBtn]}
275
- >
276
- <CloseIcon />
277
- </TouchableOpacity>
278
- )}
279
- </View>
280
- </View>
281
- <KeyboardAwareScrollView
282
- bottomOffset={32}
283
- style={isTabletNonIntrusive ? styles.autoHeightReset : undefined}
284
- contentContainerStyle={[
285
- styles.scrollContent,
286
- isTabletNonIntrusive && styles.autoHeightReset,
287
- shouldCenter && styles.centeredScrollContent,
288
- ]}
289
- showsVerticalScrollIndicator={false}
290
- >
291
- <View
292
- key={didSubmit ? 'success' : 'form'}
293
- style={[
294
- styles.scrollContentInner,
295
- isTabletNonIntrusive && styles.autoHeightFlexReset,
296
- ]}
297
- >
298
- {didSubmit ? (
299
- <AIAFeedbackSuccess
300
- submitRes={submitRes}
301
- shouldCenter={shouldCenter}
302
- getThemeColor={getThemeColor}
303
- onClose={hide}
304
- autoHeight={isTabletNonIntrusive}
305
- />
306
- ) : (
307
- <AIAFeedbackForm
308
- formData={formData}
309
- rating={rating}
310
- freeText={freeText}
311
- selectedOptions={selectedOptions}
312
- step={step}
313
- isNPSFlow={isNPSFlow}
314
- showSecondary={showSecondary}
315
- onRate={setRating}
316
- onChangeFreeText={setFreeText}
317
- onChangeSelectedOptions={setSelectedOptions}
318
- getThemeColor={getThemeColor}
319
- onPromptSelect={handlePromptSelect}
320
- onSubmit={handleSubmit}
321
- isSubmitting={isSubmitting}
322
- submissionError={submissionError}
323
- autoHeight={isTabletNonIntrusive}
324
- />
325
- )}
326
- </View>
327
- </KeyboardAwareScrollView>
328
- </View>
329
- )}
330
- </Animated.View>
331
- </>
332
- </Animated.View>
333
- )
334
-
335
- if (isTabletNonIntrusive) {
336
- return (
337
- <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
338
- {visible && feedbackContent}
339
- </View>
340
- )
341
- }
342
-
343
- return (
344
- <SafeAreaProvider>
345
- <Modal visible={visible} transparent={true} animationType="none" onRequestClose={hide}>
346
- {feedbackContent}
347
- </Modal>
348
- </SafeAreaProvider>
349
- )
350
- })
351
-
352
- export default AIAFeedback
353
-
354
- const styles = feedbackStyles