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)}
)}
:
}
>
{isRefreshing ? 'Refreshing...' : 'Refresh'}
{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.