diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle
index c35dddf..ca0c512 100644
--- a/android/app/capacitor.build.gradle
+++ b/android/app/capacitor.build.gradle
@@ -9,15 +9,19 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
+ implementation project(':capacitor-community-sqlite')
implementation project(':capacitor-app')
implementation project(':capacitor-device')
implementation project(':capacitor-local-notifications')
+ implementation project(':capacitor-network')
implementation project(':capacitor-preferences')
implementation project(':capacitor-push-notifications')
implementation project(':capacitor-status-bar')
implementation project(':capgo-capacitor-social-login')
+ implementation project(':revenuecat-purchases-capacitor')
+ implementation project(':revenuecat-purchases-capacitor-ui')
implementation project(':capacitor-plugin-safe-area')
-
+ implementation "com.android.billingclient:billing:7.1.1"
}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index ad421fd..8dc3eb2 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -30,6 +30,10 @@
+
+
+
+
diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle
index f4f93ae..6b1f767 100644
--- a/android/capacitor.settings.gradle
+++ b/android/capacitor.settings.gradle
@@ -2,6 +2,9 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
+include ':capacitor-community-sqlite'
+project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
+
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
@@ -11,6 +14,9 @@ project(':capacitor-device').projectDir = new File('../node_modules/@capacitor/d
include ':capacitor-local-notifications'
project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android')
+include ':capacitor-network'
+project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
+
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
@@ -23,5 +29,11 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
include ':capgo-capacitor-social-login'
project(':capgo-capacitor-social-login').projectDir = new File('../node_modules/@capgo/capacitor-social-login/android')
+include ':revenuecat-purchases-capacitor'
+project(':revenuecat-purchases-capacitor').projectDir = new File('../node_modules/@revenuecat/purchases-capacitor/android')
+
+include ':revenuecat-purchases-capacitor-ui'
+project(':revenuecat-purchases-capacitor-ui').projectDir = new File('../node_modules/@revenuecat/purchases-capacitor-ui/android')
+
include ':capacitor-plugin-safe-area'
project(':capacitor-plugin-safe-area').projectDir = new File('../node_modules/capacitor-plugin-safe-area/android')
diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist
new file mode 100644
index 0000000..766028d
--- /dev/null
+++ b/ios/App/App/Info.plist
@@ -0,0 +1,55 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleDisplayName
+ DoneTick
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ LSRequiresIPhoneOS
+
+ UIMainStoryboardFile
+ Main
+ UIRequiredDeviceCapabilities
+
+ armv7
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
+ NSCameraUsageDescription
+ This app needs access to camera to take photos for your profile picture.
+ NSPhotoLibraryUsageDescription
+ This app needs access to photo library to select photos for your profile picture.
+ NSPhotoLibraryAddUsageDescription
+ This app needs access to save photos to your photo library.
+
+
\ No newline at end of file
diff --git a/ios/App/Podfile b/ios/App/Podfile
index 67db58d..57c664f 100644
--- a/ios/App/Podfile
+++ b/ios/App/Podfile
@@ -1,6 +1,6 @@
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
-platform :ios, '13.0'
+platform :ios, '15.0'
use_frameworks!
# workaround to avoid Xcode caching of Pods that requires
@@ -11,14 +11,19 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
+ pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications'
+ pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapgoCapacitorSocialLogin', :path => '../../node_modules/@capgo/capacitor-social-login'
+ pod 'RevenuecatPurchasesCapacitor', :path => '../../node_modules/@revenuecat/purchases-capacitor'
+ pod 'RevenuecatPurchasesCapacitorUi', :path => '../../node_modules/@revenuecat/purchases-capacitor-ui'
pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area'
+ pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
end
target 'App' do
diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock
index 5ddce35..55ad0e8 100644
--- a/ios/App/Podfile.lock
+++ b/ios/App/Podfile.lock
@@ -10,15 +10,15 @@ PODS:
- GoogleUtilities/Environment (~> 8.0)
- GoogleUtilities/UserDefaults (~> 8.0)
- PromisesObjC (~> 2.4)
- - Capacitor (7.2.0):
+ - Capacitor (7.4.2):
- CapacitorCordova
- CapacitorApp (7.0.1):
- Capacitor
- - CapacitorCommunitySqlite (7.0.0):
+ - CapacitorCommunitySqlite (7.0.1):
- Capacitor
- SQLCipher
- ZIPFoundation
- - CapacitorCordova (7.2.0)
+ - CapacitorCordova (7.4.2)
- CapacitorDevice (7.0.1):
- Capacitor
- CapacitorLocalNotifications (7.0.1):
@@ -39,6 +39,8 @@ PODS:
- FBSDKCoreKit (= 17.4.0)
- FBSDKLoginKit (= 17.4.0)
- GoogleSignIn (~> 8.0.0)
+ - CordovaPlugins (7.4.2):
+ - CapacitorCordova
- FBAEMKit (17.4.0):
- FBSDKCoreKit_Basics (= 17.4.0)
- FBSDKCoreKit (17.4.0):
@@ -66,6 +68,20 @@ PODS:
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
- GTMSessionFetcher/Core (3.5.0)
- PromisesObjC (2.4.0)
+ - PurchasesHybridCommon (16.1.0):
+ - RevenueCat (= 5.33.1)
+ - PurchasesHybridCommonUI (16.1.0):
+ - PurchasesHybridCommon (= 16.1.0)
+ - RevenueCatUI (= 5.33.1)
+ - RevenueCat (5.33.1)
+ - RevenuecatPurchasesCapacitor (11.1.0):
+ - Capacitor
+ - PurchasesHybridCommon (= 16.1.0)
+ - RevenuecatPurchasesCapacitorUi (11.1.0):
+ - Capacitor
+ - PurchasesHybridCommonUI (= 16.1.0)
+ - RevenueCatUI (5.33.1):
+ - RevenueCat (= 5.33.1)
- SQLCipher (4.7.0):
- SQLCipher/standard (= 4.7.0)
- SQLCipher/common (4.7.0)
@@ -76,6 +92,7 @@ PODS:
DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
+ - "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorDevice (from `../../node_modules/@capacitor/device`)"
- "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)"
@@ -85,6 +102,9 @@ DEPENDENCIES:
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
- "CapgoCapacitorSocialLogin (from `../../node_modules/@capgo/capacitor-social-login`)"
+ - CordovaPlugins (from `../capacitor-cordova-ios-plugins`)
+ - "RevenuecatPurchasesCapacitor (from `../../node_modules/@revenuecat/purchases-capacitor`)"
+ - "RevenuecatPurchasesCapacitorUi (from `../../node_modules/@revenuecat/purchases-capacitor-ui`)"
SPEC REPOS:
trunk:
@@ -100,6 +120,10 @@ SPEC REPOS:
- GTMAppAuth
- GTMSessionFetcher
- PromisesObjC
+ - PurchasesHybridCommon
+ - PurchasesHybridCommonUI
+ - RevenueCat
+ - RevenueCatUI
- SQLCipher
- ZIPFoundation
@@ -108,6 +132,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/ios"
CapacitorApp:
:path: "../../node_modules/@capacitor/app"
+ CapacitorCommunitySqlite:
+ :path: "../../node_modules/@capacitor-community/sqlite"
CapacitorCordova:
:path: "../../node_modules/@capacitor/ios"
CapacitorDevice:
@@ -126,15 +152,21 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/status-bar"
CapgoCapacitorSocialLogin:
:path: "../../node_modules/@capgo/capacitor-social-login"
+ CordovaPlugins:
+ :path: "../capacitor-cordova-ios-plugins"
+ RevenuecatPurchasesCapacitor:
+ :path: "../../node_modules/@revenuecat/purchases-capacitor"
+ RevenuecatPurchasesCapacitorUi:
+ :path: "../../node_modules/@revenuecat/purchases-capacitor-ui"
SPEC CHECKSUMS:
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
- Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7
+ Capacitor: 9d9e481b79ffaeacaf7a85d6a11adec32bd33b59
CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
- CapacitorCommunitySqlite: b11e556cf5d149f9e0b6fbbb47959a6cd718c3d0
- CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f
+ CapacitorCommunitySqlite: 8b2c6bab33e3519280811d481f8bd0fa90343e1b
+ CapacitorCordova: 5e58d04631bc5094894ac106e2bf1da18a9e6151
CapacitorDevice: c6f6d587dd310527f8a48bf09c4e7b4a4cf14329
CapacitorLocalNotifications: c2212755b33d2513b8bb325096ce3a64d4039ae3
CapacitorNetwork: 15cb4385f0913a8ceb5e9a4d7af1ec554bdb8de8
@@ -143,6 +175,7 @@ SPEC CHECKSUMS:
CapacitorPushNotifications: 6a2794788c583dd89215f1805ca4bced1b13dbdf
CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f
CapgoCapacitorSocialLogin: 0daa087f32775c656fbfba09b8eae7c089b85ce2
+ CordovaPlugins: b2618e17f8dd580d2354710b7b52b0af9d6f03f2
FBAEMKit: 58cb5f302cdd715a56d4c1d0dfdd2e423ac1421a
FBSDKCoreKit: 94d7461d0cecf441b1ba7c41acfff41daa8ccd41
FBSDKCoreKit_Basics: 151b43db8b834d3f0e02f95d36a44ffd36265e45
@@ -152,9 +185,15 @@ SPEC CHECKSUMS:
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
+ PurchasesHybridCommon: acbd336f8248da5599893ecf9ac32c561a2fd446
+ PurchasesHybridCommonUI: cf20e6ac6f148385bdb6a15db38c4c0c7377f480
+ RevenueCat: b0ed01125b05a45b8264a2951ad68acb61942038
+ RevenuecatPurchasesCapacitor: 554b5d4711cac8df01feed6333b075296b5d9082
+ RevenuecatPurchasesCapacitorUi: 7863f7e39e940d937925b085a67369feecb048e4
+ RevenueCatUI: e6fad6a8bec67d910da78cd171328f1104afea77
SQLCipher: ba9d0076041ed767c5bd3d3f77098318d04a403c
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
-PODFILE CHECKSUM: 54ec5ec72c1c5e3e8d03bd96cc1ec0deaf577d9e
+PODFILE CHECKSUM: 8a9fb4ff57aa677972b999357aa5abe97d1f2b30
-COCOAPODS: 1.15.2
+COCOAPODS: 1.16.2
diff --git a/package.json b/package.json
index 094d40a..d308fca 100644
--- a/package.json
+++ b/package.json
@@ -51,12 +51,17 @@
"@mui/joy": "^5.0.0-beta.20",
"@mui/material": "^5.15.2",
"@openreplay/tracker": "^14.0.4",
+ "@revenuecat/purchases-capacitor": "^11.1.0",
+ "@revenuecat/purchases-capacitor-ui": "^11.1.0",
+ "@rollup/rollup-darwin-arm64": "^4.46.1",
"@swc/core": "^1.12.5",
+ "@swc/core-darwin-arm64": "^1.13.3",
"@tanstack/react-query": "^5.17.0",
"aos": "^2.3.4",
"browser-image-compression": "^2.0.2",
"capacitor-plugin-safe-area": "^4.0.0",
"chrono-node": "^2.7.7",
+ "cordova-plugin-purchase": "^13.12.1",
"dotenv": "^16.4.5",
"esm": "^3.2.25",
"event-source-polyfill": "^1.0.31",
diff --git a/src/components/SubscriptionModal.jsx b/src/components/SubscriptionModal.jsx
new file mode 100644
index 0000000..43bb2d3
--- /dev/null
+++ b/src/components/SubscriptionModal.jsx
@@ -0,0 +1,259 @@
+import { Check, Star } from '@mui/icons-material'
+import {
+ Box,
+ Button,
+ Card,
+ Chip,
+ Divider,
+ Modal,
+ ModalDialog,
+ Radio,
+ Typography,
+} from '@mui/joy'
+import { useState } from 'react'
+import { useNotification } from '../service/NotificationProvider'
+import { GetSubscriptionSession } from '../utils/Fetcher'
+
+const SubscriptionModal = ({ open, onClose }) => {
+ const [selectedPlan, setSelectedPlan] = useState('yearly')
+ const [isLoading, setIsLoading] = useState(false)
+ const { showError } = useNotification()
+
+ const plans = {
+ yearly: {
+ price: '$39.00',
+ period: 'year',
+ total: '$39.00/year',
+ // savings: 'Save $20.88',
+ // popular: true,
+ },
+ // monthly: {
+ // price: '$4.99',
+ // period: 'month',
+ // total: '$4.99/month',
+ // savings: null,
+ // },
+ }
+
+ const features = [
+ 'Task notifications and reminders',
+ 'Rich text descriptions with images uploads',
+ 'Thing-based task triggers',
+ 'API tokens for integrations',
+ 'Image uploads in descriptions',
+ 'Advanced task automation',
+ // 'Unlimited task history',
+ // 'Unlimited things history',
+ ]
+
+ const handleSubscribe = async () => {
+ setIsLoading(true)
+ try {
+ // Call the backend with the selected plan
+ const response = await GetSubscriptionSession()
+
+ if (!response.ok) {
+ throw new Error('Failed to create subscription session')
+ }
+
+ const data = await response.json()
+
+ // Redirect to Stripe
+ if (data.sessionURL) {
+ window.location.href = data.sessionURL
+ } else {
+ throw new Error('No session URL received')
+ }
+ } catch (error) {
+ console.error('Subscription error:', error)
+ showError({
+ title: 'Subscription Error',
+ message: 'Failed to start subscription process. Please try again.',
+ })
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+
+
+
+ {/* Header */}
+
+
+ Upgrade to Plus
+
+
+
+ {/* Features List */}
+
+
+ What's included:
+
+
+ {features.map((feature, index) => (
+
+
+ {feature}
+
+ ))}
+
+
+
+
+ {/* Plan Selection */}
+
+ {Object.entries(plans).map(([key, plan]) => (
+ setSelectedPlan(key)}
+ sx={{
+ width: '100%',
+ minHeight: 48,
+ maxHeight: 64,
+ cursor: 'pointer',
+ transition: 'all 0.2s',
+ mb: 0.2,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ px: 2.5,
+ py: 1.2,
+ position: 'relative',
+ overflow: 'visible',
+ }}
+ >
+
+ setSelectedPlan(key)}
+ value={key}
+ name='subscription-plan'
+ color='primary'
+ sx={{ mr: 1 }}
+ />
+
+ {key.charAt(0).toUpperCase() + key.slice(1)}
+
+
+ {plan.price}
+
+ {' '}
+ / {plan.period}
+
+
+
+
+ {plan.popular && (
+ }
+ sx={{
+ fontWeight: 600,
+ fontSize: 12,
+ px: 1,
+ py: 0.1,
+ boxShadow: 2,
+ mt: 0.8,
+ }}
+ >
+ Most Popular
+
+ )}
+ {plan.savings && (
+
+ {plan.savings}
+
+ )}
+
+
+ ))}
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+ {/* Footer */}
+
+ Cancel anytime. No hidden fees. Secure payment powered by Stripe.
+
+
+
+
+ )
+}
+
+export default SubscriptionModal
diff --git a/src/utils/Colors.jsx b/src/utils/Colors.jsx
index 987d830..4e93d73 100644
--- a/src/utils/Colors.jsx
+++ b/src/utils/Colors.jsx
@@ -27,7 +27,6 @@ const LABEL_COLORS = [
]
export const COLORS = {
- white: '#FFFFFF',
salmon: '#ff7961',
teal: '#26a69a',
skyBlue: '#80d8ff',
@@ -52,6 +51,7 @@ export const COLORS = {
blush: '#f8bbd0',
ash: '#90a4ae',
sand: '#d7ccc8',
+ white: '#FFFFFF',
}
export const TASK_COLOR = {
diff --git a/src/utils/Fetcher.jsx b/src/utils/Fetcher.jsx
index ca4d8eb..149ba7e 100644
--- a/src/utils/Fetcher.jsx
+++ b/src/utils/Fetcher.jsx
@@ -596,14 +596,61 @@ const ClearChoreTimer = choreId => {
})
}
+const CheckUserDeletion = (password) => {
+ return Fetch(`/users/delete/check`, {
+ method: 'POST',
+ headers: HEADERS(),
+ body: JSON.stringify({
+ password,
+ }),
+ })
+}
+
+const DeleteUser = (password, confirmation, transferOptions = []) => {
+ return Fetch(`/users/delete`, {
+ method: 'DELETE',
+ headers: HEADERS(),
+ body: JSON.stringify({
+ password,
+ confirmation,
+ transferOptions,
+ }),
+ })
+}
+
+const CreateBackup = (encryptionKey, includeAssets = true, backupName = '') => {
+ return Fetch(`/backup/create`, {
+ method: 'POST',
+ headers: HEADERS(),
+ body: JSON.stringify({
+ encryption_key: encryptionKey,
+ include_assets: includeAssets,
+ backup_name: backupName,
+ }),
+ })
+}
+
+const RestoreBackup = (encryptionKey, backupData) => {
+ return Fetch(`/backup/restore`, {
+ method: 'POST',
+ headers: HEADERS(),
+ body: JSON.stringify({
+ encryption_key: encryptionKey,
+ backup_data: backupData,
+ }),
+ })
+}
+
export {
AcceptCircleMemberRequest,
ArchiveChore,
CancelSubscription,
ChangePassword,
+ CheckUserDeletion,
ClearChoreTimer,
CompleteSubTask,
ConfirmMFA,
+ CreateBackup,
CreateChore,
CreateLabel,
CreateLongLiveToken,
@@ -615,6 +662,7 @@ export {
DeleteLongLiveToken,
DeleteThing,
DeleteTimeSession,
+ DeleteUser,
DisableMFA,
GetAllCircleMembers,
GetAllUsers,
@@ -648,6 +696,7 @@ export {
RegenerateBackupCodes,
ResetChoreTimer,
ResetPassword,
+ RestoreBackup,
SaveChore,
SaveThing,
SetupMFA,
diff --git a/src/views/Authorization/LoginView.jsx b/src/views/Authorization/LoginView.jsx
index 36e1655..da5f8ff 100644
--- a/src/views/Authorization/LoginView.jsx
+++ b/src/views/Authorization/LoginView.jsx
@@ -1,7 +1,9 @@
import { Capacitor } from '@capacitor/core'
+import { Device } from '@capacitor/device'
// import { GoogleAuth } from '@codetrix-studio/capacitor-google-auth'
import { SocialLogin } from '@capgo/capacitor-social-login'
import { Settings } from '@mui/icons-material'
+import AppleIcon from '@mui/icons-material/Apple'
import GoogleIcon from '@mui/icons-material/Google'
import {
Avatar,
@@ -35,6 +37,7 @@ const LoginView = () => {
const [password, setPassword] = useState('')
const [mfaModalOpen, setMfaModalOpen] = useState(false)
const [mfaSessionToken, setMfaSessionToken] = useState('')
+ const [isAppleSignInSupported, setIsAppleSignInSupported] = useState(false)
const { data: resource } = useResource()
const { showError } = useNotification()
const Navigate = useNavigate()
@@ -47,6 +50,21 @@ const LoginView = () => {
mode: 'online', // replaces grantOfflineAccess
},
})
+
+ // Check if Apple Sign In is supported (iOS 13+)
+ if (Capacitor.isNativePlatform()) {
+ try {
+ const deviceInfo = await Device.getInfo()
+ if (deviceInfo.platform === 'ios') {
+ const majorVersion = parseInt(deviceInfo.osVersion.split('.')[0])
+ setIsAppleSignInSupported(majorVersion >= 13)
+ }
+ } catch (error) {
+ console.log(
+ 'Could not determine device info for Apple Sign In support',
+ )
+ }
+ }
}
initializeSocialLogin()
}, [])
@@ -127,6 +145,12 @@ const LoginView = () => {
} else if (data['accessToken']) {
// data["accessToken"] is for Google Capacitor
return data['accessToken']['token']
+ } else if (data['response'] && data['response']['id_token']) {
+ // Apple Sign In returns id_token in response
+ return data['response']['id_token']
+ } else if (data['id_token']) {
+ // Direct id_token for Apple
+ return data['id_token']
}
}
@@ -165,9 +189,10 @@ const LoginView = () => {
})
}
return response.json().then(() => {
+ const providerName = provider === 'apple' ? 'Apple' : 'Google'
showError({
- title: 'Google Login Failed',
- message: "Couldn't log in with Google, please try again",
+ title: `${providerName} Login Failed`,
+ message: `Couldn't log in with ${providerName}, please try again`,
})
})
})
@@ -447,8 +472,50 @@ const LoginView = () => {
+
+ {/* */}
)}
+
{Capacitor.isNativePlatform() && (
+
+ {/* Apple Sign In Button for Native Platforms */}
+ {isAppleSignInSupported && (
+
+ )}
)}
>
diff --git a/src/views/ChoreEdit/ChoreView.jsx b/src/views/ChoreEdit/ChoreView.jsx
index 7a7f89c..abe83a2 100644
--- a/src/views/ChoreEdit/ChoreView.jsx
+++ b/src/views/ChoreEdit/ChoreView.jsx
@@ -694,23 +694,17 @@ const ChoreView = () => {
/>
{note !== null && (
- {
- if (e.target.value.trim() === '') {
- setNote(null)
- return
- }
- setNote(e.target.value)
- }}
- sx={{
- mb: 1,
- }}
- />
+
+
+ Additional Notes:
+
+
+
)}
diff --git a/src/views/History/ChoreHistory.jsx b/src/views/History/ChoreHistory.jsx
index 3259a30..2bc3dbf 100644
--- a/src/views/History/ChoreHistory.jsx
+++ b/src/views/History/ChoreHistory.jsx
@@ -1,20 +1,27 @@
-import { Checklist, EventBusy, Group, Timelapse } from '@mui/icons-material'
import {
- Avatar,
+ Analytics,
+ Checklist,
+ EventBusy,
+ Group,
+ Star,
+ Timelapse,
+ TrendingUp,
+} from '@mui/icons-material'
+import {
+ Box,
Button,
- Chip,
+ Card,
+ CardContent,
Container,
Grid,
List,
- ListItem,
- ListItemContent,
Sheet,
Typography,
} from '@mui/joy'
import moment from 'moment'
import { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom'
-import { LoadingScreen, SmoothCard } from '../../components/animations'
+import { LoadingScreen } from '../../components/animations'
import {
DeleteChoreHistory,
GetAllCircleMembers,
@@ -102,39 +109,38 @@ const ChoreHistory = () => {
subtext: `${histories.length} times`,
},
{
- icon: ,
- text: 'Usually Within',
+ icon: ,
+ text: 'Average Timing',
subtext: moment.duration(averageDelayMoment).isValid()
? moment.duration(averageDelayMoment).humanize()
- : '--',
+ : 'On time',
},
{
icon: ,
text: 'Maximum Delay',
subtext: moment.duration(maxDelayMoment).isValid()
? moment.duration(maxDelayMoment).humanize()
- : '--',
+ : 'Never late',
},
{
- icon: ,
- text: ' Completed Most',
+ icon: ,
+ text: 'Top Performer',
subtext: `${
performers.find(p => p.userId === Number(userCompletedByMost))
- ?.displayName
- } `,
+ ?.displayName || 'Unknown'
+ }`,
},
- // contributes:
{
icon: ,
- text: 'Total Performers',
- subtext: `${Object.keys(userHistories).length} users`,
+ text: 'Team Members',
+ subtext: `${Object.keys(userHistories).length} active`,
},
{
- icon: ,
- text: 'Last Completed',
+ icon: ,
+ text: 'Last Completed By',
subtext: `${
performers.find(p => p.userId === Number(histories[0].completedBy))
- ?.displayName
+ ?.displayName || 'Unknown'
}`,
},
]
@@ -183,42 +189,67 @@ const ChoreHistory = () => {
return (
-
- Summary:
-
-
-
+ {/* Enhanced Header Section */}
+
+ {/* Statistics Cards Grid */}
+
{historyInfo.map((info, index) => (
-
- {/* divider between the list items: */}
-
-
-
-
- {info.text}
-
-
- {info.subtext ? info.subtext : '--'}
-
-
-
+
+
+
+
+ {info.icon}
+
+ {info.text}
+
+
+
+
+ {info.subtext || '--'}
+
+
+
+
))}
-
+
- {/* User History Cards */}
-
- History:
-
+ {/* History Section Header */}
+
+
+
+ Completion History
+
+
{/* Chore History List (Updated Style) */}
diff --git a/src/views/Modals/Inputs/UserDeletionModal.jsx b/src/views/Modals/Inputs/UserDeletionModal.jsx
new file mode 100644
index 0000000..b48e14f
--- /dev/null
+++ b/src/views/Modals/Inputs/UserDeletionModal.jsx
@@ -0,0 +1,365 @@
+import {
+ Box,
+ Button,
+ Card,
+ CircularProgress,
+ FormControl,
+ FormLabel,
+ Input,
+ Option,
+ Select,
+ Typography,
+} from '@mui/joy'
+import { data } from 'autoprefixer'
+import { useCallback, useEffect, useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+import FadeModal from '../../../components/common/FadeModal'
+import { CheckUserDeletion, DeleteUser } from '../../../utils/Fetcher'
+
+function UserDeletionModal({ isOpen, onClose, userProfile }) {
+ const Navigate = useNavigate()
+ const [step, setStep] = useState(1) // 1: Warning, 2: Transfer, 3: Confirm
+ const [password, setPassword] = useState('')
+ const [confirmation, setConfirmation] = useState('')
+ const [transferOptions, setTransferOptions] = useState([])
+ const [circlesRequiringTransfer, setCirclesRequiringTransfer] = useState([])
+ const [availableMembers, setAvailableMembers] = useState([])
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+
+ const resetModal = useCallback(() => {
+ setStep(1)
+ setPassword('')
+ setConfirmation('')
+ setTransferOptions([])
+ setCirclesRequiringTransfer([])
+ setAvailableMembers([])
+ setError('')
+ }, [])
+
+ const handleClose = useCallback(
+ success => {
+ resetModal()
+ onClose(success)
+ },
+ [onClose, resetModal],
+ )
+
+ const checkDeletionRequirements = async () => {
+ if (password.trim() === '') {
+ setError('Please enter your password to continue')
+ return
+ }
+
+ setLoading(true)
+ setError('')
+ try {
+ const response = await CheckUserDeletion(password)
+ const data = await response.json()
+
+ if (response.ok) {
+ if (data.requiresTransfer && data.circles) {
+ setCirclesRequiringTransfer(data.circles)
+ setAvailableMembers(data.availableMembers || [])
+ setStep(2)
+ } else {
+ setStep(3)
+ }
+ } else {
+ setError(data.error || 'Failed to check deletion requirements')
+ }
+ } catch (err) {
+ setError(data.error || 'Failed to check deletion requirements')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleTransferSelection = (circleId, newOwnerId, newOwnerName) => {
+ setTransferOptions(prev => {
+ const existing = prev.find(t => t.circleId === circleId)
+ if (existing) {
+ return prev.map(t =>
+ t.circleId === circleId ? { ...t, newOwnerId, newOwnerName } : t,
+ )
+ } else {
+ return [...prev, { circleId, newOwnerId, newOwnerName }]
+ }
+ })
+ }
+
+ const proceedToConfirmation = () => {
+ if (circlesRequiringTransfer.length === transferOptions.length) {
+ setStep(3)
+ }
+ }
+
+ const executeUserDeletion = async () => {
+ if (password.trim() === '' || confirmation !== 'DELETE') {
+ setError('Please enter your password and type DELETE to confirm')
+ return
+ }
+
+ setLoading(true)
+ setError('')
+ try {
+ const response = await DeleteUser(password, confirmation, transferOptions)
+ const data = await response.json()
+ console.log(response)
+
+ if (response.status === 200) {
+ // Clear authentication tokens
+ localStorage.removeItem('ca_token')
+ localStorage.removeItem('ca_expiration')
+ Navigate('/login', { replace: true })
+ handleClose(true)
+ // Redirect to login or home page after successful deletion
+ } else {
+ setError(data.message || 'Failed to delete account')
+ }
+ } catch (err) {
+ setError('Failed to delete account')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // Keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = event => {
+ if (!isOpen) return
+
+ if (event.key === 'Escape') {
+ event.preventDefault()
+ handleClose(false)
+ return
+ }
+ }
+
+ if (isOpen) {
+ document.addEventListener('keydown', handleKeyDown)
+ }
+
+ return () => {
+ document.removeEventListener('keydown', handleKeyDown)
+ }
+ }, [isOpen, handleClose])
+
+ const renderWarningStep = () => (
+ <>
+
+ Delete Account
+
+
+
+ This action cannot be undone. Deleting your account
+ will permanently remove:
+
+
+
+
+ • Your user profile and authentication data
+
+
+ • All your chores, chore history, and time tracking sessions
+
+
+ • API tokens, MFA sessions, and password reset tokens
+
+
+ • Storage files and usage data
+
+
+ • Points history and notifications
+
+
+ • Circle memberships and relationships
+
+
+
+
+ Enter your password to continue
+ setPassword(e.target.value)}
+ placeholder='Enter your password'
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ >
+ )
+
+ const renderTransferStep = () => (
+ <>
+
+ Circle Ownership Transfer Required
+
+
+
+ You own circles that require ownership transfer before deletion. Please
+ select new owners:
+
+
+ {circlesRequiringTransfer.map(circle => (
+
+
+ Circle: {circle.name}
+
+
+ New Owner
+
+
+
+ ))}
+
+
+
+
+
+ >
+ )
+
+ const renderConfirmationStep = () => (
+ <>
+
+ Final Confirmation
+
+
+
+ Please enter your password and type DELETE to confirm
+ account deletion.
+
+
+ on successful deletion, you will be logged out and redirected to the
+ login page.
+
+
+
+ Password
+ setPassword(e.target.value)}
+ placeholder='Enter your password'
+ />
+
+
+
+ Type "DELETE" to confirm
+ setConfirmation(e.target.value)}
+ placeholder='DELETE'
+ />
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+ >
+ )
+
+ const renderStep = () => {
+ switch (step) {
+ case 1:
+ return renderWarningStep()
+ case 2:
+ return renderTransferStep()
+ case 3:
+ return renderConfirmationStep()
+ default:
+ return renderWarningStep()
+ }
+ }
+
+ return (
+ handleClose(false)}
+ size='md'
+ unmountDelay={250}
+ >
+ {loading && step === 1 ? (
+
+
+
+ ) : (
+ renderStep()
+ )}
+
+ )
+}
+
+export default UserDeletionModal
diff --git a/src/views/Settings/Settings.jsx b/src/views/Settings/Settings.jsx
index e41cb9c..fd37852 100644
--- a/src/views/Settings/Settings.jsx
+++ b/src/views/Settings/Settings.jsx
@@ -1,3 +1,4 @@
+import { Capacitor } from '@capacitor/core'
import {
Box,
Button,
@@ -14,9 +15,11 @@ import {
Select,
Typography,
} from '@mui/joy'
+import { Purchases } from '@revenuecat/purchases-capacitor'
import moment from 'moment'
import { useEffect, useState } from 'react'
import RealTimeSettings from '../../components/RealTimeSettings'
+import SubscriptionModal from '../../components/SubscriptionModal'
import Logo from '../../Logo'
import { useUserProfile } from '../../queries/UserQueries'
import { useNotification } from '../../service/NotificationProvider'
@@ -26,7 +29,6 @@ import {
DeleteCircleMember,
GetAllCircleMembers,
GetCircleMemberRequests,
- GetSubscriptionSession,
GetUserCircle,
JoinCircle,
LeaveCircle,
@@ -38,6 +40,7 @@ import { isPlusAccount } from '../../utils/Helpers'
import LoadingComponent from '../components/Loading'
import ConfirmationModal from '../Modals/Inputs/ConfirmationModal'
import PassowrdChangeModal from '../Modals/Inputs/PasswordChangeModal'
+import UserDeletionModal from '../Modals/Inputs/UserDeletionModal'
import APITokenSettings from './APITokenSettings'
import MFASettings from './MFASettings'
import NotificationSetting from './NotificationSetting'
@@ -58,6 +61,8 @@ const Settings = () => {
const [isAdmin, setIsAdmin] = useState(false)
const [changePasswordModal, setChangePasswordModal] = useState(false)
+ const [subscriptionModal, setSubscriptionModal] = useState(false)
+ const [userDeletionModal, setUserDeletionModal] = useState(false)
const [confirmModalConfig, setConfirmModalConfig] = useState({})
const showConfirmation = (
@@ -127,7 +132,7 @@ const Settings = () => {
return `You are currently subscribed to the Plus plan. Your subscription will renew on ${moment(
userProfile?.expiration,
).format('MMM DD, YYYY')}.`
- } else if (userProfile?.subscription === 'canceled') {
+ } else if (userProfile?.subscription === 'cancelled') {
return `You have cancelled your subscription. Your account will be downgraded to the Free plan on ${moment(
userProfile?.expiration,
).format('MMM DD, YYYY')}.`
@@ -138,7 +143,7 @@ const Settings = () => {
const getSubscriptionStatus = () => {
if (userProfile?.subscription === 'active') {
return `Plus`
- } else if (userProfile?.subscription === 'canceled') {
+ } else if (userProfile?.subscription === 'cancelled') {
if (moment().isBefore(userProfile?.expiration)) {
return `Plus(until ${moment(userProfile?.expiration).format(
'MMM DD, YYYY',
@@ -594,17 +599,45 @@ const Settings = () => {
}}
disabled={
userProfile?.subscription === 'active' ||
- moment(userProfile?.expiration).isAfter(moment())
+ (moment(userProfile?.expiration).isAfter(moment()) &&
+ userProfile?.subscription !== 'cancelled')
}
- onClick={() => {
- GetSubscriptionSession().then(data => {
- data.json().then(data => {
- console.log(data)
- window.location.href = data.sessionURL
- // open in new window:
- // window.open(data.sessionURL, '_blank')
- })
- })
+ onClick={async () => {
+ if (Capacitor.isNativePlatform()) {
+ try {
+ 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']) {
+ showNotification({
+ type: 'success',
+ message:
+ 'Purchase successful! Please restart the app to access Plus features.',
+ })
+ }
+ } catch (error) {
+ if (error.code !== '1') {
+ // User cancelled
+ showNotification({
+ type: 'error',
+ message: 'Purchase failed. Please try again.',
+ })
+ }
+ }
+ } else {
+ setSubscriptionModal(true)
+ }
}}
>
Upgrade
@@ -674,6 +707,23 @@ const Settings = () => {
) : null}
)}
+
+
+
+ Danger Zone
+
+
+ Once you delete your account, there is no going back. Please be
+ certain.
+
+
+
@@ -693,6 +743,25 @@ const Settings = () => {
{confirmModalConfig?.isOpen && (
)}
+
+ setSubscriptionModal(false)}
+ />
+
+ {
+ setUserDeletionModal(false)
+ if (success) {
+ showNotification({
+ type: 'success',
+ message: 'Account deleted successfully',
+ })
+ }
+ }}
+ userProfile={userProfile}
+ />
)
}
diff --git a/src/views/Things/ThingsHistory.jsx b/src/views/Things/ThingsHistory.jsx
index 12fb591..dfdf562 100644
--- a/src/views/Things/ThingsHistory.jsx
+++ b/src/views/Things/ThingsHistory.jsx
@@ -1,8 +1,20 @@
-import { EventBusy, Schedule, TrendingUp } from '@mui/icons-material'
+import {
+ Analytics,
+ BarChart,
+ CallReceived,
+ EventBusy,
+ Schedule,
+ Speed,
+ Timeline,
+ TrendingUp,
+ Update,
+} from '@mui/icons-material'
import {
Avatar,
Box,
Button,
+ Card,
+ CardContent,
Chip,
Container,
Grid,
@@ -10,8 +22,10 @@ import {
ListDivider,
ListItem,
ListItemContent,
+ Stack,
Typography,
} from '@mui/joy'
+import { useTheme } from '@mui/joy/styles'
import moment from 'moment'
import { Link, useParams } from 'react-router-dom'
import {
@@ -22,7 +36,6 @@ import {
XAxis,
YAxis,
} from 'recharts'
-import { useTheme } from '@mui/joy/styles'
import { useThingHistory } from '../../queries/ThingQueries'
import LoadingComponent from '../components/Loading'
@@ -41,6 +54,74 @@ const ThingsHistory = () => {
// Flatten all pages of history data
const thingsHistory = data?.pages.flatMap(page => page.res) || []
+ // Calculate analytics data
+ const calculateAnalytics = () => {
+ if (!thingsHistory.length) return []
+
+ // Calculate average update frequency
+ let avgUpdateFrequency = '--'
+ if (thingsHistory.length > 1) {
+ const oldestUpdate = moment(
+ thingsHistory[thingsHistory.length - 1].createdAt,
+ )
+ const newestUpdate = moment(thingsHistory[0].createdAt)
+ const totalDuration = newestUpdate.diff(oldestUpdate, 'hours')
+ const frequency = totalDuration / (thingsHistory.length - 1)
+ avgUpdateFrequency =
+ frequency < 1
+ ? `${Math.round(frequency * 60)} minutes`
+ : frequency < 24
+ ? `${Math.round(frequency)} hours`
+ : `${Math.round(frequency / 24)} days`
+ }
+
+ const lastUpdated = thingsHistory[0]
+ ? moment(thingsHistory[0].updatedAt).fromNow()
+ : '--'
+
+ // Calculate update trend value
+ let updateTrend = '--'
+ if (thingsHistory.length >= 3) {
+ const diffs = thingsHistory
+ .map((h, i, arr) =>
+ i < arr.length - 1
+ ? moment(h.createdAt).diff(arr[i + 1].createdAt, 'minutes')
+ : null,
+ )
+ .filter(d => d !== null)
+ const last = diffs[0]
+ const prev = diffs[1]
+ if (last > prev) updateTrend = 'Interval increasing'
+ else if (last < prev) updateTrend = 'Interval decreasing'
+ else updateTrend = 'Interval stable'
+ }
+
+ return [
+ {
+ icon: ,
+ text: 'Update Frequency',
+ subtext: `Every ${avgUpdateFrequency}`,
+ },
+ {
+ icon: ,
+ text: 'Last Updated',
+ subtext: lastUpdated,
+ },
+ {
+ icon: ,
+ text: 'Last Value',
+ subtext: thingsHistory[0]?.state ?? '--',
+ },
+ {
+ icon: ,
+ text: 'Update Trend',
+ subtext: updateTrend,
+ },
+ ]
+ }
+
+ const analyticsData = calculateAnalytics()
+
const handleLoadMore = () => {
fetchNextPage()
}
@@ -106,71 +187,149 @@ const ThingsHistory = () => {
return (
-
- History:
-
+ {/* Enhanced Analytics Header Section */}
+
+
+
+
+
+ Things Details
+
+
+ Quick overview of the thing's history and analytics
+
+
+
+
+ {/* Statistics Cards Grid */}
+
+ {analyticsData.map((info, index) => (
+
+
+
+
+ {info.icon}
+
+ {info.text}
+
+
+
+
+ {info.subtext || '--'}
+
+
+
+
+
+ ))}
+
+
+
+ {/* Chart Section Header */}
+ {thingsHistory.every(history => !isNaN(history.state)) &&
+ thingsHistory.length > 1 && (
+
+
+
+ Data Visualization
+
+
+ )}
{/* check if all the states are number the show it: */}
{thingsHistory.every(history => !isNaN(history.state)) &&
thingsHistory.length > 1 && (
- <>
-
- Chart:
-
+
+
+
+ {/* */}
+
+ moment(tick).format('ddd MM/DD/yyyy HH:mm:ss')
+ }
+ />
+
+
+ moment(label).format('ddd MM/DD/yyyy HH:mm:ss')
+ }
+ />
-
-
-
- {/* */}
-
- moment(tick).format('ddd MM/DD/yyyy HH:mm:ss')
- }
- />
-
-
- moment(label).format('ddd MM/DD/yyyy HH:mm:ss')
- }
- />
-
-
-
-
-
- >
+
+
+
+
)}
-
- Change log:
-
+
+ {/* History Section Header */}
+
+
+
+ Change History
+
+
{thingsHistory.map((history, index) => (
diff --git a/src/views/User/UserActivities.jsx b/src/views/User/UserActivities.jsx
index 0d4d16c..90788cf 100644
--- a/src/views/User/UserActivities.jsx
+++ b/src/views/User/UserActivities.jsx
@@ -3,7 +3,7 @@ import CheckCircleIcon from '@mui/icons-material/CheckCircle'
import CircleIcon from '@mui/icons-material/Circle'
import { Cell, Pie, PieChart, Tooltip } from 'recharts'
-import { EventBusy, Group, Toll } from '@mui/icons-material'
+import { EventBusy, Group, Timeline, Toll } from '@mui/icons-material'
import {
Avatar,
Box,
@@ -94,9 +94,12 @@ const ChoreHistoryTimeline = ({ history }) => {
return (
-
- Activities Timeline
-
+
+
+
+ Activities Timeline
+
+
{Object.entries(groupedHistory).map(([date, items]) => (
@@ -785,6 +788,7 @@ const UserActivites = () => {
)
}
+ // Calculate activities analytics
return (
{
px: { xs: 2, sm: 3 },
}}
>
-
- Activities Overview
-
-
{/* Main Content Area - Mobile: Stack vertically, Desktop: Side by side */}
{
{/* Main Chart */}
diff --git a/src/views/User/UserPoints.jsx b/src/views/User/UserPoints.jsx
index cab716d..ba092ff 100644
--- a/src/views/User/UserPoints.jsx
+++ b/src/views/User/UserPoints.jsx
@@ -7,14 +7,29 @@ import {
YAxis,
} from 'recharts'
-import { CreditCard, Toll } from '@mui/icons-material'
+import {
+ AccountBalanceWallet,
+ Analytics,
+ CreditCard,
+ EmojiEvents,
+ MilitaryTech,
+ Redeem,
+ Star,
+ SwapHoriz,
+ Timeline,
+ Toll,
+ TrendingUp,
+ WorkspacePremium,
+} from '@mui/icons-material'
import {
Avatar,
Box,
Button,
Card,
+ CardContent,
Chip,
Container,
+ Grid,
Option,
Select,
Stack,
@@ -34,6 +49,7 @@ import RedeemPointsModal from '../Modals/RedeemPointsModal'
const UserPoints = () => {
const [tabValue, setTabValue] = useState(7)
const [isRedeemModalOpen, setIsRedeemModalOpen] = useState(false)
+ const [leaderboardMode, setLeaderboardMode] = useState('points') // 'points' or 'tasks'
const {
data: circleMembersData,
@@ -206,24 +222,371 @@ const UserPoints = () => {
return
}
+ // Calculate leaderboard data for the current time period
+ const calculateLeaderboard = () => {
+ if (!choresHistoryData || !circleUsers.length) return []
+
+ // Calculate points for each user in the current time period
+ const userPeriodStats = {}
+
+ // Initialize stats for all users
+ circleUsers.forEach(user => {
+ userPeriodStats[user.userId] = {
+ userId: user.userId,
+ displayName: user.displayName,
+ image: user.image,
+ totalPoints: user.points || 0,
+ availablePoints: (user.points || 0) - (user.pointsRedeemed || 0),
+ periodPoints: 0,
+ periodTasks: 0,
+ }
+ })
+
+ // Calculate period-specific stats from history
+ choresHistoryData.forEach(historyEntry => {
+ const userId = historyEntry.completedBy
+ if (userPeriodStats[userId]) {
+ userPeriodStats[userId].periodPoints += historyEntry.points || 0
+ userPeriodStats[userId].periodTasks += 1
+ }
+ })
+
+ // Convert to array and sort by selected mode
+ const sortField =
+ leaderboardMode === 'points' ? 'periodPoints' : 'periodTasks'
+ return Object.values(userPeriodStats)
+ .sort((a, b) => b[sortField] - a[sortField])
+ .map((user, index) => ({
+ ...user,
+ rank: index + 1,
+ avgPointsPerTask:
+ user.periodTasks > 0
+ ? (user.periodPoints / user.periodTasks).toFixed(1)
+ : 0,
+ }))
+ }
+
+ const leaderboardData = calculateLeaderboard()
+
+ // Get trophy icons for top 3
+ const getTrophyIcon = rank => {
+ switch (rank) {
+ case 1:
+ return // Gold
+ case 2:
+ return (
+
+ ) // Silver
+ case 3:
+ return // Bronze
+ default:
+ return
+ }
+ }
+
return (
-
- Points Overview
-
+ {/* Enhanced Leaderboard Header Section */}
+
+
+ {/* Title Row */}
+
+
+
+
+ {leaderboardMode === 'points' ? 'Points' : 'Tasks'} Leaderboard
+
+
+ Rankings based on{' '}
+ {leaderboardMode === 'points'
+ ? 'points earned'
+ : 'tasks completed'}{' '}
+ during the selected time period
+
+
+
+
+ {/* Filters Row - Responsive */}
+
+ {/* Time Period Filter */}
+
+ {
+ setTabValue(tabValue)
+ handleChoresHistoryLimitChange(tabValue)
+ }}
+ value={tabValue}
+ size='sm'
+ sx={{
+ borderRadius: 6,
+ backgroundColor: 'background.surface',
+ border: '1px solid',
+ borderColor: 'divider',
+ }}
+ >
+
+ {[
+ { label: '7D', value: 7 },
+ { label: '6M', value: 6 * 30 },
+ { label: 'All', value: 24 * 30 },
+ ].map((tab, index) => (
+
+ {tab.label}
+
+ ))}
+
+
+
+
+ {/* Toggle between points and tasks */}
+
+ setLeaderboardMode('points')}
+ >
+ Points
+
+
+ setLeaderboardMode('tasks')}
+ >
+ Tasks
+
+
+
+
+
+ {/* Leaderboard Cards */}
+
+
+ {leaderboardData.map((user, index) => (
+
+ {
+ if (user.userId !== selectedUser) {
+ setSelectedUser(user.userId)
+ setSelectedHistory(
+ generateWeeklySummary(choresHistoryData, user.userId),
+ )
+ }
+ }}
+ >
+ {/* Rank Badge */}
+
+ {getTrophyIcon(user.rank)}
+
+ #{user.rank}
+
+
+
+ {/* User Avatar and Info */}
+
+
+ {user.displayName?.charAt(0)}
+
+
+
+ {user.displayName}
+ {user.userId === userProfile.id && (
+
+ You
+
+ )}
+
+
+ {user.periodTasks} tasks • {user.avgPointsPerTask} avg
+ per task
+
+
+
+
+ {/* Metric Display */}
+
+
+
+
+ {leaderboardMode === 'points'
+ ? user.periodPoints
+ : user.periodTasks}
+
+
+
+ {leaderboardMode === 'points'
+ ? `${user.availablePoints} available`
+ : `${user.periodPoints} points`}
+
+
+
+ {/* Selection Indicator */}
+ {user.userId === selectedUser && (
+
+ )}
+
+ {index < leaderboardData.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+
+ {/* Filters Section Header */}
+
+
+
+ Filter & Analysis
+
+
{/* Improved Filter Bar */}
{
+ {/* Points Status Cards */}
+
+ {(() => {
+ const selectedUserData = circleUsers.find(
+ user => user.userId === selectedUser,
+ )
+ const totalPoints = selectedUserData?.points || 0
+ const redeemedPoints = selectedUserData?.pointsRedeemed || 0
+ const availablePoints = totalPoints - redeemedPoints
+
+ const periodStats = leaderboardData.find(
+ user => user.userId === selectedUser,
+ )
+ const periodPoints = selectedHistory.reduce(
+ (sum, item) => sum + (item.points || 0),
+ 0,
+ )
+
+ const pointsCards = [
+ {
+ icon: ,
+ title: 'Total',
+ text: `${totalPoints} points`,
+ subtext: 'All time earned',
+ },
+ {
+ icon: ,
+ title: 'Available',
+ text: `${availablePoints} points`,
+ subtext: 'Ready to redeem',
+ },
+ {
+ icon: ,
+ title: 'Period Points',
+ text: `${periodPoints} points`,
+ subtext: `${tabValue === 24 * 30 ? 'All time' : tabValue === 6 * 30 ? 'Last 6 months' : `Last ${tabValue} days`}`,
+ },
+ {
+ icon: ,
+ title: 'Redeemed',
+ text: `${redeemedPoints} points`,
+ subtext: 'Previously used',
+ },
+ ]
+
+ return pointsCards.map((card, index) => (
+
+
+
+
+ {card.icon}
+
+ {card.title}
+
+
+
+
+ {card.text}
+
+
+ {card.subtext}
+
+
+
+
+
+ ))
+ })()}
+
+
{/* Current Filter Summary */}
@@ -430,107 +894,15 @@ const UserPoints = () => {
gap: 3,
}}
>
- {/* Points Cards */}
-
- {[
- {
- title: 'Total',
- value: circleMembersData.res.find(
- user => user.userId === selectedUser,
- )?.points,
- color: 'primary',
- },
- {
- title: 'Available',
- value: (function () {
- const user = circleMembersData.res.find(
- user => user.userId === selectedUser,
- )
- if (!user) return 0
- return user.points - user.pointsRedeemed
- })(),
- color: 'success',
- },
- {
- title: 'Redeemed',
- value: circleMembersData.res.find(
- user => user.userId === selectedUser,
- )?.pointsRedeemed,
- color: 'warning',
- },
- ].map(card => (
-
-
- {card.title}
-
-
- {card.value}
-
-
- ))}
-
-
- {/* Points History Section */}
-
- Points History
-
-
-
- {[
- {
- title: 'Points',
- value: selectedHistory.reduce((acc, cur) => acc + cur.points, 0),
- color: 'success',
- },
- {
- title: 'Tasks',
- value: selectedHistory.reduce((acc, cur) => acc + cur.tasks, 0),
- color: 'primary',
- },
- ].map(card => (
-
-
- {card.title}
-
-
- {card.value}
-
-
- ))}
+ {/* Chart Section Header */}
+
+
+
+ Points Trend
+
{/* Bar Chart for points overtime */}