tekout register, add login by fb, add menu history

This commit is contained in:
dios.one
2026-05-05 18:25:10 +07:00
parent 89e15e4263
commit 2b64d3fe65
11 changed files with 2440 additions and 3177 deletions
-6
View File
@@ -21,7 +21,6 @@
"backgroundColor": "#0A0E1A" "backgroundColor": "#0A0E1A"
}, },
"package": "com.nova40.app", "package": "com.nova40.app",
"googleServicesFile": "./google-services.json",
"permissions": [ "permissions": [
"WRITE_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE",
"READ_EXTERNAL_STORAGE" "READ_EXTERNAL_STORAGE"
@@ -36,10 +35,5 @@
} }
}, },
"owner": "heyaciell", "owner": "heyaciell",
"plugins": [
"@react-native-firebase/app",
"@react-native-firebase/crashlytics",
"expo-sharing"
]
} }
} }
+1588 -2955
View File
File diff suppressed because it is too large Load Diff
+15 -20
View File
@@ -10,38 +10,33 @@
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "2.2.0", "@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/bottom-tabs": "^7.15.9",
"@react-navigation/native": "^7.2.2", "@react-navigation/native": "^7.2.2",
"@react-navigation/native-stack": "^7.14.11", "@react-navigation/native-stack": "^7.14.11",
"@supabase/ssr": "^0.10.2", "@supabase/ssr": "^0.10.2",
"@supabase/supabase-js": "^2.103.0", "@supabase/supabase-js": "^2.103.0",
"babel-preset-expo": "~55.0.8", "babel-preset-expo": "~54.0.10",
"expo": "^55.0.19", "expo": "~54.0.0",
"expo-dev-client": "~55.0.30", "expo-dev-client": "~6.0.21",
"expo-image-picker": "~55.0.19", "expo-image-picker": "~17.0.11",
"expo-linear-gradient": "~55.0.13", "expo-linear-gradient": "~15.0.8",
"expo-media-library": "~55.0.15", "expo-media-library": "~18.2.1",
"expo-print": "~55.0.13", "expo-print": "~15.0.8",
"expo-sharing": "~55.0.18", "expo-sharing": "~14.0.8",
"expo-status-bar": "~55.0.5", "expo-status-bar": "~3.0.9",
"react": "19.2.0", "react": "19.1.0",
"react-native": "0.83.6", "react-native": "0.81.5",
"react-native-gesture-handler": "~2.30.0", "react-native-gesture-handler": "~2.28.0",
"react-native-google-mobile-ads": "^16.3.3", "react-native-reanimated": "~4.1.1",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.0", "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-url-polyfill": "^3.0.0",
"react-native-view-shot": "4.0.3", "react-native-view-shot": "4.0.3",
"react-native-worklets": "0.7.4",
"zustand": "^5.0.12" "zustand": "^5.0.12"
}, },
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@types/react": "~19.2.10", "@types/react": "~19.1.10",
"typescript": "~5.9.2" "typescript": "~5.9.2"
} }
} }
+8 -7
View File
@@ -17,6 +17,7 @@ import DailyScreen from '../screens/DailyScreen';
import StatsScreen from '../screens/StatsScreen'; import StatsScreen from '../screens/StatsScreen';
import MirrorScreen from '../screens/MirrorScreen'; import MirrorScreen from '../screens/MirrorScreen';
import MiniGameScreen from '../screens/MiniGameScreen'; import MiniGameScreen from '../screens/MiniGameScreen';
import HistoryScreen from '../screens/HistoryScreen';
import CompletionScreen from '../screens/CompletionScreen'; import CompletionScreen from '../screens/CompletionScreen';
import ProfileScreen from '../screens/ProfileScreen'; import ProfileScreen from '../screens/ProfileScreen';
import DailyJournalScreen from '../screens/DailyJournalScreen'; import DailyJournalScreen from '../screens/DailyJournalScreen';
@@ -33,7 +34,7 @@ const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator(); const Tab = createBottomTabNavigator();
function TabIcon({ label, focused }) { function TabIcon({ label, focused }) {
const icons = { Daily: '◈', Game: '◎', Stats: '◉', Profile: '◆' }; const icons = { Daily: '◈', History: '◎', Stats: '◉', Profile: '◆' };
const activeColor = colors.primary; const activeColor = colors.primary;
const inactiveColor = colors.textSecondary; // #8A8FB5 — brighter than textMuted const inactiveColor = colors.textSecondary; // #8A8FB5 — brighter than textMuted
return ( return (
@@ -63,9 +64,9 @@ function MainTabs() {
backgroundColor: colors.surface, backgroundColor: colors.surface,
borderTopColor: colors.border, borderTopColor: colors.border,
borderTopWidth: 1, borderTopWidth: 1,
height: 70, height: 90,
paddingBottom: 10, paddingBottom: 28,
paddingTop: 6, paddingTop: 8,
}, },
tabBarShowLabel: false, tabBarShowLabel: false,
}} }}
@@ -74,8 +75,8 @@ function MainTabs() {
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Daily" focused={focused} /> }} /> options={{ tabBarIcon: ({ focused }) => <TabIcon label="Daily" focused={focused} /> }} />
<Tab.Screen name="Stats" component={StatsScreen} <Tab.Screen name="Stats" component={StatsScreen}
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Stats" focused={focused} /> }} /> options={{ tabBarIcon: ({ focused }) => <TabIcon label="Stats" focused={focused} /> }} />
<Tab.Screen name="Game" component={MiniGameScreen} <Tab.Screen name="History" component={HistoryScreen}
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Game" focused={focused} /> }} /> options={{ tabBarIcon: ({ focused }) => <TabIcon label="History" focused={focused} /> }} />
<Tab.Screen name="Profile" component={ProfileScreen} <Tab.Screen name="Profile" component={ProfileScreen}
options={{ tabBarIcon: ({ focused }) => <TabIcon label="Profile" focused={focused} /> }} /> options={{ tabBarIcon: ({ focused }) => <TabIcon label="Profile" focused={focused} /> }} />
</Tab.Navigator> </Tab.Navigator>
@@ -106,7 +107,7 @@ export default function AppNavigator() {
<Stack.Screen name="Splash" component={SplashScreen} /> <Stack.Screen name="Splash" component={SplashScreen} />
<Stack.Screen name="Onboarding" component={OnboardingScreen} /> <Stack.Screen name="Onboarding" component={OnboardingScreen} />
<Stack.Screen name="Login" component={LoginScreen} /> <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="ForgotPassword" component={ForgotPasswordScreen} />
<Stack.Screen name="IdentityStory" component={IdentityStoryScreen} /> <Stack.Screen name="IdentityStory" component={IdentityStoryScreen} />
<Stack.Screen name="HabitSelection" component={HabitSelectionScreen} /> <Stack.Screen name="HabitSelection" component={HabitSelectionScreen} />
+125 -17
View File
@@ -179,38 +179,108 @@ export default function DailyScreen({ navigation }) {
// ====== COMPLETED STATE ====== // ====== COMPLETED STATE ======
if (completed && savedEntry?.ai_title) { 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 = () => ( const JournalCard = () => (
<View style={s.journalCard}> <View style={s.journalCard}>
<Text style={s.jBrand}>NOVA40</Text> <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.jEmoji}>{MOOD_EMOJIS[savedEntry.mood] || '\u2728'}</Text>
<Text style={s.jTitle}>{savedEntry.ai_title}</Text> <Text style={s.jTitle}>{savedEntry.ai_title}</Text>
<Text style={s.jSummary}>{savedEntry.ai_summary}</Text> <Text style={s.jSummary}>{savedEntry.ai_summary}</Text>
<View style={s.jDivider} /> <View style={s.jDivider} />
<Text style={s.jQuote}>"{savedEntry.ai_quote}"</Text> <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}> <View style={s.jFooter}>
<Text style={s.jDay}>Day {currentDay}</Text> <Text style={s.jDay}>Day {currentDay}/40</Text>
<Text style={s.jDate}>{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}</Text> <Text style={s.jDate}>{new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}</Text>
</View> </View>
{identity?.title && <Text style={s.jIdentity}>{identity.title}</Text>} {identity?.title && <Text style={s.jIdentity}>{identity.title}</Text>}
</View> </View>
); );
return ( return (
<ScreenWrapper> <ScreenWrapper>
<Animated.ScrollView style={[s.flex, { opacity: fadeAnim }]} contentContainerStyle={s.scrollContent} showsVerticalScrollIndicator={false}> <Animated.ScrollView style={[s.flex, { opacity: fadeAnim }]} contentContainerStyle={s.scrollContent} showsVerticalScrollIndicator={false}>
{/* Success header */}
{/* Celebration header */}
<View style={s.doneHeader}> <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.doneTitle}>Day {currentDay} Complete!</Text>
<Text style={s.doneSubtitle}>You showed up. That matters.</Text> <Text style={s.doneSubtitle}>{celebMsg}</Text>
</View> </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}> <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> </View>
{/* Entries removed */}
</Animated.ScrollView> </Animated.ScrollView>
</ScreenWrapper> </ScreenWrapper>
); );
@@ -397,24 +467,62 @@ const s = StyleSheet.create({
submitHint: { color: colors.textMuted, fontSize: fonts.sizes.xs, textAlign: 'center', marginTop: spacing.sm }, submitHint: { color: colors.textMuted, fontSize: fonts.sizes.xs, textAlign: 'center', marginTop: spacing.sm },
// ====== Completed state ====== // ====== Completed state ======
doneHeader: { alignItems: 'center', paddingTop: spacing.lg, marginBottom: spacing.xl }, doneHeader: { alignItems: 'center', paddingTop: spacing.xl, marginBottom: spacing.lg },
doneEmoji: { fontSize: 48, marginBottom: spacing.md }, doneEmojiBig: { fontSize: 56, marginBottom: spacing.md },
doneTitle: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.xs }, doneTitle: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold, marginBottom: spacing.sm },
doneSubtitle: { color: colors.textSecondary, fontSize: fonts.sizes.md }, 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' }, // Stats row
jBrand: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.bold, letterSpacing: 5, marginBottom: spacing.lg }, doneStatsRow: {
jEmoji: { fontSize: 36, marginBottom: spacing.md }, flexDirection: 'row', justifyContent: 'space-around',
jTitle: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, textAlign: 'center', lineHeight: 30, marginBottom: spacing.md }, 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 }, 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 }, 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 }, 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 }, jFooter: { flexDirection: 'row', justifyContent: 'center', gap: spacing.md, marginBottom: spacing.sm },
jDay: { color: colors.textMuted, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold }, jDay: { color: colors.textMuted, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold },
jDate: { color: colors.textMuted, fontSize: fonts.sizes.sm }, jDate: { color: colors.textMuted, fontSize: fonts.sizes.sm },
jIdentity: { color: colors.textMuted, fontSize: fonts.sizes.xs, fontStyle: 'italic' }, 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 }, 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 },
}); });
+416
View File
@@ -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
View File
@@ -23,10 +23,12 @@ export default function LoginScreen({ navigation }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [demoLoading, setDemoLoading] = useState(false); const [demoLoading, setDemoLoading] = useState(false);
const [googleLoading, setGoogleLoading] = useState(false); const [googleLoading, setGoogleLoading] = useState(false);
const [facebookLoading, setFacebookLoading] = useState(false);
const login = useAuthStore((s) => s.login); const login = useAuthStore((s) => s.login);
const loginAsDemo = useAuthStore((s) => s.loginAsDemo); const loginAsDemo = useAuthStore((s) => s.loginAsDemo);
const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle); const loginWithGoogle = useAuthStore((s) => s.loginWithGoogle);
const loginWithFacebook = useAuthStore((s) => s.loginWithFacebook);
// Load remembered email on mount // Load remembered email on mount
useEffect(() => { useEffect(() => {
@@ -96,10 +98,7 @@ export default function LoginScreen({ navigation }) {
setGoogleLoading(true); setGoogleLoading(true);
try { try {
const data = await loginWithGoogle(); 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) { } catch (error) {
showAlert('Google sign-in failed', error.message); showAlert('Google sign-in failed', error.message);
} finally { } 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 ( return (
<ScreenWrapper> <ScreenWrapper>
@@ -176,31 +187,30 @@ export default function LoginScreen({ navigation }) {
{/* Google Login */} {/* Google Login */}
<Pressable <Pressable
style={({ pressed }) => [styles.googleBtn, pressed && styles.googleBtnPressed]} style={({ pressed }) => [styles.socialBtn, styles.googleBtn, pressed && styles.socialBtnPressed]}
onPress={handleGoogleLogin} onPress={handleGoogleLogin}
disabled={anyLoading} disabled={anyLoading}
> >
<Text style={styles.googleIcon}>G</Text> <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> </Pressable>
{/* Demo Login */}
<Button
title="Try the Demo"
onPress={handleDemoLogin}
loading={demoLoading}
disabled={anyLoading && !demoLoading}
variant="secondary"
style={styles.demoBtn}
/>
</View> </View>
{/* Footer */} {/* App info */}
<View style={styles.footer}> <View style={styles.appInfo}>
<Text style={styles.footerText}>New here? </Text> <Text style={styles.appName}>NOVA40</Text>
<TouchableOpacity onPress={() => navigation.navigate('Register')}> <Text style={styles.appVersion}>v1.0.0</Text>
<Text style={styles.footerLink}>Join Nova40</Text>
</TouchableOpacity>
</View> </View>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</ScreenWrapper> </ScreenWrapper>
@@ -293,49 +303,63 @@ const styles = StyleSheet.create({
marginHorizontal: spacing.md, marginHorizontal: spacing.md,
}, },
// Google // Social buttons
googleBtn: { socialBtn: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: spacing.md, paddingVertical: spacing.md,
borderRadius: borderRadius.lg, borderRadius: borderRadius.lg,
borderWidth: 1.5, borderWidth: 1.5,
borderColor: colors.border,
backgroundColor: colors.surface,
marginBottom: spacing.sm, marginBottom: spacing.sm,
}, },
googleBtnPressed: { socialBtnPressed: {
opacity: 0.7, opacity: 0.7,
}, },
socialText: {
color: colors.text,
fontSize: fonts.sizes.md,
fontWeight: fonts.weights.medium,
},
googleBtn: {
borderColor: colors.border,
backgroundColor: colors.surface,
},
googleIcon: { googleIcon: {
fontSize: 20, fontSize: 20,
fontWeight: fonts.weights.bold, fontWeight: fonts.weights.bold,
color: '#4285F4', color: '#4285F4',
marginRight: spacing.sm, marginRight: spacing.sm,
}, },
googleText: { facebookBtn: {
color: colors.text, borderColor: 'rgba(24, 119, 242, 0.3)',
fontSize: fonts.sizes.md, backgroundColor: 'rgba(24, 119, 242, 0.08)',
fontWeight: fonts.weights.medium, },
facebookIcon: {
fontSize: 20,
fontWeight: fonts.weights.bold,
color: '#1877F2',
marginRight: spacing.sm,
}, },
demoBtn: { demoBtn: {
marginTop: spacing.xs, marginTop: spacing.xs,
}, },
// Footer // App info
footer: { appInfo: {
flexDirection: 'row', alignItems: 'center',
justifyContent: 'center', paddingTop: spacing.xl,
}, },
footerText: { appName: {
color: colors.textSecondary,
fontSize: fonts.sizes.sm,
},
footerLink: {
color: colors.primary, color: colors.primary,
fontSize: fonts.sizes.sm, fontSize: fonts.sizes.xs,
fontWeight: fonts.weights.semibold, fontWeight: fonts.weights.bold,
letterSpacing: 4,
marginBottom: spacing.xs,
},
appVersion: {
color: colors.textMuted,
fontSize: fonts.sizes.xs,
}, },
}); });
+150 -95
View File
@@ -10,8 +10,9 @@ import useAuthStore from '../store/useAuthStore';
import useIdentityStore from '../store/useIdentityStore'; import useIdentityStore from '../store/useIdentityStore';
import useHabitStore from '../store/useHabitStore'; import useHabitStore from '../store/useHabitStore';
import { getProfile } from '../services/profileService'; import { getProfile } from '../services/profileService';
import * as offline from '../services/offlineStorage';
import { colors, fonts, spacing, borderRadius } from '../utils/theme'; import { colors, fonts, spacing, borderRadius } from '../utils/theme';
import { formatDate } from '../utils/helpers'; import { formatDate, getDayPhase } from '../utils/helpers';
// --- Reusable guide components --- // --- Reusable guide components ---
function Section({ icon, title, children }) { function Section({ icon, title, children }) {
@@ -60,14 +61,23 @@ export default function ProfileScreen({ navigation }) {
const [resetting, setResetting] = useState(false); const [resetting, setResetting] = useState(false);
const [focusCount, setFocusCount] = useState(0); const [focusCount, setFocusCount] = useState(0);
const [profile, setProfile] = useState(null); const [profile, setProfile] = useState(null);
const [journeyCount, setJourneyCount] = useState(0);
const [totalDaysLogged, setTotalDaysLogged] = useState(0);
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
setFocusCount((c) => c + 1); setFocusCount((c) => c + 1);
Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start(); Animated.timing(fadeAnim, { toValue: 1, duration: 500, useNativeDriver: true }).start();
// Load profile
if (user?.id) { if (user?.id) {
getProfile(user.id).then((p) => setProfile(p)); 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]) }, [user?.id])
); );
@@ -77,14 +87,10 @@ export default function ProfileScreen({ navigation }) {
showAlert('Tell us why', "We'd like to understand why. It helps us help you."); showAlert('Tell us why', "We'd like to understand why. It helps us help you.");
return; return;
} }
showAlert( showAlert('Are you sure?', "Everything you've built will be gone. This can't be undone.", [
'Are you sure?',
"Everything you've built will be gone. This can't be undone.",
[
{ text: 'Cancel', style: 'cancel' }, { text: 'Cancel', style: 'cancel' },
{ {
text: 'Yes, Reset', text: 'Yes, Reset', style: 'destructive',
style: 'destructive',
onPress: async () => { onPress: async () => {
setResetting(true); setResetting(true);
try { try {
@@ -97,16 +103,14 @@ export default function ProfileScreen({ navigation }) {
} finally { setResetting(false); } } finally { setResetting(false); }
}, },
}, },
] ]);
);
}; };
const handleSignOut = () => { const handleSignOut = () => {
showAlert('Leaving already?', 'You can always come back.', [ showAlert('Leaving already?', 'You can always come back.', [
{ text: 'Cancel', style: 'cancel' }, { text: 'Cancel', style: 'cancel' },
{ {
text: 'Sign Out', text: 'Sign Out', style: 'destructive',
style: 'destructive',
onPress: async () => { onPress: async () => {
resetIdentity(); resetHabits(); await logout(); resetIdentity(); resetHabits(); await logout();
navigation.dispatch(CommonActions.reset({ index: 0, routes: [{ name: 'Login' }] })); 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 ( return (
<ScreenWrapper> <ScreenWrapper>
<Animated.ScrollView <Animated.ScrollView
@@ -123,62 +144,91 @@ export default function ProfileScreen({ navigation }) {
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled" keyboardShouldPersistTaps="handled"
> >
{/* ═══ Profile Header ═══ */} {/* ═══ Profile Hero ═══ */}
<FadeIn delay={0} trigger={focusCount}><View style={st.profileHeader}> <FadeIn delay={0} trigger={focusCount}>
<View style={st.heroSection}>
<Pressable onPress={() => navigation.navigate('EditProfile')}> <Pressable onPress={() => navigation.navigate('EditProfile')}>
{profile?.photoUri ? ( {profile?.photoUri ? (
<Image source={{ uri: profile.photoUri }} style={st.avatarPhoto} /> <Image source={{ uri: profile.photoUri }} style={st.avatarPhoto} />
) : ( ) : (
<View style={st.avatar}> <View style={st.avatar}>
<Text style={st.avatarText}> <Text style={st.avatarText}>
{(profile?.nickname?.[0] || profile?.fullName?.[0] || user?.email?.[0] || 'N').toUpperCase()} {displayName[0].toUpperCase()}
</Text> </Text>
</View> </View>
)} )}
</Pressable> </Pressable>
{profile?.nickname || profile?.fullName ? (
<> <Text style={st.greeting}>{greeting},</Text>
<Text style={st.displayName}>{profile.nickname || profile.fullName}</Text> <Text style={st.displayName}>{displayName}</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 <Pressable
style={({ pressed }) => [st.editBtn, pressed && st.editBtnPressed]} style={({ pressed }) => [st.editBtn, pressed && st.editBtnPressed]}
onPress={() => navigation.navigate('EditProfile')} onPress={() => navigation.navigate('EditProfile')}
> >
<Text style={st.editBtnText}>Edit Profile</Text> <Text style={st.editBtnText}>Edit Profile</Text>
</Pressable> </Pressable>
</View></FadeIn>
{/* ═══ Active Identity ═══ */}
{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>
</View> </View>
</View></FadeIn> </FadeIn>
{/* ═══ Journey Progress Card ═══ */}
{identity && (
<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>
</FadeIn>
)} )}
<NovaAd /> <NovaAd />
{/* ═══ Scoring Guide ═══ */} {/* ═══ Scoring Guide ═══ */}
<FadeIn delay={200} trigger={focusCount}><Pressable <FadeIn delay={200} trigger={focusCount}>
<Pressable
style={({ pressed }) => [st.accordion, st.accordionGuide, pressed && st.accordionPressed]} style={({ pressed }) => [st.accordion, st.accordionGuide, pressed && st.accordionPressed]}
onPress={() => setShowGuide((p) => !p)} onPress={() => setShowGuide((p) => !p)}
> >
<Text style={st.accordionIcon}>📘</Text> <Text style={st.accordionIcon}>📘</Text>
<Text style={st.accordionText}>How your score works</Text> <Text style={st.accordionText}>How your score works</Text>
<Text style={[st.accordionArrow, { color: colors.primary }]}>{showGuide ? '▲' : '▼'}</Text> <Text style={[st.accordionArrow, { color: colors.primary }]}>{showGuide ? '▲' : '▼'}</Text>
</Pressable></FadeIn> </Pressable>
</FadeIn>
{showGuide && ( {showGuide && (
<View style={st.accordionBody}> <View style={st.accordionBody}>
@@ -195,20 +245,11 @@ export default function ProfileScreen({ navigation }) {
<Section icon="🔥" title="Streak"> <Section icon="🔥" title="Streak">
<Text style={st.desc}>Consecutive "Yes" or "Almost" days. Resets on "No" or missed day.</Text> <Text style={st.desc}>Consecutive "Yes" or "Almost" days. Resets on "No" or missed day.</Text>
</Section> </Section>
<Section icon="🗺️" title="40-Day Map Colors"> <Section icon="📖" title="History">
<Rule label="Green" value="Yes" color={colors.success} /> <Text style={st.desc}>All completed days are saved in the History tab even after you reset and start a new journey.</Text>
<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> </Section>
<Section icon="🏆" title="Max Score"> <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>
<Section icon="⚡" title="Critical Days"> <Section icon="⚡" title="Critical Days">
<Text style={st.desc}>Day 3, 10, 21, 30 turning points where most give up.</Text> <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}> <FadeIn delay={300} trigger={focusCount}>
<View style={st.divider} /> <View style={st.divider} />
<View style={st.legalRow}> <View style={st.legalRow}>
<Pressable <Pressable style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]} onPress={() => navigation.navigate('PrivacyPolicy')}>
style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]}
onPress={() => navigation.navigate('PrivacyPolicy')}
>
<Text style={st.legalIcon}>🔒</Text> <Text style={st.legalIcon}>🔒</Text>
<Text style={st.legalText}>Privacy Policy</Text> <Text style={st.legalText}>Privacy Policy</Text>
<Text style={st.legalArrow}></Text> <Text style={st.legalArrow}></Text>
</Pressable> </Pressable>
<Pressable <Pressable style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]} onPress={() => navigation.navigate('Terms')}>
style={({ pressed }) => [st.legalBtn, pressed && st.legalBtnPressed]}
onPress={() => navigation.navigate('Terms')}
>
<Text style={st.legalIcon}>📄</Text> <Text style={st.legalIcon}>📄</Text>
<Text style={st.legalText}>Terms & Conditions</Text> <Text style={st.legalText}>Terms & Conditions</Text>
<Text style={st.legalArrow}></Text> <Text style={st.legalArrow}></Text>
@@ -250,7 +285,6 @@ export default function ProfileScreen({ navigation }) {
{identity && ( {identity && (
<FadeIn delay={350} trigger={focusCount}> <FadeIn delay={350} trigger={focusCount}>
<View style={st.divider} /> <View style={st.divider} />
<Pressable <Pressable
style={({ pressed }) => [st.accordion, st.accordionReset, pressed && st.accordionPressed]} style={({ pressed }) => [st.accordion, st.accordionReset, pressed && st.accordionPressed]}
onPress={() => setShowReset((p) => !p)} onPress={() => setShowReset((p) => !p)}
@@ -264,9 +298,8 @@ export default function ProfileScreen({ navigation }) {
<View style={st.resetBody}> <View style={st.resetBody}>
<View style={st.resetWarningBox}> <View style={st.resetWarningBox}>
<Text style={st.resetWarningIcon}></Text> <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> </View>
<Text style={st.resetLabel}>Before you go why the reset?</Text> <Text style={st.resetLabel}>Before you go why the reset?</Text>
<TextInput <TextInput
style={st.resetInput} style={st.resetInput}
@@ -277,7 +310,6 @@ export default function ProfileScreen({ navigation }) {
multiline multiline
selectionColor={colors.error} selectionColor={colors.error}
/> />
<Button <Button
title={resetting ? 'Resetting...' : 'Reset Journey'} title={resetting ? 'Resetting...' : 'Reset Journey'}
onPress={handleReset} onPress={handleReset}
@@ -299,6 +331,7 @@ export default function ProfileScreen({ navigation }) {
{/* ═══ Footer ═══ */} {/* ═══ Footer ═══ */}
<FadeIn delay={500} trigger={focusCount}> <FadeIn delay={500} trigger={focusCount}>
<Text style={st.version}>Nova40 v1.0.0</Text> <Text style={st.version}>Nova40 v1.0.0</Text>
<Text style={st.footerQuote}>Your identity is what you do repeatedly.</Text>
</FadeIn> </FadeIn>
</Animated.ScrollView> </Animated.ScrollView>
@@ -310,49 +343,73 @@ const st = StyleSheet.create({
flex: { flex: 1 }, flex: { flex: 1 },
scroll: { paddingHorizontal: spacing.lg, paddingTop: spacing.xl, paddingBottom: spacing.xxl * 2 }, scroll: { paddingHorizontal: spacing.lg, paddingTop: spacing.xl, paddingBottom: spacing.xxl * 2 },
// Profile header // Hero
profileHeader: { alignItems: 'center', marginBottom: spacing.lg }, heroSection: { alignItems: 'center', marginBottom: spacing.xl },
avatar: { avatar: {
width: 80, height: 80, borderRadius: 40, backgroundColor: colors.primary, width: 90, height: 90, borderRadius: 45, backgroundColor: colors.primary,
alignItems: 'center', justifyContent: 'center', marginBottom: spacing.sm, alignItems: 'center', justifyContent: 'center', marginBottom: spacing.md,
shadowColor: colors.primary, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.4, shadowRadius: 12, elevation: 6, shadowColor: colors.primary, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.5, shadowRadius: 16, elevation: 8,
}, },
avatarPhoto: { avatarPhoto: {
width: 80, height: 80, borderRadius: 40, marginBottom: spacing.sm, width: 90, height: 90, borderRadius: 45, marginBottom: spacing.md,
borderWidth: 3, borderColor: colors.primary, borderWidth: 3, borderColor: colors.primary,
}, },
avatarText: { color: colors.text, fontSize: fonts.sizes.xxl, fontWeight: fonts.weights.bold }, 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 }, greeting: { color: colors.accent, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.medium, letterSpacing: 1 },
fullName: { color: colors.textSecondary, fontSize: fonts.sizes.sm, marginBottom: 2 }, displayName: { color: colors.text, fontSize: fonts.sizes.xl, fontWeight: fonts.weights.bold, marginTop: 2 },
email: { color: colors.textMuted, fontSize: fonts.sizes.xs },
editBtn: { editBtn: {
marginTop: spacing.sm, marginTop: spacing.md, paddingVertical: spacing.sm, paddingHorizontal: spacing.xl,
paddingVertical: spacing.xs + 2, borderRadius: borderRadius.full, borderWidth: 1, borderColor: colors.primary,
paddingHorizontal: spacing.lg,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.primary,
}, },
editBtnPressed: { opacity: 0.6 }, editBtnPressed: { opacity: 0.6 },
editBtnText: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.semibold }, editBtnText: { color: colors.primary, fontSize: fonts.sizes.xs, fontWeight: fonts.weights.semibold },
// Identity card // Journey card
identityCard: { journeyCard: {
backgroundColor: colors.surface, borderRadius: borderRadius.md, backgroundColor: colors.surface, borderRadius: borderRadius.xl,
padding: spacing.md, marginBottom: spacing.lg, padding: spacing.lg, marginBottom: spacing.lg,
borderWidth: 1, borderColor: 'rgba(108,99,255,0.2)', borderWidth: 1, borderColor: 'rgba(108,99,255,0.25)',
}, },
identityLabel: { color: colors.primary, fontSize: 9, fontWeight: fonts.weights.bold, letterSpacing: 2, marginBottom: 6 }, journeyHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: spacing.md },
identityTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.semibold }, journeyLabel: { color: colors.accent, fontSize: 10, fontWeight: fonts.weights.bold, letterSpacing: 2, marginBottom: spacing.xs },
identitySep: { height: 1, backgroundColor: colors.border, marginVertical: spacing.sm }, journeyTitle: { color: colors.text, fontSize: fonts.sizes.md, fontWeight: fonts.weights.bold, maxWidth: 200 },
identityRow: { flexDirection: 'row', justifyContent: 'space-between' }, journeyDayCircle: {
identityMeta: { color: colors.textMuted, fontSize: 11 }, 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 shared
accordion: { accordion: {
flexDirection: 'row', alignItems: 'center', flexDirection: 'row', alignItems: 'center',
borderRadius: borderRadius.md, padding: spacing.md, borderRadius: borderRadius.md, padding: spacing.md, marginBottom: 6,
marginBottom: 6,
}, },
accordionGuide: { backgroundColor: 'rgba(108,99,255,0.06)', borderWidth: 1, borderColor: 'rgba(108,99,255,0.15)' }, 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)' }, 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 }, accordionIcon: { fontSize: 16, marginRight: spacing.sm },
accordionText: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold, flex: 1 }, accordionText: { color: colors.text, fontSize: fonts.sizes.sm, fontWeight: fonts.weights.semibold, flex: 1 },
accordionArrow: { fontSize: 10 }, accordionArrow: { fontSize: 10 },
// Accordion body
accordionBody: { marginBottom: spacing.md }, accordionBody: { marginBottom: spacing.md },
desc: { color: colors.textSecondary, fontSize: 11, lineHeight: 16 }, desc: { color: colors.textSecondary, fontSize: 11, lineHeight: 16 },
// Divider
divider: { height: 1, backgroundColor: colors.border, marginVertical: spacing.md }, divider: { height: 1, backgroundColor: colors.border, marginVertical: spacing.md },
// Reset // Reset
@@ -389,7 +443,7 @@ const st = StyleSheet.create({
borderWidth: 1, borderColor: colors.border, borderWidth: 1, borderColor: colors.border,
minHeight: 90, textAlignVertical: 'top', marginBottom: spacing.lg, minHeight: 90, textAlignVertical: 'top', marginBottom: spacing.lg,
}, },
// Sign out
// Legal // Legal
legalRow: { gap: spacing.sm }, legalRow: { gap: spacing.sm },
legalBtn: { legalBtn: {
@@ -406,4 +460,5 @@ const st = StyleSheet.create({
// Footer // Footer
version: { color: colors.textMuted, fontSize: 9, textAlign: 'center', marginTop: spacing.xl }, version: { color: colors.textMuted, fontSize: 9, textAlign: 'center', marginTop: spacing.xl },
footerQuote: { color: colors.textMuted, fontSize: 10, textAlign: 'center', fontStyle: 'italic', marginTop: spacing.sm },
}); });
+13
View File
@@ -1,5 +1,6 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { View, Text, Image, StyleSheet, Animated, Easing } from 'react-native'; 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 StarField from '../components/StarField';
import useAuthStore from '../store/useAuthStore'; import useAuthStore from '../store/useAuthStore';
import useAppStore from '../store/useAppStore'; import useAppStore from '../store/useAppStore';
@@ -32,6 +33,18 @@ export default function SplashScreen({ navigation }) {
try { try {
const start = Date.now(); 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) // Check onboarding + session in parallel (with timeout to prevent hang)
const timeout = new Promise((r) => setTimeout(r, 5000)); const timeout = new Promise((r) => setTimeout(r, 5000));
await Promise.race([ await Promise.race([
+12
View File
@@ -215,6 +215,18 @@ export async function signInWithGoogle() {
throw new Error('Google login is not available in offline mode.'); 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) { export function onAuthStateChange(callback) {
if (!USE_OFFLINE) { if (!USE_OFFLINE) {
return supabase.auth.onAuthStateChange(callback); return supabase.auth.onAuthStateChange(callback);
+12
View File
@@ -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) => { register: async (email, password) => {
set({ loading: true, error: null }); set({ loading: true, error: null });
try { try {