diff --git a/assets/adaptive-icon.png b/assets/adaptive-icon.png
index 3a62fb3..486f8e1 100644
Binary files a/assets/adaptive-icon.png and b/assets/adaptive-icon.png differ
diff --git a/assets/favicon.png b/assets/favicon.png
index 158a3ed..42cf071 100644
Binary files a/assets/favicon.png and b/assets/favicon.png differ
diff --git a/assets/icon.png b/assets/icon.png
index 3a62fb3..486f8e1 100644
Binary files a/assets/icon.png and b/assets/icon.png differ
diff --git a/assets/icon_square.png b/assets/icon_square.png
index 3a62fb3..486f8e1 100644
Binary files a/assets/icon_square.png and b/assets/icon_square.png differ
diff --git a/assets/splash-icon.png b/assets/splash-icon.png
index 3a62fb3..486f8e1 100644
Binary files a/assets/splash-icon.png and b/assets/splash-icon.png differ
diff --git a/package-lock.json b/package-lock.json
index 146f78b..40daf67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index df47cea..7ee5ec2 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/components/PlanetCore.js b/src/components/PlanetCore.js
index 0cec9fe..57d6f2b 100644
--- a/src/components/PlanetCore.js
+++ b/src/components/PlanetCore.js
@@ -64,8 +64,8 @@ export default function PlanetCore({ size = 90, glowIntensity = 0.5 }) {
{/* Planet image */}
);
diff --git a/src/screens/OnboardingScreen.js b/src/screens/OnboardingScreen.js
index 12298fc..b99ea63 100644
--- a/src/screens/OnboardingScreen.js
+++ b/src/screens/OnboardingScreen.js
@@ -107,8 +107,8 @@ function PageContent({ item, isActive }) {
)}
diff --git a/src/screens/ProfileScreen.js b/src/screens/ProfileScreen.js
index e399ea8..dbbf97b 100644
--- a/src/screens/ProfileScreen.js
+++ b/src/screens/ProfileScreen.js
@@ -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 }) {
+ {/* ═══ Delete Account ═══ */}
+
+ [st.deleteBtn, pressed && st.deleteBtnPressed]}
+ onPress={handleDeleteAccount}
+ disabled={deleting}
+ >
+ 🗑
+ {deleting ? 'Deleting...' : 'Delete Account'}
+
+
+
{/* ═══ Footer ═══ */}
Nova40 v1.0.0
@@ -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 },
diff --git a/src/screens/SplashScreen.js b/src/screens/SplashScreen.js
index bab4f12..1a29900 100644
--- a/src/screens/SplashScreen.js
+++ b/src/screens/SplashScreen.js
@@ -96,7 +96,7 @@ export default function SplashScreen({ navigation }) {
@@ -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,
diff --git a/src/services/authService.js b/src/services/authService.js
index 0e8324c..b9b8b58 100644
--- a/src/services/authService.js
+++ b/src/services/authService.js
@@ -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);
diff --git a/src/store/useAuthStore.js b/src/store/useAuthStore.js
index 3e8800f..7b32cfb 100644
--- a/src/store/useAuthStore.js
+++ b/src/store/useAuthStore.js
@@ -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();