import { Capacitor } from '@capacitor/core' import { Delete, Refresh } from '@mui/icons-material' import { Box, Button, Card, Checkbox, Chip, CircularProgress, Container, Divider, FormControl, FormHelperText, Input, Option, Select, Typography, } from '@mui/joy' import { Purchases } from '@revenuecat/purchases-capacitor' import { useQueryClient } from '@tanstack/react-query' import moment from 'moment' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import RealTimeSettings from '../../components/RealTimeSettings' import SubscriptionModal from '../../components/SubscriptionModal' import { useLocalization } from '../../contexts/LocalizationContext' import Logo from '../../Logo' import { useUserProfile } from '../../queries/UserQueries' import { useNotification } from '../../service/NotificationProvider' import { AcceptCircleMemberRequest, CancelSubscription, DeleteCircleMember, GetAllCircleMembers, GetCircleMemberRequests, GetUserCircle, JoinCircle, LeaveCircle, PutWebhookURL, UpdateMemberRole, UpdatePassword, } from '../../utils/Fetcher' import { isPlusAccount } from '../../utils/Helpers' import LoadingComponent from '../components/Loading' import ConfirmationModal from '../Modals/Inputs/ConfirmationModal' import NativeCancelSubscriptionModal from '../Modals/Inputs/NativeCancelSubscriptionModal' import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal' import UserDeletionModal from '../Modals/Inputs/UserDeletionModal' import APITokenSettings from './APITokenSettings' import LocalizationSettings from './LocalizationSettings' import MFASettings from './MFASettings' import NotificationSetting from './NotificationSetting' import ProfileSettings from './ProfileSettings' import SidepanelSettings from './SidepanelSettings' import StorageSettings from './StorageSettings' import ThemeToggle from './ThemeToggle' const Settings = () => { const { t } = useTranslation('settings') const { data: userProfile } = useUserProfile() const queryClient = useQueryClient() const { showNotification } = useNotification() const navigate = useNavigate() const { fmt } = useLocalization() const [userCircles, setUserCircles] = useState([]) const [circleMemberRequests, setCircleMemberRequests] = useState([]) const [circleInviteCode, setCircleInviteCode] = useState('') const [circleMembers, setCircleMembers] = useState([]) const [webhookURL, setWebhookURL] = useState(null) const [webhookError, setWebhookError] = useState(null) const [isAdmin, setIsAdmin] = useState(false) const [lastRefresh, setLastRefresh] = useState(null) const [isRefreshing, setIsRefreshing] = useState(false) const [changePasswordModal, setChangePasswordModal] = useState(false) const [subscriptionModal, setSubscriptionModal] = useState(false) const [userDeletionModal, setUserDeletionModal] = useState(false) const [nativeCancelModal, setNativeCancelModal] = useState(false) const [confirmModalConfig, setConfirmModalConfig] = useState({}) const showConfirmation = ( message, title, onConfirm, confirmText = t('common.confirm'), cancelText = t('common.cancel'), color = 'primary', ) => { setConfirmModalConfig({ isOpen: true, message, title, confirmText, cancelText, color, onClose: isConfirmed => { if (isConfirmed) { onConfirm() } setConfirmModalConfig({}) }, }) } const refreshMemberRequests = async () => { setIsRefreshing(true) try { const resp = await GetCircleMemberRequests() const data = await resp.json() setCircleMemberRequests(data.res ? data.res : []) setLastRefresh(new Date()) } catch (error) { showNotification({ type: 'error', message: t('circleSettings.refreshFailed'), }) } finally { setIsRefreshing(false) } } useEffect(() => { GetUserCircle().then(resp => { resp.json().then(data => { setUserCircles(data.res ? data.res : []) setWebhookURL(data.res ? data.res[0].webhook_url : null) }) }) GetCircleMemberRequests().then(resp => { resp.json().then(data => { setCircleMemberRequests(data.res ? data.res : []) setLastRefresh(new Date()) }) }) GetAllCircleMembers().then(data => { setCircleMembers(data.res ? data.res : []) }) }, []) useEffect(() => { async function configurePurchases() { if (Capacitor.isNativePlatform() && userProfile) { await Purchases.configure({ apiKey: Capacitor.getPlatform() === 'ios' ? import.meta.env.VITE_REACT_APP_REVENUECAT_API_KEY_IOS : import.meta.env.VITE_REACT_APP_REVENUECAT_API_KEY_ANDROID, appUserID: String(userProfile?.id), }) } } configurePurchases() }, [userProfile]) // useEffect when circleMembers and userprofile: useEffect(() => { if (userProfile && userProfile.id) { const isUserAdmin = circleMembers.some( member => member.userId === userProfile.id && member.role === 'admin', ) setIsAdmin(isUserAdmin) } }, [circleMembers, userProfile]) useEffect(() => { const handleHashChange = () => { const hash = window.location.hash if (hash) { // Small delay to ensure the component is fully rendered before scrolling setTimeout(() => { const section = document.getElementById(hash.slice(1)) if (section) { // Get the element position and scroll with some offset for the title const elementPosition = section.offsetTop const offsetPosition = elementPosition - 20 // 20px padding above the title window.scrollTo({ top: offsetPosition, behavior: 'instant', // Use 'smooth' for smooth scrolling }) } }, 500) } } // Handle initial hash on mount handleHashChange() // Listen for hash changes window.addEventListener('hashchange', handleHashChange) return () => { window.removeEventListener('hashchange', handleHashChange) } }, []) const getSubscriptionDetails = () => { if (userProfile?.subscription === 'active') { return t('accountSettings.activeDescription', { date: fmt.date(userProfile?.expiration), }) } else if (userProfile?.subscription === 'cancelled') { return t('accountSettings.cancelledDescription', { date: fmt.date(userProfile?.expiration), }) } else { return t('accountSettings.freeDescription') } } const getSubscriptionStatus = () => { if (userProfile?.subscription === 'active') { return t('accountSettings.plus') } else if (userProfile?.subscription === 'cancelled') { if (moment().isBefore(userProfile?.expiration)) { return t('accountSettings.plusUntil', { date: fmt.date(userProfile?.expiration), }) } return t('accountSettings.free') } else { return t('accountSettings.free') } } if (userProfile === null) { return ( ) } if (!userProfile) { return } return (
{t('circleSettings.title')} {t('circleSettings.description')} {userCircles[0]?.userRole === 'member' ? t('circleSettings.memberOf', { name: userCircles[0]?.name }) : t('circleSettings.yourCircleCode')} {userCircles.length > 0 && userCircles[0]?.userRole === 'member' && ( )} {t('circleSettings.circleMembers')} {circleMembers.map(member => ( {member.displayName.charAt(0).toUpperCase() + member.displayName.slice(1)} {member.userId === userProfile.id ? t('circleSettings.you') : ''}{' '} {' '} {member.isActive ? member.role : t('circleSettings.pendingApproval')} {member.isActive ? ( {t('circleSettings.joinedOn', { date: fmt.date(member.createdAt), })} ) : ( {t('circleSettings.requestedToJoin', { date: fmt.date(member.updatedAt), })} )} {member.userId !== userProfile.id && isAdmin && ( )} {isAdmin && member.userId !== userProfile.id && member.isActive && ( )} ))} {t('circleSettings.circleMemberRequests')} {lastRefresh && ( {t('circleSettings.lastUpdated', { time: fmt.dateTime(lastRefresh), })} )} {circleMemberRequests.map(request => ( {t('circleSettings.wantsToJoin', { name: request.displayName })} ))} {t('circleSettings.or')} {t('circleSettings.joinOtherDescription')} {t('circleSettings.enterCircleCode')} setCircleInviteCode(e.target.value)} size='lg' sx={{ width: '220px', mb: 1, }} /> {circleMembers.find(m => userProfile.id == m.userId)?.role === 'admin' && ( <> {t('advanced.webhookTitle')} {t('advanced.webhookDescription')} {!isPlusAccount(userProfile) && ( {t('advanced.webhookPlusNotice')} )} { if (webhookURL === null) { setWebhookURL('') } else { setWebhookURL(null) } }} variant='soft' label={t('advanced.webhookToggle')} disabled={!isPlusAccount(userProfile)} overlay /> {t('advanced.webhookHelper')}{' '} {userProfile && !isPlusAccount(userProfile) && ( {t('common.plusFeature')} )} {webhookURL !== null && ( {t('advanced.webhookURL')} setWebhookURL(e.target.value)} size='lg' sx={{ width: '220px', mb: 1, }} /> {webhookError && ( {webhookError} )} )} )} {/* WebSocket Settings */} {/* */}
{t('accountSettings.title')} {t('accountSettings.description')} {t('accountSettings.accountType', { type: getSubscriptionStatus() })} {getSubscriptionDetails()} {userProfile?.subscription === 'active' && ( )} {import.meta.env.VITE_IS_SELF_HOSTED === 'true' && ( {t('accountSettings.password')} {changePasswordModal ? ( { if (password) { UpdatePassword(password).then(resp => { if (resp.ok) { showNotification({ type: 'success', message: t('accountSettings.passwordChanged'), }) } else { showNotification({ type: 'error', message: t('accountSettings.passwordChangeFailed'), }) } }) } setChangePasswordModal(false) }} /> ) : null} )} {t('accountSettings.dangerZone')} {t('accountSettings.dangerZoneDescription')}
{t('sidepanel.title')} {t('sidepanel.detailedDescription')}
{t('theme.title')} {t('theme.description')}
{t('localization.title')} {t('localization.descriptionLong')}
{/* Modals */} {confirmModalConfig?.isOpen && ( )} setSubscriptionModal(false)} /> { setUserDeletionModal(false) if (success) { showNotification({ type: 'success', message: 'Account deleted successfully', }) } }} userProfile={userProfile} /> { setNativeCancelModal(false) if (action === 'desktop') { CancelSubscription().then(resp => { if (resp.ok) { showNotification({ type: 'success', message: 'Subscription cancelled', }) window.location.reload() } else { showNotification({ type: 'error', message: 'Failed to cancel subscription', }) } }) } }} />
) } export default Settings