Files
donetick/src/views/Settings/AccountSettings.jsx
Mo Tarbin 8c9bf0db1d feat: enhance localization across settings views
- Updated LocalizationSettings to use translation keys for titles and descriptions.
- Refactored MFASettings to utilize translation for error messages, success messages, and UI text.
- Improved NotificationSetting by integrating translation for all user-facing strings.
- Initial Hebrew translation ( testing AI translation ( some of crowdin)
- Adjusted ProfileSettings to use translation for the edit photo title.
- Enhanced SettingsOverview to utilize translation for the early access label.
- Updated SidepanelSettings to implement translation for card names and descriptions.
- Refined StorageSettings to use translation for all storage-related messages and titles.
- Improved ThemeSettings to utilize translation for theme descriptions and titles.
2026-08-11 02:19:40 -04:00

310 lines
11 KiB
JavaScript

import { Capacitor } from '@capacitor/core'
import { Box, Button, 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 SubscriptionModal from '../../components/SubscriptionModal'
import { useLocalization } from '../../contexts/LocalizationContext'
import { useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
import { CancelSubscription, UpdatePassword } from '../../utils/Fetcher'
import NativeCancelSubscriptionModal from '../Modals/Inputs/NativeCancelSubscriptionModal'
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
import SettingsLayout from './SettingsLayout'
const AccountSettings = () => {
const { t } = useTranslation('settings')
const { data: userProfile } = useUserProfile()
const queryClient = useQueryClient()
const { showNotification } = useNotification()
const { fmt } = useLocalization()
const [changePasswordModal, setChangePasswordModal] = useState(false)
const [subscriptionModal, setSubscriptionModal] = useState(false)
const [userDeletionModal, setUserDeletionModal] = useState(false)
const [nativeCancelModal, setNativeCancelModal] = useState(false)
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])
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) {
return (
<SettingsLayout title={t('accountSettings.title')}>
<div>{t('common.loading')}</div>
</SettingsLayout>
)
}
return (
<SettingsLayout title={t('accountSettings.title')}>
<div className='grid gap-4'>
<Typography level='body-md'>
{t('accountSettings.description')}
</Typography>
<Typography level='title-md' mb={-1}>
{t('accountSettings.accountType', { type: getSubscriptionStatus() })}
</Typography>
<Typography level='body-sm'>{getSubscriptionDetails()}</Typography>
<Box>
<Button
sx={{
width: '110px',
mb: 1,
}}
disabled={
userProfile?.subscription === 'active' ||
(moment(userProfile?.expiration).isAfter(moment()) &&
userProfile?.subscription !== 'cancelled')
}
onClick={async () => {
if (Capacitor.isNativePlatform()) {
try {
const { RevenueCatUI } =
await import('@revenuecat/purchases-capacitor-ui')
const offering = await Purchases.getOfferings()
await RevenueCatUI.presentPaywall({
offering: offering.current,
})
const { customerInfo } = await Purchases.getCustomerInfo()
if (customerInfo.entitlements.active['Donetick Plus']) {
queryClient.invalidateQueries(['userProfile'])
queryClient.refetchQueries(['userProfile'])
showNotification({
type: 'success',
message: t('accountSettings.purchase.success'),
})
}
} catch (error) {
console.log('Purchase error:', error)
if (error.code === '1') {
return
} else if (error.code === '2') {
showNotification({
type: 'error',
message: t('accountSettings.purchase.storeConnection'),
})
} else if (error.code === '3') {
showNotification({
type: 'error',
message: t('accountSettings.purchase.notAllowed'),
})
} else if (error.code === '4') {
showNotification({
type: 'error',
message: t('accountSettings.purchase.unavailable'),
})
} else if (error.code === '5') {
showNotification({
type: 'error',
message: t('accountSettings.purchase.alreadyProcessed'),
})
} else if (error.code === '6') {
showNotification({
type: 'error',
message: t('accountSettings.purchase.receiptMissing'),
})
} else if (error.code === '7') {
showNotification({
type: 'error',
message: t('accountSettings.purchase.networkError'),
})
} else if (error.code === '8') {
showNotification({
type: 'error',
message: t('accountSettings.purchase.invalidReceipt'),
})
} else if (error.code === '9') {
showNotification({
type: 'warning',
message: t('accountSettings.purchase.pending'),
})
} else {
console.error('Unexpected purchase error:', error)
console.error('Error occurred in purchase flow')
showNotification({
type: 'error',
message: t('accountSettings.purchase.failed', {
error:
error.message ||
t('accountSettings.purchase.unknownError'),
}),
})
}
}
} else {
setSubscriptionModal(true)
}
}}
>
{t('accountSettings.upgrade')}
</Button>
{userProfile?.subscription === 'active' && (
<Button
sx={{
width: '110px',
mb: 1,
ml: 1,
}}
variant='outlined'
color='danger'
onClick={() => {
setNativeCancelModal(true)
}}
>
{t('accountSettings.cancel')}
</Button>
)}
</Box>
{import.meta.env.VITE_IS_SELF_HOSTED === 'true' && (
<Box>
<Typography level='title-md' mb={1}>
{t('accountSettings.password')}
</Typography>
<Typography mb={1} level='body-sm'></Typography>
<Button
variant='soft'
onClick={() => {
setChangePasswordModal(true)
}}
>
{t('accountSettings.changePassword')}
</Button>
{changePasswordModal ? (
<PassowrdChangeModal
isOpen={changePasswordModal}
onClose={password => {
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}
</Box>
)}
<Box>
<Typography level='title-md' mb={1} color='danger'>
{t('accountSettings.dangerZone')}
</Typography>
<Typography level='body-sm' mb={2} color='neutral'>
{t('accountSettings.dangerZoneDescription')}
</Typography>
<Button
variant='outlined'
color='danger'
onClick={() => setUserDeletionModal(true)}
>
{t('accountSettings.deleteAccount')}
</Button>
</Box>
</div>
<SubscriptionModal
open={subscriptionModal}
onClose={() => setSubscriptionModal(false)}
/>
<UserDeletionModal
isOpen={userDeletionModal}
onClose={success => {
setUserDeletionModal(false)
if (success) {
showNotification({
type: 'success',
message: t('accountSettings.accountDeleted'),
})
}
}}
userProfile={userProfile}
/>
<NativeCancelSubscriptionModal
isOpen={nativeCancelModal}
onClose={action => {
setNativeCancelModal(false)
if (action === 'desktop') {
CancelSubscription().then(resp => {
if (resp.ok) {
showNotification({
type: 'success',
message: t('accountSettings.subscriptionCancelled'),
})
window.location.reload()
} else {
showNotification({
type: 'error',
message: t('accountSettings.subscriptionCancelFailed'),
})
}
})
}
}}
/>
</SettingsLayout>
)
}
export default AccountSettings