diff --git a/.env.example b/.env.example index c006d5c..2ca31e6 100644 --- a/.env.example +++ b/.env.example @@ -3,7 +3,10 @@ EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY= EXPO_PUBLIC_SUPABASE_URL= EXPO_PUBLIC_SUPABASE_ANON_KEY= EXPO_PUBLIC_ACCOUNT_DELETION_URL= +EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY= +EXPO_PUBLIC_REVENUECAT_IOS_API_KEY= # Server-side only. Set these in Supabase Edge Function secrets, never in Expo public env. SUPABASE_SERVICE_ROLE_KEY= CLERK_SECRET_KEY= +REVENUECAT_SECRET_API_KEY= diff --git a/.env.preview.example b/.env.preview.example index f755f4b..f46e867 100644 --- a/.env.preview.example +++ b/.env.preview.example @@ -4,6 +4,10 @@ EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY= EXPO_PUBLIC_SUPABASE_URL= EXPO_PUBLIC_SUPABASE_ANON_KEY= EXPO_PUBLIC_ACCOUNT_DELETION_URL= +EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY= +EXPO_PUBLIC_REVENUECAT_IOS_API_KEY= # Keep server-only credentials in Supabase Edge Function secrets. # Do not add them to this file or an EAS mobile build environment. +# Required by AI Edge Functions for server-side subscription validation: +# REVENUECAT_SECRET_API_KEY diff --git a/.env.production.example b/.env.production.example index a43c147..d2d5be0 100644 --- a/.env.production.example +++ b/.env.production.example @@ -4,6 +4,10 @@ EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY= EXPO_PUBLIC_SUPABASE_URL= EXPO_PUBLIC_SUPABASE_ANON_KEY= EXPO_PUBLIC_ACCOUNT_DELETION_URL= +EXPO_PUBLIC_REVENUECAT_ANDROID_API_KEY= +EXPO_PUBLIC_REVENUECAT_IOS_API_KEY= # Production server credentials are intentionally pending. # Keep them in backend secret storage, never in the mobile build environment. +# Required by AI Edge Functions for server-side subscription validation: +# REVENUECAT_SECRET_API_KEY diff --git a/.gitignore b/.gitignore index 32d1d2c..8eb225a 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,4 @@ PROMPTS.md task.md DB.md AI.md +DearDiary.md diff --git a/.maestro/00-launch.yaml b/.maestro/00-launch.yaml new file mode 100644 index 0000000..9473a93 --- /dev/null +++ b/.maestro/00-launch.yaml @@ -0,0 +1,27 @@ +appId: com.aryan.deardiary +--- +- launchApp: + clearState: false + +- assertVisible: + text: "DearDiary" + optional: true + +- assertVisible: + text: "Today" + +- assertVisible: + text: "Reflect" + optional: true + +- assertVisible: + text: "History" + optional: true + +- assertVisible: + text: "Insights" + optional: true + +- assertVisible: + text: "Profile" + optional: true diff --git a/.maestro/00-smoke-launch.yaml b/.maestro/00-smoke-launch.yaml new file mode 100644 index 0000000..1b33a6e --- /dev/null +++ b/.maestro/00-smoke-launch.yaml @@ -0,0 +1,35 @@ +appId: com.aryan.deardiary +--- +- launchApp: + clearState: false + +- assertVisible: + id: "login-screen" + optional: true + +- assertVisible: + id: "signup-screen" + optional: true + +- assertVisible: + id: "home-screen" + optional: true + +- assertVisible: + id: "app-lock-screen" + optional: true + +- tapOn: + id: "tab-profile-button" + optional: true + +- assertVisible: + id: "profile-screen" + optional: true + +- tapOn: + id: "tab-today-button" + optional: true + +- assertVisible: + id: "home-screen" diff --git a/app.json b/app.json index d2ac7b5..0fccd7c 100644 --- a/app.json +++ b/app.json @@ -2,14 +2,14 @@ "expo": { "name": "DearDiary", "slug": "dear-diary", - "version": "1.0.4", + "version": "1.0.6", "orientation": "portrait", "icon": "./assets/images/icon.png", "scheme": "deardiary", "userInterfaceStyle": "automatic", "newArchEnabled": true, "ios": { - "buildNumber": "4", + "buildNumber": "6", "supportsTablet": true }, "android": { @@ -21,7 +21,8 @@ ], "icon": "./assets/images/icon.png", "package": "com.aryan.deardiary", - "versionCode": 4, + "permissions": ["com.android.vending.BILLING"], + "versionCode": 6, "adaptiveIcon": { "backgroundColor": "#FFDDE8", "foregroundImage": "./assets/images/icon.png" diff --git a/app/(onboarding)/onboarding-screen-1.tsx b/app/(onboarding)/onboarding-screen-1.tsx index dbc60bf..721d454 100644 --- a/app/(onboarding)/onboarding-screen-1.tsx +++ b/app/(onboarding)/onboarding-screen-1.tsx @@ -308,6 +308,9 @@ export default function OnboardingScreenOne() { > diff --git a/app/(onboarding)/onboarding-screen-2.tsx b/app/(onboarding)/onboarding-screen-2.tsx index c7f4b3e..d5c4662 100644 --- a/app/(onboarding)/onboarding-screen-2.tsx +++ b/app/(onboarding)/onboarding-screen-2.tsx @@ -372,6 +372,9 @@ export default function OnboardingScreenTwo() { > - + { + if (isLoaded && !isSignedIn) { + router.replace("/login"); + } + }, [isLoaded, isSignedIn, router]); + if (!isLoaded) { return null; } if (!isSignedIn) { - return ; + return null; } return ( diff --git a/app/_layout.tsx b/app/_layout.tsx index d3bbda8..4bc22e0 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -18,6 +18,7 @@ import { useNativeTransitionOptions } from "@/navigation/transitions"; import { AppLockProvider } from "@/providers/AppLockProvider"; import { AppDialogProvider } from "@/providers/AppDialogProvider"; import { ConnectivityProvider } from "@/providers/ConnectivityProvider"; +import { SubscriptionProvider } from "@/providers/SubscriptionProvider"; export default function RootLayout() { if (!publicEnvironmentResult.isValid) { @@ -40,7 +41,9 @@ export default function RootLayout() { > - + + + @@ -111,6 +114,7 @@ function RootNavigator() { name="legal/privacy-policy" options={standardDetailOptions} /> + diff --git a/app/insights/report/[periodType].tsx b/app/insights/report/[periodType].tsx index 690eaf7..91f3b9d 100644 --- a/app/insights/report/[periodType].tsx +++ b/app/insights/report/[periodType].tsx @@ -1,5 +1,11 @@ import { useAuth } from "@clerk/expo"; -import { Redirect, Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { + Redirect, + Stack, + type Href, + useLocalSearchParams, + useRouter, +} from "expo-router"; import { ChevronLeft, RefreshCw } from "lucide-react-native"; import { useMemo } from "react"; import { @@ -39,18 +45,18 @@ import { import { FeatureErrorBoundary } from "@/components/errors/FeatureErrorBoundary"; import { ReportStatGrid } from "@/components/insights/report/ReportStatGrid"; import { reportColors } from "@/constants/report-theme"; -import { useAppDialog } from "@/hooks/useAppDialog"; import { useAIInsightReport } from "@/hooks/useAIInsightReport"; +import { useFeatureAccess } from "@/hooks/useFeatureAccess"; +import { useReportAccessActions } from "@/hooks/useReportAccessActions"; import { getCurrentReportPeriod, isAIInsightPeriodType, } from "@/lib/insights/reportPeriods"; export default function AIInsightReportScreen() { - const { isLoaded, isSignedIn } = useAuth(); + const { isLoaded, isSignedIn, userId } = useAuth(); const insets = useSafeAreaInsets(); const router = useRouter(); - const { showDialog } = useAppDialog(); const params = useLocalSearchParams<{ periodType?: string }>(); const periodTypeParam = Array.isArray(params.periodType) ? params.periodType[0] @@ -64,29 +70,23 @@ export default function AIInsightReportScreen() { const reportState = useAIInsightReport(period, { enabled: isValidPeriodType, }); + const reportFeature = + period.type === "weekly" ? "weekly_report" : "monthly_report"; + const reportAccess = useFeatureAccess(reportFeature, userId); + const { + fairUseLimitMessage, + handleGenerate, + handleRegenerate, + } = useReportAccessActions({ + feature: reportFeature, + reportAccess, + reportState, + userId, + }); const bottomNavHeight = bottomTabBarBaseHeight + insets.bottom; const minimumEntries = period.type === "weekly" ? 2 : 3; const hasEnoughEntries = reportState.availableEntryCount >= minimumEntries; - function handleGenerate() { - void reportState.generate(); - } - - function handleRegenerate() { - showDialog({ - cancelText: "Keep Current", - confirmText: "Regenerate", - icon: "✦", - message: - "DearDiary will analyze this period again and replace the current visual reflection.", - onConfirm: () => { - void reportState.regenerate(); - }, - showCancel: true, - title: "Regenerate reflection?", - }); - } - if (!isLoaded) { return null; } @@ -117,6 +117,41 @@ export default function AIInsightReportScreen() { ); } + if ( + period.type === "monthly" && + !reportAccess.allowed && + reportAccess.reason !== "Pro_fair_use_exhausted" + ) { + return ( + + + + Monthly AI reports are included with DearDiary Pro. + + + Unlock monthly summaries, advanced patterns, and long-term writing + insights. + + + router.push({ + pathname: "/paywall", + params: { feature: "monthly_report" }, + } as unknown as Href) + } + /> + + + ); + } + return ( @@ -126,6 +161,7 @@ export default function AIInsightReportScreen() { onRetry={reportState.refresh} > router.back()} > @@ -150,6 +187,7 @@ export default function AIInsightReportScreen() { Reflection Report + {fairUseLimitMessage ? ( + + ) : null} + {reportState.error ? ( (); + const feature = getSingleRouteParam(params.feature); + + return ( + <> + + + + ); +} diff --git a/app/settings/app-lock/setup.tsx b/app/settings/app-lock/setup.tsx index e990f4e..cde6560 100644 --- a/app/settings/app-lock/setup.tsx +++ b/app/settings/app-lock/setup.tsx @@ -5,12 +5,12 @@ import { ActivityIndicator, Pressable, ScrollView, - Switch, Text, View, } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { BiometricLockSwitch } from "@/components/app-lock/BiometricLockSwitch"; import { PinInput } from "@/components/app-lock/PinInput"; import { AnimatedIconButton } from "@/components/ui/animated-icon-button"; import { appLockColors, appLockLayout } from "@/constants/app-lock-theme"; @@ -108,6 +108,7 @@ export default function AppLockSetupScreen() { style={{ backgroundColor: appLockColors.background }} > router.back()} shadow="0 2px 6px rgba(39, 39, 42, 0.16)" @@ -153,7 +155,9 @@ export default function AppLockSetupScreen() { Create a six-digit PIN {helperMessage ? ( @@ -218,7 +225,8 @@ export default function AppLockSetupScreen() { : "Biometrics are not available or enrolled on this device."} - {delayOptions.map((option) => ( router.push(privacyPolicyHref)} + testID="profile-privacy-policy-link" value="" /> @@ -107,6 +110,7 @@ export default function PrivacySettingsScreen() { icon="clipboard" label="Terms & Conditions" onPress={() => router.push(termsHref)} + testID="profile-terms-link" value="" /> {accountDeletionUrl ? ( @@ -116,6 +120,7 @@ export default function PrivacySettingsScreen() { icon="external-link" label="External Deletion Page" onPress={() => router.push(accountDeletionUrl as Href)} + testID="settings-external-deletion-row" value="View" /> @@ -130,6 +135,7 @@ export default function PrivacySettingsScreen() { isBusy={busyAction === "disable-lock"} label="App Lock" onPress={handleAppLockPress} + testID="settings-app-lock-row" value={isEnabled ? "On" : "Off"} /> diff --git a/app/sso.tsx b/app/sso.tsx index eeece81..82ae6d1 100644 --- a/app/sso.tsx +++ b/app/sso.tsx @@ -12,7 +12,10 @@ export default function SsoCallbackScreen() { >