@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 +1 -2
- package/src/AIAContentUsefulness.tsx +0 -296
- package/src/AIAFeedback.tsx +0 -354
- package/src/AIAFeedbackForm.tsx +0 -267
- package/src/AIAFeedbackSplash.tsx +0 -49
- package/src/AIAFeedbackStyles.ts +0 -311
- package/src/AIAFeedbackSuccess.tsx +0 -67
- package/src/assets/CheckIcon.tsx +0 -18
- package/src/assets/CloseIcon.tsx +0 -18
- package/src/assets/ErrorIcon.tsx +0 -18
- package/src/assets/PlusIcon.tsx +0 -18
- package/src/assets/StarIcon.tsx +0 -18
- package/src/component/Button.tsx +0 -68
- package/src/component/ButtonSubmit.tsx +0 -335
- package/src/component/Input.tsx +0 -288
- package/src/component/MultiSelectButtons.tsx +0 -272
- package/src/component/README.md +0 -215
- package/src/component/READMEVI.md +0 -192
- package/src/component/Rating.tsx +0 -248
- package/src/component/RatingNumber.tsx +0 -421
- package/src/component/Textarea.tsx +0 -282
- package/src/component/YesNoButtons.tsx +0 -236
- package/src/index.tsx +0 -33
- package/src/service/feedbackService.ts +0 -108
- package/src/utils/common.ts +0 -241
- package/src/utils/constants.ts +0 -60
- package/src/utils/index.ts +0 -167
- package/src/utils/networking.ts +0 -134
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@originallyus/feedback-rn-sdk",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
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
|
package/src/AIAFeedback.tsx
DELETED
|
@@ -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
|