import { Browser } from '@capacitor/browser' import { Android, Apple, Favorite, GitHub } from '@mui/icons-material' import { Box, Button, Chip, CircularProgress, Divider, FormControl, FormLabel, Stack, Textarea, Typography, } from '@mui/joy' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useLocation } from 'react-router-dom' import { useResponsiveModal } from '../../hooks/useResponsiveModal.js' import { useUserProfile } from '../../queries/UserQueries' import { FEEDBACK_CATEGORIES, isCloudInstance, markSentiment, requestStoreReview, SENTIMENTS, storeLinks, submitFeedback, SUBMIT_RESULT, } from '../../service/FeedbackService' import { Capacitor } from '@capacitor/core' const STEP = { SENTIMENT: 'sentiment', DETAILS: 'details', THANKS: 'thanks', WEB_REVIEW: 'webReview', GITHUB: 'github', } // Native webviews swallow target="_blank"; route through the system browser. const openUrl = async url => { if (Capacitor.isNativePlatform()) { await Browser.open({ url }) } else { window.open(url, '_blank', 'noopener,noreferrer') } } const SENTIMENT_OPTIONS = [ { value: SENTIMENTS.LOVE, emoji: '😍' }, { value: SENTIMENTS.OKAY, emoji: '🙂' }, { value: SENTIMENTS.ISSUES, emoji: '😕' }, ] /** * Sentiment-first feedback flow. "Love it" routes to the native store review * dialog (or star links on web); anything else collects structured feedback * and never asks for a review. */ const FeedbackModal = ({ open, onClose, onDismiss }) => { const { t } = useTranslation() const { ResponsiveModal } = useResponsiveModal() const { data: userProfile } = useUserProfile() const location = useLocation() const [step, setStep] = useState(STEP.SENTIMENT) const [sentiment, setSentiment] = useState(null) const [category, setCategory] = useState(null) const [message, setMessage] = useState('') const [submitting, setSubmitting] = useState(false) const [isCloud, setIsCloud] = useState(true) const [githubUrl, setGithubUrl] = useState(null) useEffect(() => { if (open) { setStep(STEP.SENTIMENT) setSentiment(null) setCategory(null) setMessage('') setSubmitting(false) setGithubUrl(null) isCloudInstance().then(setIsCloud) } }, [open]) const handleClose = () => { // Backing out before answering counts as a dismissal for the cooldown. if (step === STEP.SENTIMENT) onDismiss?.() onClose() } const handleSentiment = async value => { setSentiment(value) await markSentiment(value) if (value !== SENTIMENTS.LOVE) { setStep(STEP.DETAILS) return } if (Capacitor.isNativePlatform()) { const requested = await requestStoreReview() if (requested) { onClose() return } } setStep(STEP.WEB_REVIEW) } const handleSubmit = async () => { setSubmitting(true) const { result, githubUrl: url } = await submitFeedback({ sentiment, category, message, feature: location.pathname, userProfile, }) setSubmitting(false) // Self-hosted feedback is never relayed; hand the user a pre-filled issue // instead so they choose what gets published. if (result === SUBMIT_RESULT.SELF_HOSTED) { setGithubUrl(url) setStep(STEP.GITHUB) return } setStep(STEP.THANKS) } const canSubmit = Boolean(category) || message.trim().length > 0 return ( {step === STEP.SENTIMENT && ( <> {t('feedback.sentiment.title')} {t('feedback.sentiment.subtitle')} {SENTIMENT_OPTIONS.map(option => ( ))} )} {step === STEP.DETAILS && ( <> {t('feedback.details.title')} {FEEDBACK_CATEGORIES.map(item => ( setCategory(category === item ? null : item)} > {t(`feedback.categories.${item}`)} ))} {t('feedback.details.messageLabel')}