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"
},
"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"
]
}
}
+1590 -2957
View File
File diff suppressed because it is too large Load Diff
+15 -20
View File
@@ -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"
}
}
+8 -7
View File
@@ -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
View File
@@ -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 },
});
+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 [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
View File
@@ -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 },
});
+13
View File
@@ -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([
+12
View File
@@ -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);
+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) => {
set({ loading: true, error: null });
try {