UNPKG

@oxyhq/services

Version:

OxyHQ Expo/React Native SDK — UI components, screens, and native features

669 lines (668 loc) 23.6 kB
"use strict"; import React, { useState, useRef, useCallback, useMemo } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator, Platform, KeyboardAvoidingView, ScrollView, Animated, StatusBar } from 'react-native'; import { useThemeColors } from "../styles/index.js"; import { normalizeTheme } from "../utils/themeUtils.js"; import { Ionicons } from '@expo/vector-icons'; import { toast } from '../../lib/sonner'; import { packageInfo } from '@oxyhq/core'; import { GroupedSection } from "../components/index.js"; import { useI18n } from "../hooks/useI18n.js"; import { useOxy } from "../context/OxyContext.js"; import { FormInput, ProgressIndicator, useFeedbackForm, createFeedbackStyles, FEEDBACK_TYPES, PRIORITY_LEVELS, CATEGORIES } from "../components/feedback/index.js"; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; const FeedbackScreen = ({ navigate, goBack, onClose, theme }) => { const { user, oxyServices } = useOxy(); const normalizedTheme = normalizeTheme(theme); const colors = useThemeColors(normalizedTheme); const { t } = useI18n(); const { feedbackData, feedbackState, setFeedbackState, updateField, resetForm } = useFeedbackForm(); const [currentStep, setCurrentStep] = useState(0); const [errorMessage, setErrorMessage] = useState(''); const fadeAnim = useRef(new Animated.Value(1)).current; const slideAnim = useRef(new Animated.Value(0)).current; const styles = useMemo(() => createFeedbackStyles(colors), [colors]); const animateTransition = useCallback(nextStep => { Animated.timing(fadeAnim, { toValue: 0, duration: 250, useNativeDriver: Platform.OS !== 'web' }).start(() => { setCurrentStep(nextStep); slideAnim.setValue(-100); Animated.parallel([Animated.timing(fadeAnim, { toValue: 1, duration: 250, useNativeDriver: Platform.OS !== 'web' }), Animated.timing(slideAnim, { toValue: 0, duration: 300, useNativeDriver: Platform.OS !== 'web' })]).start(); }); }, [fadeAnim, slideAnim]); const nextStep = useCallback(() => { if (currentStep < 3) { animateTransition(currentStep + 1); } }, [currentStep, animateTransition]); const prevStep = useCallback(() => { if (currentStep > 0) { animateTransition(currentStep - 1); } }, [currentStep, animateTransition]); const isTypeStepValid = useCallback(() => { return feedbackData.type && feedbackData.category; }, [feedbackData.type, feedbackData.category]); const isDetailsStepValid = useCallback(() => { return feedbackData.title.trim() && feedbackData.description.trim(); }, [feedbackData.title, feedbackData.description]); const isContactStepValid = useCallback(() => { return feedbackData.contactEmail.trim() || user?.email; }, [feedbackData.contactEmail, user?.email]); const handleSubmitFeedback = useCallback(async () => { if (!isTypeStepValid() || !isDetailsStepValid() || !isContactStepValid()) { toast.error(t('feedback.toasts.fillRequired') || 'Please fill in all required fields'); return; } try { setFeedbackState({ status: 'submitting', message: '' }); setErrorMessage(''); const feedbackPayload = { type: feedbackData.type, title: feedbackData.title, description: feedbackData.description, priority: feedbackData.priority, category: feedbackData.category, contactEmail: feedbackData.contactEmail || user?.email, systemInfo: feedbackData.systemInfo ? { platform: Platform.OS, version: Platform.Version?.toString() || 'Unknown', appVersion: packageInfo.version, userId: user?.id, username: user?.username, timestamp: new Date().toISOString() } : undefined }; await oxyServices.submitFeedback(feedbackPayload); setFeedbackState({ status: 'success', message: t('feedback.toasts.submitSuccess') || 'Feedback submitted successfully!' }); toast.success(t('feedback.toasts.thanks') || 'Thank you for your feedback!'); setTimeout(() => { resetForm(); setCurrentStep(0); }, 3000); } catch (error) { setFeedbackState({ status: 'error', message: error.message || t('feedback.toasts.submitFailed') || 'Failed to submit feedback' }); toast.error(error.message || t('feedback.toasts.submitFailed') || 'Failed to submit feedback'); } }, [feedbackData, user, isTypeStepValid, isDetailsStepValid, isContactStepValid, resetForm, setFeedbackState, t]); const feedbackTypeItems = useMemo(() => FEEDBACK_TYPES.map(type => ({ id: type.id, icon: type.icon, iconColor: type.color, title: type.label, subtitle: type.description, onPress: () => { updateField('type', type.id); updateField('category', ''); }, selected: feedbackData.type === type.id, showChevron: false, multiRow: true, dense: true })), [feedbackData.type, updateField]); const categoryItems = useMemo(() => feedbackData.type ? (CATEGORIES[feedbackData.type] || []).map(cat => ({ id: cat, icon: feedbackData.category === cat ? 'check-circle' : 'ellipse-outline', iconColor: feedbackData.category === cat ? colors.primary : colors.secondaryText, title: cat, onPress: () => updateField('category', cat), selected: feedbackData.category === cat, showChevron: false, dense: true })) : [], [feedbackData.type, feedbackData.category, colors.primary, colors.secondaryText, updateField]); const priorityItems = useMemo(() => PRIORITY_LEVELS.map(p => ({ id: p.id, icon: p.icon, iconColor: p.color, title: p.label, onPress: () => updateField('priority', p.id), selected: feedbackData.priority === p.id, showChevron: false, dense: true })), [feedbackData.priority, updateField]); const renderTypeStep = () => /*#__PURE__*/_jsxs(Animated.View, { style: [styles.stepContainer, { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }], children: [/*#__PURE__*/_jsxs(View, { style: styles.modernHeader, children: [/*#__PURE__*/_jsx(Text, { style: [styles.stepTitle, { color: colors.text }], children: t('feedback.type.title') || 'What type of feedback?' }), /*#__PURE__*/_jsx(Text, { style: [styles.modernSubtitle, { color: colors.secondaryText }], children: t('feedback.type.subtitle') || 'Choose the category that best describes your feedback' })] }), /*#__PURE__*/_jsx(View, { style: styles.fullBleed, children: /*#__PURE__*/_jsx(GroupedSection, { items: feedbackTypeItems }) }), feedbackData.type && /*#__PURE__*/_jsxs(View, { style: styles.categoryContainer, children: [/*#__PURE__*/_jsx(Text, { style: [styles.modernLabel, { color: colors.secondaryText, marginBottom: 8 }], children: t('feedback.category.label') || 'Category' }), /*#__PURE__*/_jsx(View, { style: styles.fullBleed, children: /*#__PURE__*/_jsx(GroupedSection, { items: categoryItems }) })] }), /*#__PURE__*/_jsxs(View, { style: styles.navigationButtons, children: [/*#__PURE__*/_jsxs(TouchableOpacity, { style: [styles.navButton, { backgroundColor: 'transparent', borderColor: colors.border, borderRadius: 35 }], onPress: goBack, accessibilityRole: "button", accessibilityLabel: "Go back", children: [/*#__PURE__*/_jsx(Ionicons, { name: "arrow-back", size: 16, color: colors.text }), /*#__PURE__*/_jsx(Text, { style: [styles.navButtonText, { color: colors.text }], children: t('common.actions.back') || 'Back' })] }), /*#__PURE__*/_jsxs(TouchableOpacity, { style: [styles.navButton, { backgroundColor: colors.primary, borderColor: colors.primary, borderRadius: 35 }], onPress: nextStep, disabled: !isTypeStepValid(), accessibilityRole: "button", accessibilityLabel: "Continue to next step", children: [/*#__PURE__*/_jsx(Text, { style: [styles.navButtonText, { color: '#FFFFFF' }], children: t('common.actions.next') || 'Next' }), /*#__PURE__*/_jsx(Ionicons, { name: "arrow-forward", size: 16, color: "#FFFFFF" })] })] })] }); const renderDetailsStep = () => /*#__PURE__*/_jsxs(Animated.View, { style: [styles.stepContainer, { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }], children: [/*#__PURE__*/_jsxs(View, { style: styles.modernHeader, children: [/*#__PURE__*/_jsx(Text, { style: [styles.stepTitle, { color: colors.text }], children: t('feedback.details.title') || 'Tell us more' }), /*#__PURE__*/_jsx(Text, { style: [styles.modernSubtitle, { color: colors.secondaryText }], children: t('feedback.details.subtitle') || 'Provide details about your feedback' })] }), /*#__PURE__*/_jsx(FormInput, { icon: "create-outline", label: t('feedback.fields.title.label') || 'Title', value: feedbackData.title, onChangeText: text => { updateField('title', text); setErrorMessage(''); }, placeholder: t('feedback.fields.title.placeholder') || 'Brief summary of your feedback', testID: "feedback-title-input", colors: colors, styles: styles, accessibilityLabel: "Feedback title", accessibilityHint: "Enter a brief summary of your feedback" }), /*#__PURE__*/_jsx(FormInput, { icon: "document-text-outline", label: t('feedback.fields.description.label') || 'Description', value: feedbackData.description, onChangeText: text => { updateField('description', text); setErrorMessage(''); }, placeholder: t('feedback.fields.description.placeholder') || 'Please provide detailed information...', multiline: true, numberOfLines: 6, testID: "feedback-description-input", colors: colors, styles: styles, accessibilityLabel: "Feedback description", accessibilityHint: "Provide detailed information about your feedback" }), /*#__PURE__*/_jsxs(View, { style: { marginBottom: 24 }, children: [/*#__PURE__*/_jsx(Text, { style: [styles.modernLabel, { color: colors.secondaryText, marginBottom: 8 }], children: t('feedback.priority.label') || 'Priority Level' }), /*#__PURE__*/_jsx(View, { style: styles.fullBleed, children: /*#__PURE__*/_jsx(GroupedSection, { items: priorityItems }) })] }), /*#__PURE__*/_jsxs(View, { style: styles.navigationButtons, children: [/*#__PURE__*/_jsxs(TouchableOpacity, { style: [styles.navButton, styles.backButton, { borderColor: colors.border }], onPress: prevStep, accessibilityRole: "button", accessibilityLabel: "Go back", children: [/*#__PURE__*/_jsx(Ionicons, { name: "arrow-back", size: 16, color: colors.text }), /*#__PURE__*/_jsx(Text, { style: [styles.navButtonText, { color: colors.text }], children: t('common.actions.back') || 'Back' })] }), /*#__PURE__*/_jsxs(TouchableOpacity, { style: [styles.navButton, styles.nextButton, { backgroundColor: colors.primary, borderColor: colors.primary }], onPress: nextStep, disabled: !isDetailsStepValid(), accessibilityRole: "button", accessibilityLabel: "Continue to next step", children: [/*#__PURE__*/_jsx(Text, { style: [styles.navButtonText, { color: '#FFFFFF' }], children: t('common.actions.next') || 'Next' }), /*#__PURE__*/_jsx(Ionicons, { name: "arrow-forward", size: 16, color: "#FFFFFF" })] })] })] }); const renderContactStep = () => /*#__PURE__*/_jsxs(Animated.View, { style: [styles.stepContainer, { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }], children: [/*#__PURE__*/_jsxs(View, { style: styles.modernHeader, children: [/*#__PURE__*/_jsx(Text, { style: [styles.stepTitle, { color: colors.text }], children: t('feedback.contact.title') || 'Contact Information' }), /*#__PURE__*/_jsx(Text, { style: [styles.modernSubtitle, { color: colors.secondaryText }], children: t('feedback.contact.subtitle') || 'Help us get back to you' })] }), /*#__PURE__*/_jsx(FormInput, { icon: "mail-outline", label: t('feedback.fields.email.label') || 'Email Address', value: feedbackData.contactEmail, onChangeText: text => { updateField('contactEmail', text); setErrorMessage(''); }, placeholder: user?.email || t('feedback.fields.email.placeholder') || 'Enter your email address', testID: "feedback-email-input", colors: colors, styles: styles, accessibilityLabel: "Email address", accessibilityHint: "Enter your email so we can respond" }), /*#__PURE__*/_jsxs(View, { style: styles.checkboxContainer, children: [/*#__PURE__*/_jsx(TouchableOpacity, { style: [styles.checkbox, { borderColor: feedbackData.systemInfo ? colors.primary : colors.border, backgroundColor: feedbackData.systemInfo ? colors.primary : 'transparent' }], onPress: () => updateField('systemInfo', !feedbackData.systemInfo), accessibilityRole: "checkbox", accessibilityState: { checked: feedbackData.systemInfo }, accessibilityLabel: "Include system information", children: feedbackData.systemInfo && /*#__PURE__*/_jsx(Ionicons, { name: "checkmark", size: 16, color: "#FFFFFF" }) }), /*#__PURE__*/_jsx(Text, { style: [styles.checkboxText, { color: colors.text }], children: t('feedback.contact.includeSystemInfo') || 'Include system information to help us better understand your issue' })] }), /*#__PURE__*/_jsxs(View, { style: styles.navigationButtons, children: [/*#__PURE__*/_jsxs(TouchableOpacity, { style: [styles.navButton, styles.backButton, { borderColor: colors.border }], onPress: prevStep, accessibilityRole: "button", accessibilityLabel: "Go back", children: [/*#__PURE__*/_jsx(Ionicons, { name: "arrow-back", size: 16, color: colors.text }), /*#__PURE__*/_jsx(Text, { style: [styles.navButtonText, { color: colors.text }], children: "Back" })] }), /*#__PURE__*/_jsxs(TouchableOpacity, { style: [styles.navButton, styles.nextButton, { backgroundColor: colors.primary, borderColor: colors.primary }], onPress: nextStep, disabled: !isContactStepValid(), accessibilityRole: "button", accessibilityLabel: "Continue to summary", children: [/*#__PURE__*/_jsx(Text, { style: [styles.navButtonText, { color: '#FFFFFF' }], children: "Next" }), /*#__PURE__*/_jsx(Ionicons, { name: "arrow-forward", size: 16, color: "#FFFFFF" })] })] })] }); const renderSummaryStep = () => /*#__PURE__*/_jsxs(Animated.View, { style: [styles.stepContainer, { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }], children: [/*#__PURE__*/_jsxs(View, { style: styles.modernHeader, children: [/*#__PURE__*/_jsx(Text, { style: [styles.stepTitle, { color: colors.text }], children: t('feedback.summary.title') || 'Summary' }), /*#__PURE__*/_jsx(Text, { style: [styles.modernSubtitle, { color: colors.secondaryText }], children: t('feedback.summary.subtitle') || 'Please review your feedback before submitting' })] }), /*#__PURE__*/_jsxs(View, { style: styles.summaryContainer, children: [/*#__PURE__*/_jsxs(View, { style: styles.summaryRow, children: [/*#__PURE__*/_jsx(Text, { style: [styles.summaryLabel, { color: colors.secondaryText }], children: "Type:" }), /*#__PURE__*/_jsx(Text, { style: [styles.summaryValue, { color: colors.text }], children: FEEDBACK_TYPES.find(t => t.id === feedbackData.type)?.label })] }), /*#__PURE__*/_jsxs(View, { style: styles.summaryRow, children: [/*#__PURE__*/_jsx(Text, { style: [styles.summaryLabel, { color: colors.secondaryText }], children: "Category:" }), /*#__PURE__*/_jsx(Text, { style: [styles.summaryValue, { color: colors.text }], children: feedbackData.category })] }), /*#__PURE__*/_jsxs(View, { style: styles.summaryRow, children: [/*#__PURE__*/_jsx(Text, { style: [styles.summaryLabel, { color: colors.secondaryText }], children: "Priority:" }), /*#__PURE__*/_jsx(Text, { style: [styles.summaryValue, { color: colors.text }], children: PRIORITY_LEVELS.find(p => p.id === feedbackData.priority)?.label })] }), /*#__PURE__*/_jsxs(View, { style: styles.summaryRow, children: [/*#__PURE__*/_jsx(Text, { style: [styles.summaryLabel, { color: colors.secondaryText }], children: "Title:" }), /*#__PURE__*/_jsx(Text, { style: [styles.summaryValue, { color: colors.text }], children: feedbackData.title })] }), /*#__PURE__*/_jsxs(View, { style: styles.summaryRow, children: [/*#__PURE__*/_jsx(Text, { style: [styles.summaryLabel, { color: colors.secondaryText }], children: "Contact:" }), /*#__PURE__*/_jsx(Text, { style: [styles.summaryValue, { color: colors.text }], children: feedbackData.contactEmail || user?.email })] })] }), /*#__PURE__*/_jsx(TouchableOpacity, { style: [styles.button, { backgroundColor: colors.primary }], onPress: handleSubmitFeedback, disabled: feedbackState.status === 'submitting', testID: "submit-feedback-button", accessibilityRole: "button", accessibilityLabel: "Submit feedback", children: feedbackState.status === 'submitting' ? /*#__PURE__*/_jsx(ActivityIndicator, { color: "#FFFFFF", size: "small" }) : /*#__PURE__*/_jsxs(_Fragment, { children: [/*#__PURE__*/_jsx(Text, { style: styles.buttonText, children: t('feedback.actions.submit') || 'Submit Feedback' }), /*#__PURE__*/_jsx(Ionicons, { name: "send", size: 20, color: "#FFFFFF" })] }) }), /*#__PURE__*/_jsx(View, { style: styles.navigationButtons, children: /*#__PURE__*/_jsxs(TouchableOpacity, { style: [styles.navButton, { backgroundColor: 'transparent', borderColor: colors.border, borderRadius: 35 }], onPress: prevStep, accessibilityRole: "button", accessibilityLabel: "Go back", children: [/*#__PURE__*/_jsx(Ionicons, { name: "arrow-back", size: 16, color: colors.text }), /*#__PURE__*/_jsx(Text, { style: [styles.navButtonText, { color: colors.text }], children: t('common.actions.back') || 'Back' })] }) })] }); const renderSuccessStep = () => /*#__PURE__*/_jsx(Animated.View, { style: [styles.stepContainer, { opacity: fadeAnim, transform: [{ translateX: slideAnim }] }], children: /*#__PURE__*/_jsxs(View, { style: styles.successContainer, children: [/*#__PURE__*/_jsx(View, { style: [styles.successIcon, { backgroundColor: (colors.success || '#34C759') + '20', padding: 24, borderRadius: 50 }], children: /*#__PURE__*/_jsx(Ionicons, { name: "checkmark-circle", size: 48, color: colors.success || '#34C759' }) }), /*#__PURE__*/_jsx(Text, { style: [styles.successTitle, { color: colors.text }], children: t('feedback.success.thanks') || 'Thank You!' }), /*#__PURE__*/_jsx(Text, { style: [styles.successMessage, { color: colors.secondaryText }], children: t('feedback.success.message') || "Your feedback has been submitted successfully. We'll review it and get back to you soon." }), /*#__PURE__*/_jsx(TouchableOpacity, { style: [styles.button, { backgroundColor: colors.primary }], onPress: () => { resetForm(); setCurrentStep(0); }, accessibilityRole: "button", accessibilityLabel: "Submit another feedback", children: /*#__PURE__*/_jsx(Text, { style: styles.buttonText, children: t('feedback.actions.submitAnother') || 'Submit Another' }) })] }) }); const renderCurrentStep = () => { if (feedbackState.status === 'success') return renderSuccessStep(); switch (currentStep) { case 0: return renderTypeStep(); case 1: return renderDetailsStep(); case 2: return renderContactStep(); case 3: return renderSummaryStep(); default: return renderTypeStep(); } }; return /*#__PURE__*/_jsxs(KeyboardAvoidingView, { style: [styles.container, { backgroundColor: theme === 'dark' ? colors.background : '#F7F9FC' }], behavior: Platform.OS === 'ios' ? 'padding' : 'height', children: [/*#__PURE__*/_jsx(StatusBar, { barStyle: theme === 'dark' ? 'light-content' : 'dark-content', backgroundColor: theme === 'dark' ? colors.background : '#F7F9FC' }), /*#__PURE__*/_jsxs(ScrollView, { contentContainerStyle: [styles.scrollContent, { backgroundColor: 'transparent' }], showsVerticalScrollIndicator: false, keyboardShouldPersistTaps: "handled", children: [feedbackState.status !== 'success' && /*#__PURE__*/_jsx(ProgressIndicator, { currentStep: currentStep, totalSteps: 4, colors: colors, styles: styles }), renderCurrentStep()] })] }); }; export default FeedbackScreen; //# sourceMappingURL=FeedbackScreen.js.map