diff --git a/app.json b/app.json index d975833..d2ac7b5 100644 --- a/app.json +++ b/app.json @@ -2,14 +2,14 @@ "expo": { "name": "DearDiary", "slug": "dear-diary", - "version": "1.0.3", + "version": "1.0.4", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "deardiary", "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { - "buildNumber": "3", + "buildNumber": "4", "supportsTablet": true }, "android": { @@ -21,7 +21,7 @@ ], "icon": "./assets/images/icon.png", "package": "com.aryan.deardiary", - "versionCode": 3, + "versionCode": 4, "adaptiveIcon": { "backgroundColor": "#FFDDE8", "foregroundImage": "./assets/images/icon.png" diff --git a/app/_layout.tsx b/app/_layout.tsx index 7acab16..cf46656 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,6 +1,7 @@ import "../global.css"; import { ClerkProvider, useAuth } from "@clerk/expo"; +import { resourceCache } from "@clerk/expo/resource-cache"; import { tokenCache } from "@clerk/expo/token-cache"; import { Stack, type ErrorBoundaryProps } from "expo-router"; import { useEffect } from "react"; @@ -30,6 +31,7 @@ export default function RootLayout() { return ( diff --git a/components/auth/auth-screen.tsx b/components/auth/auth-screen.tsx index 8f8b533..33d0168 100644 --- a/components/auth/auth-screen.tsx +++ b/components/auth/auth-screen.tsx @@ -21,8 +21,10 @@ import { View, } from "react-native"; +import { OfflineNotice } from "@/components/connectivity/OfflineNotice"; import { images } from "@/constants/images"; import { useAppDialog } from "@/hooks/useAppDialog"; +import { useConnectivity } from "@/hooks/useConnectivity"; import { useOnboardingStore } from "@/store/onboarding-store"; import { AuthTextField } from "./auth-text-field"; @@ -92,10 +94,15 @@ export function AuthScreen({ const { fetchStatus: signUpFetchStatus, signUp } = useSignUp(); const { startSSOFlow } = useSSO(); const { showDialog } = useAppDialog(); + const connectivity = useConnectivity(); const resetOnboarding = useOnboardingStore((state) => state.resetOnboarding); const isFetching = signInFetchStatus === "fetching" || signUpFetchStatus === "fetching"; + const isOffline = connectivity.status === "offline"; const isClerkReady = isAuthLoaded && !isFetching; + const isAuthActionDisabled = !isClerkReady || isOffline || isSubmitting; + const isSocialActionDisabled = + !isClerkReady || isOffline || socialStrategy !== null; const emailFeedback = getEmailFeedback(email); const passwordFeedback = getPasswordFeedback(password, isLogin); @@ -104,6 +111,11 @@ export function AuthScreen({ } async function handlePrimaryPress() { + if (isOffline) { + showError("No Internet Access. Connect to the internet to continue."); + return; + } + if (!isClerkReady || isSubmitting) { return; } @@ -317,6 +329,11 @@ export function AuthScreen({ } async function handleSocialPress(strategy: "oauth_google" | "oauth_apple") { + if (isOffline) { + showError("No Internet Access. Connect to the internet to continue."); + return; + } + if (!isClerkReady || socialStrategy) { return; } @@ -401,6 +418,16 @@ export function AuthScreen({ }} > + {isOffline ? ( + + ) : null} + {!isLogin ? ( @@ -471,7 +500,7 @@ export function AuthScreen({ void handleSocialPress("oauth_apple")} onGooglePress={() => void handleSocialPress("oauth_google")} @@ -596,6 +625,7 @@ function SocialButtons({ className="h-10 flex-row items-center justify-center gap-2 rounded-full border border-zinc-200 bg-zinc-50" disabled={disabled} onPress={onGooglePress} + style={{ opacity: disabled ? 0.55 : 1 }} > {isGoogleLoading ? ( @@ -620,6 +650,7 @@ function SocialButtons({ className="h-10 flex-row items-center justify-center gap-2 rounded-full border border-zinc-200 bg-zinc-50" disabled={disabled} onPress={onApplePress} + style={{ opacity: disabled ? 0.55 : 1 }} > {isAppleLoading ? ( diff --git a/components/home/home-screen.tsx b/components/home/home-screen.tsx index 1951065..c858822 100644 --- a/components/home/home-screen.tsx +++ b/components/home/home-screen.tsx @@ -197,6 +197,10 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) { function openDailyReflection() { if (!reflectionPrompt) { + router.push({ + pathname: "/journal/new", + params: { source: "home" }, + }); return; } @@ -328,12 +332,10 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) { accessibilityLabel={ reflectionPrompt ? `Start writing about: ${reflectionPrompt}` - : "Preparing today's reflection prompt" + : "Start writing a journal entry" } accessibilityRole="button" - accessibilityState={{ disabled: !reflectionPrompt }} className="mt-auto h-12 items-center justify-center rounded-[17px] bg-[#FF2056]" - disabled={!reflectionPrompt} onPress={openDailyReflection} > @@ -438,7 +440,7 @@ export function HomeScreen({ avatarUrl, firstName }: HomeScreenProps) { ) : ( diff --git a/supabase/functions/generate-insight-report/index.ts b/supabase/functions/generate-insight-report/index.ts index ece1dfb..c25caf6 100644 --- a/supabase/functions/generate-insight-report/index.ts +++ b/supabase/functions/generate-insight-report/index.ts @@ -122,6 +122,17 @@ type AIInsightReportRow = { user_id: string; }; +type ChatCompletionRequestBody = { + max_tokens: number; + messages: { + content: string; + role: "system" | "user"; + }[]; + model: string; + response_format?: { type: "json_object" }; + temperature: number; +}; + class AIProviderError extends Error { constructor( message: string, @@ -813,16 +824,22 @@ async function callAIProvider(finalPrompt: string) { const timeout = setTimeout(() => controller.abort(), 45000); try { + const requestBody: ChatCompletionRequestBody = { + messages: [ + { content: systemPrompt, role: "system" }, + { content: finalPrompt, role: "user" }, + ], + max_tokens: maxNarrativeTokens, + model, + temperature: 0.25, + }; + + if (supportsJsonObjectResponseFormat(baseUrl)) { + requestBody.response_format = { type: "json_object" }; + } + const response = await fetch(`${baseUrl}/chat/completions`, { - body: JSON.stringify({ - messages: [ - { content: systemPrompt, role: "system" }, - { content: finalPrompt, role: "user" }, - ], - max_tokens: maxNarrativeTokens, - model, - temperature: 0.45, - }), + body: JSON.stringify(requestBody), headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", @@ -926,6 +943,26 @@ async function getProviderErrorCode(response: Response) { return `provider_http_${response.status}`; } +function supportsJsonObjectResponseFormat(baseUrl: string) { + const hostname = getProviderHostname(baseUrl); + + return ( + hostname === "api.openai.com" || + hostname === "api.groq.com" || + hostname === "api.openrouter.ai" || + hostname === "openrouter.ai" || + hostname.endsWith(".openai.azure.com") + ); +} + +function getProviderHostname(baseUrl: string) { + try { + return new URL(baseUrl).hostname.toLowerCase(); + } catch { + return ""; + } +} + function getProviderMessage(body: unknown) { if (!isRecord(body) || !Array.isArray(body.choices)) { return null; diff --git a/testsprite-deardiary-prd.md b/testsprite-deardiary-prd.md new file mode 100644 index 0000000..68fd150 --- /dev/null +++ b/testsprite-deardiary-prd.md @@ -0,0 +1,80 @@ +# DearDiary Test PRD + +## Product overview + +DearDiary is a local-first AI-powered journaling application built with React Native, Expo Router, TypeScript, Clerk, Supabase, Zustand, and AsyncStorage. + +The application enables users to create private journal entries, log moods, receive AI reflections, view AI-generated insights and reports, and synchronize their data across sessions. + +## Critical privacy requirements + +- Data must be isolated by authenticated Clerk user. +- One user must never see another user’s entries, moods, AI messages, reports, or settings. +- No private content may flash during authentication, account switching, or App Lock. +- Account deletion must remove the Clerk account, Supabase data, local storage, SecureStore data, and scheduled reminders. +- Tests must use disposable accounts and non-sensitive content. + +## Critical user flows + +1. First launch and onboarding +2. Sign up and verification +3. Login and session restoration +4. Home dashboard +5. Mood logging +6. Dynamic morning, afternoon, and evening reflection prompts +7. Journal creation, editing, autosave, deletion, and restoration +8. Entry tags and AI-generated themes +9. Journal History, search, filtering, and Calendar +10. AI Chat +11. Per-entry AI reflection +12. Weekly and monthly AI reports +13. Insights and achievements +14. Offline creation and reconnect synchronization +15. App Lock with PIN and biometrics +16. Local reminder creation, editing, and cancellation +17. Export, backup, and restore where implemented +18. Sign out and account switching +19. Complete account deletion +20. Privacy Policy and Terms + +## AI content requirements + +- Long AI responses must never be visually clipped. +- Markdown paragraphs, headings, lists, links, blockquotes, and code blocks must remain readable. +- Streaming must not drop or overwrite chunks. +- Failed generation must preserve previous successful content. +- AI-generated themes may become entry tags only when the entry still has no tags. +- Dynamic daily prompts must remain stable for the same user, date, and time period. + +## Navigation requirements + +- All Expo Router routes must remain reachable. +- Back navigation must work. +- Bottom-tab state must remain stable. +- Deep links must not bypass authentication or App Lock. +- Premium screen transitions must not duplicate routes or lose state. +- Transition behavior must not clip dynamic content or interfere with gestures. + +## Reliability requirements + +- Offline entries must persist locally. +- Reconnection must not create duplicate records. +- App restart must preserve data. +- Failed network requests must show sanitized errors. +- Loading, empty, partial, offline, and retry states must remain usable. +- No raw Clerk, Supabase, or AI-provider errors may be exposed. + +## Accessibility requirements + +- Large fonts must not clip text. +- Interactive controls must have accessible labels. +- AI text must remain readable by screen readers. +- Reduced-motion preferences must be respected. +- Core navigation must not depend only on gestures. + +## Test restrictions + +- Use Preview services only. +- Do not use production accounts or data. +- Do not expose or record secrets. +- Do not modify application code automatically before the initial report is reviewed.