Skip to content

feat(purchases): paywall non-core features with RevenueCat#2597

Open
mikib0 wants to merge 2 commits into
developmentfrom
feat/revenuecat-paywall
Open

feat(purchases): paywall non-core features with RevenueCat#2597
mikib0 wants to merge 2 commits into
developmentfrom
feat/revenuecat-paywall

Conversation

@mikib0

@mikib0 mikib0 commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds react-native-purchases + react-native-purchases-ui to the Expo app and wires up a full RevenueCat subscription integration
  • Creates a features/purchases/ module with SDK init, entitlement hook, user-identification hook, and reusable PaywallGate / UpgradePrompt components
  • Gates 10 premium screens behind a pro entitlement check; core screens (Packs, Catalog, Profile, Dashboard, Guides) remain free

What changed

New: features/purchases/

File Purpose
lib/config.ts ENTITLEMENT_PRO = 'pro' constant + PRO_FEATURES list shown in upgrade UI
lib/init.ts Calls Purchases.configure() at launch; skips gracefully when no API key is set (dev builds without credentials still launch)
hooks/useEntitlement.ts React Query hook — fetches CustomerInfo, 5-min cache, disabled until SDK is initialised
hooks/useIdentifyUser.ts Syncs Better Auth session → RevenueCat login/logout so entitlements follow the user
components/PaywallGate.tsx Renders children for Pro subscribers, UpgradePrompt otherwise
components/UpgradePrompt.tsx Branded upgrade screen listing Pro features + "View Plans" button that calls RevenueCatUI.presentPaywallIfNeeded()

New: providers/PurchasesProvider.tsx

Initialises RevenueCat on mount and mounts PurchasesEffects to 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)

Route Feature
app/(app)/ai-chat.tsx AI Chat — cloud + on-device LLM
app/(app)/season-suggestions.tsx AI-powered gear recommendations
app/(app)/season-suggestions-results.tsx Season suggestions results
app/(app)/trail-conditions.tsx Community trail data
app/(app)/weather-alerts.tsx Severe weather notifications
app/(app)/weight-analysis/[id].tsx Pack weight breakdown
app/(app)/wildlife/index.tsx ML species identification
app/(app)/shared-packs.tsx Collaborative packing
app/(app)/shopping-list.tsx Gear shopping assistant
app/(app)/pack-templates/index.tsx Curated starter packs

Env + types

  • packages/env/src/expo-client.ts — adds optional EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY / EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY
  • apps/expo/types/react-native-purchases*.d.ts — minimal shims so tsc passes before bun install runs

Test plan

  • bun check-types passes
  • bun lint / bun check passes
  • After bun install + new EAS dev build: app launches without errors in Expo Go (RC mock APIs active)
  • With valid RC API keys + a pro entitlement configured: subscribed user sees premium screens; unsubscribed user sees UpgradePrompt
  • "View Plans" button presents the RevenueCat paywall configured in the RC dashboard
  • After purchase: invalidate() re-fetches CustomerInfo and PaywallGate transitions to show the screen
  • "Restore Purchases" re-presents the paywall which handles the restore flow
  • Logging out clears the RC identity; logging in re-identifies the user

RevenueCat dashboard setup (required before testing purchases)

  1. Create a project at app.revenuecat.com
  2. Connect Apple App Store and Google Play
  3. Create a pro entitlement and attach products
  4. Configure a Paywall UI in the RC dashboard
  5. Add to .env.local / EAS secrets:
    EXPO_PUBLIC_REVENUECAT_APPLE_API_KEY=appl_...
    EXPO_PUBLIC_REVENUECAT_GOOGLE_API_KEY=goog_...
    

Note: react-native-purchases uses native modules — a new EAS development build is required after merging. Expo Go runs in mock mode (no real purchases, but app doesn't crash).

Summary by CodeRabbit

  • New Features
    • Introduced Pro subscription tier: Premium features (AI chat, weather alerts, trail conditions, shopping list, weight analysis, season suggestions, shared packs, pack templates, and wildlife identification) now require a Pro subscription.
    • Added in-app purchase management with upgrade prompts and purchase restoration functionality for seamless subscription handling.

mikib0 added 2 commits June 18, 2026 15:06
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
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file mobile labels Jun 18, 2026
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Introduces a full RevenueCat Pro paywall system: SDK initialization (initRevenueCat, isRevenueCatInitialized), useEntitlement and useIdentifyUser hooks, a PaywallGate component with UpgradePrompt UI, a PurchasesProvider wired into the root provider tree, and paywall gating applied uniformly to 10 existing app routes.

Changes

RevenueCat Pro Paywall Integration

Layer / File(s) Summary
RevenueCat SDK config, init, and type shims
apps/expo/features/purchases/lib/config.ts, apps/expo/features/purchases/lib/init.ts, packages/env/src/expo-client.ts, apps/expo/types/react-native-purchases.d.ts, apps/expo/types/react-native-purchases-ui.d.ts, apps/expo/package.json
Defines ENTITLEMENT_PRO/PRO_FEATURES constants, implements initRevenueCat/isRevenueCatInitialized with platform key selection and re-init guard, adds optional EXPO_PUBLIC_REVENUECAT_* env vars to the Zod schema, adds react-native-purchases + react-native-purchases-ui dependencies, and provides TypeScript declaration shims for both packages.
useEntitlement and useIdentifyUser hooks
apps/expo/features/purchases/hooks/useEntitlement.ts, apps/expo/features/purchases/hooks/useIdentifyUser.ts, apps/expo/features/purchases/index.ts
useEntitlement fetches getCustomerInfo() via React Query (enabled only after SDK init), derives isPro, and exposes an invalidate helper. useIdentifyUser syncs the auth session with RevenueCat login/logout in a useEffect, invalidates CUSTOMER_INFO_QUERY_KEY, and reports errors to Sentry. Barrel-exported via index.ts.
PaywallGate component and UpgradePrompt UI
apps/expo/features/purchases/components/PaywallGate.tsx, apps/expo/features/purchases/components/UpgradePrompt.tsx, apps/expo/lib/i18n/locales/en.json
PaywallGate renders a loading spinner, UpgradePrompt, or children based on useEntitlement state. UpgradePrompt presents a PRO_FEATURES list with handleViewPlans/handleRestore handlers that call RevenueCatUI.presentPaywallIfNeeded, conditionally invalidate entitlements, and capture errors in Sentry. Adds purchases.* i18n strings.
PurchasesProvider wiring
apps/expo/providers/PurchasesProvider.tsx, apps/expo/providers/index.tsx
PurchasesProvider calls initRevenueCat() on mount and renders a side-effect PurchasesEffects child that invokes useIdentifyUser. Inserted between TanstackProvider and SafeAreaProvider in the root provider tree.
PaywallGate applied to 10 routes
apps/expo/app/(app)/ai-chat.tsx, apps/expo/app/(app)/pack-templates/index.tsx, apps/expo/app/(app)/season-suggestions*.tsx, apps/expo/app/(app)/shared-packs.tsx, apps/expo/app/(app)/shopping-list.tsx, apps/expo/app/(app)/trail-conditions.tsx, apps/expo/app/(app)/weather-alerts.tsx, apps/expo/app/(app)/weight-analysis/[id].tsx, apps/expo/app/(app)/wildlife/index.tsx
Each route demotes its Screen component from default export to named function and adds a new *Route default export that wraps the Screen in PaywallGate.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PackRat-AI/PackRat#1906: Introduced the wildlife route in apps/expo/app/(app)/wildlife/index.tsx, which this PR now wraps with PaywallGate.
  • PackRat-AI/PackRat#2432: Modifies apps/expo/app/(app)/ai-chat.tsx for AI transport refactoring; this PR gates the same file behind PaywallGate.

Suggested labels

mobile, dependencies

Suggested reviewers

  • andrew-bierman
  • Isthisanmol
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding RevenueCat paywall integration to gate premium features, which matches the PR's primary objective.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/revenuecat-paywall

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between f09e802 and b2b059a.

📒 Files selected for processing (24)
  • apps/expo/app/(app)/ai-chat.tsx
  • apps/expo/app/(app)/pack-templates/index.tsx
  • apps/expo/app/(app)/season-suggestions-results.tsx
  • apps/expo/app/(app)/season-suggestions.tsx
  • apps/expo/app/(app)/shared-packs.tsx
  • apps/expo/app/(app)/shopping-list.tsx
  • apps/expo/app/(app)/trail-conditions.tsx
  • apps/expo/app/(app)/weather-alerts.tsx
  • apps/expo/app/(app)/weight-analysis/[id].tsx
  • apps/expo/app/(app)/wildlife/index.tsx
  • apps/expo/features/purchases/components/PaywallGate.tsx
  • apps/expo/features/purchases/components/UpgradePrompt.tsx
  • apps/expo/features/purchases/hooks/useEntitlement.ts
  • apps/expo/features/purchases/hooks/useIdentifyUser.ts
  • apps/expo/features/purchases/index.ts
  • apps/expo/features/purchases/lib/config.ts
  • apps/expo/features/purchases/lib/init.ts
  • apps/expo/lib/i18n/locales/en.json
  • apps/expo/package.json
  • apps/expo/providers/PurchasesProvider.tsx
  • apps/expo/providers/index.tsx
  • apps/expo/types/react-native-purchases-ui.d.ts
  • apps/expo/types/react-native-purchases.d.ts
  • packages/env/src/expo-client.ts

Comment on lines +10 to +23
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 />;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the useEntitlement hook implementation
fd -type f -name "*.ts*" | xargs grep -l "useEntitlement" | head -20

Repository: 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 f

Repository: 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/null

Repository: PackRat-AI/PackRat

Length of output: 153


🏁 Script executed:

cat -n apps/expo/features/purchases/hooks/useEntitlement.ts

Repository: PackRat-AI/PackRat

Length of output: 1203


🏁 Script executed:

cat -n apps/expo/features/purchases/components/PaywallGate.tsx

Repository: 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.

Suggested change
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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and view its content
cat -n apps/expo/features/purchases/components/UpgradePrompt.tsx

Repository: 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 2

Repository: 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 cat

Repository: 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' }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n apps/expo/features/purchases/components/UpgradePrompt.tsx | head -100

Repository: 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 -20

Repository: 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 2

Repository: 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 -10

Repository: PackRat-AI/PackRat

Length of output: 269


🏁 Script executed:

cat apps/expo/tailwind.config.js

Repository: PackRat-AI/PackRat

Length of output: 2334


🏁 Script executed:

# Check how useColorScheme is implemented
cat apps/expo/lib/hooks/useColorScheme.ts | head -50

Repository: 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 -40

Repository: PackRat-AI/PackRat

Length of output: 90


🏁 Script executed:

fd "useColorScheme" apps/expo --type f

Repository: PackRat-AI/PackRat

Length of output: 144


🏁 Script executed:

# Search for useColorScheme implementation
rg "useColorScheme" apps/expo --type ts -l

Repository: 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 -30

Repository: PackRat-AI/PackRat

Length of output: 2457


🏁 Script executed:

head -60 apps/expo/lib/hooks/useColorScheme.tsx

Repository: 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 -50

Repository: 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

Comment on lines +18 to +25
enabled: isRevenueCatInitialized(),
staleTime: 1000 * 60 * 5,
retry: 1,
});

const isPro =
isRevenueCatInitialized() &&
typeof customerInfo?.entitlements.active[ENTITLEMENT_PRO] !== 'undefined';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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 tsx

Repository: PackRat-AI/PackRat

Length of output: 1325


🏁 Script executed:

cat apps/expo/features/purchases/hooks/useEntitlement.ts

Repository: 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 2

Repository: 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 2

Repository: 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.

Comment on lines +18 to +34
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' },
});
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: 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.ts

Repository: 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 -80

Repository: 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

Comment on lines +23 to +28
if (__DEV__) {
Purchases.setLogLevel(LOG_LEVEL.DEBUG);
}

Purchases.configure({ apiKey });
initialized = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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

Comment thread apps/expo/package.json
Comment on lines +151 to +152
"react-native-purchases": "^9.0.0",
"react-native-purchases-ui": "^9.0.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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 --json

Repository: 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.

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",
🤖 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.

Comment on lines +9 to +17
export function PurchasesProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
initRevenueCat();
}, []);

return (
<>
<PurchasesEffects />
{children}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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.ts

Repository: 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.ts

Repository: 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/purchases

Repository: 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/purchases

Repository: PackRat-AI/PackRat

Length of output: 901


🏁 Script executed:

cat apps/expo/features/purchases/hooks/useIdentifyUser.ts

Repository: 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.

Suggested change
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.

Comment on lines +13 to +15
entitlements: {
active: Record<string, EntitlementInfo>;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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:


🏁 Script executed:

find apps/expo -name "react-native-purchases.d.ts" -type f

Repository: PackRat-AI/PackRat

Length of output: 106


🏁 Script executed:

cat -n apps/expo/types/react-native-purchases.d.ts 2>/dev/null | head -30

Repository: PackRat-AI/PackRat

Length of output: 999


🏁 Script executed:

rg "entitlements\.active" apps/expo --type ts --type tsx -B 2 -A 2

Repository: PackRat-AI/PackRat

Length of output: 90


🏁 Script executed:

rg "entitlements\.active" apps/expo -B 2 -A 2

Repository: 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file mobile

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant