diff --git a/AGENTS.md b/AGENTS.md
index b6cab1e..8496a72 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -280,3 +280,7 @@ Before every feature:
- Don't change the design of the Nav, only add the routing functionality.
- Keep 'leading-6' in all components, and fix if you find any un-matching ones.
+
+- If you see any svgs in the attached design look for it in the '/assets' folder. If we don't have it, try to create them yourself like you created the google icon in the login screens.
+
+- Keep the text sizes like : headings, subheadings, body text, etc consistent on each screen.
diff --git a/UI/Onb v2/onbv2 scr2 card.png b/UI/Onb v2/onbv2 scr2 card.png
new file mode 100644
index 0000000..307d3fc
Binary files /dev/null and b/UI/Onb v2/onbv2 scr2 card.png differ
diff --git a/UI/Onb v2/scr1.png b/UI/Onb v2/scr1.png
new file mode 100644
index 0000000..061ed0f
Binary files /dev/null and b/UI/Onb v2/scr1.png differ
diff --git a/UI/Onb v2/scr2.png b/UI/Onb v2/scr2.png
new file mode 100644
index 0000000..427cbf3
Binary files /dev/null and b/UI/Onb v2/scr2.png differ
diff --git a/UI/Onb v2/scr3.png b/UI/Onb v2/scr3.png
new file mode 100644
index 0000000..eaefe25
Binary files /dev/null and b/UI/Onb v2/scr3.png differ
diff --git a/UI/Onb v2/scr4.png b/UI/Onb v2/scr4.png
new file mode 100644
index 0000000..bff5b00
Binary files /dev/null and b/UI/Onb v2/scr4.png differ
diff --git a/UI/Onb v2/scr5.png b/UI/Onb v2/scr5.png
new file mode 100644
index 0000000..2eff784
Binary files /dev/null and b/UI/Onb v2/scr5.png differ
diff --git a/app/(auth)/login.tsx b/app/(auth)/login.tsx
index 4ef8aa5..e3fdd26 100644
--- a/app/(auth)/login.tsx
+++ b/app/(auth)/login.tsx
@@ -18,6 +18,7 @@ export default function LoginScreen() {
footerText="Don't have an account?"
heading="Welcome back"
mode="login"
+ showTemporaryOnboardingBackButton
subheading="Your thoughts are waiting for you."
/>
>
diff --git a/app/(onboarding)/onboarding-screen-1.tsx b/app/(onboarding)/onboarding-screen-1.tsx
index 456364f..dbc60bf 100644
--- a/app/(onboarding)/onboarding-screen-1.tsx
+++ b/app/(onboarding)/onboarding-screen-1.tsx
@@ -3,62 +3,204 @@ import { LinearGradient } from "expo-linear-gradient";
import type { Href } from "expo-router";
import { Link, router, Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
-import { Pressable, ScrollView, Text, View } from "react-native";
+import {
+ Platform,
+ Pressable,
+ ScrollView,
+ Text,
+ useWindowDimensions,
+ View,
+} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header";
import { images } from "@/constants/images";
+import { colors } from "@/constants/theme";
import { useOnboardingStore } from "@/store/onboarding-store";
const loginHref = "/login" as Href;
+const nextHref = "/onboarding-screen-2" as Href;
+const cream = "#FFF9F3";
+const deepPlum = "#251238";
+const mutedPlum = "#777188";
+const headingFontFamily = Platform.select({
+ android: "serif",
+ default: "Georgia",
+ ios: "Georgia",
+});
export default function OnboardingScreenOne() {
+ const { height, width } = useWindowDimensions();
+ const insets = useSafeAreaInsets();
const completeOnboarding = useOnboardingStore(
(state) => state.completeOnboarding,
);
+ const usableHeight = height - insets.top - insets.bottom;
+ const isTiny = usableHeight < 600;
+ const isVeryCompact = usableHeight < 740;
+ const isCompact = usableHeight < 860;
+ const isTextCompact = usableHeight < 720;
+ const isTextMedium = usableHeight < 850;
+ const isNarrow = width < 380;
+
+ const horizontalPadding = isNarrow ? 28 : 34;
+ const heroWidth = Math.min(
+ width - horizontalPadding * 2,
+ isVeryCompact ? 244 : isCompact ? 272 : 306,
+ );
+ const heroHeight = isVeryCompact ? 270 : isCompact ? 306 : 354;
+ const headingSize = isTiny ? 27 : isTextCompact ? 31 : isTextMedium ? 36 : 40;
+ const headingLineHeight = headingSize + 4;
+ const bodySize = isTiny ? 11.5 : isTextCompact ? 13 : isTextMedium ? 15 : 16;
+ const bodyLineHeight = isTiny ? 17 : isTextCompact ? 20 : isTextMedium ? 23 : 25;
function handleExistingAccountPress() {
completeOnboarding();
router.replace(loginHref);
}
+ function handleSkipPress() {
+ completeOnboarding();
+ router.replace(loginHref);
+ }
+
return (
-
-
-
-
-
-
-
+
+
+
-
-
+
+
+
+
+ ✦
+
+
+ ✦
+
+
+ ✿
+
+
+
-
- Your space to reflect.
-
- 🌸
-
- Reflect, understand, and grow.
+
+
+
+ Your thoughts
+
+
+ deserve a
+
+
+
+ safe place.
+
+
+ ✧
+
+
+
+
+
+ DearDiary is your personal space to write freely, reflect deeply,
+ and grow every day.
+
+
+
+
+ ✿
+
+
+
-
-
+
+
-
- Get Started
+
+ Get Started →
-
-
+
+
I already have an account
diff --git a/app/(onboarding)/onboarding-screen-2.tsx b/app/(onboarding)/onboarding-screen-2.tsx
index e9dcd8e..e46128b 100644
--- a/app/(onboarding)/onboarding-screen-2.tsx
+++ b/app/(onboarding)/onboarding-screen-2.tsx
@@ -1,252 +1,467 @@
-import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
+import { Feather, MaterialCommunityIcons } from "@expo/vector-icons";
+import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
-import { Link, Stack } from "expo-router";
+import type { Href } from "expo-router";
+import { Link, router, Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import {
+ Platform,
Pressable,
ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header";
+import { images } from "@/constants/images";
+import { colors } from "@/constants/theme";
+import { useOnboardingStore } from "@/store/onboarding-store";
+
+const backHref = "/onboarding-screen-1" as Href;
+const loginHref = "/login" as Href;
+const nextHref = "/onboarding-screen-3" as Href;
+const cream = "#FFF9F3";
+const deepPlum = "#251238";
+const mutedPlum = "#777188";
+const headingFontFamily = Platform.select({
+ android: "serif",
+ default: "Georgia",
+ ios: "Georgia",
+});
export default function OnboardingScreenTwo() {
- const { height } = useWindowDimensions();
- const isCompact = height < 760;
+ const { height, width } = useWindowDimensions();
+ const insets = useSafeAreaInsets();
+ const completeOnboarding = useOnboardingStore(
+ (state) => state.completeOnboarding,
+ );
+ const usableHeight = height - insets.top - insets.bottom;
+ const isTiny = usableHeight < 600;
+ const isVeryCompact = usableHeight < 720;
+ const isCompact = usableHeight < 850;
+ const isNarrow = width < 380;
+
+ const topPadding = Math.max(
+ insets.top + 4,
+ isTiny ? 12 : isVeryCompact ? 16 : isCompact ? 22 : 34,
+ );
+ const bottomPadding = Math.max(
+ insets.bottom,
+ isTiny ? 6 : isVeryCompact ? 8 : isCompact ? 12 : 20,
+ );
+ const headerHeight = isTiny ? 34 : isVeryCompact ? 40 : isCompact ? 44 : 48;
+ const buttonHeight = isTiny ? 46 : isVeryCompact ? 50 : isCompact ? 54 : 60;
+ const ctaTopPadding = isTiny ? 8 : isVeryCompact ? 10 : isCompact ? 14 : 24;
+ const ctaGap = isTiny ? 0 : isVeryCompact ? 2 : isCompact ? 6 : 10;
+ const backLineHeight = isTiny ? 24 : 28;
+ const mainHeight =
+ height -
+ topPadding -
+ bottomPadding -
+ headerHeight -
+ ctaTopPadding -
+ buttonHeight -
+ ctaGap -
+ backLineHeight;
+
+ const contentWidth = Math.min(width - (isNarrow ? 48 : 64), 390);
+ const chatHeight = isTiny
+ ? 198
+ : isVeryCompact
+ ? Math.min(258, contentWidth * 0.92)
+ : isCompact
+ ? Math.min(312, contentWidth * 0.94)
+ : Math.min(360, contentWidth * 1.02);
+ const chatWidth = Math.min(contentWidth - 40, chatHeight * 0.82, 306);
+ const headingSize = isTiny ? 23 : isVeryCompact ? 27 : isCompact ? 28 : 30;
+ const headingLineHeight = headingSize + 4;
+ const bodySize = isTiny ? 10 : isVeryCompact ? 11.5 : isCompact ? 13 : 14;
+ const bodyLineHeight = isTiny ? 14 : isVeryCompact ? 17 : isCompact ? 19 : 21;
+ const featureIconSize = isTiny ? 34 : isVeryCompact ? 36 : isCompact ? 40 : 46;
+ const featureLabelSize = isTiny ? 9 : isVeryCompact ? 9.5 : isCompact ? 10 : 11;
+ const featureLabelLineHeight = isTiny ? 12 : isVeryCompact ? 13 : isCompact ? 14 : 16;
+ const featureItemHeight =
+ featureIconSize + 8 + featureLabelLineHeight * 2;
- const artworkHeight = isCompact ? 236 : 340;
- const mainCardSize = isCompact ? 128 : 176;
- const mainIconSize = isCompact ? 72 : 94;
- const badgeSize = isCompact ? 40 : 48;
- const featureIconSize = isCompact ? 48 : 56;
+ function handleSkipPress() {
+ completeOnboarding();
+ router.replace(loginHref);
+ }
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ✨ AI-Powered{"\n"}Reflection
-
-
- Discover patterns in your thoughts. DearDiary AI reads between the
- lines to help you understand yourself better.
-
-
-
-
-
-
+
+
-
-
-
- AI Prompts
+ ✦
+
+
+ ✦
+
+
+ ✧
-
-
-
+
-
- Patterns
-
-
-
+
-
-
-
- Insights
+ Never wonder
+
+
+ where to{" "}
+
+ begin.
+
-
-
-
-
-
+
+
-
- Next →
-
-
-
-
-
-
- Back
-
-
-
+ DearDiary AI asks the right questions, listens to your thoughts,
+ and guides you toward deeper self-reflection.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Next →
+
+
+
+
+
+
+
+ Back
+
+
+
+
);
}
+
+type FeatureItemProps = {
+ background: string;
+ color: string;
+ icon: "brain" | "heart" | "message-square";
+ iconSize: number;
+ label: string;
+ labelLineHeight: number;
+ labelSize: number;
+};
+
+function FeatureItem({
+ background,
+ color,
+ icon,
+ iconSize,
+ label,
+ labelLineHeight,
+ labelSize,
+}: FeatureItemProps) {
+ const labelHeight = labelLineHeight * 2;
+
+ return (
+
+
+ {icon === "brain" ? (
+
+ ) : (
+
+ )}
+
+
+ {label}
+
+
+ );
+}
diff --git a/app/(onboarding)/onboarding-screen-3.tsx b/app/(onboarding)/onboarding-screen-3.tsx
index 1e2fc5e..00da5fb 100644
--- a/app/(onboarding)/onboarding-screen-3.tsx
+++ b/app/(onboarding)/onboarding-screen-3.tsx
@@ -1,289 +1,324 @@
import { LinearGradient } from "expo-linear-gradient";
-import { Link, Stack } from "expo-router";
+import type { Href } from "expo-router";
+import { Link, router, Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import {
+ Platform,
Pressable,
- ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
-const moods = [
- {
- emoji: "😊",
- label: "Happy",
- background: "#FFDDE8",
- color: "#ff2056",
- selected: true,
- shadow: "0 8px 20px -8px rgba(255, 32, 86, 0.4)",
- },
- {
- emoji: "😌",
- label: "Calm",
- background: "#D8EEDB",
- color: "#3f3f46",
- shadow: "0 8px 20px -8px rgba(120, 170, 130, 0.4)",
- },
- {
- emoji: "😔",
- label: "Sad",
- background: "#DDEFFF",
- color: "#3f3f46",
- shadow: "0 8px 20px -8px rgba(120, 150, 200, 0.4)",
- },
- {
- emoji: "🔥",
- label: "Motivated",
- background: "#F4EFFA",
- color: "#3f3f46",
- shadow: "0 8px 20px -8px rgba(160, 140, 200, 0.4)",
- },
- {
- emoji: "🙏",
- label: "Grateful",
- background: "#D8EEDB",
- color: "#3f3f46",
- shadow: "0 8px 20px -8px rgba(120, 170, 130, 0.4)",
- },
-];
+import {
+ InsightFeature,
+ OnboardingInsightsPreview,
+} from "@/components/onboarding/onboarding-insights-preview";
+import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header";
+import { colors } from "@/constants/theme";
+import { useOnboardingStore } from "@/store/onboarding-store";
+
+const backHref = "/onboarding-screen-2" as Href;
+const loginHref = "/login" as Href;
+const nextHref = "/onboarding-screen-4" as Href;
+const deepPlum = "#251238";
+const mutedPlum = "#777188";
+const headingFontFamily = Platform.select({
+ android: "serif",
+ default: "Georgia",
+ ios: "Georgia",
+});
export default function OnboardingScreenThree() {
- const { height } = useWindowDimensions();
- const isCompact = height < 760;
+ const { height, width } = useWindowDimensions();
+ const insets = useSafeAreaInsets();
+ const completeOnboarding = useOnboardingStore(
+ (state) => state.completeOnboarding,
+ );
- const sceneHeight = isCompact ? 280 : 340;
- const sceneMarginTop = isCompact ? 24 : 32;
+ const usableHeight = height - insets.top - insets.bottom;
+ const isVeryCompact = usableHeight < 600;
+ const isCompact = usableHeight < 720;
+ const isMedium = usableHeight < 850;
+ const isNarrow = width < 380;
+ const horizontalPadding = isNarrow ? 22 : 30;
+ const contentWidth = Math.min(width - horizontalPadding * 2, 390);
+ const featureWidth = Math.min(contentWidth, 340);
+ const headerHeight = isVeryCompact
+ ? 34
+ : isCompact
+ ? 40
+ : isMedium
+ ? 44
+ : 48;
+ const previewHeight = isVeryCompact
+ ? 215
+ : isCompact
+ ? Math.min(282, contentWidth)
+ : isMedium
+ ? Math.min(340, contentWidth * 1.02)
+ : Math.min(390, contentWidth * 1.12);
+ const headingSize = isVeryCompact ? 23 : isCompact ? 27 : isMedium ? 28 : 30;
+ const headingLineHeight = headingSize + 4;
+ const buttonHeight =
+ height < 680 ? 46 : height < 740 ? 50 : height < 860 ? 54 : 60;
+ const ctaTopPadding =
+ height < 680 ? 8 : height < 740 ? 10 : height < 860 ? 14 : 24;
+ const ctaGap = height < 680 ? 0 : height < 740 ? 2 : height < 860 ? 6 : 10;
+
+ function handleSkipPress() {
+ completeOnboarding();
+ router.replace(loginHref);
+ }
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
- 😊
-
-
- 😌
-
-
- 😔
-
-
- 🔥
-
-
- 🙏
-
-
-
-
- 😊 Understand Your{"\n"}Emotions
-
-
-
- Track how you feel every day. Visualize your emotional world and
- discover what truly moves you.
-
+
+
+
-
- {moods.map((mood) => (
-
- {mood.emoji}
+
- {mood.label}
+ See how you feel.
+
+
+ Grow with{" "}
+
+ insight.
+
+
+
+ ✦
+
+
+
- ))}
-
-
-
-
-
- Next →
-
- {/* */}
-
-
+ Track your emotions over time and discover patterns that reveal
+ what truly matters to you.
+
-
-
-
- Back
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Next →
+
+
+
+
+
+
+
+ Back
+
+
+
+
-
);
}
diff --git a/app/(onboarding)/onboarding-screen-4.tsx b/app/(onboarding)/onboarding-screen-4.tsx
index c4b6b0c..981d5da 100644
--- a/app/(onboarding)/onboarding-screen-4.tsx
+++ b/app/(onboarding)/onboarding-screen-4.tsx
@@ -1,173 +1,271 @@
import { LinearGradient } from "expo-linear-gradient";
-import { Link, Stack } from "expo-router";
+import type { Href } from "expo-router";
+import { Link, router, Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import {
+ Platform,
Pressable,
- ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
-const ritualCards = [
- {
- icon: "☀️",
- title: "Morning Intention ☀️",
- body: "Start each day with clarity. Set one focus and carry it with you.",
- prompt: "What will you focus on today?",
- background: "#D8EEDB",
- shadow: "0 12px 30px -12px rgba(120, 180, 140, 0.55)",
- },
- {
- icon: "🌙",
- title: "Evening Reflection 🌙",
- body: "End each day with gratitude. Reflect on what moved you.",
- prompt: "How did today feel?",
- background: "#F4EFFA",
- shadow: "0 12px 30px -12px rgba(160, 140, 200, 0.55)",
- },
-];
+import {
+ OnboardingRitualCard,
+ OnboardingRitualFeatures,
+} from "@/components/onboarding/onboarding-ritual-content";
+import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header";
+import { colors } from "@/constants/theme";
+import { useOnboardingStore } from "@/store/onboarding-store";
+
+const backHref = "/onboarding-screen-3" as Href;
+const loginHref = "/login" as Href;
+const nextHref = "/onboarding-screen-5" as Href;
+const deepPlum = "#251238";
+const mutedPlum = "#777188";
+const headingFontFamily = Platform.select({
+ android: "serif",
+ default: "Georgia",
+ ios: "Georgia",
+});
export default function OnboardingScreenFour() {
- const { height } = useWindowDimensions();
- const isCompact = height < 760;
+ const { height, width } = useWindowDimensions();
+ const insets = useSafeAreaInsets();
+ const completeOnboarding = useOnboardingStore(
+ (state) => state.completeOnboarding,
+ );
+
+ const usableHeight = height - insets.top - insets.bottom;
+ const isTiny = usableHeight < 600;
+ const isCompact = usableHeight < 720;
+ const isMedium = usableHeight < 850;
+ const isNarrow = width < 380;
+ const size = isTiny
+ ? "tiny"
+ : isCompact
+ ? "compact"
+ : isMedium
+ ? "medium"
+ : "wide";
+ const horizontalPadding = isNarrow ? 22 : 30;
+ const contentWidth = Math.min(width - horizontalPadding * 2, 390);
+ const ritualWidth = Math.min(contentWidth, 340);
+ const headerHeight = isTiny ? 34 : isCompact ? 40 : isMedium ? 44 : 48;
+ const headingSize = isTiny ? 23 : isCompact ? 27 : isMedium ? 28 : 30;
+ const headingLineHeight = headingSize + 4;
+ const cardHeight = isTiny ? 128 : isCompact ? 140 : isMedium ? 154 : 170;
+ const cardGap = isTiny ? 5 : isCompact ? 6 : isMedium ? 8 : 10;
+ const featureHeight = isTiny ? 72 : isCompact ? 84 : isMedium ? 96 : 110;
+ const buttonHeight =
+ height < 680 ? 46 : height < 740 ? 50 : height < 860 ? 54 : 60;
+ const ctaTopPadding =
+ height < 680 ? 8 : height < 740 ? 10 : height < 860 ? 14 : 24;
+ const ctaGap = height < 680 ? 0 : height < 740 ? 2 : height < 860 ? 6 : 10;
+
+ function handleSkipPress() {
+ completeOnboarding();
+ router.replace(loginHref);
+ }
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
- 🌅 Build Daily Rituals
+ ✦
- Morning intentions and evening reflections — small habits that
- transform your inner world.
+ ✦
-
-
- {ritualCards.map((card) => (
-
-
-
- {card.icon}
-
+
+
+
+
+
+ Build daily rituals
+
+
+ that{" "}
- {card.title}
-
-
-
+ transform
+ {" "}
+ you.
+
- {card.body}
+ Morning intentions and evening reflections help you stay mindful
+ and grow consistently.
+
-
-
- {card.prompt}
-
-
+
+
+
- ))}
-
-
-
-
-
- Next →
-
-
-
+
+
+
+
-
-
-
- Back
-
-
-
+
+
+
+
+ Next →
+
+
+
+
+
+
+
+ Back
+
+
+
+
-
);
}
diff --git a/app/(onboarding)/onboarding-screen-5.tsx b/app/(onboarding)/onboarding-screen-5.tsx
index 513cfb3..8b69214 100644
--- a/app/(onboarding)/onboarding-screen-5.tsx
+++ b/app/(onboarding)/onboarding-screen-5.tsx
@@ -1,49 +1,58 @@
-import { Feather, Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { router, Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import {
+ Platform,
Pressable,
- ScrollView,
Text,
useWindowDimensions,
View,
} from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+import { OnboardingProgressHeader } from "@/components/onboarding/onboarding-progress-header";
+import {
+ OnboardingOverviewFeatureStrip,
+ OnboardingOverviewPreview,
+} from "@/components/onboarding/onboarding-overview-content";
+import { colors } from "@/constants/theme";
import { useOnboardingStore } from "@/store/onboarding-store";
-const featureCards = [
- {
- icon: "book-open",
- label: "Daily\nPrompts",
- background: "#FFDDE8",
- color: "#ff2056",
- },
- {
- icon: "sparkles-outline",
- label: "AI Insights",
- background: "#D8EEDB",
- color: "#059669",
- },
- {
- icon: "bar-chart-2",
- label: "Mood Trends",
- background: "#F4EFFA",
- color: "#8b5cf6",
- },
-] as const;
+const deepPlum = "#251238";
+const mutedPlum = "#777188";
+const headingFontFamily = Platform.select({
+ android: "serif",
+ default: "Georgia",
+ ios: "Georgia",
+});
export default function OnboardingScreenFive() {
- const { height } = useWindowDimensions();
+ const { height, width } = useWindowDimensions();
+ const insets = useSafeAreaInsets();
const completeOnboarding = useOnboardingStore(
(state) => state.completeOnboarding,
);
- const isCompact = height < 760;
- const heroHeight = isCompact ? 260 : 332;
- const outerSize = isCompact ? 156 : 176;
- const innerSize = isCompact ? 112 : 128;
- const featureIconSize = isCompact ? 50 : 56;
+ const usableHeight = height - insets.top - insets.bottom;
+ const isTiny = usableHeight < 600;
+ const isCompact = usableHeight < 720;
+ const isMedium = usableHeight < 850;
+ const isNarrow = width < 380;
+ const horizontalPadding = isNarrow ? 18 : 24;
+ const contentWidth = Math.min(width - horizontalPadding * 2, 390);
+ const headerHeight = isTiny ? 34 : isCompact ? 40 : isMedium ? 44 : 48;
+ const headingSize = isTiny ? 23 : isCompact ? 27 : isMedium ? 29 : 31;
+ const headingLineHeight = headingSize + 4;
+ const subtitleSize = isTiny ? 10 : isCompact ? 11.5 : isMedium ? 13 : 14;
+ const subtitleLineHeight = isTiny ? 14 : isCompact ? 17 : isMedium ? 19 : 21;
+ const previewHeight = isTiny ? 140 : isCompact ? 205 : isMedium ? 270 : 330;
+ const featureHeight = isTiny ? 88 : isCompact ? 98 : isMedium ? 112 : 126;
+ const featureIconSize = isTiny ? 38 : isCompact ? 42 : isMedium ? 46 : 52;
+ const buttonHeight =
+ height < 680 ? 46 : height < 740 ? 50 : height < 860 ? 54 : 60;
+ const ctaTopPadding =
+ height < 680 ? 6 : height < 740 ? 8 : height < 860 ? 12 : 18;
+ const ctaGap = height < 680 ? 0 : height < 740 ? 2 : height < 860 ? 6 : 10;
function handleStartWritingPress() {
completeOnboarding();
@@ -57,236 +66,191 @@ export default function OnboardingScreenFive() {
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
+ ✧
+
+
+ >
+ ✦
+
-
-
-
-
-
-
-
-
+ Everything you need in one{" "}
+
-
-
-
-
+ >
+ peaceful
+ {" "}
+ space.
+
+
+ A beautiful journaling experience designed to support your mind,
+ every day.
+
+
-
-
- {"You're all set! 🎉"}
-
-
- Your reflection journey begins now. Write freely, grow deeply.
-
-
+
-
-
- {featureCards.map((feature) => (
-
-
- {feature.icon === "sparkles-outline" ? (
-
- ) : (
-
- )}
-
-
- {feature.label}
-
-
- ))}
+
-
-
-
-
- Start Writing ✨
-
-
+
+
+ Start Writing ✨
+
+
-
-
- Maybe later
-
-
+
+
+ Maybe later
+
+
+
-
);
}
diff --git a/assets/images/AI-chat-mockup.png b/assets/images/AI-chat-mockup.png
new file mode 100644
index 0000000..307d3fc
Binary files /dev/null and b/assets/images/AI-chat-mockup.png differ
diff --git a/assets/images/ai-card.png b/assets/images/ai-card.png
new file mode 100644
index 0000000..18e6d62
Binary files /dev/null and b/assets/images/ai-card.png differ
diff --git a/assets/images/evening-card.png b/assets/images/evening-card.png
new file mode 100644
index 0000000..e88fba9
Binary files /dev/null and b/assets/images/evening-card.png differ
diff --git a/assets/images/mood card heart.png b/assets/images/mood card heart.png
new file mode 100644
index 0000000..143bedc
Binary files /dev/null and b/assets/images/mood card heart.png differ
diff --git a/assets/images/morning-card.png b/assets/images/morning-card.png
new file mode 100644
index 0000000..701feb4
Binary files /dev/null and b/assets/images/morning-card.png differ
diff --git a/assets/images/noon.png b/assets/images/noon.png
new file mode 100644
index 0000000..1432a53
Binary files /dev/null and b/assets/images/noon.png differ
diff --git a/components/auth/auth-screen.tsx b/components/auth/auth-screen.tsx
index beb83c4..3d6636d 100644
--- a/components/auth/auth-screen.tsx
+++ b/components/auth/auth-screen.tsx
@@ -23,6 +23,7 @@ import {
import { images } from "@/constants/images";
import { useAppDialog } from "@/hooks/useAppDialog";
+import { useOnboardingStore } from "@/store/onboarding-store";
import { AuthTextField } from "./auth-text-field";
import {
@@ -45,11 +46,13 @@ type AuthScreenProps = {
heading: string;
mode?: "login" | "signup";
subheading: string;
+ showTemporaryOnboardingBackButton?: boolean;
};
type VerificationFlow = "signup" | "login-email-code" | null;
const ssoRedirectUrl = AuthSession.makeRedirectUri({ path: "sso" });
+const onboardingHref = "/onboarding-screen-1" as Href;
export function AuthScreen({
buttonText,
@@ -60,6 +63,7 @@ export function AuthScreen({
footerText,
heading,
mode = "signup",
+ showTemporaryOnboardingBackButton = false,
subheading,
}: AuthScreenProps) {
const { height, width } = useWindowDimensions();
@@ -88,6 +92,7 @@ export function AuthScreen({
const { fetchStatus: signUpFetchStatus, signUp } = useSignUp();
const { startSSOFlow } = useSSO();
const { showDialog } = useAppDialog();
+ const resetOnboarding = useOnboardingStore((state) => state.resetOnboarding);
const isFetching =
signInFetchStatus === "fetching" || signUpFetchStatus === "fetching";
const isClerkReady = isAuthLoaded && !isFetching;
@@ -346,6 +351,11 @@ export function AuthScreen({
}
}
+ function handleTemporaryOnboardingBackPress() {
+ resetOnboarding();
+ router.replace(onboardingHref);
+ }
+
return (
Take a moment. Breathe. Begin again.
+
+ {isLogin && showTemporaryOnboardingBackButton ? (
+
+
+ Back to onboarding
+
+
+ ) : null}
diff --git a/components/home/home-screen.tsx b/components/home/home-screen.tsx
index 166b605..94e9e59 100644
--- a/components/home/home-screen.tsx
+++ b/components/home/home-screen.tsx
@@ -3,8 +3,14 @@ import { Image } from "expo-image";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter, type Href } from "expo-router";
import { StatusBar } from "expo-status-bar";
-import { useMemo } from "react";
-import { Pressable, ScrollView, Text, View } from "react-native";
+import { useEffect, useMemo, useState } from "react";
+import {
+ Pressable,
+ ScrollView,
+ Text,
+ useWindowDimensions,
+ View,
+} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { HomeMoodCheckInCard } from "@/components/home/mood/HomeMoodCheckInCard";
@@ -13,6 +19,7 @@ import {
bottomTabBarBaseHeight,
} from "@/components/navigation/bottom-tab-bar";
import { ScreenErrorState } from "@/components/states/ScreenErrorState";
+import { ScenicCardBackground } from "@/components/ui/scenic-card-background";
import { images } from "@/constants/images";
import { fallbackMoodMetadata, moodMetadata } from "@/constants/moods";
import { useDelayedVisibility } from "@/hooks/useDelayedVisibility";
@@ -21,7 +28,10 @@ import {
useJournalHydrationStore,
useJournalStore,
} from "@/store/journal-store";
-import type { JournalEntry as StoredJournalEntry } from "@/types/journal";
+import type {
+ EntryType,
+ JournalEntry as StoredJournalEntry,
+} from "@/types/journal";
type HomeScreenProps = {
avatarUrl?: string;
@@ -31,6 +41,51 @@ type HomeScreenProps = {
const colors = {
primary: "#FF2056",
};
+const homeScenicCardAspectRatio = 1.6;
+
+type GreetingPeriod = "evening" | "morning" | "noon";
+
+const greetingBackgrounds = {
+ evening: images.eveningCard,
+ morning: images.morningCard,
+ noon: images.noon,
+} as const;
+
+const entryTypeMetadata: Record<
+ EntryType,
+ { color: string; icon: string; label: string }
+> = {
+ ai_reflection: {
+ color: "#A3B31E",
+ icon: "✨",
+ label: "AI prompt",
+ },
+ daily_prompt: {
+ color: "#7C9FD9",
+ icon: "💭",
+ label: "Daily prompt",
+ },
+ evening_reflection: {
+ color: "#7C9FD9",
+ icon: "🌙",
+ label: "Evening reflection",
+ },
+ free_write: {
+ color: "#71717B",
+ icon: "✍️",
+ label: "Journal entry",
+ },
+ gratitude: {
+ color: "#FF8A3D",
+ icon: "🙏",
+ label: "Gratitude",
+ },
+ morning_intention: {
+ color: "#FFB02E",
+ icon: "☀️",
+ label: "Morning intention",
+ },
+};
const aiReflectionPrompt = "What made you smile unexpectedly today?";
const journalEditorHref = {
@@ -56,14 +111,21 @@ const morningIntentionHref = {
type HomeRecentEntry = {
backgroundColor: string;
+ borderColor: string;
date: string;
emoji: string;
excerpt: string;
id: string;
+ moodColor: string;
+ moodLabel: string;
title: string;
+ typeColor: string;
+ typeIcon: string;
+ typeLabel: string;
};
export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
+ const { width } = useWindowDimensions();
const insets = useSafeAreaInsets();
const router = useRouter();
const entries = useJournalStore((state) => state.entries);
@@ -75,9 +137,13 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
);
const showHydrationState = useDelayedVisibility(!hasHydrated);
const bottomNavHeight = bottomTabBarBaseHeight + insets.bottom;
- const displayName = firstName?.trim() || "Aryan";
- const greeting = useMemo(() => getGreeting(), []);
- const todayLabel = useMemo(() => formatTodayDate(), []);
+ const topBackgroundHeight = Math.max(188, insets.top + 148);
+ const homeCardWidth = Math.max(width - 56, 0);
+ const currentTime = useCurrentMinute();
+ const displayName = firstName?.trim() || "";
+ const greeting = useMemo(() => getGreeting(currentTime), [currentTime]);
+ const greetingBackground = greetingBackgrounds[greeting.period];
+ const todayLabel = useMemo(() => formatTodayDate(currentTime), [currentTime]);
const reflectionStreak = useMemo(() => getReflectionStreak(entries), [entries]);
const morningIntention = useMemo(
() => getTodayMorningIntention(entries),
@@ -120,13 +186,31 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
return (
+
-
+
{todayLabel}
- {greeting},{"\n"}
- {displayName}
+ {displayName ? `${greeting.label},\n${displayName}` : greeting.label}
@@ -160,7 +243,7 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
style={{ boxShadow: "0 8px 18px rgba(39, 39, 42, 0.16)" }}
>
🔥
@@ -184,35 +267,42 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
-
-
-
+
+
+
+
+
+
+
+
+ AI Reflection Prompt
+
-
- AI Reflection Prompt
-
-
-
- {aiReflectionPrompt.replace(" unexpectedly", "\nunexpectedly")}
-
-
- router.push(journalEditorHref)}
- >
-
- Start Writing ✨
+
+ {aiReflectionPrompt.replace(" unexpectedly", "\nunexpectedly")}
-
+
+ router.push(journalEditorHref)}
+ >
+
+ Start Writing ✨
+
+
+
@@ -222,47 +312,57 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
Morning Intention
-
-
-
-
-
- {intentionTitle}
-
-
-
- {intentionSubtitle}
-
- {
- if (!hasHydrated) {
- return;
- }
-
- if (morningIntention) {
- router.push({
- pathname: "/journal/[id]",
- params: { id: morningIntention.id, source: "home" },
- });
- return;
- }
+
- router.push(morningIntentionHref);
- }}
- >
+
+
+
+
+
+
+ {intentionTitle}
+
+
- {intentionBody}
+ {intentionSubtitle}
-
+ {
+ if (!hasHydrated) {
+ return;
+ }
+
+ if (morningIntention) {
+ router.push({
+ pathname: "/journal/[id]",
+ params: { id: morningIntention.id, source: "home" },
+ });
+ return;
+ }
+
+ router.push(morningIntentionHref);
+ }}
+ >
+
+ {intentionBody}
+
+
+
@@ -307,7 +407,7 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
router.push({
@@ -316,25 +416,63 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) {
})
}
style={{
- backgroundColor: entry.backgroundColor,
- boxShadow: "0 6px 20px rgba(0, 0, 0, 0.08)",
+ borderColor: entry.borderColor,
+ boxShadow: "0 14px 34px rgba(39, 39, 42, 0.06)",
}}
>
-
-
+
+
+ {entry.emoji}
+
+ {entry.moodLabel}
+
+
+
+
{entry.date}
- {entry.emoji}
-
+
+
{entry.title}
{entry.excerpt}
+
+
+
+
+
+
+ {entry.typeIcon}
+
+
+ {entry.typeLabel}
+
+
+
+
+
))
)}
@@ -393,14 +531,21 @@ function RecentEntriesEmptyState({
function toHomeRecentEntry(entry: StoredJournalEntry): HomeRecentEntry {
const visual = entry.mood ? moodMetadata[entry.mood] : fallbackMoodMetadata;
const content = entry.content.trim();
+ const entryType = entryTypeMetadata[entry.type];
return {
backgroundColor: visual.backgroundColor,
+ borderColor: visual.backgroundColor,
date: formatRecentEntryDate(entry.createdAt),
emoji: visual.emoji,
excerpt: content || entry.prompt || "No body text yet.",
id: entry.id,
+ moodColor: visual.dotColor,
+ moodLabel: visual.label,
title: entry.title,
+ typeColor: entryType.color,
+ typeIcon: entryType.icon,
+ typeLabel: entryType.label,
};
}
@@ -424,26 +569,40 @@ function getEntryPreview(entry: StoredJournalEntry) {
return entry.content.trim() || entry.title || "Open your morning intention...";
}
-function getGreeting() {
- const hour = new Date().getHours();
+function useCurrentMinute() {
+ const [currentTime, setCurrentTime] = useState(() => new Date());
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 60 * 1000);
+
+ return () => clearInterval(intervalId);
+ }, []);
+
+ return currentTime;
+}
+
+function getGreeting(date: Date): { label: string; period: GreetingPeriod } {
+ const hour = date.getHours();
if (hour < 12) {
- return "Good Morning";
+ return { label: "Good Morning", period: "morning" };
}
if (hour < 17) {
- return "Good Afternoon";
+ return { label: "Good Afternoon", period: "noon" };
}
- return "Good Evening";
+ return { label: "Good Evening", period: "evening" };
}
-function formatTodayDate() {
+function formatTodayDate(date: Date) {
return new Intl.DateTimeFormat("en-US", {
day: "numeric",
month: "long",
weekday: "long",
- }).format(new Date());
+ }).format(date);
}
function formatRecentEntryDate(value: string) {
diff --git a/components/home/mood/HomeMoodCheckInCard.tsx b/components/home/mood/HomeMoodCheckInCard.tsx
index 38b658f..5040a23 100644
--- a/components/home/mood/HomeMoodCheckInCard.tsx
+++ b/components/home/mood/HomeMoodCheckInCard.tsx
@@ -1,9 +1,11 @@
import { useMemo, useState } from "react";
+import { Image } from "expo-image";
import { Pressable, Text, View } from "react-native";
import { HomeSelectedMood } from "@/components/home/mood/HomeSelectedMood";
import { MoodCheckInAction } from "@/components/home/mood/MoodCheckInAction";
import { MoodSpectrumSelector } from "@/components/home/mood/MoodSpectrumSelector";
+import { images } from "@/constants/images";
import { moodList, moodMetadata } from "@/constants/moods";
import { useAutoSync } from "@/hooks/useAutoSync";
import { useConnectivity } from "@/hooks/useConnectivity";
@@ -140,13 +142,16 @@ export function HomeMoodCheckInCard() {
className="mb-9 gap-5 rounded-[24px] bg-white px-5 py-6"
style={{ boxShadow: "0 12px 34px rgba(0, 0, 0, 0.08)" }}
>
-
-
- {homeMoodPrompt}
-
-
- Take a quiet moment to check in with yourself.
-
+
+
+
+ {homeMoodPrompt}
+
+
+ Take a quiet moment to check in with yourself.
+
+
+
+
+
+ );
+}
+
function retryMoodLogHydration() {
void useMoodLogStore.persist.rehydrate();
}
diff --git a/components/home/mood/HomeSelectedMood.tsx b/components/home/mood/HomeSelectedMood.tsx
index d9673b0..75cfd97 100644
--- a/components/home/mood/HomeSelectedMood.tsx
+++ b/components/home/mood/HomeSelectedMood.tsx
@@ -1,3 +1,4 @@
+import { Feather } from "@expo/vector-icons";
import LottieView from "lottie-react-native";
import { Text, View } from "react-native";
@@ -45,26 +46,92 @@ export function HomeSelectedMood({
return (
-
-
-
-
-
- {isSaved ? `Feeling ${mood.label}` : `You selected ${mood.label}`}
-
- {isSaved && savedAt ? (
-
- Logged today at {formatSavedTime(savedAt)}
+
+
+
+ ✦
+
+
+ ✦
+
+
+
+
+
+
+
+
+ {isSaved ? `Feeling ${mood.label}` : `You selected ${mood.label}`}
- ) : null}
+ {isSaved ? (
+
+
+
+ Logged today{savedAt ? ` at ${formatSavedTime(savedAt)}` : ""}
+
+
+ ) : (
+
+ Save this as today's check-in.
+
+ )}
+
);
diff --git a/components/onboarding/onboarding-insights-preview.tsx b/components/onboarding/onboarding-insights-preview.tsx
new file mode 100644
index 0000000..7bb276f
--- /dev/null
+++ b/components/onboarding/onboarding-insights-preview.tsx
@@ -0,0 +1,125 @@
+import { BarChart3, Star, TrendingUp } from "lucide-react-native";
+import { Text, View } from "react-native";
+
+const previewColors = {
+ green: "#0BA574",
+ lavender: "#9B51E0",
+ muted: "#777188",
+ paleGreen: "#DDF4E8",
+ palePink: "#FFE1EA",
+ palePurple: "#EEE2FA",
+} as const;
+
+type OnboardingInsightsPreviewProps = {
+ height: number;
+ width: number;
+};
+
+export function OnboardingInsightsPreview({
+ height,
+ width,
+}: OnboardingInsightsPreviewProps) {
+ const previewHeight = Math.min(height * 0.96, 378);
+ const previewWidth = Math.min(width * 0.68, previewHeight * 0.61);
+
+ return (
+
+
+
+
+ );
+}
+
+type InsightFeatureProps = {
+ compact: boolean;
+ icon: "mood" | "personal" | "trend";
+ label: string;
+ subtitle: string;
+};
+
+export function InsightFeature({
+ compact,
+ icon,
+ label,
+ subtitle,
+}: InsightFeatureProps) {
+ const iconSize = compact ? 34 : 38;
+ const iconProps = { size: compact ? 19 : 22, strokeWidth: 2.2 } as const;
+
+ return (
+
+
+ {icon === "mood" ? (
+
+ ) : icon === "trend" ? (
+
+ ) : (
+
+ )}
+
+
+ {label}
+
+
+ {subtitle}
+
+
+ );
+}
diff --git a/components/onboarding/onboarding-overview-content.tsx b/components/onboarding/onboarding-overview-content.tsx
new file mode 100644
index 0000000..d9fca80
--- /dev/null
+++ b/components/onboarding/onboarding-overview-content.tsx
@@ -0,0 +1,256 @@
+import { Feather, Ionicons } from "@expo/vector-icons";
+import { Text, View } from "react-native";
+
+import { colors } from "@/constants/theme";
+
+const deepPlum = "#251238";
+const mutedPlum = "#777188";
+
+const featureCards = [
+ {
+ background: "#FFE1EA",
+ color: colors.brandPrimary,
+ description: "Write freely in a clean, distraction-free space.",
+ icon: "book-open",
+ iconSet: "feather",
+ label: "Beautiful\nJournaling",
+ },
+ {
+ background: "#EEE2FA",
+ color: "#9B51E0",
+ description: "Your journal is always just yours.",
+ icon: "lock",
+ iconSet: "feather",
+ label: "Private &\nSecure",
+ },
+ {
+ background: "#DDF4E8",
+ color: "#0BA574",
+ description: "Your memories are safe and accessible.",
+ icon: "cloud-outline",
+ iconSet: "ionicons",
+ label: "Backup &\nSync",
+ },
+ {
+ background: "#FFEAD8",
+ color: "#F97316",
+ description: "Themes and settings that feel like you.",
+ icon: "color-palette-outline",
+ iconSet: "ionicons",
+ label: "Personalize\nYour Space",
+ },
+] as const;
+
+type OnboardingOverviewPreviewProps = {
+ height: number;
+ narrow: boolean;
+ width: number;
+};
+
+export function OnboardingOverviewPreview({
+ height,
+ narrow,
+ width,
+}: OnboardingOverviewPreviewProps) {
+ const centerHeight = height;
+ const centerWidth = Math.min(
+ width * (narrow ? 0.54 : 0.58),
+ centerHeight * 0.58,
+ );
+ const sideHeight = centerHeight * 0.82;
+ const sideWidth = centerWidth * 0.82;
+ const sideOffset = Math.min(width * 0.3, centerWidth * 1.02);
+
+ return (
+
+
+
+
+
+
+ );
+}
+
+type OnboardingOverviewFeatureStripProps = {
+ compact: boolean;
+ height: number;
+ iconSize: number;
+ tiny: boolean;
+};
+
+export function OnboardingOverviewFeatureStrip({
+ compact,
+ height,
+ iconSize,
+ tiny,
+}: OnboardingOverviewFeatureStripProps) {
+ return (
+
+ {featureCards.map((feature, index) => (
+
+ ))}
+
+ );
+}
+
+type EmptyPreviewCardProps = {
+ height: number;
+ opacity: number;
+ style: {
+ left?: number;
+ right?: number;
+ top: number;
+ };
+ width: number;
+};
+
+function EmptyPreviewCard({
+ height,
+ opacity,
+ style,
+ width,
+}: EmptyPreviewCardProps) {
+ return (
+
+ );
+}
+
+type FeatureItemProps = {
+ compact: boolean;
+ feature: (typeof featureCards)[number];
+ iconSize: number;
+ showDivider: boolean;
+};
+
+function FeatureItem({
+ compact,
+ feature,
+ iconSize,
+ showDivider,
+}: FeatureItemProps) {
+ const iconProps = {
+ color: feature.color,
+ size: compact ? 20 : 23,
+ } as const;
+
+ return (
+ <>
+
+
+ {feature.iconSet === "feather" ? (
+
+ ) : (
+
+ )}
+
+
+ {feature.label}
+
+
+ {feature.description}
+
+
+ {showDivider ? : null}
+ >
+ );
+}
diff --git a/components/onboarding/onboarding-progress-header.tsx b/components/onboarding/onboarding-progress-header.tsx
new file mode 100644
index 0000000..b620dbd
--- /dev/null
+++ b/components/onboarding/onboarding-progress-header.tsx
@@ -0,0 +1,78 @@
+import { Pressable, Text, View } from "react-native";
+
+import { colors } from "@/constants/theme";
+
+const inactiveDotColor = "#FFC1D2";
+const skipTextColor = "#777188";
+
+type OnboardingProgressHeaderProps = {
+ activeIndex: number;
+ compact?: boolean;
+ height?: number;
+ maxWidth?: number;
+ onSkipPress?: () => void;
+ total?: number;
+};
+
+export function OnboardingProgressHeader({
+ activeIndex,
+ compact = false,
+ height,
+ maxWidth = 390,
+ onSkipPress,
+ total = 5,
+}: OnboardingProgressHeaderProps) {
+ const headerHeight = height ?? (compact ? 44 : 56);
+ const skipHeight = Math.min(compact ? 40 : 44, headerHeight);
+ const dotsTop = Math.min(12, Math.max(10, headerHeight * 0.27));
+
+ return (
+
+
+ {Array.from({ length: total }).map((_, index) => (
+
+ ))}
+
+
+ {onSkipPress ? (
+
+
+ Skip
+
+
+ ) : null}
+
+ );
+}
diff --git a/components/onboarding/onboarding-ritual-content.tsx b/components/onboarding/onboarding-ritual-content.tsx
new file mode 100644
index 0000000..e03836d
--- /dev/null
+++ b/components/onboarding/onboarding-ritual-content.tsx
@@ -0,0 +1,252 @@
+import { MoonStar, Sprout, Sun, Sunrise } from "lucide-react-native";
+import { Text, View } from "react-native";
+
+import { ScenicCardBackground } from "@/components/ui/scenic-card-background";
+
+const ritualColors = {
+ deepPlum: "#251238",
+ green: "#4DB47B",
+ muted: "#777188",
+ pink: "#FF2056",
+ purple: "#8F55D6",
+} as const;
+
+type RitualSize = "compact" | "medium" | "tiny" | "wide";
+
+type OnboardingRitualCardProps = {
+ height: number;
+ size: RitualSize;
+ variant: "evening" | "morning";
+ width: number;
+};
+
+export function OnboardingRitualCard({
+ height,
+ size,
+ variant,
+ width,
+}: OnboardingRitualCardProps) {
+ const isMorning = variant === "morning";
+ const isTiny = size === "tiny";
+ const isCompact = size === "compact";
+ const isMedium = size === "medium";
+ const padding = isTiny ? 10 : isCompact ? 12 : isMedium ? 14 : 16;
+ const iconSize = isTiny ? 30 : isCompact ? 34 : isMedium ? 38 : 42;
+ const titleSize = isTiny ? 10 : isCompact ? 11 : isMedium ? 12 : 13;
+ const titleLineHeight = titleSize + 4;
+ const bodySize = isTiny ? 8 : isCompact ? 9 : isMedium ? 10 : 11;
+ const bodyLineHeight = bodySize + 4;
+ const bodyOffset = isTiny ? 0 : isCompact ? 2 : 4;
+ const promptHeight = isTiny ? 25 : isCompact ? 28 : isMedium ? 31 : 34;
+ const cardInnerWidth = width - 12;
+ const textColumnRight =
+ padding + bodyOffset + (cardInnerWidth - padding * 2) * 0.58;
+ const imageEffectWidth = textColumnRight + 1;
+
+ return (
+
+
+
+
+
+
+ {isMorning ? (
+
+ ) : (
+
+ )}
+
+
+ {isMorning ? "Morning Intention" : "Evening Reflection"}
+
+
+
+
+ {isMorning
+ ? "Start your day with clarity. Set one focus and carry it through your day."
+ : "End your day with gratitude. Reflect on what moved you and what you learned."}
+
+
+
+
+ {isMorning
+ ? "What will you focus on today?"
+ : "How did today feel?"}
+
+
+
+
+ );
+}
+
+const ritualFeatures = [
+ {
+ icon: "sunrise",
+ label: "Start with Purpose",
+ subtitle: "Set intentions that guide your day.",
+ },
+ {
+ icon: "moon",
+ label: "End with Peace",
+ subtitle: "Reflect and let go before you sleep.",
+ },
+ {
+ icon: "sprout",
+ label: "Create Lasting Growth",
+ subtitle: "Small daily rituals lead to big change.",
+ },
+] as const;
+
+type OnboardingRitualFeaturesProps = {
+ height: number;
+ size: RitualSize;
+};
+
+export function OnboardingRitualFeatures({
+ height,
+ size,
+}: OnboardingRitualFeaturesProps) {
+ const isTiny = size === "tiny";
+ const isCompact = size === "compact";
+ const isMedium = size === "medium";
+ const iconSize = isTiny ? 28 : isCompact ? 32 : isMedium ? 36 : 40;
+ const labelSize = isTiny ? 7 : isCompact ? 8 : isMedium ? 8.5 : 9;
+ const subtitleSize = isTiny ? 6.5 : isCompact ? 7 : isMedium ? 8 : 9;
+
+ return (
+
+ {ritualFeatures.map((feature, index) => (
+
+
+
+ {feature.icon === "sunrise" ? (
+
+ ) : feature.icon === "moon" ? (
+
+ ) : (
+
+ )}
+
+
+ {feature.label}
+
+
+ {feature.subtitle}
+
+
+ {index < ritualFeatures.length - 1 ? (
+
+ ) : null}
+
+ ))}
+
+ );
+}
diff --git a/components/reflect/reflect-screen.tsx b/components/reflect/reflect-screen.tsx
index b8e85fd..1e2e679 100644
--- a/components/reflect/reflect-screen.tsx
+++ b/components/reflect/reflect-screen.tsx
@@ -3,13 +3,20 @@ import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
import { StatusBar } from "expo-status-bar";
import { ChevronRight, Sparkles } from "lucide-react-native";
-import { Pressable, ScrollView, Text, View } from "react-native";
+import {
+ Pressable,
+ ScrollView,
+ Text,
+ useWindowDimensions,
+ View,
+} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import {
BottomTabBar,
bottomTabBarBaseHeight,
} from "@/components/navigation/bottom-tab-bar";
+import { ScenicCardBackground } from "@/components/ui/scenic-card-background";
import { TabScreenHeader } from "@/components/ui/tab-screen-header";
import { reflectPrompts, type ReflectPrompt } from "@/data/reflect";
import {
@@ -22,8 +29,10 @@ const colors = {
body: "#71717B",
primary: "#FF2056",
};
+const reflectionCardAspectRatio = 1.9;
export function ReflectScreen() {
+ const { width } = useWindowDimensions();
const insets = useSafeAreaInsets();
const router = useRouter();
const entries = useJournalStore((state) => state.entries);
@@ -31,6 +40,7 @@ export function ReflectScreen() {
(state) => state.hasHydrated,
);
const bottomNavHeight = bottomTabBarBaseHeight + insets.bottom;
+ const reflectCardWidth = Math.max(width - 48, 0);
function handlePromptPress(prompt: ReflectPrompt) {
const existingEntry = hasHydrated
@@ -101,6 +111,7 @@ export function ReflectScreen() {
{reflectPrompts.map((prompt) => (
handlePromptPress(prompt)}
@@ -110,33 +121,46 @@ export function ReflectScreen() {
-
-
-
+
+
+
+
+
+
+
+
+ Reflect With DearDiary AI
+
-
- Reflect With DearDiary AI
-
-
-
-
- {"Talk to your journaling companion about your day."}
-
- router.push("/ai-chat")}
- style={{ boxShadow: "0 12px 22px rgba(255, 32, 86, 0.28)" }}
- >
-
-
- Start Reflection
+
+ {"Talk to your journaling companion about your day."}
-
+
+ router.push("/ai-chat")}
+ style={{ boxShadow: "0 12px 22px rgba(255, 32, 86, 0.28)" }}
+ >
+
+
+ Start Reflection
+
+
+
@@ -146,12 +170,18 @@ export function ReflectScreen() {
}
type ReflectPromptCardProps = {
+ cardWidth: number;
entry?: JournalEntry;
onPress: () => void;
prompt: ReflectPrompt;
};
-function ReflectPromptCard({ entry, onPress, prompt }: ReflectPromptCardProps) {
+function ReflectPromptCard({
+ cardWidth,
+ entry,
+ onPress,
+ prompt,
+}: ReflectPromptCardProps) {
const Icon = prompt.Icon;
const previewText = entry ? getEntryPreview(entry) : "Tap to reflect...";
@@ -159,31 +189,41 @@ function ReflectPromptCard({ entry, onPress, prompt }: ReflectPromptCardProps) {
-
-
-
+
-
- {prompt.title}
-
+
+
+
+
+
-
-
+ {prompt.title}
+
+
+
+
- {previewText}
-
-
+
+ {previewText}
+
+
+
);
diff --git a/components/ui/scenic-card-background.tsx b/components/ui/scenic-card-background.tsx
new file mode 100644
index 0000000..d03f101
--- /dev/null
+++ b/components/ui/scenic-card-background.tsx
@@ -0,0 +1,89 @@
+import { Image } from "expo-image";
+import { LinearGradient } from "expo-linear-gradient";
+import { View } from "react-native";
+
+import { images } from "@/constants/images";
+
+const cardFadeColors = {
+ ai: [
+ "rgba(255, 250, 247, 0.82)",
+ "rgba(255, 250, 247, 0.62)",
+ "rgba(255, 250, 247, 0.2)",
+ "rgba(255, 250, 247, 0)",
+ ],
+ evening: [
+ "rgba(255, 248, 252, 0.76)",
+ "rgba(255, 248, 252, 0.56)",
+ "rgba(255, 248, 252, 0.18)",
+ "rgba(255, 248, 252, 0)",
+ ],
+ morning: [
+ "rgba(250, 252, 248, 0.82)",
+ "rgba(250, 252, 248, 0.62)",
+ "rgba(250, 252, 248, 0.2)",
+ "rgba(250, 252, 248, 0)",
+ ],
+} as const;
+
+const cardImages = {
+ ai: images.aiCard,
+ evening: images.eveningCard,
+ morning: images.morningCard,
+} as const;
+
+export type ScenicCardVariant = keyof typeof cardImages;
+
+type ScenicCardBackgroundProps = {
+ blurRadius?: number;
+ cardWidth: number;
+ effectWidth?: number;
+ variant: ScenicCardVariant;
+};
+
+export function ScenicCardBackground({
+ blurRadius = 0,
+ cardWidth,
+ effectWidth,
+ variant,
+}: ScenicCardBackgroundProps) {
+ const imageWidth = Math.max(cardWidth - 12, 0);
+ const resolvedEffectWidth = Math.min(
+ Math.max(effectWidth ?? imageWidth * 0.62, 0),
+ imageWidth,
+ );
+ const source = cardImages[variant];
+
+ return (
+ <>
+
+ {blurRadius > 0 ? (
+
+
+
+ ) : null}
+
+ >
+ );
+}
diff --git a/constants/images.ts b/constants/images.ts
index 1c59d6c..568d221 100644
--- a/constants/images.ts
+++ b/constants/images.ts
@@ -1,11 +1,23 @@
+import aiCard from "@/assets/images/ai-card.png";
import appLogo from "@/assets/images/app-logo.png";
+import eveningCard from "@/assets/images/evening-card.png";
import googleLogo from "@/assets/images/google-logo.png";
+import morningCard from "@/assets/images/morning-card.png";
+import moodCardHeart from "@/assets/images/mood card heart.png";
+import noon from "@/assets/images/noon.png";
import onboardingReflect from "@/assets/images/onboarding-reflect.jpg";
+import onboardingAiChatCard from "@/UI/Onb v2/onbv2 scr2 card.png";
import splashLogo from "@/assets/images/splash-logo.png";
export const images = {
+ aiCard,
appLogo,
+ eveningCard,
googleLogo,
+ morningCard,
+ moodCardHeart,
+ noon,
+ onboardingAiChatCard,
splashLogo,
onboardingReflect,
};
diff --git a/data/reflect.ts b/data/reflect.ts
index 8dc4b2c..64e155b 100644
--- a/data/reflect.ts
+++ b/data/reflect.ts
@@ -9,7 +9,6 @@ import {
import type { EntryType } from "@/types/journal";
export type ReflectPrompt = {
- backgroundColor: string;
iconColor: string;
Icon: LucideIcon;
prompt: string;
@@ -19,7 +18,6 @@ export type ReflectPrompt = {
export const reflectPrompts: ReflectPrompt[] = [
{
- backgroundColor: "#F0E9FA",
iconColor: "#FF2056",
Icon: Heart,
prompt: "How did today feel?",
@@ -27,7 +25,6 @@ export const reflectPrompts: ReflectPrompt[] = [
type: "evening_reflection",
},
{
- backgroundColor: "#E8EEFC",
iconColor: "#367DBB",
Icon: Mountain,
prompt: "What challenged you today?",
@@ -35,7 +32,6 @@ export const reflectPrompts: ReflectPrompt[] = [
type: "evening_reflection",
},
{
- backgroundColor: "#DFF4E4",
iconColor: "#1F8C50",
Icon: Sparkle,
prompt: "What are you grateful for?",
@@ -43,7 +39,6 @@ export const reflectPrompts: ReflectPrompt[] = [
type: "gratitude",
},
{
- backgroundColor: "#DFF2FC",
iconColor: "#2F83B3",
Icon: Wind,
prompt: "What would you like to let go of?",
diff --git a/hooks/useAIInsightReport.ts b/hooks/useAIInsightReport.ts
index 9c7877d..73455f8 100644
--- a/hooks/useAIInsightReport.ts
+++ b/hooks/useAIInsightReport.ts
@@ -157,9 +157,17 @@ export function useAIInsightReport(
return;
}
+ if (result.report && result.report.userId !== userId) {
+ if (!reportRef.current) {
+ setReport(null);
+ removeCachedReport(userId, cacheKey);
+ }
+ return;
+ }
+
setLegacyReportAvailable(result.legacyReportAvailable);
- if (result.report?.userId === userId) {
+ if (result.report) {
setReport(result.report);
setCachedReport(userId, cacheKey, result.report);
} else if (!reportRef.current) {
diff --git a/lib/sync/requestSync.ts b/lib/sync/requestSync.ts
index 5e86b08..8c46626 100644
--- a/lib/sync/requestSync.ts
+++ b/lib/sync/requestSync.ts
@@ -346,6 +346,15 @@ async function syncAchievements(userId: string) {
const notifiedAchievementIds =
achievementStore.achievementNotificationsByUserId[userId]
?.notifiedAchievementIds ?? [];
+
+ if (!isActiveUser(userId, useJournalStore.getState().activeUserId)) {
+ return {
+ failedCount: 0,
+ pulledNotifiedIds: [],
+ syncedCount: 0,
+ };
+ }
+
const achievementResult = await syncAchievementStatesTwoWay({
notifiedAchievementIds,
unlockedAchievementIds,
diff --git a/store/useMoodLogStore.ts b/store/useMoodLogStore.ts
index 31dc04d..01b441f 100644
--- a/store/useMoodLogStore.ts
+++ b/store/useMoodLogStore.ts
@@ -271,7 +271,7 @@ function dedupeMoodLogs(moodLogs: MoodLog[]) {
const moodLogsByScopedId = new Map();
moodLogs.forEach((moodLog) => {
- const scopedId = `${moodLog.userId}:${moodLog.id}`;
+ const scopedId = JSON.stringify([moodLog.userId, moodLog.id]);
const existingMoodLog = moodLogsByScopedId.get(scopedId);
if (!existingMoodLog) {