feat: Implement User Switcher and add subscription cancellation modal
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
export const DEFAULT_SIDEPANEL_CONFIG = [
|
||||
{
|
||||
id: 'welcome',
|
||||
name: 'Welcome Card',
|
||||
description: 'Shows greeting and quick stats',
|
||||
iconName: 'WavingHand',
|
||||
id: 'welcome', // legacy name, now represents User Switcher
|
||||
name: 'User Switcher',
|
||||
description: 'Allows admins/managers to view tasks as different users',
|
||||
iconName: 'SupervisorAccount',
|
||||
enabled: true,
|
||||
order: 0,
|
||||
},
|
||||
|
||||
@@ -150,7 +150,7 @@ const LoginView = () => {
|
||||
// Apple Sign In returns id_token in response
|
||||
return data['response']['id_token']
|
||||
} else if (data['id_token']) {
|
||||
// Direct id_token for Apple
|
||||
// Direct id_token for Apple (fallback)
|
||||
return data['id_token']
|
||||
}
|
||||
}
|
||||
@@ -639,6 +639,29 @@ const LoginView = () => {
|
||||
>
|
||||
Create new account
|
||||
</Button>
|
||||
|
||||
<Box
|
||||
sx={{ display: 'flex', justifyContent: 'center', gap: 2, mt: 2 }}
|
||||
>
|
||||
<Button
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
window.open('https://donetick.com/privacy-policy', '_blank')
|
||||
}}
|
||||
>
|
||||
Privacy Policy
|
||||
</Button>
|
||||
<Button
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
window.open('https://donetick.com/terms', '_blank')
|
||||
}}
|
||||
>
|
||||
Terms of Use
|
||||
</Button>
|
||||
</Box>
|
||||
</Sheet>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -268,6 +268,27 @@ const SignupView = () => {
|
||||
>
|
||||
Login
|
||||
</Button>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', gap: 2, mt: 2 }}>
|
||||
<Button
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
window.open('https://donetick.com/privacy-policy', '_blank')
|
||||
}}
|
||||
>
|
||||
Privacy Policy
|
||||
</Button>
|
||||
<Button
|
||||
variant='plain'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
window.open('https://donetick.com/terms', '_blank')
|
||||
}}
|
||||
>
|
||||
Terms of Use
|
||||
</Button>
|
||||
</Box>
|
||||
</Sheet>
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
@@ -125,7 +125,15 @@ const MyChores = () => {
|
||||
choresData?.res
|
||||
) {
|
||||
setPerformers(membersData.res)
|
||||
const sortedChores = choresData.res.sort(ChoreSorter)
|
||||
let sortedChores = choresData.res.sort(ChoreSorter)
|
||||
|
||||
// Filter chores based on impersonated user
|
||||
if (impersonatedUser) {
|
||||
sortedChores = sortedChores.filter(chore =>
|
||||
chore.assignedTo === impersonatedUser.userId
|
||||
)
|
||||
}
|
||||
|
||||
setChores(sortedChores)
|
||||
setFilteredChores(sortedChores)
|
||||
const sections = ChoresGrouper(
|
||||
@@ -161,6 +169,7 @@ const MyChores = () => {
|
||||
choresData,
|
||||
membersData,
|
||||
userProfile,
|
||||
impersonatedUser,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -445,7 +454,17 @@ const MyChores = () => {
|
||||
if (searchTerm?.length > 0 || searchFilter !== 'All') {
|
||||
return filteredChores
|
||||
}
|
||||
return chores.filter(ChoreFilters(userProfile)[selectedChoreFilter])
|
||||
|
||||
let choresToFilter = chores
|
||||
|
||||
// Filter by impersonated user first if set
|
||||
if (impersonatedUser) {
|
||||
choresToFilter = choresToFilter.filter(chore =>
|
||||
chore.assignedTo === impersonatedUser.userId
|
||||
)
|
||||
}
|
||||
|
||||
return choresToFilter.filter(ChoreFilters(userProfile)[selectedChoreFilter])
|
||||
}
|
||||
|
||||
// Helper function to get chores for a specific date
|
||||
@@ -460,8 +479,15 @@ const MyChores = () => {
|
||||
}
|
||||
|
||||
const updateChores = newChore => {
|
||||
const newChores = chores
|
||||
newChores.push(newChore)
|
||||
let newChores = [...chores, newChore]
|
||||
|
||||
// Filter chores based on impersonated user
|
||||
if (impersonatedUser) {
|
||||
newChores = newChores.filter(chore =>
|
||||
chore.assignedTo === impersonatedUser.userId
|
||||
)
|
||||
}
|
||||
|
||||
setChores(newChores)
|
||||
setFilteredChores(newChores)
|
||||
setChoreSections(
|
||||
@@ -1587,7 +1613,9 @@ const MyChores = () => {
|
||||
Current Filter: {searchFilter}
|
||||
</Chip>
|
||||
)}
|
||||
{filteredChores.length === 0 && (
|
||||
{filteredChores.length === 0 &&
|
||||
// only if not in calendar view:
|
||||
viewMode !== 'calendar' && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
||||
@@ -7,10 +7,10 @@ import { getSidepanelConfig } from '../../utils/SidepanelConfig'
|
||||
import CalendarView from '../components/CalendarView'
|
||||
import ActivitiesCard from './ActivitesCard'
|
||||
import TasksByAssigneeCard from './TasksByAssigneeCard'
|
||||
import WelcomeCard from './WelcomeCard'
|
||||
import UserSwitcher from './UserSwitcher'
|
||||
|
||||
const Sidepanel = ({ chores }) => {
|
||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('md'))
|
||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('lg'))
|
||||
const [dueDatePieChartData, setDueDatePieChartData] = useState([])
|
||||
const [sidepanelConfig, setSidepanelConfig] = useState([])
|
||||
const {
|
||||
@@ -54,7 +54,7 @@ const Sidepanel = ({ chores }) => {
|
||||
|
||||
switch (cardConfig.id) {
|
||||
case 'welcome':
|
||||
return <WelcomeCard key='welcome' chores={chores} />
|
||||
return <UserSwitcher key='welcome' chores={chores} />
|
||||
case 'assignees':
|
||||
return <TasksByAssigneeCard key='assignees' chores={chores} />
|
||||
case 'calendar':
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Person } from '@mui/icons-material'
|
||||
import { SupervisorAccount } from '@mui/icons-material'
|
||||
import { Avatar, Box, Button, Sheet, Typography } from '@mui/joy'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useImpersonateUser } from '../../contexts/ImpersonateUserContext'
|
||||
import { useCircleMembers, useUserProfile } from '../../queries/UserQueries'
|
||||
import UserModal from '../Modals/Inputs/UserModal'
|
||||
const WelcomeCard = () => {
|
||||
const UserSwitcher = () => {
|
||||
const { impersonatedUser, setImpersonatedUser } = useImpersonateUser()
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
const { data: userProfile } = useUserProfile()
|
||||
@@ -57,13 +57,16 @@ const WelcomeCard = () => {
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Person color='' />
|
||||
<Typography level='title-md'>Current User</Typography>
|
||||
<SupervisorAccount color='' />
|
||||
<Typography level='title-md'>View tasks as</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography level='title-md' sx={{ mb: 0.5 }}>
|
||||
Who's checking in?
|
||||
Switch to user view
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ mb: 1, color: 'text.secondary' }}>
|
||||
Tasks will be filtered to show only assignments for selected user
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
@@ -72,7 +75,7 @@ const WelcomeCard = () => {
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
size='sm'
|
||||
>
|
||||
Select User
|
||||
Choose User
|
||||
</Button>
|
||||
<UserModal
|
||||
isOpen={isModalOpen}
|
||||
@@ -122,8 +125,8 @@ const WelcomeCard = () => {
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Person color='' />
|
||||
<Typography level='title-md'>Current User</Typography>
|
||||
<SupervisorAccount color='' />
|
||||
<Typography level='title-md'>View tasks as</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -187,4 +190,4 @@ const WelcomeCard = () => {
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
export default WelcomeCard
|
||||
export default UserSwitcher
|
||||
119
src/views/Modals/Inputs/NativeCancelSubscriptionModal.jsx
Normal file
119
src/views/Modals/Inputs/NativeCancelSubscriptionModal.jsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Box, Button, Typography } from '@mui/joy'
|
||||
import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
|
||||
|
||||
const NativeCancelSubscriptionModal = ({ isOpen, onClose }) => {
|
||||
const { ResponsiveModal } = useResponsiveModal()
|
||||
|
||||
return (
|
||||
<ResponsiveModal open={isOpen} onClose={onClose} size='md' fullWidth>
|
||||
<Typography level='h4' sx={{ mb: 2 }}>
|
||||
Cancel Subscription
|
||||
</Typography>
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Typography level='body-md' mb={3}>
|
||||
To cancel your subscription, please follow the instructions for your
|
||||
platform (you should cancel through the same platform you used to
|
||||
subscribe).
|
||||
</Typography>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='title-md' mb={2} color='primary'>
|
||||
For iOS (iPhone/iPad):
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
1. Open the <strong>Settings</strong> app on your device
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
2. Tap your name at the top of the screen
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
3. Tap <strong>Subscriptions</strong>
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
4. Find and tap <strong>Donetick</strong>
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2}>
|
||||
5. Tap <strong>Cancel Subscription</strong>
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2} color='warning'>
|
||||
<strong>Note:</strong> If you subscribed through iOS and are using
|
||||
the web/desktop version, you must cancel through iOS Settings as
|
||||
described above.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='title-md' mb={2} color='primary'>
|
||||
For Android:
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
1. Open the <strong>Google Play Store</strong> app
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
2. Tap the profile icon in the top right
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
3. Tap <strong>Payments & subscriptions</strong>
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
4. Tap <strong>Subscriptions</strong>
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={1}>
|
||||
5. Find and tap <strong>Donetick</strong>
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2}>
|
||||
6. Tap <strong>Cancel subscription</strong>
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2} color='warning'>
|
||||
<strong>Note:</strong> If you subscribed through Google Play and are
|
||||
using the web/desktop version, you must cancel through Google Play
|
||||
as described above.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box mb={3}>
|
||||
<Typography level='title-md' mb={2} color='primary'>
|
||||
For Web/Desktop Subscriptions:
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2}>
|
||||
If you originally subscribed through our website or desktop app, you
|
||||
can cancel your subscription by going to the Account Settings
|
||||
section on our website. using a web browser
|
||||
</Typography>
|
||||
<Typography level='body-sm' mb={2} color='warning'>
|
||||
<strong>Important:</strong> You must cancel your subscription
|
||||
through the same platform where you originally subscribed. If you
|
||||
subscribed through the iOS App Store or Google Play Store (even if
|
||||
you're now using the web/desktop version), you must cancel through
|
||||
that original platform using the instructions above.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Typography level='body-sm' mb={3} color='neutral'>
|
||||
Your subscription will remain active until the end of your current
|
||||
billing period.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Button size='lg' onClick={onClose} variant='outlined' fullWidth>
|
||||
I'll cancel from my app store
|
||||
</Button>
|
||||
<Button
|
||||
size='lg'
|
||||
onClick={() => onClose('desktop')}
|
||||
variant='solid'
|
||||
color='danger'
|
||||
fullWidth
|
||||
>
|
||||
I subscribed via desktop - Cancel now
|
||||
</Button>
|
||||
<Button size='lg' onClick={onClose} fullWidth>
|
||||
Dismiss
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</ResponsiveModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default NativeCancelSubscriptionModal
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
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'
|
||||
@@ -41,6 +42,7 @@ import {
|
||||
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'
|
||||
@@ -53,6 +55,7 @@ import ThemeToggle from './ThemeToggle'
|
||||
|
||||
const Settings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const queryClient = useQueryClient()
|
||||
const { showNotification } = useNotification()
|
||||
const navigate = useNavigate()
|
||||
|
||||
@@ -69,6 +72,7 @@ const Settings = () => {
|
||||
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 = (
|
||||
@@ -128,6 +132,17 @@ const Settings = () => {
|
||||
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(() => {
|
||||
@@ -679,30 +694,95 @@ const Settings = () => {
|
||||
const { RevenueCatUI } = await import(
|
||||
'@revenuecat/purchases-capacitor-ui'
|
||||
)
|
||||
await Purchases.configure({
|
||||
apiKey: import.meta.env.VITE_REACT_APP_REVENUECAT_API_KEY,
|
||||
appUserID: String(userProfile?.id),
|
||||
})
|
||||
|
||||
const offering = await Purchases.getOfferings()
|
||||
await RevenueCatUI.presentPaywall({
|
||||
offering: offering.current,
|
||||
})
|
||||
|
||||
// Check if user now has entitlement after paywall interaction
|
||||
const customerInfo = await Purchases.getCustomerInfo()
|
||||
if (customerInfo.entitlements.active['plus']) {
|
||||
const { customerInfo } = await Purchases.getCustomerInfo()
|
||||
if (customerInfo.entitlements.active['Donetick Plus']) {
|
||||
queryClient.invalidateQueries(['userProfile'])
|
||||
queryClient.refetchQueries(['userProfile'])
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message:
|
||||
'Purchase successful! Please restart the app to access Plus features.',
|
||||
})
|
||||
// invalidate user profile to get new subscription status:
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== '1') {
|
||||
// User cancelled
|
||||
console.log('Purchase error:', error)
|
||||
|
||||
// Handle different error types
|
||||
if (error.code === '1') {
|
||||
// User cancelled - don't show error
|
||||
return
|
||||
} else if (error.code === '2') {
|
||||
// Store problem
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: 'Purchase failed. Please try again.',
|
||||
message:
|
||||
'Store connection issue. Please check your network and try again.',
|
||||
})
|
||||
} else if (error.code === '3') {
|
||||
// Purchase not allowed
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Purchases are not allowed on this device. Please check your device restrictions.',
|
||||
})
|
||||
} else if (error.code === '4') {
|
||||
// Product not available
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'This subscription is not available. Please try again later.',
|
||||
})
|
||||
} else if (error.code === '5') {
|
||||
// Receipt already in use
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'This purchase has already been processed. If you believe this is an error, please contact support.',
|
||||
})
|
||||
} else if (error.code === '6') {
|
||||
// Missing receipt file
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Purchase receipt missing. Please try purchasing again.',
|
||||
})
|
||||
} else if (error.code === '7') {
|
||||
// Network error
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Network error. Please check your connection and try again.',
|
||||
})
|
||||
} else if (error.code === '8') {
|
||||
// Invalid receipt
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message:
|
||||
'Invalid purchase receipt. Please contact support if this persists.',
|
||||
})
|
||||
} else if (error.code === '9') {
|
||||
// Payment pending
|
||||
showNotification({
|
||||
type: 'warning',
|
||||
message:
|
||||
'Payment is pending approval. You will receive access once approved.',
|
||||
})
|
||||
} else {
|
||||
// Generic error
|
||||
// log on what part of the code the error happened
|
||||
console.error('Unexpected purchase error:', error)
|
||||
console.error('Error occurred in purchase flow')
|
||||
showNotification({
|
||||
type: 'error',
|
||||
message: `Purchase failed: ${error.message || 'Unknown error'}. Please try again or contact support.`,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -724,15 +804,7 @@ const Settings = () => {
|
||||
variant='outlined'
|
||||
color='danger'
|
||||
onClick={() => {
|
||||
CancelSubscription().then(resp => {
|
||||
if (resp.ok) {
|
||||
showNotification({
|
||||
type: 'success',
|
||||
message: 'Subscription cancelled',
|
||||
})
|
||||
window.location.reload()
|
||||
}
|
||||
})
|
||||
setNativeCancelModal(true)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
@@ -844,6 +916,29 @@ const Settings = () => {
|
||||
}}
|
||||
userProfile={userProfile}
|
||||
/>
|
||||
|
||||
<NativeCancelSubscriptionModal
|
||||
isOpen={nativeCancelModal}
|
||||
onClose={action => {
|
||||
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',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user