feat(purchases): paywall non-core features with RevenueCat#2597
Conversation
Adds react-native-purchases + react-native-purchases-ui to the Expo app and gates 10 premium screens behind a Pro entitlement check. New feature module (features/purchases/): - lib/config.ts — ENTITLEMENT_PRO constant + PRO_FEATURES list - lib/init.ts — configures SDK on startup, skips gracefully when no API key is present (dev builds without RC credentials still launch) - hooks/useEntitlement.ts — React Query hook that fetches CustomerInfo (5-min stale, disabled until SDK is initialised) - hooks/useIdentifyUser.ts — logs in / out of RevenueCat whenever the Better Auth session changes, keeping entitlements in sync - components/PaywallGate.tsx — renders children for Pro users, UpgradePrompt otherwise - components/UpgradePrompt.tsx — branded upgrade screen that calls RevenueCatUI.presentPaywallIfNeeded() on "View Plans" Infrastructure: - PurchasesProvider wraps the native provider tree; initialises SDK on mount and mounts PurchasesEffects for user identification - Web provider (index.web.tsx) intentionally excluded — web billing requires separate RevenueCat Web Billing setup - packages/env: added optional EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY / EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY env vars - Minimal .d.ts shims so tsc passes before bun install runs Gated screens (require Pro): - AI Chat — cloud + on-device LLM (expensive compute) - Season Suggestions + Results — AI-powered gear recommendations - Trail Conditions — community trail data - Weather Alerts — push-style severe weather notifications - Pack Weight Analysis — per-pack weight breakdown - Wildlife Identification — ML species ID (when feature flag enabled) - Shared Packs — collaborative packing (when feature flag enabled) - Shopping List — gear shopping assistant - Pack Templates — curated starter packs Free / core (unchanged): - Dashboard, Packs (CRUD), Catalog, Profile, Guides, Gear Inventory, basic Weather, Trips After merging: run `bun install` and create a new EAS development build — react-native-purchases requires native modules and will not work in Expo Go (mock APIs are active in Expo Go so the app still launches, but real purchases require a dev client). RevenueCat dashboard setup required: 1. Create a project at app.revenuecat.com 2. Add Apple + Google store connections 3. Create a "pro" entitlement and attach products 4. Configure a Paywall in the dashboard 5. Set EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY and EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY in .env.local / EAS secrets
WalkthroughIntroduces a full RevenueCat Pro paywall system: SDK initialization ( ChangesRevenueCat Pro Paywall Integration
Sequence Diagram(s)sequenceDiagram
participant User
participant Route as *Route (default export)
participant PaywallGate
participant useEntitlement
participant RevenueCat as Purchases SDK
participant UpgradePrompt
User->>Route: navigate to screen
Route->>PaywallGate: render children (Screen)
PaywallGate->>useEntitlement: isPro, isLoading
useEntitlement->>RevenueCat: getCustomerInfo()
RevenueCat-->>useEntitlement: CustomerInfo
alt isLoading
PaywallGate-->>User: ActivityIndicator
else not Pro
PaywallGate->>UpgradePrompt: render
UpgradePrompt->>RevenueCat: presentPaywallIfNeeded(ENTITLEMENT_PRO)
RevenueCat-->>UpgradePrompt: PAYWALL_RESULT
UpgradePrompt->>useEntitlement: invalidate()
else isPro
PaywallGate-->>User: render Screen children
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/expo/features/purchases/components/PaywallGate.tsx`:
- Around line 10-23: The PaywallGate component does not handle errors from the
useEntitlement hook, causing it to incorrectly show the UpgradePrompt when the
entitlement fetch fails transiently. Destructure the error property from
useEntitlement() alongside isPro and isLoading, then add an explicit error
handling branch after the isLoading check but before the isPro check. When error
exists, render a retry UI component instead of falling through to UpgradePrompt,
ensuring transient RevenueCat failures do not block already-entitled users.
In `@apps/expo/features/purchases/components/UpgradePrompt.tsx`:
- Line 5: Replace the React useState hook with Jotai for state management in the
UpgradePrompt component. Remove the useState import from react and add an import
for useAtom from jotai. Create a module-scoped atom at the top of the file (or
in a separate atoms file following the project pattern) for the
isPresentingPaywall state, then replace any useState(isPresentingPaywall) call
with useAtom(isPresentingPaywallAtom) throughout the component to follow the
apps/expo guidelines for local state management.
- Line 61: The inline backgroundColor style in the UpgradePrompt component using
colors.primary with concatenated opacity bypasses the NativeWind CSS variable
system and breaks dark mode support. Replace the style prop containing
backgroundColor set to colors.primary + '20' with the NativeWind utility class
bg-primary/20, which properly references the theme's CSS variables through
Tailwind's opacity syntax to ensure consistent dark mode styling.
In `@apps/expo/features/purchases/hooks/useEntitlement.ts`:
- Around line 18-25: The `enabled: isRevenueCatInitialized()` check in the
useQuery hook within `useEntitlement` evaluates to a static boolean at render
time and doesn't trigger re-renders when the initialization status changes. If
the hook renders before RevenueCat is initialized, the query disables
permanently with no mechanism to re-enable it. Create a Jotai atom to hold the
initialization status (e.g., revenueCatInitializedAtom), update this atom when
RevenueCat finishes initializing, and replace the `enabled` field to depend on
reading this atom instead of calling the plain `isRevenueCatInitialized()`
function. This ensures the query properly re-evaluates and re-enables
automatically when the atom updates.
In `@apps/expo/features/purchases/hooks/useIdentifyUser.ts`:
- Around line 18-34: Add Sentry.addBreadcrumb calls before the RevenueCat async
operations in the useIdentifyUser hook. Before the Purchases.logIn(userId) call
in the if branch, add a breadcrumb with category 'purchases', message 'Logging
in to RevenueCat', and level 'info'. Similarly, before the Purchases.logOut()
call in the else branch, add a breadcrumb with category 'purchases', message
'Logging out of RevenueCat', and level 'info'. This follows the coding
guidelines pattern of calling Sentry helpers before async operations, consistent
with other hooks like useAuthActions and useUpdateProfile.
In `@apps/expo/features/purchases/lib/init.ts`:
- Around line 23-28: The Purchases.configure({ apiKey }) call can throw an error
but currently has no error handling, which will crash app startup. Wrap the
Purchases.configure call in a try-catch block to handle potential failures. In
the catch block, import and use Sentry from `@sentry/react-native` to report the
error, then prevent the initialized flag from being set to true so the init
failure is properly tracked. This ensures RevenueCat SDK initialization failures
are caught and reported rather than breaking app startup.
In `@apps/expo/package.json`:
- Around line 151-152: In the package.json file, change the version constraints
for both react-native-purchases and react-native-purchases-ui from the caret
range (^9.0.0) to exact pinned versions (9.0.0) without any version prefix. This
ensures that the peer dependency requirement of react-native-purchases-ui at
exactly version 9.0.0 is respected and prevents unintended minor or patch
upgrades that could cause EAS build failures.
In `@apps/expo/providers/PurchasesProvider.tsx`:
- Around line 9-17: Convert the module-level `initialized` boolean into React
state using `useState` in the PurchasesProvider function to track RevenueCat
initialization status. Update the `useEffect` hook to set this state to true
after `initRevenueCat()` completes, and conditionally render the
`<PurchasesEffects />` component only when this initialization state is true.
This ensures that downstream hooks like `useIdentifyUser()` and
`useEntitlement()` inside `PurchasesEffects` will re-run once RevenueCat is
actually initialized, allowing them to properly check the initialization status
and execute their logic.
In `@apps/expo/types/react-native-purchases.d.ts`:
- Around line 13-15: The `active` property type definition uses `Record<string,
EntitlementInfo>` which incorrectly suggests all indexed keys are guaranteed to
exist, but accessing non-existent keys at runtime returns undefined. Change the
type of the `active` property from `Record<string, EntitlementInfo>` to
`Record<string, EntitlementInfo | undefined>` to accurately reflect that indexed
lookups can return undefined values and eliminate the type-safety gap that
currently requires defensive checks in the codebase.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 996100fd-08dd-404a-bee9-cffafdbc1277
📒 Files selected for processing (24)
apps/expo/app/(app)/ai-chat.tsxapps/expo/app/(app)/pack-templates/index.tsxapps/expo/app/(app)/season-suggestions-results.tsxapps/expo/app/(app)/season-suggestions.tsxapps/expo/app/(app)/shared-packs.tsxapps/expo/app/(app)/shopping-list.tsxapps/expo/app/(app)/trail-conditions.tsxapps/expo/app/(app)/weather-alerts.tsxapps/expo/app/(app)/weight-analysis/[id].tsxapps/expo/app/(app)/wildlife/index.tsxapps/expo/features/purchases/components/PaywallGate.tsxapps/expo/features/purchases/components/UpgradePrompt.tsxapps/expo/features/purchases/hooks/useEntitlement.tsapps/expo/features/purchases/hooks/useIdentifyUser.tsapps/expo/features/purchases/index.tsapps/expo/features/purchases/lib/config.tsapps/expo/features/purchases/lib/init.tsapps/expo/lib/i18n/locales/en.jsonapps/expo/package.jsonapps/expo/providers/PurchasesProvider.tsxapps/expo/providers/index.tsxapps/expo/types/react-native-purchases-ui.d.tsapps/expo/types/react-native-purchases.d.tspackages/env/src/expo-client.ts
| export function PaywallGate({ children }: PaywallGateProps) { | ||
| const { isPro, isLoading } = useEntitlement(); | ||
|
|
||
| if (isLoading) { | ||
| return ( | ||
| <View className="flex-1 items-center justify-center bg-background"> | ||
| <ActivityIndicator /> | ||
| </View> | ||
| ); | ||
| } | ||
|
|
||
| if (!isPro) { | ||
| return <UpgradePrompt />; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the useEntitlement hook implementation
fd -type f -name "*.ts*" | xargs grep -l "useEntitlement" | head -20Repository: PackRat-AI/PackRat
Length of output: 233
🏁 Script executed:
# Read the PaywallGate.tsx file to see the complete current implementation
find apps/expo -name "PaywallGate.tsx" -type fRepository: PackRat-AI/PackRat
Length of output: 118
🏁 Script executed:
# Look for the hook definition in the purchases feature
find apps/expo/features/purchases -type f -name "*.ts*" | xargs grep -l "export.*useEntitlement" 2>/dev/nullRepository: PackRat-AI/PackRat
Length of output: 153
🏁 Script executed:
cat -n apps/expo/features/purchases/hooks/useEntitlement.tsRepository: PackRat-AI/PackRat
Length of output: 1203
🏁 Script executed:
cat -n apps/expo/features/purchases/components/PaywallGate.tsxRepository: PackRat-AI/PackRat
Length of output: 865
Handle entitlement fetch errors separately from the "not Pro" branch.
If useEntitlement() fails, the component falls through to <UpgradePrompt />, which can incorrectly block already-entitled users during transient RevenueCat failures. The hook already exposes error and invalidate—add an explicit error branch before if (!isPro) to render a retry UI instead of an upsell.
Proposed fix
export function PaywallGate({ children }: PaywallGateProps) {
- const { isPro, isLoading } = useEntitlement();
+ const { isPro, isLoading, error, invalidate } = useEntitlement();
if (isLoading) {
return (
<View className="flex-1 items-center justify-center bg-background">
<ActivityIndicator />
</View>
);
}
+ if (error) {
+ return (
+ <View className="flex-1 items-center justify-center bg-background">
+ <Text>Failed to load. Tap to retry.</Text>
+ <Pressable onPress={invalidate}>
+ <Text>Retry</Text>
+ </Pressable>
+ </View>
+ );
+ }
+
if (!isPro) {
return <UpgradePrompt />;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function PaywallGate({ children }: PaywallGateProps) { | |
| const { isPro, isLoading } = useEntitlement(); | |
| if (isLoading) { | |
| return ( | |
| <View className="flex-1 items-center justify-center bg-background"> | |
| <ActivityIndicator /> | |
| </View> | |
| ); | |
| } | |
| if (!isPro) { | |
| return <UpgradePrompt />; | |
| } | |
| export function PaywallGate({ children }: PaywallGateProps) { | |
| const { isPro, isLoading, error, invalidate } = useEntitlement(); | |
| if (isLoading) { | |
| return ( | |
| <View className="flex-1 items-center justify-center bg-background"> | |
| <ActivityIndicator /> | |
| </View> | |
| ); | |
| } | |
| if (error) { | |
| return ( | |
| <View className="flex-1 items-center justify-center bg-background"> | |
| <Text>Failed to load. Tap to retry.</Text> | |
| <Pressable onPress={invalidate}> | |
| <Text>Retry</Text> | |
| </Pressable> | |
| </View> | |
| ); | |
| } | |
| if (!isPro) { | |
| return <UpgradePrompt />; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/features/purchases/components/PaywallGate.tsx` around lines 10 -
23, The PaywallGate component does not handle errors from the useEntitlement
hook, causing it to incorrectly show the UpgradePrompt when the entitlement
fetch fails transiently. Destructure the error property from useEntitlement()
alongside isPro and isLoading, then add an explicit error handling branch after
the isLoading check but before the isPro check. When error exists, render a
retry UI component instead of falling through to UpgradePrompt, ensuring
transient RevenueCat failures do not block already-entitled users.
| import * as Sentry from '@sentry/react-native'; | ||
| import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme'; | ||
| import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; | ||
| import { useState } from 'react'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if the file exists and view its content
cat -n apps/expo/features/purchases/components/UpgradePrompt.tsxRepository: PackRat-AI/PackRat
Length of output: 4540
🏁 Script executed:
# Search for other uses of this component or atom-related patterns in the purchases feature
rg "UpgradePrompt|isPresentingPaywall" apps/expo/features/purchases/ -A 2 -B 2Repository: PackRat-AI/PackRat
Length of output: 3399
🏁 Script executed:
# Check how other features in apps/expo structure their state (look at an existing feature using Jotai)
find apps/expo/features -name "*.tsx" -type f | head -5 | xargs grep -l "useAtom" | head -2 | xargs catRepository: PackRat-AI/PackRat
Length of output: 9432
Replace useState with Jotai for local state management.
Per the apps/expo guidelines, local state must use Jotai instead of React's useState. Move the isPresentingPaywall state to a module-scoped atom and use useAtom to access it (see pattern in similar features like AI mode selector).
Suggested refactor
-import { useState } from 'react';
+import { atom, useAtom } from 'jotai';
@@
+const isPresentingPaywallAtom = atom(false);
+
export function UpgradePrompt() {
@@
- const [isPresentingPaywall, setIsPresentingPaywall] = useState(false);
+ const [isPresentingPaywall, setIsPresentingPaywall] = useAtom(isPresentingPaywallAtom);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/features/purchases/components/UpgradePrompt.tsx` at line 5, Replace
the React useState hook with Jotai for state management in the UpgradePrompt
component. Remove the useState import from react and add an import for useAtom
from jotai. Create a module-scoped atom at the top of the file (or in a separate
atoms file following the project pattern) for the isPresentingPaywall state,
then replace any useState(isPresentingPaywall) call with
useAtom(isPresentingPaywallAtom) throughout the component to follow the
apps/expo guidelines for local state management.
Source: Coding guidelines
| <View className="mb-6 items-center"> | ||
| <View | ||
| className="mb-4 h-20 w-20 items-center justify-center rounded-full" | ||
| style={{ backgroundColor: colors.primary + '20' }} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/expo/features/purchases/components/UpgradePrompt.tsx | head -100Repository: PackRat-AI/PackRat
Length of output: 4194
🏁 Script executed:
# Check NativeWind configuration and color token definitions
fd -t f "(tailwind|nativewind)" apps/expo --type f | head -20Repository: PackRat-AI/PackRat
Length of output: 121
🏁 Script executed:
# Search for color token definitions in the project
rg "colors\.primary" apps/expo --type tsx --type ts -B 2 -A 2Repository: PackRat-AI/PackRat
Length of output: 90
🏁 Script executed:
# Check for NativeWind config or theme definitions
find apps/expo -name "*.config.*" -o -name "tailwind.config.*" -o -name "nativewind.config.*" | head -10Repository: PackRat-AI/PackRat
Length of output: 269
🏁 Script executed:
cat apps/expo/tailwind.config.jsRepository: PackRat-AI/PackRat
Length of output: 2334
🏁 Script executed:
# Check how useColorScheme is implemented
cat apps/expo/lib/hooks/useColorScheme.ts | head -50Repository: PackRat-AI/PackRat
Length of output: 132
🏁 Script executed:
# Search for color definitions and how colors are used in the project
rg "colors\.primary|colors\.background" apps/expo --type ts --type tsx -B 1 -A 1 | head -40Repository: PackRat-AI/PackRat
Length of output: 90
🏁 Script executed:
fd "useColorScheme" apps/expo --type fRepository: PackRat-AI/PackRat
Length of output: 144
🏁 Script executed:
# Search for useColorScheme implementation
rg "useColorScheme" apps/expo --type ts -lRepository: PackRat-AI/PackRat
Length of output: 6969
🏁 Script executed:
# Let's verify if bg-primary/20 syntax is valid by checking how opacity is used in the codebase
rg "bg-.*/" apps/expo --type ts -A 1 -B 1 | head -30Repository: PackRat-AI/PackRat
Length of output: 2457
🏁 Script executed:
head -60 apps/expo/lib/hooks/useColorScheme.tsxRepository: PackRat-AI/PackRat
Length of output: 1622
🏁 Script executed:
# Check if there are examples of dynamic color values being replaced with static classes
rg "style.*backgroundColor.*colors\." apps/expo -B 2 -A 2 | head -50Repository: PackRat-AI/PackRat
Length of output: 4466
Replace inline color styles with NativeWind token classes.
Inline backgroundColor styles with colors.primary bypass the NativeWind CSS variable system and break dark mode consistency. Use bg-primary and bg-primary/20 classes instead, which properly reference the theme's CSS variables via Tailwind's opacity syntax.
Proposed refactor
- <View
- className="mb-4 h-20 w-20 items-center justify-center rounded-full"
- style={{ backgroundColor: colors.primary + '20' }}
- >
+ <View className="mb-4 h-20 w-20 items-center justify-center rounded-full bg-primary/20">
@@
- <View
- className="h-5 w-5 items-center justify-center rounded-full"
- style={{ backgroundColor: colors.primary }}
- >
+ <View className="h-5 w-5 items-center justify-center rounded-full bg-primary">🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/features/purchases/components/UpgradePrompt.tsx` at line 61, The
inline backgroundColor style in the UpgradePrompt component using colors.primary
with concatenated opacity bypasses the NativeWind CSS variable system and breaks
dark mode support. Replace the style prop containing backgroundColor set to
colors.primary + '20' with the NativeWind utility class bg-primary/20, which
properly references the theme's CSS variables through Tailwind's opacity syntax
to ensure consistent dark mode styling.
Source: Coding guidelines
| enabled: isRevenueCatInitialized(), | ||
| staleTime: 1000 * 60 * 5, | ||
| retry: 1, | ||
| }); | ||
|
|
||
| const isPro = | ||
| isRevenueCatInitialized() && | ||
| typeof customerInfo?.entitlements.active[ENTITLEMENT_PRO] !== 'undefined'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect init implementation and provider wiring for reactive readiness updates
fd -i 'init.ts' apps/expo/features/purchases -x sed -n '1,220p' {}
fd -i 'PurchasesProvider.tsx' apps/expo/providers -x sed -n '1,260p' {}
# Find all readiness checks/usages
rg -n -C3 'isRevenueCatInitialized|initRevenueCat|useEntitlement' apps/expo --type ts --type tsxRepository: PackRat-AI/PackRat
Length of output: 1325
🏁 Script executed:
cat apps/expo/features/purchases/hooks/useEntitlement.tsRepository: PackRat-AI/PackRat
Length of output: 979
🏁 Script executed:
# Search for Jotai/Legend State atoms or reactive setup for RevenueCat
rg -n "atom|jotai|legendState|Entitlement|RevenueCat" apps/expo/features/purchases --type ts --type=tsx -A 2Repository: PackRat-AI/PackRat
Length of output: 90
🏁 Script executed:
# Check if isRevenueCatInitialized is used as a reactive dependency anywhere
rg -n "isRevenueCatInitialized" apps/expo -A 2 -B 2Repository: PackRat-AI/PackRat
Length of output: 2894
Make RevenueCat readiness reactive using Jotai to properly gate the entitlement query.
Line 18 uses enabled: isRevenueCatInitialized() from a plain boolean check evaluated at render time. If useEntitlement() renders before or during initialization completion, the query disables with no mechanism to re-enable it—React Query won't re-run the query unless the enabled value changes and triggers a re-render. Since initialized is a plain boolean variable with no observable state wrapper, changing it won't cause a re-render.
Per the coding guidelines, use Jotai (or Legend State) for reactive state. Create an atom for the initialization status and depend on it in the query's enabled field to ensure automatic re-renders when RevenueCat becomes ready.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/features/purchases/hooks/useEntitlement.ts` around lines 18 - 25,
The `enabled: isRevenueCatInitialized()` check in the useQuery hook within
`useEntitlement` evaluates to a static boolean at render time and doesn't
trigger re-renders when the initialization status changes. If the hook renders
before RevenueCat is initialized, the query disables permanently with no
mechanism to re-enable it. Create a Jotai atom to hold the initialization status
(e.g., revenueCatInitializedAtom), update this atom when RevenueCat finishes
initializing, and replace the `enabled` field to depend on reading this atom
instead of calling the plain `isRevenueCatInitialized()` function. This ensures
the query properly re-evaluates and re-enables automatically when the atom
updates.
| Purchases.logIn(userId) | ||
| .then(() => queryClient.invalidateQueries({ queryKey: CUSTOMER_INFO_QUERY_KEY })) | ||
| .catch((error: unknown) => { | ||
| Sentry.captureException(error, { | ||
| tags: { feature: 'purchases', action: 'logIn' }, | ||
| extra: { userId }, | ||
| }); | ||
| }); | ||
| } else { | ||
| Purchases.logOut() | ||
| .then(() => queryClient.invalidateQueries({ queryKey: CUSTOMER_INFO_QUERY_KEY })) | ||
| .catch((error: unknown) => { | ||
| Sentry.captureException(error, { | ||
| tags: { feature: 'purchases', action: 'logOut' }, | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find existing Sentry helper patterns used before async operations
rg -n -C3 'Sentry\.(startSpan|addBreadcrumb|withScope|captureException)' apps/expo --type ts --type tsx
# Inspect this hook's async call sites
sed -n '1,220p' apps/expo/features/purchases/hooks/useIdentifyUser.tsRepository: PackRat-AI/PackRat
Length of output: 90
🏁 Script executed:
# Fix the rg command - search for Sentry patterns in TypeScript files
rg -n 'Sentry\.(startSpan|addBreadcrumb|withScope|captureException)' apps/expo -t ts
# Also inspect the actual file mentioned in the review
cat -n apps/expo/features/purchases/hooks/useIdentifyUser.tsRepository: PackRat-AI/PackRat
Length of output: 16011
🏁 Script executed:
# Search for Sentry usage patterns before async calls in similar hooks/features
rg -B5 'await.*\(' apps/expo/features --type ts | grep -A5 -B5 'Sentry' | head -80Repository: PackRat-AI/PackRat
Length of output: 6161
Add Sentry.addBreadcrumb before RevenueCat async calls.
Lines 18 and 27 execute async operations without calling Sentry.addBreadcrumb first. Per coding guidelines, helpers must be called before async operations. Follow the pattern used elsewhere (e.g., useAuthActions.ts, useUpdateProfile.ts):
Example fix
if (userId) {
Sentry.addBreadcrumb({
category: 'purchases',
message: 'Logging in to RevenueCat',
level: 'info',
});
Purchases.logIn(userId)
.then(() => queryClient.invalidateQueries({ queryKey: CUSTOMER_INFO_QUERY_KEY }))
.catch((error: unknown) => {
Sentry.captureException(error, {
tags: { feature: 'purchases', action: 'logIn' },
extra: { userId },
});
});
} else {
Sentry.addBreadcrumb({
category: 'purchases',
message: 'Logging out of RevenueCat',
level: 'info',
});
Purchases.logOut()
.then(() => queryClient.invalidateQueries({ queryKey: CUSTOMER_INFO_QUERY_KEY }))
.catch((error: unknown) => {
Sentry.captureException(error, {
tags: { feature: 'purchases', action: 'logOut' },
});
});
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/features/purchases/hooks/useIdentifyUser.ts` around lines 18 - 34,
Add Sentry.addBreadcrumb calls before the RevenueCat async operations in the
useIdentifyUser hook. Before the Purchases.logIn(userId) call in the if branch,
add a breadcrumb with category 'purchases', message 'Logging in to RevenueCat',
and level 'info'. Similarly, before the Purchases.logOut() call in the else
branch, add a breadcrumb with category 'purchases', message 'Logging out of
RevenueCat', and level 'info'. This follows the coding guidelines pattern of
calling Sentry helpers before async operations, consistent with other hooks like
useAuthActions and useUpdateProfile.
Source: Coding guidelines
| if (__DEV__) { | ||
| Purchases.setLogLevel(LOG_LEVEL.DEBUG); | ||
| } | ||
|
|
||
| Purchases.configure({ apiKey }); | ||
| initialized = true; |
There was a problem hiding this comment.
Handle RevenueCat SDK init failures to prevent provider-mount crashes.
Purchases.configure can throw, and this path currently has no catch/reporting. A thrown init here can break app startup.
Proposed fix
+import * as Sentry from '`@sentry/react-native`';
import { clientEnvs } from '`@packrat/env/expo-client`';
import { Platform } from 'react-native';
import Purchases, { LOG_LEVEL } from 'react-native-purchases';
@@
- if (__DEV__) {
- Purchases.setLogLevel(LOG_LEVEL.DEBUG);
- }
-
- Purchases.configure({ apiKey });
- initialized = true;
+ try {
+ if (__DEV__) {
+ Purchases.setLogLevel(LOG_LEVEL.DEBUG);
+ }
+ Purchases.configure({ apiKey });
+ initialized = true;
+ } catch (error) {
+ Sentry.captureException(error, {
+ tags: { feature: 'purchases', action: 'initRevenueCat' },
+ extra: { platform: Platform.OS },
+ });
+ }
}As per coding guidelines, "In apps/expo, import Sentry instrumentation from @sentry/react-native and call helpers before async operations and in all catch blocks".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (__DEV__) { | |
| Purchases.setLogLevel(LOG_LEVEL.DEBUG); | |
| } | |
| Purchases.configure({ apiKey }); | |
| initialized = true; | |
| import * as Sentry from '`@sentry/react-native`'; | |
| import { clientEnvs } from '`@packrat/env/expo-client`'; | |
| import { Platform } from 'react-native'; | |
| import Purchases, { LOG_LEVEL } from 'react-native-purchases'; | |
| // ... other code ... | |
| try { | |
| if (__DEV__) { | |
| Purchases.setLogLevel(LOG_LEVEL.DEBUG); | |
| } | |
| Purchases.configure({ apiKey }); | |
| initialized = true; | |
| } catch (error) { | |
| Sentry.captureException(error, { | |
| tags: { feature: 'purchases', action: 'initRevenueCat' }, | |
| extra: { platform: Platform.OS }, | |
| }); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/features/purchases/lib/init.ts` around lines 23 - 28, The
Purchases.configure({ apiKey }) call can throw an error but currently has no
error handling, which will crash app startup. Wrap the Purchases.configure call
in a try-catch block to handle potential failures. In the catch block, import
and use Sentry from `@sentry/react-native` to report the error, then prevent the
initialized flag from being set to true so the init failure is properly tracked.
This ensures RevenueCat SDK initialization failures are caught and reported
rather than breaking app startup.
Source: Coding guidelines
| "react-native-purchases": "^9.0.0", | ||
| "react-native-purchases-ui": "^9.0.0", |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect react-native dependency spec style in apps/expo/package.json
jq -r '.dependencies | to_entries[] | select(.key|test("^react-native-")) | "\(.key)=\(.value)"' apps/expo/package.json
# Verify RevenueCat package peer constraints for the pinned version
npm view react-native-purchases@9.0.0 peerDependencies --json
npm view react-native-purchases-ui@9.0.0 peerDependencies --jsonRepository: PackRat-AI/PackRat
Length of output: 809
Pin RevenueCat packages to exact versions for reproducible builds.
react-native-purchases-ui@9.0.0 explicitly requires react-native-purchases at exactly 9.0.0 (not a range). Using ^9.0.0 allows unplanned patch/minor upgrades that could violate the peer dependency constraint and break EAS builds.
Suggested change
- "react-native-purchases": "^9.0.0",
- "react-native-purchases-ui": "^9.0.0",
+ "react-native-purchases": "9.0.0",
+ "react-native-purchases-ui": "9.0.0",📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "react-native-purchases": "^9.0.0", | |
| "react-native-purchases-ui": "^9.0.0", | |
| "react-native-purchases": "9.0.0", | |
| "react-native-purchases-ui": "9.0.0", |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/package.json` around lines 151 - 152, In the package.json file,
change the version constraints for both react-native-purchases and
react-native-purchases-ui from the caret range (^9.0.0) to exact pinned versions
(9.0.0) without any version prefix. This ensures that the peer dependency
requirement of react-native-purchases-ui at exactly version 9.0.0 is respected
and prevents unintended minor or patch upgrades that could cause EAS build
failures.
| export function PurchasesProvider({ children }: { children: React.ReactNode }) { | ||
| useEffect(() => { | ||
| initRevenueCat(); | ||
| }, []); | ||
|
|
||
| return ( | ||
| <> | ||
| <PurchasesEffects /> | ||
| {children} |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the init->entitlement contract is non-reactive in current code.
sed -n '1,120p' apps/expo/providers/PurchasesProvider.tsx
sed -n '1,140p' apps/expo/features/purchases/hooks/useEntitlement.tsRepository: PackRat-AI/PackRat
Length of output: 1374
🏁 Script executed:
# Find RevenueCat initialization implementation
fd -t f init\.ts apps/expo/features/purchases
cat apps/expo/features/purchases/lib/init.tsRepository: PackRat-AI/PackRat
Length of output: 925
🏁 Script executed:
# Find useIdentifyUser implementation to see if it depends on initialization
rg "export.*useIdentifyUser" apps/expo/features/purchasesRepository: PackRat-AI/PackRat
Length of output: 249
🏁 Script executed:
# Check if initRevenueCat updates any reactive state
rg -A 10 "export.*function.*initRevenueCat" apps/expo/features/purchasesRepository: PackRat-AI/PackRat
Length of output: 901
🏁 Script executed:
cat apps/expo/features/purchases/hooks/useIdentifyUser.tsRepository: PackRat-AI/PackRat
Length of output: 1320
Make RevenueCat initialization reactive before downstream entitlement logic runs.
initRevenueCat() runs in a deferred effect while hooks like useIdentifyUser() and useEntitlement() check isRevenueCatInitialized() during setup. Since initialized is a module-level boolean (not React state), the check returns false on first render. The effect then sets it to true, but the hooks won't re-run—their dependency arrays don't include initialization state. This leaves customer identification and entitlement queries permanently disabled on initial mount unless the user's state changes after initialization.
Render <PurchasesEffects /> only after initialization via React state:
Fix
import { initRevenueCat, useIdentifyUser } from 'expo-app/features/purchases';
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
function PurchasesEffects() {
useIdentifyUser();
return null;
}
export function PurchasesProvider({ children }: { children: React.ReactNode }) {
+ const [hasInitializedPurchases, setHasInitializedPurchases] = useState(false);
+
useEffect(() => {
initRevenueCat();
+ setHasInitializedPurchases(true);
}, []);
return (
<>
- <PurchasesEffects />
+ {hasInitializedPurchases ? <PurchasesEffects /> : null}
{children}
</>
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function PurchasesProvider({ children }: { children: React.ReactNode }) { | |
| useEffect(() => { | |
| initRevenueCat(); | |
| }, []); | |
| return ( | |
| <> | |
| <PurchasesEffects /> | |
| {children} | |
| export function PurchasesProvider({ children }: { children: React.ReactNode }) { | |
| const [hasInitializedPurchases, setHasInitializedPurchases] = useState(false); | |
| useEffect(() => { | |
| initRevenueCat(); | |
| setHasInitializedPurchases(true); | |
| }, []); | |
| return ( | |
| <> | |
| {hasInitializedPurchases ? <PurchasesEffects /> : null} | |
| {children} | |
| </> | |
| ); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/providers/PurchasesProvider.tsx` around lines 9 - 17, Convert the
module-level `initialized` boolean into React state using `useState` in the
PurchasesProvider function to track RevenueCat initialization status. Update the
`useEffect` hook to set this state to true after `initRevenueCat()` completes,
and conditionally render the `<PurchasesEffects />` component only when this
initialization state is true. This ensures that downstream hooks like
`useIdentifyUser()` and `useEntitlement()` inside `PurchasesEffects` will re-run
once RevenueCat is actually initialized, allowing them to properly check the
initialization status and execute their logic.
| entitlements: { | ||
| active: Record<string, EntitlementInfo>; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
For react-native-purchases v9 TypeScript types, what is the exact type of CustomerInfo.entitlements.active, and should indexed lookups be typed as possibly undefined?
💡 Result:
In react-native-purchases v9, the exact type of CustomerInfo.entitlements.active is a dictionary (record) object keyed by string identifiers, where each value is an instance of the PurchasesEntitlementInfo interface [1][2][3]. Formally, the type is defined as: { [key: string]: PurchasesEntitlementInfo } [1][3] Regarding indexed lookups, you should treat the resulting value as possibly undefined [4]. While the TypeScript type definition provided by the SDK is a simple map interface [1][3], a lookup using a specific key (e.g., customerInfo.entitlements.active['my_entitlement_id']) will return a value of type PurchasesEntitlementInfo | undefined in practice, because the key may not exist in the map [4]. Using optional chaining (?.), such as customerInfo.entitlements.active['my_entitlement_id']?.isActive, is the recommended approach to safely handle these lookups [4].
Citations:
- 1: https://github.com/RevenueCat/react-native-purchases/blob/934b450/src/customerInfo.ts
- 2: https://github.com/RevenueCat/react-native-purchases/blob/b60c45f/src/customerInfo.ts
- 3: https://revenuecat.github.io/react-native-purchases-docs/8.2.4/interfaces/PurchasesEntitlementInfos.html
- 4: https://community.revenuecat.com/general-questions-7/entitlements-not-properly-working-2616?sort=mostLiked
🏁 Script executed:
find apps/expo -name "react-native-purchases.d.ts" -type fRepository: PackRat-AI/PackRat
Length of output: 106
🏁 Script executed:
cat -n apps/expo/types/react-native-purchases.d.ts 2>/dev/null | head -30Repository: PackRat-AI/PackRat
Length of output: 999
🏁 Script executed:
rg "entitlements\.active" apps/expo --type ts --type tsx -B 2 -A 2Repository: PackRat-AI/PackRat
Length of output: 90
🏁 Script executed:
rg "entitlements\.active" apps/expo -B 2 -A 2Repository: PackRat-AI/PackRat
Length of output: 482
Fix the type definition to reflect that indexed entitlements may not exist.
The type Record<string, EntitlementInfo> incorrectly suggests all keys are present, but indexed lookups can return undefined at runtime. This forces defensive checks like typeof customerInfo?.entitlements.active[ENTITLEMENT_PRO] !== 'undefined' in the codebase. Change active to Record<string, EntitlementInfo | undefined> to align the type with the actual SDK behavior and eliminate the type-safety gap.
Proposed fix
export interface CustomerInfo {
entitlements: {
- active: Record<string, EntitlementInfo>;
+ active: Record<string, EntitlementInfo | undefined>;
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| entitlements: { | |
| active: Record<string, EntitlementInfo>; | |
| }; | |
| export interface CustomerInfo { | |
| entitlements: { | |
| active: Record<string, EntitlementInfo | undefined>; | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/expo/types/react-native-purchases.d.ts` around lines 13 - 15, The
`active` property type definition uses `Record<string, EntitlementInfo>` which
incorrectly suggests all indexed keys are guaranteed to exist, but accessing
non-existent keys at runtime returns undefined. Change the type of the `active`
property from `Record<string, EntitlementInfo>` to `Record<string,
EntitlementInfo | undefined>` to accurately reflect that indexed lookups can
return undefined values and eliminate the type-safety gap that currently
requires defensive checks in the codebase.
Summary
react-native-purchases+react-native-purchases-uito the Expo app and wires up a full RevenueCat subscription integrationfeatures/purchases/module with SDK init, entitlement hook, user-identification hook, and reusablePaywallGate/UpgradePromptcomponentsproentitlement check; core screens (Packs, Catalog, Profile, Dashboard, Guides) remain freeWhat changed
New:
features/purchases/lib/config.tsENTITLEMENT_PRO = 'pro'constant +PRO_FEATURESlist shown in upgrade UIlib/init.tsPurchases.configure()at launch; skips gracefully when no API key is set (dev builds without credentials still launch)hooks/useEntitlement.tsCustomerInfo, 5-min cache, disabled until SDK is initialisedhooks/useIdentifyUser.tscomponents/PaywallGate.tsxUpgradePromptotherwisecomponents/UpgradePrompt.tsxRevenueCatUI.presentPaywallIfNeeded()New:
providers/PurchasesProvider.tsxInitialises RevenueCat on mount and mounts
PurchasesEffectsto drive user identification. Included only in the native provider tree (providers/index.tsx); the web provider (index.web.tsx) is intentionally unchanged since Web Billing requires a separate setup.Gated screens (Pro required)
app/(app)/ai-chat.tsxapp/(app)/season-suggestions.tsxapp/(app)/season-suggestions-results.tsxapp/(app)/trail-conditions.tsxapp/(app)/weather-alerts.tsxapp/(app)/weight-analysis/[id].tsxapp/(app)/wildlife/index.tsxapp/(app)/shared-packs.tsxapp/(app)/shopping-list.tsxapp/(app)/pack-templates/index.tsxEnv + types
packages/env/src/expo-client.ts— adds optionalEXPO_PUBLIC_REVENUECAT_APPLE_API_KEY/EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEYapps/expo/types/react-native-purchases*.d.ts— minimal shims sotscpasses beforebun installrunsTest plan
bun check-typespassesbun lint/bun checkpassesbun install+ new EAS dev build: app launches without errors in Expo Go (RC mock APIs active)proentitlement configured: subscribed user sees premium screens; unsubscribed user seesUpgradePromptinvalidate()re-fetchesCustomerInfoandPaywallGatetransitions to show the screenRevenueCat dashboard setup (required before testing purchases)
proentitlement and attach products.env.local/ EAS secrets:Summary by CodeRabbit