/** * Modal for submitting feedback. Matches design: category, feedback textarea, optional email. * Sends feedback to Sentry (breadcrumbs, custom message, optional captureFeedback). */ import { __ } from '@wordpress/i18n'; import { useState, useCallback } from '@wordpress/element'; import { Modal, Flex, FlexItem, Button, TextareaControl, TextControl, SelectControl, Spinner, Notice, Card, CardBody, } from '@wordpress/components'; import * as Sentry from '@sentry/react'; import { addBreadcrumb, captureMessage, captureException, setTag, } from '../../shared/sentry'; const FEEDBACK_CATEGORIES = [ { value: '', label: __('Select a category', 'parcel2go-shipping'), disabled: true, }, { value: 'features', label: __('Feature Request', 'parcel2go-shipping') }, { value: 'performance', label: __('Performance', 'parcel2go-shipping') }, { value: 'ux', label: __('User Experience', 'parcel2go-shipping') }, { value: 'documentation', label: __('Documentation', 'parcel2go-shipping') }, { value: 'bug', label: __('Bug Report', 'parcel2go-shipping') }, { value: 'general', label: __('General Feedback', 'parcel2go-shipping') }, ]; const MAX_FEEDBACK_LENGTH = 1000; const MIN_FEEDBACK_LENGTH = 10; function getAppVersion(): string { const config = (window as any).parcel2goShipping; return (config?.version as string) || 'unknown'; } interface FieldErrors { category?: string; message?: string; } function validateForm( category: string, message: string ): { valid: boolean; errors: string[]; fieldErrors: FieldErrors } { const errors: string[] = []; const fieldErrors: FieldErrors = {}; if (!category) { errors.push('category'); fieldErrors.category = __('Please select a category.', 'parcel2go-shipping'); } const trimmed = message.trim(); if (!trimmed) { errors.push('message'); fieldErrors.message = __('Please enter your feedback.', 'parcel2go-shipping'); } else if (trimmed.length < MIN_FEEDBACK_LENGTH) { errors.push('messageLength'); fieldErrors.message = __('Please enter at least 10 characters.', 'parcel2go-shipping'); } else if (message.length > MAX_FEEDBACK_LENGTH) { errors.push('messageLength'); fieldErrors.message = __('Feedback must be 1000 characters or less.', 'parcel2go-shipping'); } return { valid: errors.length === 0, errors, fieldErrors, }; } export interface FeedbackModalProps { isOpen: boolean; onRequestClose: () => void; } export default function FeedbackModal({ isOpen, onRequestClose, }: FeedbackModalProps) { const [category, setCategory] = useState(''); const [feedback, setFeedback] = useState(''); const [email, setEmail] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [showThankYou, setShowThankYou] = useState(false); const [validationErrors, setValidationErrors] = useState(null); const handleSubmitFeedback = useCallback(async () => { const message = feedback.trim(); addBreadcrumb('submitted', { category, hasEmail: !!email.trim(), messageLength: message.length, }, 'feedback_form'); const { valid, errors, fieldErrors } = validateForm(category, feedback); if (!valid) { setValidationErrors(fieldErrors); addBreadcrumb('error', { errors }, 'feedback_form'); return; } setValidationErrors(null); setIsSubmitting(true); try { const feedbackPayload = { category, message, userEmail: email.trim() || null, timestamp: new Date().toISOString(), userAgent: navigator.userAgent, url: window.location.href, appVersion: getAppVersion(), }; addBreadcrumb('User submitted feedback', feedbackPayload, 'user.feedback'); setTag('feedback_category', category); setTag('has_email', email.trim() ? 'true' : 'false'); captureMessage('User Feedback Submitted', 'info', { ...feedbackPayload, feedback_category: category, has_email: !!email.trim(), }); if (typeof Sentry.captureFeedback === 'function') { Sentry.captureFeedback({ message, name: email.trim() || 'Anonymous', email: email.trim() || undefined, url: window.location.href, associatedEventId: Sentry.lastEventId?.() ?? undefined, }); } addBreadcrumb('submitted', { success: true }, 'feedback_form'); setShowThankYou(true); } catch (error) { captureException( error instanceof Error ? error : new Error(String(error)), { tags: { component: 'feedback_modal', action: 'submit_feedback', }, } ); addBreadcrumb('error', { error: error instanceof Error ? error.message : 'Unknown error', }, 'feedback_form'); } finally { setIsSubmitting(false); } }, [category, feedback, email]); const handleClose = useCallback(() => { setCategory(''); setFeedback(''); setEmail(''); setShowThankYou(false); setValidationErrors(null); onRequestClose(); }, [onRequestClose]); const clearValidationOnChange = useCallback(() => { if (validationErrors) setValidationErrors(null); }, [validationErrors]); const canSubmit = category && feedback.trim().length >= MIN_FEEDBACK_LENGTH && feedback.length <= MAX_FEEDBACK_LENGTH; if (!isOpen) return null; if (showThankYou) { return (

{'🎉 '} {__('Thank you for your feedback!', 'parcel2go-shipping')}

{__( 'Your feedback has been sent successfully. We appreciate you taking the time to help us improve the app.', 'parcel2go-shipping' )}

{__( 'Our team will review your feedback and use it to make the app even better.', 'parcel2go-shipping' )}

); } return ( {validationErrors && (validationErrors.category || validationErrors.message) && ( {validationErrors.category && {validationErrors.category}} {validationErrors.message && {validationErrors.message}} )}

{__( 'Help us improve by sharing your experience. Your feedback is valuable to us!', 'parcel2go-shipping' )}

{ setCategory(val); clearValidationOnChange(); }} disabled={isSubmitting} help={validationErrors?.category} /> { setFeedback(val); clearValidationOnChange(); }} placeholder={__( "Please share your thoughts, suggestions, or report any issues you've encountered...", 'parcel2go-shipping' )} help={ validationErrors?.message || __( 'Be as specific as possible to help us understand your feedback better.', 'parcel2go-shipping' ) } rows={5} disabled={isSubmitting} />
MAX_FEEDBACK_LENGTH ? '#d63638' : '#646970', marginTop: 4, }} > {feedback.length}/{MAX_FEEDBACK_LENGTH}
); }