diff --git a/src/utils/SidepanelConfig.js b/src/utils/SidepanelConfig.js
index 9916c8a..ed065d3 100644
--- a/src/utils/SidepanelConfig.js
+++ b/src/utils/SidepanelConfig.js
@@ -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,
},
@@ -62,4 +62,4 @@ export const saveSidepanelConfig = config => {
export const resetSidepanelConfig = () => {
saveSidepanelConfig(DEFAULT_SIDEPANEL_CONFIG)
return DEFAULT_SIDEPANEL_CONFIG
-}
\ No newline at end of file
+}
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index 217ca19..5a5a6ba 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -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']
}
}
@@ -259,7 +259,7 @@ const LoginView = () => {
if (Capacitor.isNativePlatform()) {
// For mobile devices, use a custom URL scheme for the redirect
const redirectUri = 'donetick://auth/oauth2'
-
+
const params = new URLSearchParams({
response_type: 'code',
client_id: resource?.identity_provider?.client_id,
@@ -274,7 +274,7 @@ const LoginView = () => {
try {
// Open OAuth flow in system browser
await Browser.open({ url: authUrl })
-
+
// Note: The OAuth callback will be handled by deep link handling
// You'll need to implement deep link handling to catch the redirect
// and extract the authorization code
@@ -294,7 +294,7 @@ const LoginView = () => {
scope: 'openid profile email',
state: state,
})
-
+
console.log('redirect', `${authentikAuthorizeUrl}?${params.toString()}`)
window.location.href = `${authentikAuthorizeUrl}?${params.toString()}`
}
@@ -639,6 +639,29 @@ const LoginView = () => {
>
Create new account
+
+
+
+
+
diff --git a/src/views/Authorization/Signup.jsx b/src/views/Authorization/Signup.jsx
index 3838601..e0d5e84 100644
--- a/src/views/Authorization/Signup.jsx
+++ b/src/views/Authorization/Signup.jsx
@@ -268,6 +268,27 @@ const SignupView = () => {
>
Login
+
+
+
+
+
diff --git a/src/views/Chores/MyChores.jsx b/src/views/Chores/MyChores.jsx
index 03668f1..4373800 100644
--- a/src/views/Chores/MyChores.jsx
+++ b/src/views/Chores/MyChores.jsx
@@ -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,42 +1613,44 @@ const MyChores = () => {
Current Filter: {searchFilter}
)}
- {filteredChores.length === 0 && (
-
-
-
- Nothing scheduled
-
- {chores.length > 0 && (
- <>
-
- >
- )}
-
- )}
+ >
+
+
+ Nothing scheduled
+
+ {chores.length > 0 && (
+ <>
+
+ >
+ )}
+
+ )}
{(searchTerm?.length > 0 || searchFilter !== 'All') &&
viewMode !== 'calendar' &&
filteredChores.map(chore =>
diff --git a/src/views/Chores/Sidepanel.jsx b/src/views/Chores/Sidepanel.jsx
index e374b28..d4cd990 100644
--- a/src/views/Chores/Sidepanel.jsx
+++ b/src/views/Chores/Sidepanel.jsx
@@ -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
+ return
case 'assignees':
return
case 'calendar':
diff --git a/src/views/Chores/WelcomeCard.jsx b/src/views/Chores/UserSwitcher.jsx
similarity index 89%
rename from src/views/Chores/WelcomeCard.jsx
rename to src/views/Chores/UserSwitcher.jsx
index f9157aa..c48956e 100644
--- a/src/views/Chores/WelcomeCard.jsx
+++ b/src/views/Chores/UserSwitcher.jsx
@@ -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,
}}
>
-
- Current User
+
+ View tasks as
- Who's checking in?
+ Switch to user view
+
+
+ Tasks will be filtered to show only assignments for selected user
{
gap: 1,
}}
>
-
- Current User
+
+ View tasks as
@@ -187,4 +190,4 @@ const WelcomeCard = () => {
)
}
-export default WelcomeCard
+export default UserSwitcher
diff --git a/src/views/Modals/Inputs/NativeCancelSubscriptionModal.jsx b/src/views/Modals/Inputs/NativeCancelSubscriptionModal.jsx
new file mode 100644
index 0000000..96ea708
--- /dev/null
+++ b/src/views/Modals/Inputs/NativeCancelSubscriptionModal.jsx
@@ -0,0 +1,119 @@
+import { Box, Button, Typography } from '@mui/joy'
+import { useResponsiveModal } from '../../../hooks/useResponsiveModal'
+
+const NativeCancelSubscriptionModal = ({ isOpen, onClose }) => {
+ const { ResponsiveModal } = useResponsiveModal()
+
+ return (
+
+
+ Cancel Subscription
+
+
+
+ To cancel your subscription, please follow the instructions for your
+ platform (you should cancel through the same platform you used to
+ subscribe).
+
+
+
+
+ For iOS (iPhone/iPad):
+
+
+ 1. Open the Settings app on your device
+
+
+ 2. Tap your name at the top of the screen
+
+
+ 3. Tap Subscriptions
+
+
+ 4. Find and tap Donetick
+
+
+ 5. Tap Cancel Subscription
+
+
+ Note: If you subscribed through iOS and are using
+ the web/desktop version, you must cancel through iOS Settings as
+ described above.
+
+
+
+
+
+ For Android:
+
+
+ 1. Open the Google Play Store app
+
+
+ 2. Tap the profile icon in the top right
+
+
+ 3. Tap Payments & subscriptions
+
+
+ 4. Tap Subscriptions
+
+
+ 5. Find and tap Donetick
+
+
+ 6. Tap Cancel subscription
+
+
+ Note: If you subscribed through Google Play and are
+ using the web/desktop version, you must cancel through Google Play
+ as described above.
+
+
+
+
+
+ For Web/Desktop Subscriptions:
+
+
+ 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
+
+
+ Important: 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.
+
+
+
+
+ Your subscription will remain active until the end of your current
+ billing period.
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default NativeCancelSubscriptionModal
diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx
index b7cbedf..ad1ce66 100644
--- a/src/views/Settings/Settings.jsx
+++ b/src/views/Settings/Settings.jsx
@@ -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}
/>
+
+ {
+ 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',
+ })
+ }
+ })
+ }
+ }}
+ />
)
}