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 { 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 { 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 = 'Confirm', cancelText = '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: 'Failed to refresh member requests', }) } 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: import.meta.env.VITE_REACT_APP_REVENUECAT_API_KEY, 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 `You are currently subscribed to the Plus plan. Your subscription will renew on ${fmt.date( userProfile?.expiration, )}.` } else if (userProfile?.subscription === 'cancelled') { return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${fmt.date( userProfile?.expiration, )}.` } else { return `You are currently on the Free plan. Upgrade to the Plus plan to unlock more features.` } } const getSubscriptionStatus = () => { if (userProfile?.subscription === 'active') { return `Plus` } else if (userProfile?.subscription === 'cancelled') { if (moment().isBefore(userProfile?.expiration)) { return `Plus(until ${fmt.date(userProfile?.expiration)})` } return `Free` } else { return `Free` } } if (userProfile === null) { return ( ) } if (!userProfile) { return } return (
Circle settings Your account is automatically connected to a Circle when you create or join one. Easily invite friends by sharing the unique Circle code or link below. You'll receive a notification below when someone requests to join your Circle. {userCircles[0]?.userRole === 'member' ? `You part of ${userCircles[0]?.name} ` : `You circle code is:`} {userCircles.length > 0 && userCircles[0]?.userRole === 'member' && ( )} Circle Members {circleMembers.map(member => ( {member.displayName.charAt(0).toUpperCase() + member.displayName.slice(1)} {member.userId === userProfile.id ? '(You)' : ''}{' '} {' '} {member.isActive ? member.role : 'Pending Approval'} {member.isActive ? ( Joined on {fmt.date(member.createdAt)} ) : ( Request to join{' '} {fmt.date(member.updatedAt)} )} {member.userId !== userProfile.id && isAdmin && ( )} {isAdmin && member.userId !== userProfile.id && member.isActive && ( )} ))} Circle Member Requests {lastRefresh && ( Last updated: {fmt.dateTime(lastRefresh)} )} {circleMemberRequests.map(request => ( {request.displayName} wants to join your circle. ))} or if want to join someone else's Circle? Ask them for their unique Circle code or join link. Enter the code below to join their Circle. Enter Circle code: setCircleInviteCode(e.target.value)} size='lg' sx={{ width: '220px', mb: 1, }} /> {circleMembers.find(m => userProfile.id == m.userId)?.role === 'admin' && ( <> Webhook Webhooks allow you to send real-time notifications to other services when events happen in your Circle. Configure a webhook URL to receive real-time updates. {!isPlusAccount(userProfile) && ( Webhook notifications are not available in the Basic plan. Upgrade to Plus to receive real-time updates via webhooks. )} { if (webhookURL === null) { setWebhookURL('') } else { setWebhookURL(null) } }} variant='soft' label='Enable Webhook' disabled={!isPlusAccount(userProfile)} overlay /> Enable webhook notifications for tasks and things updates.{' '} {userProfile && !isPlusAccount(userProfile) && ( Plus Feature )} {webhookURL !== null && ( Webhook URL setWebhookURL(e.target.value)} size='lg' sx={{ width: '220px', mb: 1, }} /> {webhookError && ( {webhookError} )} )} )} {/* WebSocket Settings */} {/* */}
Account Settings Change your account settings, type or update your password Account Type : {getSubscriptionStatus()} {getSubscriptionDetails()} {userProfile?.subscription === 'active' && ( )} {import.meta.env.VITE_IS_SELF_HOSTED === 'true' && ( Password : {changePasswordModal ? ( { if (password) { UpdatePassword(password).then(resp => { if (resp.ok) { showNotification({ type: 'success', message: 'Password changed successfully', }) } else { showNotification({ type: 'error', message: 'Password change failed', }) } }) } setChangePasswordModal(false) }} /> ) : null} )} Danger Zone Once you delete your account, there is no going back. Please be certain.
Sidepanel Customization Customize the layout and visibility of cards in the sidepanel. the section only available on large screen devices such as tablets and desktops..
Theme preferences Choose how the site looks to you. Select a single theme, or sync with your system and automatically switch between day and night themes.
Localization Customize language, date format, and regional preferences for your account. These settings will apply throughout the application.
{/* 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