Compare commits
1 Commits
53a75adfdd
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c9bf094384 |
|
Before Width: | Height: | Size: 582 KiB After Width: | Height: | Size: 934 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 582 KiB After Width: | Height: | Size: 934 KiB |
|
Before Width: | Height: | Size: 582 KiB After Width: | Height: | Size: 934 KiB |
|
Before Width: | Height: | Size: 582 KiB After Width: | Height: | Size: 934 KiB |
@@ -26,6 +26,7 @@
|
||||
"expo-media-library": "~55.0.16",
|
||||
"expo-print": "~55.0.14",
|
||||
"expo-sharing": "~55.0.18",
|
||||
"expo-splash-screen": "~55.0.20",
|
||||
"expo-status-bar": "~55.0.6",
|
||||
"expo-web-browser": "~55.0.15",
|
||||
"react": "19.2.0",
|
||||
@@ -1466,6 +1467,24 @@
|
||||
"node-forge": "^1.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/config": {
|
||||
"version": "55.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.16.tgz",
|
||||
"integrity": "sha512-H5dpQv5TfyZDNheZAWO3SmP10diGWZwN5QOUsArkDJih0QKNtahQBOmrV2xbhgln/nrUGoy41U/ZIY/MEx63Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config-plugins": "~55.0.8",
|
||||
"@expo/config-types": "^55.0.5",
|
||||
"@expo/json-file": "^10.0.14",
|
||||
"@expo/require-utils": "^55.0.5",
|
||||
"deepmerge": "^4.3.1",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"resolve-workspace-root": "^2.0.0",
|
||||
"semver": "^7.6.0",
|
||||
"slugify": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/config-plugins": {
|
||||
"version": "55.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-55.0.8.tgz",
|
||||
@@ -1604,24 +1623,6 @@
|
||||
"chalk": "^4.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/local-build-cache-provider/node_modules/@expo/config": {
|
||||
"version": "55.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.16.tgz",
|
||||
"integrity": "sha512-H5dpQv5TfyZDNheZAWO3SmP10diGWZwN5QOUsArkDJih0QKNtahQBOmrV2xbhgln/nrUGoy41U/ZIY/MEx63Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config-plugins": "~55.0.8",
|
||||
"@expo/config-types": "^55.0.5",
|
||||
"@expo/json-file": "^10.0.14",
|
||||
"@expo/require-utils": "^55.0.5",
|
||||
"deepmerge": "^4.3.1",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"resolve-workspace-root": "^2.0.0",
|
||||
"semver": "^7.6.0",
|
||||
"slugify": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/metro": {
|
||||
"version": "55.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@expo/metro/-/metro-55.1.1.tgz",
|
||||
@@ -1681,6 +1682,27 @@
|
||||
"xmlbuilder": "^15.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/prebuild-config": {
|
||||
"version": "55.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.17.tgz",
|
||||
"integrity": "sha512-Mcs+dg4Ripu0yCtzf66KZr18PehI1O8HxzJw+G5SUF8VWX+ic99aci1PltvmydWepLwTQL6ykmpXicAUA31IqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config": "~55.0.16",
|
||||
"@expo/config-plugins": "~55.0.8",
|
||||
"@expo/config-types": "^55.0.5",
|
||||
"@expo/image-utils": "^0.8.14",
|
||||
"@expo/json-file": "^10.0.14",
|
||||
"@react-native/normalize-colors": "0.83.6",
|
||||
"debug": "^4.3.1",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.6.0",
|
||||
"xml2js": "0.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/require-utils": {
|
||||
"version": "55.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@expo/require-utils/-/require-utils-55.0.5.tgz",
|
||||
@@ -4155,6 +4177,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-splash-screen": {
|
||||
"version": "55.0.20",
|
||||
"resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-55.0.20.tgz",
|
||||
"integrity": "sha512-WI5T0dutiZhxsqlF+jhEP4JRpQNILLlP8IpmKehsnV53Cncv6AQrKE7y1sOWwDyC2m2GBufZ/Vwam1RMt2EfmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/prebuild-config": "^55.0.17"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-status-bar": {
|
||||
"version": "55.0.6",
|
||||
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-55.0.6.tgz",
|
||||
@@ -4268,27 +4302,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/@expo/cli/node_modules/@expo/prebuild-config": {
|
||||
"version": "55.0.17",
|
||||
"resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-55.0.17.tgz",
|
||||
"integrity": "sha512-Mcs+dg4Ripu0yCtzf66KZr18PehI1O8HxzJw+G5SUF8VWX+ic99aci1PltvmydWepLwTQL6ykmpXicAUA31IqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config": "~55.0.16",
|
||||
"@expo/config-plugins": "~55.0.8",
|
||||
"@expo/config-types": "^55.0.5",
|
||||
"@expo/image-utils": "^0.8.14",
|
||||
"@expo/json-file": "^10.0.14",
|
||||
"@react-native/normalize-colors": "0.83.6",
|
||||
"debug": "^4.3.1",
|
||||
"resolve-from": "^5.0.0",
|
||||
"semver": "^7.6.0",
|
||||
"xml2js": "0.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/@expo/cli/node_modules/@expo/router-server": {
|
||||
"version": "55.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@expo/router-server/-/router-server-55.0.16.tgz",
|
||||
@@ -4344,24 +4357,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/@expo/config": {
|
||||
"version": "55.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@expo/config/-/config-55.0.16.tgz",
|
||||
"integrity": "sha512-H5dpQv5TfyZDNheZAWO3SmP10diGWZwN5QOUsArkDJih0QKNtahQBOmrV2xbhgln/nrUGoy41U/ZIY/MEx63Ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@expo/config-plugins": "~55.0.8",
|
||||
"@expo/config-types": "^55.0.5",
|
||||
"@expo/json-file": "^10.0.14",
|
||||
"@expo/require-utils": "^55.0.5",
|
||||
"deepmerge": "^4.3.1",
|
||||
"getenv": "^2.0.0",
|
||||
"glob": "^13.0.0",
|
||||
"resolve-workspace-root": "^2.0.0",
|
||||
"semver": "^7.6.0",
|
||||
"slugify": "^1.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/@expo/log-box": {
|
||||
"version": "55.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@expo/log-box/-/log-box-55.0.12.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"expo-media-library": "~55.0.16",
|
||||
"expo-print": "~55.0.14",
|
||||
"expo-sharing": "~55.0.18",
|
||||
"expo-splash-screen": "~55.0.20",
|
||||
"expo-status-bar": "~55.0.6",
|
||||
"expo-web-browser": "~55.0.15",
|
||||
"react": "19.2.0",
|
||||
|
||||
@@ -64,8 +64,8 @@ export default function PlanetCore({ size = 90, glowIntensity = 0.5 }) {
|
||||
{/* Planet image */}
|
||||
<Image
|
||||
source={require('../../assets/icon.png')}
|
||||
style={{ width: size, height: size * 0.88 }}
|
||||
resizeMode="cover"
|
||||
style={{ width: size, height: size }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -107,8 +107,8 @@ function PageContent({ item, isActive }) {
|
||||
)}
|
||||
<Image
|
||||
source={require('../../assets/icon.png')}
|
||||
style={{ width: item.iconSize, height: item.iconSize * 0.88 }}
|
||||
resizeMode="cover"
|
||||
style={{ width: item.iconSize * 1.2, height: item.iconSize * 1.2 }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
|
||||
@@ -54,11 +54,13 @@ export default function ProfileScreen({ navigation }) {
|
||||
const resetIdentity = useIdentityStore((s) => s.reset);
|
||||
const resetHabits = useHabitStore((s) => s.reset);
|
||||
const resetJourney = useIdentityStore((s) => s.resetJourney);
|
||||
const deleteAccount = useAuthStore((s) => s.deleteAccount);
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const [showGuide, setShowGuide] = useState(false);
|
||||
const [showReset, setShowReset] = useState(false);
|
||||
const [resetReason, setResetReason] = useState('');
|
||||
const [resetting, setResetting] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [focusCount, setFocusCount] = useState(0);
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [journeyCount, setJourneyCount] = useState(0);
|
||||
@@ -119,6 +121,45 @@ export default function ProfileScreen({ navigation }) {
|
||||
]);
|
||||
};
|
||||
|
||||
const handleDeleteAccount = () => {
|
||||
showAlert(
|
||||
'Delete your account?',
|
||||
'This will permanently delete your account, all your data, habits, journals, and progress. This cannot be undone.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Delete Forever', style: 'destructive',
|
||||
onPress: () => {
|
||||
// Double confirm
|
||||
showAlert(
|
||||
'Last chance',
|
||||
'Are you absolutely sure? Everything will be gone permanently.',
|
||||
[
|
||||
{ text: 'Keep my account', style: 'cancel' },
|
||||
{
|
||||
text: 'Yes, delete everything', style: 'destructive',
|
||||
onPress: async () => {
|
||||
setDeleting(true);
|
||||
try {
|
||||
resetIdentity();
|
||||
resetHabits();
|
||||
await deleteAccount();
|
||||
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Login' }] }));
|
||||
} catch (e) {
|
||||
showAlert('Error', e?.message || 'Failed to delete account.');
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
const phase = getDayPhase(currentDay);
|
||||
const progressPct = Math.round((currentDay / 40) * 100);
|
||||
const displayName = profile?.nickname || profile?.fullName || user?.email?.split('@')[0] || 'Nova';
|
||||
@@ -328,6 +369,18 @@ export default function ProfileScreen({ navigation }) {
|
||||
<Button title="Sign Out" onPress={handleSignOut} variant="secondary" style={st.signOutBtn} />
|
||||
</FadeIn>
|
||||
|
||||
{/* ═══ Delete Account ═══ */}
|
||||
<FadeIn delay={450} trigger={focusCount}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [st.deleteBtn, pressed && st.deleteBtnPressed]}
|
||||
onPress={handleDeleteAccount}
|
||||
disabled={deleting}
|
||||
>
|
||||
<Text style={st.deleteIcon}>🗑</Text>
|
||||
<Text style={st.deleteText}>{deleting ? 'Deleting...' : 'Delete Account'}</Text>
|
||||
</Pressable>
|
||||
</FadeIn>
|
||||
|
||||
{/* ═══ Footer ═══ */}
|
||||
<FadeIn delay={500} trigger={focusCount}>
|
||||
<Text style={st.version}>Nova40 v1.0.0</Text>
|
||||
@@ -457,6 +510,13 @@ const st = StyleSheet.create({
|
||||
legalArrow: { color: colors.textMuted, fontSize: fonts.sizes.lg },
|
||||
|
||||
signOutBtn: { borderColor: colors.error },
|
||||
deleteBtn: {
|
||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center',
|
||||
paddingVertical: spacing.md, marginTop: spacing.md,
|
||||
},
|
||||
deleteBtnPressed: { opacity: 0.5 },
|
||||
deleteIcon: { fontSize: 14, marginRight: spacing.sm },
|
||||
deleteText: { color: colors.textMuted, fontSize: fonts.sizes.sm },
|
||||
|
||||
// Footer
|
||||
version: { color: colors.textMuted, fontSize: 9, textAlign: 'center', marginTop: spacing.xl },
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function SplashScreen({ navigation }) {
|
||||
<Image
|
||||
source={require('../../assets/icon.png')}
|
||||
style={styles.logo}
|
||||
resizeMode="cover"
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
@@ -114,12 +114,12 @@ export default function SplashScreen({ navigation }) {
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1, backgroundColor: colors.background },
|
||||
content: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40 },
|
||||
logoContainer: { alignItems: 'center', justifyContent: 'center', marginBottom: 40 },
|
||||
logoContainer: { alignItems: 'center', justifyContent: 'center', marginBottom: 32 },
|
||||
glow: {
|
||||
width: 200, height: 200, borderRadius: 100,
|
||||
width: 240, height: 240, borderRadius: 120,
|
||||
backgroundColor: colors.planetGlow, opacity: 0.4, position: 'absolute',
|
||||
},
|
||||
logo: { width: 160, height: 140 },
|
||||
logo: { width: 180, height: 180 },
|
||||
title: {
|
||||
color: colors.text, fontSize: fonts.sizes.hero,
|
||||
fontWeight: fonts.weights.bold, letterSpacing: 8, marginBottom: 16,
|
||||
|
||||
@@ -290,6 +290,60 @@ export async function signInWithFacebook() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteAccount() {
|
||||
if (!USE_OFFLINE) {
|
||||
const { error } = await supabase.rpc('delete_user');
|
||||
if (error) throw error;
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await getSession();
|
||||
if (!session?.user?.email) throw new Error('No active session.');
|
||||
|
||||
const email = session.user.email.toLowerCase();
|
||||
const userId = session.user.id;
|
||||
|
||||
// Import offline storage
|
||||
const offline = await import('./offlineStorage');
|
||||
|
||||
// Get all user's identities to cascade delete related data
|
||||
const identities = await offline.getAll('identities', { user_id: userId });
|
||||
const identityIds = identities.map((i) => i.id);
|
||||
|
||||
// Remove all identity-related data
|
||||
for (const identityId of identityIds) {
|
||||
await offline.removeAll('habits', { identity_id: identityId });
|
||||
await offline.removeAll('daily_logs', { identity_id: identityId });
|
||||
await offline.removeAll('habit_logs', { identity_id: identityId });
|
||||
await offline.removeAll('stats', { identity_id: identityId });
|
||||
await offline.removeAll('game_sessions', { identity_id: identityId });
|
||||
await offline.removeAll('reset_logs', { identity_id: identityId });
|
||||
}
|
||||
|
||||
// Remove identities & profile
|
||||
await offline.removeAll('identities', { user_id: userId });
|
||||
await offline.removeAll('profiles', { user_id: userId });
|
||||
|
||||
// Remove profile AsyncStorage key
|
||||
await AsyncStorage.removeItem(`nova40_profile_${userId}`);
|
||||
|
||||
// Remove user from auth store
|
||||
const users = await getStoredUsers();
|
||||
delete users[email];
|
||||
await saveUsers(users);
|
||||
|
||||
// Clear session
|
||||
await clearSession();
|
||||
|
||||
// Clean up misc keys
|
||||
const remembered = await AsyncStorage.getItem('remember_email');
|
||||
if (remembered?.toLowerCase() === email) {
|
||||
await AsyncStorage.removeItem('remember_email');
|
||||
}
|
||||
await AsyncStorage.removeItem('nova40_last_oauth');
|
||||
await AsyncStorage.removeItem('nova40_onboarding_completed');
|
||||
}
|
||||
|
||||
export function onAuthStateChange(callback) {
|
||||
if (!USE_OFFLINE) {
|
||||
return supabase.auth.onAuthStateChange(callback);
|
||||
|
||||
@@ -137,6 +137,17 @@ const useAuthStore = create((set) => ({
|
||||
set({ session: null, user: null, error: null });
|
||||
},
|
||||
|
||||
deleteAccount: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
await authService.deleteAccount();
|
||||
set({ session: null, user: null, loading: false, error: null });
|
||||
} catch (e) {
|
||||
set({ loading: false, error: e.message });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
fetchSession: async () => {
|
||||
try {
|
||||
const session = await authService.getSession();
|
||||
|
||||