tekout register, add login by fb, add menu history
This commit is contained in:
@@ -21,7 +21,6 @@
|
||||
"backgroundColor": "#0A0E1A"
|
||||
},
|
||||
"package": "com.nova40.app",
|
||||
"googleServicesFile": "./google-services.json",
|
||||
"permissions": [
|
||||
"WRITE_EXTERNAL_STORAGE",
|
||||
"READ_EXTERNAL_STORAGE"
|
||||
@@ -36,10 +35,5 @@
|
||||
}
|
||||
},
|
||||
"owner": "heyaciell",
|
||||
"plugins": [
|
||||
"@react-native-firebase/app",
|
||||
"@react-native-firebase/crashlytics",
|
||||
"expo-sharing"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1590
-2957
File diff suppressed because it is too large
Load Diff
+15
-20
@@ -10,38 +10,33 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-firebase/analytics": "^24.0.0",
|
||||
"@react-native-firebase/app": "^24.0.0",
|
||||
"@react-native-firebase/crashlytics": "^24.0.0",
|
||||
"@react-navigation/bottom-tabs": "^7.15.9",
|
||||
"@react-navigation/native": "^7.2.2",
|
||||
"@react-navigation/native-stack": "^7.14.11",
|
||||
"@supabase/ssr": "^0.10.2",
|
||||
"@supabase/supabase-js": "^2.103.0",
|
||||
"babel-preset-expo": "~55.0.8",
|
||||
"expo": "^55.0.19",
|
||||
"expo-dev-client": "~55.0.30",
|
||||
"expo-image-picker": "~55.0.19",
|
||||
"expo-linear-gradient": "~55.0.13",
|
||||
"expo-media-library": "~55.0.15",
|
||||
"expo-print": "~55.0.13",
|
||||
"expo-sharing": "~55.0.18",
|
||||
"expo-status-bar": "~55.0.5",
|
||||
"react": "19.2.0",
|
||||
"react-native": "0.83.6",
|
||||
"react-native-gesture-handler": "~2.30.0",
|
||||
"react-native-google-mobile-ads": "^16.3.3",
|
||||
"react-native-reanimated": "4.2.1",
|
||||
"babel-preset-expo": "~54.0.10",
|
||||
"expo": "~54.0.0",
|
||||
"expo-dev-client": "~6.0.21",
|
||||
"expo-image-picker": "~17.0.11",
|
||||
"expo-linear-gradient": "~15.0.8",
|
||||
"expo-media-library": "~18.2.1",
|
||||
"expo-print": "~15.0.8",
|
||||
"expo-sharing": "~14.0.8",
|
||||
"expo-status-bar": "~3.0.9",
|
||||
"react": "19.1.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-reanimated": "~4.1.1",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.23.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-url-polyfill": "^3.0.0",
|
||||
"react-native-view-shot": "4.0.3",
|
||||
"react-native-worklets": "0.7.4",
|
||||
"zustand": "^5.0.12"
|
||||
},
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/react": "~19.2.10",
|
||||
"@types/react": "~19.1.10",
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import DailyScreen from '../screens/DailyScreen';
|
||||
import StatsScreen from '../screens/StatsScreen';
|
||||
import MirrorScreen from '../screens/MirrorScreen';
|
||||
import MiniGameScreen from '../screens/MiniGameScreen';
|
||||
import HistoryScreen from '../screens/HistoryScreen';
|
||||
import CompletionScreen from '../screens/CompletionScreen';
|
||||
import ProfileScreen from '../screens/ProfileScreen';
|
||||
import DailyJournalScreen from '../screens/DailyJournalScreen';
|
||||
@@ -33,7 +34,7 @@ const Stack = createNativeStackNavigator();
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
function TabIcon({ label, focused }) {
|
||||
const icons = { Daily: '◈', Game: '◎', Stats: '◉', Profile: '◆' };
|
||||
const icons = { Daily: '◈', History: '◎', Stats: '◉', Profile: '◆' };
|
||||
const activeColor = colors.primary;
|
||||
const inactiveColor = colors.textSecondary; // #8A8FB5 — brighter than textMuted
|
||||
return (
|
||||
@@ -63,9 +64,9 @@ function MainTabs() {
|
||||
backgroundColor: colors.surface,
|
||||
borderTopColor: colors.border,
|
||||
borderTopWidth: 1,
|
||||
height: 70,
|
||||
paddingBottom: 10,
|
||||
paddingTop: 6,
|
||||
height: 90,
|
||||
paddingBottom: 28,
|
||||
paddingTop: 8,
|
||||
},
|
||||
tabBarShowLabel: false,
|
||||
}}
|
||||
@@ -74,8 +75,8 @@ function MainTabs() {
|
||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Daily" focused={focused} /> }} />
|
||||
<Tab.Screen name="Stats" component={StatsScreen}
|
||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Stats" focused={focused} /> }} />
|
||||
<Tab.Screen name="Game" component={MiniGameScreen}
|
||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Game" focused={focused} /> }} />
|
||||
<Tab.Screen name="History" component={HistoryScreen}
|
||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="History" focused={focused} /> }} />
|
||||
<Tab.Screen name="Profile" component={ProfileScreen}
|
||||
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Profile" focused={focused} /> }} />
|
||||
</Tab.Navigator>
|
||||
@@ -106,7 +107,7 @@ export default function AppNavigator() {
|
||||
<Stack.Screen name="Splash" component={SplashScreen} />
|
||||
<Stack.Screen name="Onboarding" component={OnboardingScreen} />
|
||||
<Stack.Screen name="Login" component={LoginScreen} />
|
||||
<Stack.Screen name="Register" component={RegisterScreen} />
|
||||
{/* Register removed — users sign in via email, Google, or Facebook */}
|
||||
<Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
|
||||
<Stack.Screen name="IdentityStory" component={IdentityStoryScreen} />
|
||||
<Stack.Screen name="HabitSelection" component={HabitSelectionScreen} />
|
||||
|
||||
+125
-17
@@ -179,38 +179,108 @@ export default function DailyScreen({ navigation }) {
|
||||
|
||||
// ====== COMPLETED STATE ======
|
||||
if (completed && savedEntry?.ai_title) {
|
||||
const progressPct = Math.round((currentDay / 40) * 100);
|
||||
const daysLeft = Math.max(0, 40 - currentDay);
|
||||
const streak = currentDay; // simplified for display
|
||||
|
||||
const celebrationMessages = [
|
||||
'You showed up when it mattered most.',
|
||||
'Another day closer to the person you want to be.',
|
||||
'This is what transformation looks like.',
|
||||
"Consistency isn't sexy, but it's powerful.",
|
||||
'You chose growth today. That takes courage.',
|
||||
"Most people quit by now. You're still here.",
|
||||
'Small steps. Massive change. Keep going.',
|
||||
];
|
||||
const celebMsg = celebrationMessages[currentDay % celebrationMessages.length];
|
||||
|
||||
const JournalCard = () => (
|
||||
<View style={s.journalCard}>
|
||||
<Text style={s.jBrand}>NOVA40</Text>
|
||||
|
||||
{/* Big day number */}
|
||||
<View style={s.jDayCircle}>
|
||||
<Text style={s.jDayNum}>{currentDay}</Text>
|
||||
<Text style={s.jDayLabel}>DAY</Text>
|
||||
</View>
|
||||
|
||||
<Text style={s.jEmoji}>{MOOD_EMOJIS[savedEntry.mood] || '\u2728'}</Text>
|
||||
<Text style={s.jTitle}>{savedEntry.ai_title}</Text>
|
||||
<Text style={s.jSummary}>{savedEntry.ai_summary}</Text>
|
||||
<View style={s.jDivider} />
|
||||
<Text style={s.jQuote}>"{savedEntry.ai_quote}"</Text>
|
||||
|
||||
{/* Progress bar inside card */}
|
||||
<View style={s.jProgressRow}>
|
||||
<View style={s.jProgressBar}>
|
||||
<View style={[s.jProgressFill, { width: `${progressPct}%` }]} />
|
||||
</View>
|
||||
<Text style={s.jProgressText}>{progressPct}%</Text>
|
||||
</View>
|
||||
|
||||
<View style={s.jFooter}>
|
||||
<Text style={s.jDay}>Day {currentDay}</Text>
|
||||
<Text style={s.jDate}>{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</Text>
|
||||
<Text style={s.jDay}>Day {currentDay}/40</Text>
|
||||
<Text style={s.jDate}>{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</Text>
|
||||
</View>
|
||||
{identity?.title && <Text style={s.jIdentity}>{identity.title}</Text>}
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenWrapper>
|
||||
<Animated.ScrollView style={[s.flex, { opacity: fadeAnim }]} contentContainerStyle={s.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
{/* Success header */}
|
||||
|
||||
{/* Celebration header */}
|
||||
<View style={s.doneHeader}>
|
||||
<Text style={s.doneEmoji}>{'\u2728'}</Text>
|
||||
<Text style={s.doneEmojiBig}>🔥</Text>
|
||||
<Text style={s.doneTitle}>Day {currentDay} Complete!</Text>
|
||||
<Text style={s.doneSubtitle}>You showed up. That matters.</Text>
|
||||
<Text style={s.doneSubtitle}>{celebMsg}</Text>
|
||||
</View>
|
||||
|
||||
{ViewShot ? (<ViewShot ref={viewShotRef} options={{ format: 'png', quality: 1 }}><JournalCard /></ViewShot>) : (<JournalCard />)}
|
||||
{/* Stats row */}
|
||||
<View style={s.doneStatsRow}>
|
||||
<View style={s.doneStat}>
|
||||
<Text style={s.doneStatValue}>{progressPct}%</Text>
|
||||
<Text style={s.doneStatLabel}>Progress</Text>
|
||||
</View>
|
||||
<View style={[s.doneStat, s.doneStatCenter]}>
|
||||
<Text style={[s.doneStatValue, { color: colors.accent }]}>{currentDay}</Text>
|
||||
<Text style={s.doneStatLabel}>Day streak</Text>
|
||||
</View>
|
||||
<View style={s.doneStat}>
|
||||
<Text style={s.doneStatValue}>{daysLeft}</Text>
|
||||
<Text style={s.doneStatLabel}>Days left</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Shareable journal card */}
|
||||
{ViewShot ? (
|
||||
<ViewShot ref={viewShotRef} options={{ format: 'png', quality: 1 }}><JournalCard /></ViewShot>
|
||||
) : (
|
||||
<JournalCard />
|
||||
)}
|
||||
|
||||
{/* Share CTA */}
|
||||
<View style={s.shareCTA}>
|
||||
<Text style={s.shareCTAText}>Show the world you're transforming</Text>
|
||||
</View>
|
||||
|
||||
{/* Action buttons */}
|
||||
<View style={s.actions}>
|
||||
<Button title="Share This" onPress={handleShare} style={s.actionBtn} />
|
||||
<Button title="Share to Social Media" onPress={handleShare} style={s.actionBtn} />
|
||||
</View>
|
||||
|
||||
<NovaAd />
|
||||
|
||||
{/* Motivational footer */}
|
||||
<View style={s.doneFooter}>
|
||||
<Text style={s.doneFooterText}>
|
||||
{daysLeft > 0
|
||||
? `${daysLeft} more days to become who you're meant to be.`
|
||||
: 'You made it. 40 days. A new identity.'}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Entries removed */}
|
||||
</Animated.ScrollView>
|
||||
</ScreenWrapper>
|
||||
);
|
||||
@@ -397,24 +467,62 @@ const s = StyleSheet.create({
|
||||
submitHint: { color: colors.textMuted, fontSize: fonts.sizes.xs, textAlign: 'center', marginTop: spacing.sm },
|
||||
|
||||
// ====== Completed state ======
|
||||
doneHeader: { alignItems: 'center', paddingTop: spacing.lg, marginBottom: spacing.xl },
|
||||
doneEmoji: { fontSize: 48, marginBottom: spacing.md },
|
||||
doneTitle: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.xs },
|
||||
doneSubtitle: { color: colors.textSecondary, fontSize: fonts.sizes.md },
|
||||
doneHeader: { alignItems: 'center', paddingTop: spacing.xl, marginBottom: spacing.lg },
|
||||
doneEmojiBig: { fontSize: 56, marginBottom: spacing.md },
|
||||
doneTitle: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.sm },
|
||||
doneSubtitle: { color: colors.textSecondary, fontSize: fonts.sizes.md, textAlign: 'center', lineHeight: 22, paddingHorizontal: spacing.md },
|
||||
|
||||
journalCard: { backgroundColor: colors.background, borderRadius: borderRadius.xl, padding: spacing.xl, borderWidth: 1, borderColor: 'rgba(108,99,255,0.25)', alignItems: 'center' },
|
||||
jBrand: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 5, marginBottom: spacing.lg },
|
||||
jEmoji: { fontSize: 36, marginBottom: spacing.md },
|
||||
jTitle: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, textAlign: 'center', lineHeight: 30, marginBottom: spacing.md },
|
||||
// Stats row
|
||||
doneStatsRow: {
|
||||
flexDirection: 'row', justifyContent: 'space-around',
|
||||
backgroundColor: colors.surface, borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg, marginBottom: spacing.xl,
|
||||
borderWidth: 1, borderColor: colors.border,
|
||||
},
|
||||
doneStat: { alignItems: 'center', flex: 1 },
|
||||
doneStatCenter: { borderLeftWidth: 1, borderRightWidth: 1, borderColor: colors.border },
|
||||
doneStatValue: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, marginBottom: 2 },
|
||||
doneStatLabel: { color: colors.textMuted, fontSize: 10 },
|
||||
|
||||
// Journal card
|
||||
journalCard: {
|
||||
backgroundColor: colors.background, borderRadius: borderRadius.xl,
|
||||
padding: spacing.xl, borderWidth: 1,
|
||||
borderColor: 'rgba(108,99,255,0.3)', alignItems: 'center',
|
||||
},
|
||||
jBrand: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 5, marginBottom: spacing.md },
|
||||
jDayCircle: {
|
||||
width: 70, height: 70, borderRadius: 35,
|
||||
borderWidth: 2, borderColor: colors.accent,
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
marginBottom: spacing.md,
|
||||
backgroundColor: 'rgba(0,229,255,0.06)',
|
||||
},
|
||||
jDayNum: { color: colors.accent, fontSize: 28, fontWeight: fonts.weights.bold, lineHeight: 32 },
|
||||
jDayLabel: { color: colors.accent, fontSize: 9, fontWeight: fonts.weights.bold, letterSpacing: 2 },
|
||||
jEmoji: { fontSize: 32, marginBottom: spacing.sm },
|
||||
jTitle: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, textAlign: 'center', lineHeight: 30, marginBottom: spacing.sm },
|
||||
jSummary: { color: colors.textSecondary, fontSize: fonts.sizes.md, textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg, paddingHorizontal: spacing.sm },
|
||||
jDivider: { width: 40, height: 2, backgroundColor: colors.primary, borderRadius: 1, marginBottom: spacing.lg },
|
||||
jQuote: { color: colors.accent, fontSize: fonts.sizes.md, fontStyle: 'italic', textAlign: 'center', lineHeight: 24, marginBottom: spacing.lg },
|
||||
jProgressRow: { flexDirection: 'row', alignItems: 'center', width: '100%', marginBottom: spacing.lg, gap: spacing.sm },
|
||||
jProgressBar: { flex: 1, height: 4, backgroundColor: colors.surfaceLight, borderRadius: 2, overflow: 'hidden' },
|
||||
jProgressFill: { height: '100%', backgroundColor: colors.primary, borderRadius: 2 },
|
||||
jProgressText: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold },
|
||||
jFooter: { flexDirection: 'row', justifyContent: 'center', gap: spacing.md, marginBottom: spacing.sm },
|
||||
jDay: { color: colors.textMuted, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
|
||||
jDate: { color: colors.textMuted, fontSize: fonts.sizes.sm },
|
||||
jIdentity: { color: colors.textMuted, fontSize: fonts.sizes.xs, fontStyle: 'italic' },
|
||||
|
||||
actions: { marginTop: spacing.xl },
|
||||
// Share CTA
|
||||
shareCTA: { alignItems: 'center', marginTop: spacing.lg, marginBottom: spacing.sm },
|
||||
shareCTAText: { color: colors.textSecondary, fontSize: fonts.sizes.sm, fontStyle: 'italic' },
|
||||
|
||||
actions: { marginTop: spacing.sm },
|
||||
actionBtn: { marginBottom: spacing.sm },
|
||||
|
||||
// Footer motivation
|
||||
doneFooter: { alignItems: 'center', paddingTop: spacing.xl, paddingBottom: spacing.lg },
|
||||
doneFooterText: { color: colors.textMuted, fontSize: fonts.sizes.sm, textAlign: 'center', lineHeight: 20 },
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
import React, { useState, useRef, useCallback } from 'react';
|
||||
import { View, Text, StyleSheet, Animated, Pressable } from 'react-native';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import ScreenWrapper from '../components/ScreenWrapper';
|
||||
import NovaAd from '../components/NovaAd';
|
||||
import useAuthStore from '../store/useAuthStore';
|
||||
import * as offline from '../services/offlineStorage';
|
||||
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
|
||||
|
||||
const MOOD_EMOJIS = { great: '😄', good: '🙂', okay: '😐', bad: '😔', terrible: '😞' };
|
||||
const CHECK_COLORS = { yes: colors.success, almost: colors.warning, no: colors.error };
|
||||
const CHECK_LABELS = { yes: 'Aligned', almost: 'Almost', no: 'Missed' };
|
||||
|
||||
function HistoryCard({ entry, isFirst }) {
|
||||
const [expanded, setExpanded] = useState(isFirst);
|
||||
const hasJournal = entry.win || entry.struggle || entry.highlight || entry.note;
|
||||
const hasAI = entry.ai_title;
|
||||
|
||||
return (
|
||||
<Pressable style={styles.card} onPress={() => setExpanded((p) => !p)}>
|
||||
{/* Timeline dot + line */}
|
||||
<View style={styles.timeline}>
|
||||
<View style={[
|
||||
styles.timelineDot,
|
||||
{ backgroundColor: CHECK_COLORS[entry.identity_check] || colors.primary },
|
||||
]} />
|
||||
<View style={styles.timelineLine} />
|
||||
</View>
|
||||
|
||||
<View style={styles.cardContent}>
|
||||
{/* Header row */}
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={styles.cardHeaderLeft}>
|
||||
<Text style={styles.dayNum}>Day {entry.day_number}</Text>
|
||||
{entry._identityTitle && (
|
||||
<Text style={styles.identityTag}>{entry._identityTitle}</Text>
|
||||
)}
|
||||
</View>
|
||||
<View style={styles.cardHeaderRight}>
|
||||
<Text style={styles.moodEmoji}>{MOOD_EMOJIS[entry.mood] || '•'}</Text>
|
||||
<Text style={styles.dateText}>{formatDate(entry.date)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Identity check badge */}
|
||||
{entry.identity_check && (
|
||||
<View style={[styles.checkBadge, { borderColor: CHECK_COLORS[entry.identity_check] }]}>
|
||||
<View style={[styles.checkDot, { backgroundColor: CHECK_COLORS[entry.identity_check] }]} />
|
||||
<Text style={[styles.checkText, { color: CHECK_COLORS[entry.identity_check] }]}>
|
||||
{CHECK_LABELS[entry.identity_check]}
|
||||
</Text>
|
||||
{entry.habits_completed > 0 && (
|
||||
<Text style={styles.habitsBadge}>{entry.habits_completed} habits done</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* AI reflection (always visible) */}
|
||||
{hasAI && (
|
||||
<View style={styles.aiSection}>
|
||||
<Text style={styles.aiTitle}>{entry.ai_title}</Text>
|
||||
{entry.ai_quote && (
|
||||
<Text style={styles.aiQuote}>"{entry.ai_quote}"</Text>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Expanded: full details */}
|
||||
{expanded && (
|
||||
<View style={styles.expandedSection}>
|
||||
{entry.ai_summary && (
|
||||
<Text style={styles.aiSummary}>{entry.ai_summary}</Text>
|
||||
)}
|
||||
|
||||
{hasJournal && (
|
||||
<View style={styles.journalSection}>
|
||||
<View style={styles.journalDivider}>
|
||||
<View style={styles.journalDividerLine} />
|
||||
<Text style={styles.journalDividerText}>Your words</Text>
|
||||
<View style={styles.journalDividerLine} />
|
||||
</View>
|
||||
|
||||
{entry.win && (
|
||||
<JournalEntry icon="🏆" label="Win" value={entry.win} />
|
||||
)}
|
||||
{entry.struggle && (
|
||||
<JournalEntry icon="💪" label="Struggle" value={entry.struggle} />
|
||||
)}
|
||||
{entry.highlight && (
|
||||
<JournalEntry icon="✨" label="Highlight" value={entry.highlight} />
|
||||
)}
|
||||
{entry.note && (
|
||||
<JournalEntry icon="💭" label="Reflection" value={entry.note} />
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Expand hint */}
|
||||
{(hasAI || hasJournal) && (
|
||||
<Text style={styles.expandHint}>{expanded ? 'Tap to collapse' : 'Tap to read more'}</Text>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
function JournalEntry({ icon, label, value }) {
|
||||
return (
|
||||
<View style={styles.journalRow}>
|
||||
<Text style={styles.journalIcon}>{icon}</Text>
|
||||
<View style={styles.journalBody}>
|
||||
<Text style={styles.journalLabel}>{label}</Text>
|
||||
<Text style={styles.journalValue}>{value}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const d = new Date(dateStr);
|
||||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
} catch (_) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
export default function HistoryScreen() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const [entries, setEntries] = useState([]);
|
||||
const [journeyCount, setJourneyCount] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
let active = true;
|
||||
const load = async () => {
|
||||
setLoading(true);
|
||||
if (user?.id) {
|
||||
const allIdentities = await offline.getAll('identities', { user_id: user.id });
|
||||
if (active) setJourneyCount(allIdentities.length);
|
||||
const allLogs = await offline.getAll('daily_logs');
|
||||
const identityMap = {};
|
||||
allIdentities.forEach((id) => { identityMap[id.id] = id.title; });
|
||||
const identityIds = allIdentities.map((id) => id.id);
|
||||
const userLogs = allLogs
|
||||
.filter((l) => identityIds.includes(l.identity_id))
|
||||
.filter((l) => l.identity_check || l.ai_title || l.mood)
|
||||
.map((l) => ({ ...l, _identityTitle: identityMap[l.identity_id] || '' }))
|
||||
.sort((a, b) => {
|
||||
if (a.date !== b.date) return a.date > b.date ? -1 : 1;
|
||||
return (b.day_number || 0) - (a.day_number || 0);
|
||||
});
|
||||
if (active) setEntries(userLogs);
|
||||
}
|
||||
if (active) {
|
||||
setLoading(false);
|
||||
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
|
||||
}
|
||||
};
|
||||
load();
|
||||
return () => { active = false; };
|
||||
}, [user?.id])
|
||||
);
|
||||
|
||||
// Stats summary
|
||||
const totalDays = entries.length;
|
||||
const yesCount = entries.filter((e) => e.identity_check === 'yes').length;
|
||||
const streakCount = (() => {
|
||||
let streak = 0;
|
||||
for (const e of entries) {
|
||||
if (e.identity_check === 'yes' || e.identity_check === 'almost') streak++;
|
||||
else break;
|
||||
}
|
||||
return streak;
|
||||
})();
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<ScreenWrapper>
|
||||
<View style={styles.loadingContainer}>
|
||||
<Text style={styles.loadingText}>Loading your journey...</Text>
|
||||
</View>
|
||||
</ScreenWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScreenWrapper>
|
||||
<Animated.ScrollView
|
||||
style={[styles.scroll, { opacity: fadeAnim }]}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Header */}
|
||||
<Text style={styles.title}>Your Journey</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{totalDays > 0
|
||||
? 'Every entry is proof that you showed up.'
|
||||
: 'Your story begins when you complete your first day.'}
|
||||
</Text>
|
||||
|
||||
{entries.length === 0 ? (
|
||||
/* Empty state */
|
||||
<View style={styles.emptyState}>
|
||||
<Text style={styles.emptyEmoji}>🌱</Text>
|
||||
<Text style={styles.emptyTitle}>Your story starts today</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Complete your first day on the Daily tab.{'\n'}
|
||||
Every small step gets recorded here as part of your transformation.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
{/* Summary stats */}
|
||||
<View style={styles.statsRow}>
|
||||
<View style={styles.statCard}>
|
||||
<Text style={styles.statEmoji}>🚀</Text>
|
||||
<Text style={[styles.statValue, { color: colors.primary }]}>{journeyCount}</Text>
|
||||
<Text style={styles.statLabel}>{journeyCount === 1 ? 'Journey' : 'Journeys'}</Text>
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Text style={styles.statEmoji}>📅</Text>
|
||||
<Text style={styles.statValue}>{totalDays}</Text>
|
||||
<Text style={styles.statLabel}>Days logged</Text>
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Text style={styles.statEmoji}>🎯</Text>
|
||||
<Text style={[styles.statValue, { color: colors.success }]}>{yesCount}</Text>
|
||||
<Text style={styles.statLabel}>Aligned</Text>
|
||||
</View>
|
||||
<View style={styles.statCard}>
|
||||
<Text style={styles.statEmoji}>🔥</Text>
|
||||
<Text style={[styles.statValue, { color: colors.accent }]}>{streakCount}</Text>
|
||||
<Text style={styles.statLabel}>Streak</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<NovaAd />
|
||||
|
||||
{/* Motivational banner */}
|
||||
<View style={styles.motivBanner}>
|
||||
<Text style={styles.motivText}>
|
||||
{totalDays < 5
|
||||
? "You're just getting started. The hardest part is showing up — and you did."
|
||||
: totalDays < 15
|
||||
? "You're building something real. Most people never make it this far."
|
||||
: totalDays < 30
|
||||
? "Look at this. Look at all these days. You are not the same person who started."
|
||||
: "This is what commitment looks like. You didn't just try — you transformed."}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Timeline */}
|
||||
<Text style={styles.timelineTitle}>Timeline</Text>
|
||||
{entries.map((entry, i) => (
|
||||
<HistoryCard key={entry.id || i} entry={entry} isFirst={i === 0} />
|
||||
))}
|
||||
|
||||
<NovaAd />
|
||||
|
||||
{/* Journey end message */}
|
||||
<View style={styles.endMessage}>
|
||||
<Text style={styles.endEmoji}>🌟</Text>
|
||||
<Text style={styles.endText}>
|
||||
{totalDays} days of choosing who you want to be.{'\n'}That's not nothing. That's everything.
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Animated.ScrollView>
|
||||
</ScreenWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scroll: { flex: 1 },
|
||||
scrollContent: { padding: spacing.lg, paddingBottom: spacing.xxl * 2 },
|
||||
loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||
loadingText: { color: colors.textSecondary, fontSize: fonts.sizes.md },
|
||||
|
||||
// Header
|
||||
title: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.xs },
|
||||
subtitle: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 20, marginBottom: spacing.lg },
|
||||
|
||||
// Empty state
|
||||
emptyState: { alignItems: 'center', paddingTop: spacing.xxl * 2 },
|
||||
emptyEmoji: { fontSize: 56, marginBottom: spacing.lg },
|
||||
emptyTitle: { color: colors.text, fontSize: fonts.sizes.lg, fontWeight: fonts.weights.bold, marginBottom: spacing.sm },
|
||||
emptyText: { color: colors.textMuted, fontSize: fonts.sizes.sm, textAlign: 'center', lineHeight: 22 },
|
||||
|
||||
// Stats
|
||||
statsRow: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.lg },
|
||||
statCard: {
|
||||
flex: 1, alignItems: 'center',
|
||||
backgroundColor: colors.surface, borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.md, borderWidth: 1, borderColor: colors.border,
|
||||
},
|
||||
statEmoji: { fontSize: 20, marginBottom: spacing.xs },
|
||||
statValue: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold },
|
||||
statLabel: { color: colors.textMuted, fontSize: 10, marginTop: 2 },
|
||||
|
||||
// Motivational banner
|
||||
motivBanner: {
|
||||
backgroundColor: 'rgba(108, 99, 255, 0.06)',
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.lg,
|
||||
borderLeftWidth: 3,
|
||||
borderLeftColor: colors.primary,
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
motivText: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 22, fontStyle: 'italic' },
|
||||
|
||||
// Timeline
|
||||
timelineTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.bold, marginBottom: spacing.md },
|
||||
|
||||
// Card
|
||||
card: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 0,
|
||||
},
|
||||
timeline: {
|
||||
width: 24,
|
||||
alignItems: 'center',
|
||||
paddingTop: 6,
|
||||
},
|
||||
timelineDot: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
shadowColor: colors.primary,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.5,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
timelineLine: {
|
||||
flex: 1,
|
||||
width: 2,
|
||||
backgroundColor: colors.border,
|
||||
marginTop: 4,
|
||||
},
|
||||
cardContent: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.surface,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.md,
|
||||
marginLeft: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
},
|
||||
|
||||
// Card header
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
cardHeaderLeft: {},
|
||||
cardHeaderRight: { alignItems: 'flex-end' },
|
||||
dayNum: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.bold },
|
||||
identityTag: { color: colors.primary, fontSize: 10, fontWeight: fonts.weights.medium, marginTop: 2 },
|
||||
moodEmoji: { fontSize: 18, marginBottom: 2 },
|
||||
dateText: { color: colors.textMuted, fontSize: 10 },
|
||||
|
||||
// Check badge
|
||||
checkBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderRadius: borderRadius.full,
|
||||
paddingVertical: spacing.xs,
|
||||
paddingHorizontal: spacing.sm,
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: spacing.sm,
|
||||
gap: 6,
|
||||
},
|
||||
checkDot: { width: 6, height: 6, borderRadius: 3 },
|
||||
checkText: { fontSize: 11, fontWeight: fonts.weights.semibold },
|
||||
habitsBadge: { color: colors.textMuted, fontSize: 10, marginLeft: 4 },
|
||||
|
||||
// AI section
|
||||
aiSection: { marginBottom: spacing.sm },
|
||||
aiTitle: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold, lineHeight: 20, marginBottom: spacing.xs },
|
||||
aiQuote: { color: colors.accent, fontSize: fonts.sizes.xs, fontStyle: 'italic', lineHeight: 18 },
|
||||
|
||||
// Expanded
|
||||
expandedSection: { marginTop: spacing.sm },
|
||||
aiSummary: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 20, marginBottom: spacing.md },
|
||||
|
||||
// Journal section
|
||||
journalSection: { marginTop: spacing.xs },
|
||||
journalDivider: { flexDirection: 'row', alignItems: 'center', marginBottom: spacing.md },
|
||||
journalDividerLine: { flex: 1, height: 1, backgroundColor: colors.border },
|
||||
journalDividerText: { color: colors.textMuted, fontSize: 10, marginHorizontal: spacing.sm, fontWeight: fonts.weights.medium },
|
||||
|
||||
journalRow: { flexDirection: 'row', marginBottom: spacing.sm },
|
||||
journalIcon: { fontSize: 14, marginRight: spacing.sm, marginTop: 1 },
|
||||
journalBody: { flex: 1 },
|
||||
journalLabel: { color: colors.textMuted, fontSize: 10, fontWeight: fonts.weights.medium, marginBottom: 1 },
|
||||
journalValue: { color: colors.textSecondary, fontSize: fonts.sizes.sm, lineHeight: 20 },
|
||||
|
||||
// Expand hint
|
||||
expandHint: { color: colors.textMuted, fontSize: 10, textAlign: 'center', marginTop: spacing.xs },
|
||||
|
||||
// End message
|
||||
endMessage: { alignItems: 'center', paddingTop: spacing.xl, paddingBottom: spacing.lg },
|
||||
endEmoji: { fontSize: 32, marginBottom: spacing.sm },
|
||||
endText: { color: colors.textSecondary, fontSize: fonts.sizes.sm, textAlign: 'center', lineHeight: 22 },
|
||||
});
|
||||
+66
-42
@@ -23,10 +23,12 @@ export default function LoginScreen({ navigation }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [demoLoading, setDemoLoading] = useState(false);
|
||||
const [googleLoading, setGoogleLoading] = useState(false);
|
||||
const [facebookLoading, setFacebookLoading] = useState(false);
|
||||
|
||||
const login = useAuthStore((s) => s.login);
|
||||
const loginAsDemo = useAuthStore((s) => s.loginAsDemo);
|
||||
const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle);
|
||||
const loginWithFacebook = useAuthStore((s) => s.loginWithFacebook);
|
||||
|
||||
// Load remembered email on mount
|
||||
useEffect(() => {
|
||||
@@ -96,10 +98,7 @@ export default function LoginScreen({ navigation }) {
|
||||
setGoogleLoading(true);
|
||||
try {
|
||||
const data = await loginWithGoogle();
|
||||
// OAuth opens a browser — session will be picked up by onAuthStateChange
|
||||
if (data?.url) {
|
||||
await Linking.openURL(data.url);
|
||||
}
|
||||
if (data?.url) await Linking.openURL(data.url);
|
||||
} catch (error) {
|
||||
showAlert('Google sign-in failed', error.message);
|
||||
} finally {
|
||||
@@ -107,7 +106,19 @@ export default function LoginScreen({ navigation }) {
|
||||
}
|
||||
};
|
||||
|
||||
const anyLoading = loading || demoLoading || googleLoading;
|
||||
const handleFacebookLogin = async () => {
|
||||
setFacebookLoading(true);
|
||||
try {
|
||||
const data = await loginWithFacebook();
|
||||
if (data?.url) await Linking.openURL(data.url);
|
||||
} catch (error) {
|
||||
showAlert('Facebook sign-in failed', error.message);
|
||||
} finally {
|
||||
setFacebookLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const anyLoading = loading || demoLoading || googleLoading || facebookLoading;
|
||||
|
||||
return (
|
||||
<ScreenWrapper>
|
||||
@@ -176,31 +187,30 @@ export default function LoginScreen({ navigation }) {
|
||||
|
||||
{/* Google Login */}
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.googleBtn, pressed && styles.googleBtnPressed]}
|
||||
style={({ pressed }) => [styles.socialBtn, styles.googleBtn, pressed && styles.socialBtnPressed]}
|
||||
onPress={handleGoogleLogin}
|
||||
disabled={anyLoading}
|
||||
>
|
||||
<Text style={styles.googleIcon}>G</Text>
|
||||
<Text style={styles.googleText}>Continue with Google</Text>
|
||||
<Text style={styles.socialText}>Continue with Google</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Facebook Login */}
|
||||
<Pressable
|
||||
style={({ pressed }) => [styles.socialBtn, styles.facebookBtn, pressed && styles.socialBtnPressed]}
|
||||
onPress={handleFacebookLogin}
|
||||
disabled={anyLoading}
|
||||
>
|
||||
<Text style={styles.facebookIcon}>f</Text>
|
||||
<Text style={styles.socialText}>Continue with Facebook</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Demo Login */}
|
||||
<Button
|
||||
title="Try the Demo"
|
||||
onPress={handleDemoLogin}
|
||||
loading={demoLoading}
|
||||
disabled={anyLoading && !demoLoading}
|
||||
variant="secondary"
|
||||
style={styles.demoBtn}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>New here? </Text>
|
||||
<TouchableOpacity onPress={() => navigation.navigate('Register')}>
|
||||
<Text style={styles.footerLink}>Join Nova40</Text>
|
||||
</TouchableOpacity>
|
||||
{/* App info */}
|
||||
<View style={styles.appInfo}>
|
||||
<Text style={styles.appName}>NOVA40</Text>
|
||||
<Text style={styles.appVersion}>v1.0.0</Text>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</ScreenWrapper>
|
||||
@@ -293,49 +303,63 @@ const styles = StyleSheet.create({
|
||||
marginHorizontal: spacing.md,
|
||||
},
|
||||
|
||||
// Google
|
||||
googleBtn: {
|
||||
// Social buttons
|
||||
socialBtn: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
borderWidth: 1.5,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
googleBtnPressed: {
|
||||
socialBtnPressed: {
|
||||
opacity: 0.7,
|
||||
},
|
||||
socialText: {
|
||||
color: colors.text,
|
||||
fontSize: fonts.sizes.md,
|
||||
fontWeight: fonts.weights.medium,
|
||||
},
|
||||
googleBtn: {
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surface,
|
||||
},
|
||||
googleIcon: {
|
||||
fontSize: 20,
|
||||
fontWeight: fonts.weights.bold,
|
||||
color: '#4285F4',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
googleText: {
|
||||
color: colors.text,
|
||||
fontSize: fonts.sizes.md,
|
||||
fontWeight: fonts.weights.medium,
|
||||
facebookBtn: {
|
||||
borderColor: 'rgba(24, 119, 242, 0.3)',
|
||||
backgroundColor: 'rgba(24, 119, 242, 0.08)',
|
||||
},
|
||||
facebookIcon: {
|
||||
fontSize: 20,
|
||||
fontWeight: fonts.weights.bold,
|
||||
color: '#1877F2',
|
||||
marginRight: spacing.sm,
|
||||
},
|
||||
|
||||
demoBtn: {
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
|
||||
// Footer
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
// App info
|
||||
appInfo: {
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing.xl,
|
||||
},
|
||||
footerText: {
|
||||
color: colors.textSecondary,
|
||||
fontSize: fonts.sizes.sm,
|
||||
},
|
||||
footerLink: {
|
||||
appName: {
|
||||
color: colors.primary,
|
||||
fontSize: fonts.sizes.sm,
|
||||
fontWeight: fonts.weights.semibold,
|
||||
fontSize: fonts.sizes.xs,
|
||||
fontWeight: fonts.weights.bold,
|
||||
letterSpacing: 4,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
appVersion: {
|
||||
color: colors.textMuted,
|
||||
fontSize: fonts.sizes.xs,
|
||||
},
|
||||
});
|
||||
|
||||
+183
-128
@@ -10,8 +10,9 @@ import useAuthStore from '../store/useAuthStore';
|
||||
import useIdentityStore from '../store/useIdentityStore';
|
||||
import useHabitStore from '../store/useHabitStore';
|
||||
import { getProfile } from '../services/profileService';
|
||||
import * as offline from '../services/offlineStorage';
|
||||
import { colors, fonts, spacing, borderRadius } from '../utils/theme';
|
||||
import { formatDate } from '../utils/helpers';
|
||||
import { formatDate, getDayPhase } from '../utils/helpers';
|
||||
|
||||
// --- Reusable guide components ---
|
||||
function Section({ icon, title, children }) {
|
||||
@@ -60,14 +61,23 @@ export default function ProfileScreen({ navigation }) {
|
||||
const [resetting, setResetting] = useState(false);
|
||||
const [focusCount, setFocusCount] = useState(0);
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [journeyCount, setJourneyCount] = useState(0);
|
||||
const [totalDaysLogged, setTotalDaysLogged] = useState(0);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setFocusCount((c) => c + 1);
|
||||
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
|
||||
// Load profile
|
||||
if (user?.id) {
|
||||
getProfile(user.id).then((p) => setProfile(p));
|
||||
// Load journey stats
|
||||
offline.getAll('identities', { user_id: user.id }).then((ids) => setJourneyCount(ids.length));
|
||||
offline.getAll('daily_logs').then((logs) => {
|
||||
offline.getAll('identities', { user_id: user.id }).then((ids) => {
|
||||
const idSet = new Set(ids.map((i) => i.id));
|
||||
setTotalDaysLogged(logs.filter((l) => idSet.has(l.identity_id) && (l.identity_check || l.ai_title)).length);
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [user?.id])
|
||||
);
|
||||
@@ -77,36 +87,30 @@ export default function ProfileScreen({ navigation }) {
|
||||
showAlert('Tell us why', "We'd like to understand why. It helps us help you.");
|
||||
return;
|
||||
}
|
||||
showAlert(
|
||||
'Are you sure?',
|
||||
"Everything you've built will be gone. This can't be undone.",
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Yes, Reset',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setResetting(true);
|
||||
try {
|
||||
await resetJourney(resetReason.trim());
|
||||
setShowReset(false);
|
||||
setResetReason('');
|
||||
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'IdentityStory' }] }));
|
||||
} catch (e) {
|
||||
showAlert('Error', e?.message || 'Failed to reset.');
|
||||
} finally { setResetting(false); }
|
||||
},
|
||||
showAlert('Are you sure?', "Everything you've built will be gone. This can't be undone.", [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Yes, Reset', style: 'destructive',
|
||||
onPress: async () => {
|
||||
setResetting(true);
|
||||
try {
|
||||
await resetJourney(resetReason.trim());
|
||||
setShowReset(false);
|
||||
setResetReason('');
|
||||
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'IdentityStory' }] }));
|
||||
} catch (e) {
|
||||
showAlert('Error', e?.message || 'Failed to reset.');
|
||||
} finally { setResetting(false); }
|
||||
},
|
||||
]
|
||||
);
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const handleSignOut = () => {
|
||||
showAlert('Leaving already?', 'You can always come back.', [
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Sign Out',
|
||||
style: 'destructive',
|
||||
text: 'Sign Out', style: 'destructive',
|
||||
onPress: async () => {
|
||||
resetIdentity(); resetHabits(); await logout();
|
||||
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Login' }] }));
|
||||
@@ -115,6 +119,23 @@ export default function ProfileScreen({ navigation }) {
|
||||
]);
|
||||
};
|
||||
|
||||
const phase = getDayPhase(currentDay);
|
||||
const progressPct = Math.round((currentDay / 40) * 100);
|
||||
const displayName = profile?.nickname || profile?.fullName || user?.email?.split('@')[0] || 'Nova';
|
||||
|
||||
// Dynamic greeting
|
||||
const hour = new Date().getHours();
|
||||
const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening';
|
||||
|
||||
// Phase motivational message
|
||||
const phaseMessages = {
|
||||
awakening: "You're planting the seeds of change.",
|
||||
building: "Momentum is building. Can you feel it?",
|
||||
testing: "This is where champions are made.",
|
||||
strengthening: "Your new identity is taking shape.",
|
||||
transcending: "You're becoming unstoppable.",
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenWrapper>
|
||||
<Animated.ScrollView
|
||||
@@ -123,62 +144,91 @@ export default function ProfileScreen({ navigation }) {
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* ═══ Profile Header ═══ */}
|
||||
<FadeIn delay={0} trigger={focusCount}><View style={st.profileHeader}>
|
||||
<Pressable onPress={() => navigation.navigate('EditProfile')}>
|
||||
{profile?.photoUri ? (
|
||||
<Image source={{ uri: profile.photoUri }} style={st.avatarPhoto} />
|
||||
) : (
|
||||
<View style={st.avatar}>
|
||||
<Text style={st.avatarText}>
|
||||
{(profile?.nickname?.[0] || profile?.fullName?.[0] || user?.email?.[0] || 'N').toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
{profile?.nickname || profile?.fullName ? (
|
||||
<>
|
||||
<Text style={st.displayName}>{profile.nickname || profile.fullName}</Text>
|
||||
{profile?.fullName && profile?.nickname ? (
|
||||
<Text style={st.fullName}>{profile.fullName}</Text>
|
||||
) : null}
|
||||
<Text style={st.email} numberOfLines={1}>{user?.email}</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text style={st.email} numberOfLines={1} adjustsFontSizeToFit>{user?.email}</Text>
|
||||
)}
|
||||
<Pressable
|
||||
style={({ pressed }) => [st.editBtn, pressed && st.editBtnPressed]}
|
||||
onPress={() => navigation.navigate('EditProfile')}
|
||||
>
|
||||
<Text style={st.editBtnText}>Edit Profile</Text>
|
||||
</Pressable>
|
||||
</View></FadeIn>
|
||||
{/* ═══ Profile Hero ═══ */}
|
||||
<FadeIn delay={0} trigger={focusCount}>
|
||||
<View style={st.heroSection}>
|
||||
<Pressable onPress={() => navigation.navigate('EditProfile')}>
|
||||
{profile?.photoUri ? (
|
||||
<Image source={{ uri: profile.photoUri }} style={st.avatarPhoto} />
|
||||
) : (
|
||||
<View style={st.avatar}>
|
||||
<Text style={st.avatarText}>
|
||||
{displayName[0].toUpperCase()}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
{/* ═══ Active Identity ═══ */}
|
||||
<Text style={st.greeting}>{greeting},</Text>
|
||||
<Text style={st.displayName}>{displayName}</Text>
|
||||
|
||||
<Pressable
|
||||
style={({ pressed }) => [st.editBtn, pressed && st.editBtnPressed]}
|
||||
onPress={() => navigation.navigate('EditProfile')}
|
||||
>
|
||||
<Text style={st.editBtnText}>Edit Profile</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</FadeIn>
|
||||
|
||||
{/* ═══ Journey Progress Card ═══ */}
|
||||
{identity && (
|
||||
<FadeIn delay={100} trigger={focusCount}><View style={st.identityCard}>
|
||||
<Text style={st.identityLabel}>Your identity</Text>
|
||||
<Text style={st.identityTitle}>{identity.title}</Text>
|
||||
<View style={st.identitySep} />
|
||||
<View style={st.identityRow}>
|
||||
<Text style={st.identityMeta}>Day {currentDay}/40</Text>
|
||||
<Text style={st.identityMeta}>Started {formatDate(identity.start_date)}</Text>
|
||||
<FadeIn delay={100} trigger={focusCount}>
|
||||
<View style={st.journeyCard}>
|
||||
<View style={st.journeyHeader}>
|
||||
<View>
|
||||
<Text style={st.journeyLabel}>{phase.toUpperCase()} PHASE</Text>
|
||||
<Text style={st.journeyTitle}>{identity.title}</Text>
|
||||
</View>
|
||||
<View style={st.journeyDayCircle}>
|
||||
<Text style={st.journeyDayNum}>{currentDay}</Text>
|
||||
<Text style={st.journeyDayLabel}>DAY</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Progress bar */}
|
||||
<View style={st.progressRow}>
|
||||
<View style={st.progressBar}>
|
||||
<View style={[st.progressFill, { width: `${progressPct}%` }]} />
|
||||
</View>
|
||||
<Text style={st.progressPct}>{progressPct}%</Text>
|
||||
</View>
|
||||
|
||||
{/* Phase message */}
|
||||
<Text style={st.phaseMessage}>{phaseMessages[phase] || 'Keep going.'}</Text>
|
||||
|
||||
{/* Journey meta */}
|
||||
<View style={st.journeyMeta}>
|
||||
<View style={st.journeyMetaItem}>
|
||||
<Text style={st.journeyMetaValue}>{40 - currentDay}</Text>
|
||||
<Text style={st.journeyMetaLabel}>days left</Text>
|
||||
</View>
|
||||
<View style={[st.journeyMetaItem, st.journeyMetaCenter]}>
|
||||
<Text style={st.journeyMetaValue}>{formatDate(identity.start_date)}</Text>
|
||||
<Text style={st.journeyMetaLabel}>started</Text>
|
||||
</View>
|
||||
<View style={st.journeyMetaItem}>
|
||||
<Text style={st.journeyMetaValue}>{formatDate(identity.end_date)}</Text>
|
||||
<Text style={st.journeyMetaLabel}>finish</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View></FadeIn>
|
||||
</FadeIn>
|
||||
)}
|
||||
|
||||
<NovaAd />
|
||||
|
||||
{/* ═══ Scoring Guide ═══ */}
|
||||
<FadeIn delay={200} trigger={focusCount}><Pressable
|
||||
style={({ pressed }) => [st.accordion, st.accordionGuide, pressed && st.accordionPressed]}
|
||||
onPress={() => setShowGuide((p) => !p)}
|
||||
>
|
||||
<Text style={st.accordionIcon}>📘</Text>
|
||||
<Text style={st.accordionText}>How your score works</Text>
|
||||
<Text style={[st.accordionArrow, { color: colors.primary }]}>{showGuide ? '▲' : '▼'}</Text>
|
||||
</Pressable></FadeIn>
|
||||
<FadeIn delay={200} trigger={focusCount}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [st.accordion, st.accordionGuide, pressed && st.accordionPressed]}
|
||||
onPress={() => setShowGuide((p) => !p)}
|
||||
>
|
||||
<Text style={st.accordionIcon}>📘</Text>
|
||||
<Text style={st.accordionText}>How your score works</Text>
|
||||
<Text style={[st.accordionArrow, { color: colors.primary }]}>{showGuide ? '▲' : '▼'}</Text>
|
||||
</Pressable>
|
||||
</FadeIn>
|
||||
|
||||
{showGuide && (
|
||||
<View style={st.accordionBody}>
|
||||
@@ -195,20 +245,11 @@ export default function ProfileScreen({ navigation }) {
|
||||
<Section icon="🔥" title="Streak">
|
||||
<Text style={st.desc}>Consecutive "Yes" or "Almost" days. Resets on "No" or missed day.</Text>
|
||||
</Section>
|
||||
<Section icon="🗺️" title="40-Day Map Colors">
|
||||
<Rule label="Green" value="Yes" color={colors.success} />
|
||||
<Rule label="Yellow" value="Almost" color={colors.warning} />
|
||||
<Rule label="Red" value="No" color={colors.error} />
|
||||
<Rule label="Cyan border" value="Today" color={colors.accent} />
|
||||
</Section>
|
||||
<Section icon="🎮" title="Game Bonus">
|
||||
<Rule label="Reflex Tap" value="Disc ×1.0" color={colors.primary} />
|
||||
<Rule label="Focus Hold" value="Focus ×1.0" color={colors.accent} />
|
||||
<Rule label="Temptation" value="Cons ×1.0" color={colors.success} />
|
||||
<Rule label="Timing Tap" value="Focus ×0.8" color={colors.warning} />
|
||||
<Section icon="📖" title="History">
|
||||
<Text style={st.desc}>All completed days are saved in the History tab — even after you reset and start a new journey.</Text>
|
||||
</Section>
|
||||
<Section icon="🏆" title="Max Score">
|
||||
<Text style={st.desc}>8 pts/day × 40 days = 320 max (before game bonus)</Text>
|
||||
<Text style={st.desc}>8 pts/day × 40 days = 320 max score possible.</Text>
|
||||
</Section>
|
||||
<Section icon="⚡" title="Critical Days">
|
||||
<Text style={st.desc}>Day 3, 10, 21, 30 — turning points where most give up.</Text>
|
||||
@@ -227,18 +268,12 @@ export default function ProfileScreen({ navigation }) {
|
||||
<FadeIn delay={300} trigger={focusCount}>
|
||||
<View style={st.divider} />
|
||||
<View style={st.legalRow}>
|
||||
<Pressable
|
||||
style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]}
|
||||
onPress={() => navigation.navigate('PrivacyPolicy')}
|
||||
>
|
||||
<Pressable style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]} onPress={() => navigation.navigate('PrivacyPolicy')}>
|
||||
<Text style={st.legalIcon}>🔒</Text>
|
||||
<Text style={st.legalText}>Privacy Policy</Text>
|
||||
<Text style={st.legalArrow}>›</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]}
|
||||
onPress={() => navigation.navigate('Terms')}
|
||||
>
|
||||
<Pressable style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]} onPress={() => navigation.navigate('Terms')}>
|
||||
<Text style={st.legalIcon}>📄</Text>
|
||||
<Text style={st.legalText}>Terms & Conditions</Text>
|
||||
<Text style={st.legalArrow}>›</Text>
|
||||
@@ -250,7 +285,6 @@ export default function ProfileScreen({ navigation }) {
|
||||
{identity && (
|
||||
<FadeIn delay={350} trigger={focusCount}>
|
||||
<View style={st.divider} />
|
||||
|
||||
<Pressable
|
||||
style={({ pressed }) => [st.accordion, st.accordionReset, pressed && st.accordionPressed]}
|
||||
onPress={() => setShowReset((p) => !p)}
|
||||
@@ -264,9 +298,8 @@ export default function ProfileScreen({ navigation }) {
|
||||
<View style={st.resetBody}>
|
||||
<View style={st.resetWarningBox}>
|
||||
<Text style={st.resetWarningIcon}>⚠️</Text>
|
||||
<Text style={st.resetWarningText}>This will erase everything — your identity, habits, journal entries, stats, and game scores. There's no going back.</Text>
|
||||
<Text style={st.resetWarningText}>This will erase your current identity, habits, and stats. Your history will be kept.</Text>
|
||||
</View>
|
||||
|
||||
<Text style={st.resetLabel}>Before you go — why the reset?</Text>
|
||||
<TextInput
|
||||
style={st.resetInput}
|
||||
@@ -277,7 +310,6 @@ export default function ProfileScreen({ navigation }) {
|
||||
multiline
|
||||
selectionColor={colors.error}
|
||||
/>
|
||||
|
||||
<Button
|
||||
title={resetting ? 'Resetting...' : 'Reset Journey'}
|
||||
onPress={handleReset}
|
||||
@@ -299,6 +331,7 @@ export default function ProfileScreen({ navigation }) {
|
||||
{/* ═══ Footer ═══ */}
|
||||
<FadeIn delay={500} trigger={focusCount}>
|
||||
<Text style={st.version}>Nova40 v1.0.0</Text>
|
||||
<Text style={st.footerQuote}>Your identity is what you do repeatedly.</Text>
|
||||
</FadeIn>
|
||||
|
||||
</Animated.ScrollView>
|
||||
@@ -310,49 +343,73 @@ const st = StyleSheet.create({
|
||||
flex: { flex: 1 },
|
||||
scroll: { paddingHorizontal: spacing.lg, paddingTop: spacing.xl, paddingBottom: spacing.xxl * 2 },
|
||||
|
||||
// Profile header
|
||||
profileHeader: { alignItems: 'center', marginBottom: spacing.lg },
|
||||
// Hero
|
||||
heroSection: { alignItems: 'center', marginBottom: spacing.xl },
|
||||
avatar: {
|
||||
width: 80, height: 80, borderRadius: 40, backgroundColor: colors.primary,
|
||||
alignItems: 'center', justifyContent: 'center', marginBottom: spacing.sm,
|
||||
shadowColor: colors.primary, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.4, shadowRadius: 12, elevation: 6,
|
||||
width: 90, height: 90, borderRadius: 45, backgroundColor: colors.primary,
|
||||
alignItems: 'center', justifyContent: 'center', marginBottom: spacing.md,
|
||||
shadowColor: colors.primary, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.5, shadowRadius: 16, elevation: 8,
|
||||
},
|
||||
avatarPhoto: {
|
||||
width: 80, height: 80, borderRadius: 40, marginBottom: spacing.sm,
|
||||
width: 90, height: 90, borderRadius: 45, marginBottom: spacing.md,
|
||||
borderWidth: 3, borderColor: colors.primary,
|
||||
},
|
||||
avatarText: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold },
|
||||
displayName: { color: colors.text, fontSize: fonts.sizes.lg, fontWeight: fonts.weights.bold, marginBottom: 2 },
|
||||
fullName: { color: colors.textSecondary, fontSize: fonts.sizes.sm, marginBottom: 2 },
|
||||
email: { color: colors.textMuted, fontSize: fonts.sizes.xs },
|
||||
greeting: { color: colors.accent, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.medium, letterSpacing: 1 },
|
||||
displayName: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, marginTop: 2 },
|
||||
editBtn: {
|
||||
marginTop: spacing.sm,
|
||||
paddingVertical: spacing.xs + 2,
|
||||
paddingHorizontal: spacing.lg,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.primary,
|
||||
marginTop: spacing.md, paddingVertical: spacing.sm, paddingHorizontal: spacing.xl,
|
||||
borderRadius: borderRadius.full, borderWidth: 1, borderColor: colors.primary,
|
||||
},
|
||||
editBtnPressed: { opacity: 0.6 },
|
||||
editBtnText: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.semibold },
|
||||
|
||||
// Identity card
|
||||
identityCard: {
|
||||
backgroundColor: colors.surface, borderRadius: borderRadius.md,
|
||||
padding: spacing.md, marginBottom: spacing.lg,
|
||||
borderWidth: 1, borderColor: 'rgba(108,99,255,0.2)',
|
||||
// Journey card
|
||||
journeyCard: {
|
||||
backgroundColor: colors.surface, borderRadius: borderRadius.xl,
|
||||
padding: spacing.lg, marginBottom: spacing.lg,
|
||||
borderWidth: 1, borderColor: 'rgba(108,99,255,0.25)',
|
||||
},
|
||||
identityLabel: { color: colors.primary, fontSize: 9, fontWeight: fonts.weights.bold, letterSpacing: 2, marginBottom: 6 },
|
||||
identityTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.semibold },
|
||||
identitySep: { height: 1, backgroundColor: colors.border, marginVertical: spacing.sm },
|
||||
identityRow: { flexDirection: 'row', justifyContent: 'space-between' },
|
||||
identityMeta: { color: colors.textMuted, fontSize: 11 },
|
||||
journeyHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.md },
|
||||
journeyLabel: { color: colors.accent, fontSize: 10, fontWeight: fonts.weights.bold, letterSpacing: 2, marginBottom: spacing.xs },
|
||||
journeyTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.bold, maxWidth: 200 },
|
||||
journeyDayCircle: {
|
||||
width: 56, height: 56, borderRadius: 28,
|
||||
borderWidth: 2, borderColor: colors.accent,
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
backgroundColor: 'rgba(0,229,255,0.06)',
|
||||
},
|
||||
journeyDayNum: { color: colors.accent, fontSize: 22, fontWeight: fonts.weights.bold, lineHeight: 26 },
|
||||
journeyDayLabel: { color: colors.accent, fontSize: 8, fontWeight: fonts.weights.bold, letterSpacing: 1 },
|
||||
|
||||
progressRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, marginBottom: spacing.sm },
|
||||
progressBar: { flex: 1, height: 5, backgroundColor: colors.surfaceLight, borderRadius: 3, overflow: 'hidden' },
|
||||
progressFill: { height: '100%', backgroundColor: colors.primary, borderRadius: 3 },
|
||||
progressPct: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, width: 36, textAlign: 'right' },
|
||||
|
||||
phaseMessage: { color: colors.textSecondary, fontSize: fonts.sizes.sm, fontStyle: 'italic', marginBottom: spacing.md },
|
||||
|
||||
journeyMeta: { flexDirection: 'row', borderTopWidth: 1, borderTopColor: colors.border, paddingTop: spacing.md },
|
||||
journeyMetaItem: { flex: 1, alignItems: 'center' },
|
||||
journeyMetaCenter: { borderLeftWidth: 1, borderRightWidth: 1, borderColor: colors.border },
|
||||
journeyMetaValue: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
|
||||
journeyMetaLabel: { color: colors.textMuted, fontSize: 9, marginTop: 2 },
|
||||
|
||||
// Lifetime stats
|
||||
lifetimeRow: { flexDirection: 'row', gap: spacing.sm, marginBottom: spacing.lg },
|
||||
lifetimeCard: {
|
||||
flex: 1, alignItems: 'center',
|
||||
backgroundColor: colors.surface, borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.lg, borderWidth: 1, borderColor: colors.border,
|
||||
},
|
||||
lifetimeEmoji: { fontSize: 24, marginBottom: spacing.sm },
|
||||
lifetimeValue: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold },
|
||||
lifetimeLabel: { color: colors.textMuted, fontSize: 10, marginTop: 2 },
|
||||
|
||||
// Accordion shared
|
||||
accordion: {
|
||||
flexDirection: 'row', alignItems: 'center',
|
||||
borderRadius: borderRadius.md, padding: spacing.md,
|
||||
marginBottom: 6,
|
||||
borderRadius: borderRadius.md, padding: spacing.md, marginBottom: 6,
|
||||
},
|
||||
accordionGuide: { backgroundColor: 'rgba(108,99,255,0.06)', borderWidth: 1, borderColor: 'rgba(108,99,255,0.15)' },
|
||||
accordionReset: { backgroundColor: 'rgba(255,82,82,0.05)', borderWidth: 1, borderColor: 'rgba(255,82,82,0.12)' },
|
||||
@@ -360,12 +417,9 @@ const st = StyleSheet.create({
|
||||
accordionIcon: { fontSize: 16, marginRight: spacing.sm },
|
||||
accordionText: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold, flex: 1 },
|
||||
accordionArrow: { fontSize: 10 },
|
||||
|
||||
// Accordion body
|
||||
accordionBody: { marginBottom: spacing.md },
|
||||
desc: { color: colors.textSecondary, fontSize: 11, lineHeight: 16 },
|
||||
|
||||
// Divider
|
||||
divider: { height: 1, backgroundColor: colors.border, marginVertical: spacing.md },
|
||||
|
||||
// Reset
|
||||
@@ -389,7 +443,7 @@ const st = StyleSheet.create({
|
||||
borderWidth: 1, borderColor: colors.border,
|
||||
minHeight: 90, textAlignVertical: 'top', marginBottom: spacing.lg,
|
||||
},
|
||||
// Sign out
|
||||
|
||||
// Legal
|
||||
legalRow: { gap: spacing.sm },
|
||||
legalBtn: {
|
||||
@@ -406,4 +460,5 @@ const st = StyleSheet.create({
|
||||
|
||||
// Footer
|
||||
version: { color: colors.textMuted, fontSize: 9, textAlign: 'center', marginTop: spacing.xl },
|
||||
footerQuote: { color: colors.textMuted, fontSize: 10, textAlign: 'center', fontStyle: 'italic', marginTop: spacing.sm },
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View, Text, Image, StyleSheet, Animated, Easing } from 'react-native';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import StarField from '../components/StarField';
|
||||
import useAuthStore from '../store/useAuthStore';
|
||||
import useAppStore from '../store/useAppStore';
|
||||
@@ -32,6 +33,18 @@ export default function SplashScreen({ navigation }) {
|
||||
try {
|
||||
const start = Date.now();
|
||||
|
||||
// === DEBUG: List registered users ===
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem('nova40_users');
|
||||
const users = raw ? JSON.parse(raw) : {};
|
||||
const emails = Object.keys(users);
|
||||
console.log('=== REGISTERED USERS ===');
|
||||
console.log('Total:', emails.length);
|
||||
emails.forEach((e) => console.log(' -', e));
|
||||
console.log('========================');
|
||||
} catch (_) {}
|
||||
// === END DEBUG ===
|
||||
|
||||
// Check onboarding + session in parallel (with timeout to prevent hang)
|
||||
const timeout = new Promise((r) => setTimeout(r, 5000));
|
||||
await Promise.race([
|
||||
|
||||
@@ -215,6 +215,18 @@ export async function signInWithGoogle() {
|
||||
throw new Error('Google login is not available in offline mode.');
|
||||
}
|
||||
|
||||
export async function signInWithFacebook() {
|
||||
if (!USE_OFFLINE) {
|
||||
const { data, error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'facebook',
|
||||
options: { redirectTo: 'nova40://auth/callback' },
|
||||
});
|
||||
if (error) throw error;
|
||||
return data;
|
||||
}
|
||||
throw new Error('Facebook login is not available in offline mode.');
|
||||
}
|
||||
|
||||
export function onAuthStateChange(callback) {
|
||||
if (!USE_OFFLINE) {
|
||||
return supabase.auth.onAuthStateChange(callback);
|
||||
|
||||
@@ -56,6 +56,18 @@ const useAuthStore = create((set) => ({
|
||||
}
|
||||
},
|
||||
|
||||
loginWithFacebook: async () => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
const data = await authService.signInWithFacebook();
|
||||
set({ loading: false });
|
||||
return data;
|
||||
} catch (e) {
|
||||
set({ loading: false, error: e.message });
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
|
||||
register: async (email, password) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user