Skip to content

feat(expo): RevenueCat paywall integration & subscription management UI#2601

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

feat(expo): RevenueCat paywall integration & subscription management UI#2601
mikib0 wants to merge 18 commits into
developmentfrom
feat/revenuecat-paywall-v2

Conversation

@mikib0

@mikib0 mikib0 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Integrate RevenueCat (react-native-purchases / react-native-purchases-ui) for paywall and subscription management
  • Gate non-core features (catalog browse, scan, gap analysis, weight analysis, pack categories) behind Pro paywall via ProGate
  • Replace Customer Center with a proper in-app subscription management section in Settings
  • Add contentInsetAdjustmentBehavior="automatic" to Settings ScrollView for correct iOS inset handling
  • Add react-native-purchases and react-native-purchases-ui to root workspace dependencies

Test plan

  • Verify paywall appears for free users on gated screens (catalog browse, scan, gap analysis, weight analysis, pack categories)
  • Verify Pro users can access all gated screens without paywall
  • Confirm Upgrade to Pro / Manage Subscription / Restore Purchases actions in Settings work correctly
  • Check Settings screen scrolls correctly under the navigation bar on iOS (large title)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Pro subscription system with paywall integration across premium features
    • Added subscription management and restore purchases functionality in settings
    • Premium features now require Pro membership activation
  • Dependencies

    • Integrated RevenueCat payment processing system

mikib0 added 17 commits June 19, 2026 17:57
- Configure RevenueCat SDK with API key and Pro entitlement
- Add purchases feature module: hooks for customer info, entitlement,
  offerings, purchase, restore, and paywall presentation
- Add ProGate component that auto-presents the RevenueCat paywall when
  a non-pro user navigates to a paywalled screen
- Wrap 49 non-core screens with ProGate (weather, wildlife, AI chat,
  guides, catalog, trips, feed, messages, pack templates, and more)
- Header and search bar handled correctly behind the gate: screen's own
  Stack.Screen always mounts so the title is preserved; search bar is
  stripped from paywalled state
- Use useFocusEffect + module-level lock to prevent concurrent paywall
  sheets and avoid triggering on background tab mounts
…miss

- Remove ProGate from admin/ai-packs.tsx — admin screens are unrestricted
- Remove router.back() after paywall dismissal — it was closing modal-
  presented screens (pack-categories, weight-analysis) immediately after
  the paywall closed, causing the double-dismiss flicker
- Show ProUpgradePrompt as fallback when paywall is dismissed without
  purchasing, giving users a clear path to upgrade or navigate back
…wall

- Wrap pack/items-scan route with ProGate (consistent with pack-templates/items-scan)
- Gate "add from catalog" and "scan from photo" in AddPackItemActions with presentPaywallIfNeeded before opening the picker/modal
- Gate gap analysis flow in PackDetailScreen with presentPaywallIfNeeded before opening the activity picker
Shows Pro/Free status with a crown icon. Pro users get "Manage
Subscription" (RevenueCat Customer Center); free users get
"Upgrade to Pro" (paywall).
…r and surface RC errors

presentCustomerCenter was silently swallowing errors (no re-throw),
so failures were invisible. Now it re-throws, and the settings onPress
wraps both paths in a try/catch that shows a Burnt error toast on
failure.
…ion button

Adds an immediate toast on press to confirm the handler fires, and
falls back to the platform subscription management URL when RevenueCat
returns NOT_PRESENTED (no offerings configured).
…r-center screens

presentPaywall() and presentCustomerCenter() both silently do nothing
when called from a modal screen (known RC issue #1201). The fix is to
use RevenueCatUI.Paywall and RevenueCatUI.CustomerCenterView as actual
screen components in dedicated routes and navigate to them instead.

- Add app/(app)/paywall.tsx — renders RevenueCatUI.Paywall, invalidates
  customer info on purchase/restore, goes back on dismiss
- Add app/(app)/customer-center.tsx — renders RevenueCatUI.CustomerCenterView
  with shouldShowCloseButton=false (header back handles dismiss)
- Register both in the (app) stack as non-modal card screens
- Settings handler now does router.push('/paywall') or '/customer-center'
…imperative RC APIs

Modal presentation was blocking presentPaywall/presentCustomerCenter
(RC issue #1201). Converting settings to a normal push screen with
headerLargeTitle unblocks the imperative APIs. Removes the dedicated
paywall/customer-center route workaround.
…ement UI

- Pro users: "Manage Subscription" opens App Store/Play Store subscriptions page
- Free users: "Upgrade to Pro" presents the RC paywall
- All users: "Restore Purchases" row with loading state and toast feedback
- Set contentInsetAdjustmentBehavior="automatic" on settings ScrollView
- Add react-native-purchases and react-native-purchases-ui to root deps
- Update dev bundle identifier to use .devrc suffix
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file mobile labels Jun 20, 2026
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@mikib0, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 49 minutes and 24 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1fc49946-2f7b-440e-b0d4-871880e94bd2

📥 Commits

Reviewing files that changed from the base of the PR and between db9d282 and 429ec22.

📒 Files selected for processing (2)
  • apps/expo/features/purchases/lib/revenueCat.ts
  • packages/env/src/expo-client.ts

Walkthrough

Introduces a full RevenueCat in-app purchase integration: SDK wrapper, React Query-backed hooks (customer info, entitlement, paywall, purchase, restore, offerings, user sync), a ProGate component that presents the paywall on focus for non-Pro users, a CustomerCenter component, a Subscription section in Settings, paywall gates in pack action handlers, and ProGate wraps on ~40 Pro-only route screens.

Changes

RevenueCat Pro Gating Integration

Layer / File(s) Summary
Purchases feature types, SDK wrapper, and barrel exports
apps/expo/features/purchases/types.ts, apps/expo/features/purchases/lib/revenueCat.ts, apps/expo/features/purchases/index.ts, packages/config/src/config.ts, apps/expo/package.json, package.json
Defines PACKRAT_PRO_ENTITLEMENT, ProductId, PurchaseResult, the RevenueCat SDK wrapper (configure/identify/reset with Sentry), feature barrel exports, enableRevenueCat feature flag, and react-native-purchases/react-native-purchases-ui dependencies.
Purchase hooks
apps/expo/features/purchases/hooks/useCustomerInfo.ts, apps/expo/features/purchases/hooks/useEntitlement.ts, apps/expo/features/purchases/hooks/usePresentPaywall.ts, apps/expo/features/purchases/hooks/usePurchase.ts, apps/expo/features/purchases/hooks/useRestorePurchases.ts, apps/expo/features/purchases/hooks/useOfferings.ts, apps/expo/features/purchases/hooks/useRevenueCatUser.ts, apps/expo/features/purchases/hooks/index.ts
Creates all React Query-backed hooks: customer info fetch + live listener, isProMember entitlement derivation, presentPaywall/presentPaywallIfNeeded with cache invalidation, purchase mutation, restore mutation, offerings query, and auth-identity sync effect.
ProGate component and CustomerCenter
apps/expo/features/purchases/components/ProGate.tsx, apps/expo/features/purchases/components/CustomerCenter.tsx
ProGate presents the paywall on focus for non-Pro users via a concurrency-guarded useFocusEffect, shows a spinner while loading, renders children directly for Pro users, and invisibly mounts children for non-Pro to preserve navigation headers. CustomerCenterButton wraps RevenueCatUI.presentCustomerCenter().
SDK init and user identity wired into root layouts
apps/expo/app/_layout.tsx, apps/expo/app/(app)/_layout.tsx
configureRevenueCat() called at module load in root layout; useRevenueCatUser() called in AppLayout to sync auth user with RevenueCat identity. Navigation presentation options updated for weight-analysis/[id], pack-categories/[id] (modal→card), and settings (headerLargeTitle: true).
Settings Subscription section
apps/expo/app/(app)/settings/index.tsx
Adds useEntitlement, usePresentPaywall, useRestorePurchases to SettingsScreen. Implements handleManageSubscription (platform subscription URL via Linking) and handleRestore (toast on success/no-purchases/error). Inserts a Subscription UI section with plan status, upgrade/manage CTA, and restore button.
Paywall gates in pack action handlers
apps/expo/features/packs/components/AddPackItemActions.tsx, apps/expo/features/packs/screens/PackDetailScreen.tsx
handleAddFromPhoto and handleAddFromCatalog in AddPackItemActions and handleAnalyzeGapsPress in PackDetailScreen now await presentPaywallIfNeeded() and return early on CANCELLED/ERROR.
ProGate wrapping across all Pro-only route screens
apps/expo/app/(app)/(tabs)/catalog/index.tsx, apps/expo/app/(app)/(tabs)/feed/index.tsx, apps/expo/app/(app)/ai-chat.tsx, apps/expo/app/(app)/catalog/..., apps/expo/app/(app)/feed/..., apps/expo/app/(app)/guides/..., apps/expo/app/(app)/messages/..., apps/expo/app/(app)/pack-stats/..., apps/expo/app/(app)/pack-templates/..., apps/expo/app/(app)/pack/..., apps/expo/app/(app)/reported-ai-content.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)/templateItem/..., apps/expo/app/(app)/trail-conditions.tsx, apps/expo/app/(app)/trip/..., apps/expo/app/(app)/upcoming-trips.tsx, apps/expo/app/(app)/weather*/..., apps/expo/app/(app)/wildlife/...
~40 route files now render their screen content inside <ProGate> instead of directly, uniformly gating all Pro-only screens.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ProGate
  participant useEntitlement
  participant usePresentPaywall
  participant RevenueCatUI
  participant QueryCache

  User->>ProGate: screen focused
  ProGate->>useEntitlement: isProMember, isLoading
  alt isLoading
    ProGate-->>User: ActivityIndicator
  else isProMember
    ProGate-->>User: render children
  else not Pro
    ProGate->>usePresentPaywall: presentPaywall()
    usePresentPaywall->>RevenueCatUI: presentPaywall()
    RevenueCatUI-->>usePresentPaywall: PAYWALL_RESULT
    alt purchased
      usePresentPaywall->>QueryCache: invalidateQueries(CUSTOMER_INFO_QUERY_KEY)
      QueryCache->>useEntitlement: refresh isProMember=true
      ProGate-->>User: render children
    else CANCELLED or ERROR
      ProGate->>ProGate: router.back() if canGoBack
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PackRat-AI/PackRat#2357: Modifies the same pack-stats/[id].tsx render tree (SafeAreaView/ScrollView inset behavior) that this PR wraps with ProGate.
  • PackRat-AI/PackRat#2571: Rewrites the location/permission flow in season-suggestions.tsx, which this PR simultaneously wraps with ProGate.
  • PackRat-AI/PackRat#2587: Changes the gap-analysis/cart flow in PackDetailScreen.tsx at the same code path where this PR inserts the paywall gate.

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 1.96% 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 'feat(expo): RevenueCat paywall integration & subscription management UI' directly and clearly summarizes the main change: adding RevenueCat's paywall and subscription management capabilities to the Expo app.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/revenuecat-paywall-v2

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.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/mcp (./packages/mcp)

Status Category Percentage Covered / Total
🔵 Lines 98.87% (🎯 95%) 176 / 178
🔵 Statements 98.87% (🎯 95%) 176 / 178
🔵 Functions 100% (🎯 95%) 13 / 13
🔵 Branches 98.38% (🎯 90%) 61 / 62
File CoverageNo changed files found.
Generated in workflow #330 for commit 429ec22 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/overpass (./packages/overpass)

Status Category Percentage Covered / Total
🔵 Lines 100% (🎯 80%) 155 / 155
🔵 Statements 100% (🎯 80%) 155 / 155
🔵 Functions 100% (🎯 80%) 13 / 13
🔵 Branches 95.65% (🎯 70%) 44 / 46
File CoverageNo changed files found.
Generated in workflow #330 for commit 429ec22 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/units (./packages/units)

Status Category Percentage Covered / Total
🔵 Lines 100% (🎯 100%) 35 / 35
🔵 Statements 100% (🎯 100%) 35 / 35
🔵 Functions 100% (🎯 100%) 6 / 6
🔵 Branches 100% (🎯 100%) 11 / 11
File CoverageNo changed files found.
Generated in workflow #330 for commit 429ec22 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for apps/expo (./apps/expo)

Status Category Percentage Covered / Total
🔵 Lines 97.51% (🎯 95%) 589 / 604
🔵 Statements 97.51% (🎯 95%) 589 / 604
🔵 Functions 100% (🎯 97%) 51 / 51
🔵 Branches 95.3% (🎯 92%) 203 / 213
File CoverageNo changed files found.
Generated in workflow #330 for commit 429ec22 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/analytics (./packages/analytics)

Status Category Percentage Covered / Total
🔵 Lines 100% (🎯 80%) 744 / 744
🔵 Statements 100% (🎯 80%) 744 / 744
🔵 Functions 100% (🎯 85%) 48 / 48
🔵 Branches 87.35% (🎯 80%) 152 / 174
File CoverageNo changed files found.
Generated in workflow #330 for commit 429ec22 by the Vitest Coverage Report Action

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/api (./packages/api)

Status Category Percentage Covered / Total
🔵 Lines 98.93% (🎯 95%) 1304 / 1318
🔵 Statements 98.93% (🎯 95%) 1304 / 1318
🔵 Functions 100% (🎯 97%) 71 / 71
🔵 Branches 95.64% (🎯 92%) 483 / 505
File CoverageNo changed files found.
Generated in workflow #330 for commit 429ec22 by the Vitest Coverage Report Action

@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: 23

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/expo/features/packs/screens/PackDetailScreen.tsx (1)

189-204: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard presentPaywallIfNeeded() with try/catch in handleAnalyzeGapsPress.

Line 200 awaits a throwing async call without local handling. A RevenueCat failure will reject the press handler and skip controlled fallback/telemetry in this screen flow.

Proposed fix
+import * as Sentry from '`@sentry/react-native`';
...
   const handleAnalyzeGapsPress = async () => {
     if (!isAuthed.peek()) {
       return router.push({
         pathname: '/auth',
         params: {
           redirectTo: `/pack/${pack.id}`,
           showSignInCopy: 'true',
         },
       });
     }

-    const paywallResult = await presentPaywallIfNeeded();
-    if (paywallResult === PAYWALL_RESULT.CANCELLED || paywallResult === PAYWALL_RESULT.ERROR) {
-      return;
-    }
+    try {
+      Sentry.addBreadcrumb({ category: 'packs', message: 'Analyze gaps paywall check' });
+      const paywallResult = await presentPaywallIfNeeded();
+      if (paywallResult === PAYWALL_RESULT.CANCELLED || paywallResult === PAYWALL_RESULT.ERROR) {
+        return;
+      }
+    } catch (error) {
+      Sentry.captureException(error, {
+        tags: { feature: 'packs', action: 'analyzeGapsPaywallCheck' },
+      });
+      Burnt.toast({ title: t('common.error'), preset: 'error' });
+      return;
+    }

     // Start with activity selection
     setSelectedActivity(undefined);

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

🤖 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/packs/screens/PackDetailScreen.tsx` around lines 189 -
204, The handleAnalyzeGapsPress function is awaiting presentPaywallIfNeeded() on
line 200 without a try/catch block, which means any thrown errors will propagate
unhandled and skip controlled fallback behavior. Wrap the
presentPaywallIfNeeded() call in a try/catch block, import Sentry
instrumentation from `@sentry/react-native`, and call Sentry methods in the catch
block to capture any errors, ensuring proper error logging and telemetry as per
the coding guidelines for apps/expo.

Source: Coding guidelines

🤖 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/app/_layout.tsx`:
- Around line 24-25: The configureRevenueCat() function is being called before
Sentry initialization, which means any early failures in RevenueCat setup will
not be captured by Sentry. Reorder the initialization sequence by moving the
Sentry.init() call to execute before the configureRevenueCat() call to ensure
all errors are properly captured and monitored.

In `@apps/expo/app/`(app)/_layout.tsx:
- Line 37: The useRevenueCatUser() hook is being called unconditionally before
auth hydration from useAuthInit() completes, causing its effect to run
prematurely with a null user and trigger both resetRevenueCatUser() followed
immediately by identifyRevenueCatUser() on next render. Modify the
useRevenueCatUser() hook to defer its synchronization effects until hydration is
complete by either: (1) accepting an isLoading parameter from the calling
component and skipping the effect when isLoading is true, or (2) checking
userSyncState.isPersistLoaded internally before calling resetRevenueCatUser()
and identifyRevenueCatUser(). This ensures the hook only runs its effects after
the user store is populated.

In `@apps/expo/app/`(app)/ai-chat.tsx:
- Around line 438-562: The entire AI chat screen is wrapped in ProGate, which
still mounts children invisibly for non-Pro users, causing expensive effects and
state setup to execute unnecessarily. Move the Stack.Screen component with the
header outside of ProGate so it always renders, then wrap only the heavy subtree
(the KeyboardAvoidingView containing ScrollView, messages rendering, Composer,
and the arrow button) inside ProGate to prevent expensive state initialization
and effect execution for non-Pro users who don't have access.

In `@apps/expo/app/`(app)/feed/[id].tsx:
- Around line 40-44: The ProGate component only gates the rendering of
PostDetailScreen but does not prevent the post data fetching that occurs earlier
in the component. Move the post query logic that happens before the return
statement inside the ProGate component so that non-Pro users do not trigger
unnecessary API calls. This ensures the gate controls both data fetching and
rendering, not just the render output.

In `@apps/expo/app/`(app)/guides/index.tsx:
- Around line 4-9: The GuidesListScreen component is being mounted as a child of
ProGate regardless of the user's Pro status, causing its hooks and queries to
execute unnecessarily for non-Pro users. Move the conditional rendering logic so
that GuidesListScreen is only mounted when the user has Pro entitlement. This
means either checking the Pro status before rendering GuidesListScreen or
restructuring the component so that ProGate renders only the gate/upgrade flow
without mounting the feature screen until Pro access is confirmed. Ensure the
header setup is preserved in the flow but GuidesListScreen itself should not be
instantiated for non-Pro users.

In `@apps/expo/app/`(app)/pack-templates/items-scan.tsx:
- Around line 4-9: The issue is that ItemsScanScreen is mounted inside ProGate,
which keeps children mounted even for non-Pro users, allowing scan logic to
execute on mount before paywall presentation. Instead of always mounting
ItemsScanScreen inside ProGate, conditionally render ItemsScanScreen only when
the user has Pro access. Use ProGate or a similar access check to determine
whether to render ItemsScanScreen or show the paywall, ensuring ItemsScanScreen
is never mounted when access is locked.

In `@apps/expo/app/`(app)/pack/items-scan.tsx:
- Around line 6-8: The ProGate component in
apps/expo/features/purchases/components/ProGate.tsx currently renders blocked
children invisibly, allowing ItemsScanScreen to still mount and execute its
initial effects like handleAnalyzeImage and scanImage even for non-Pro users.
Modify ProGate to conditionally skip mounting children entirely when the user
lacks Pro access, rather than just hiding them visually. Additionally, move any
header configuration or navigation setup that ItemsScanScreen relies on to the
route level in the items-scan.tsx file so these setup tasks occur regardless of
whether the content is blocked, ensuring proper navigation and header display
while preventing the execution of Pro-only functionality for gated users.

In `@apps/expo/app/`(app)/settings/index.tsx:
- Around line 24-28: The settings file hardcodes the string 'PackRat Pro'
instead of using the shared PACKRAT_PRO_ENTITLEMENT constant, which creates a
maintenance risk if the entitlement identifier changes. Import the
PACKRAT_PRO_ENTITLEMENT constant from the purchases module and replace the
hardcoded 'PackRat Pro' string with this constant reference to keep entitlement
checks consistent across the codebase.
- Around line 57-63: The handleManageSubscription function and the
presentPaywall call at line 243 lack proper error handling for async operations.
Wrap both the Linking.openURL call in handleManageSubscription and the
presentPaywall call with try-catch blocks to handle potential promise
rejections. In each catch block, capture the error using Sentry with appropriate
breadcrumbs and error context following the coding guidelines for apps/expo,
ensuring both operations gracefully handle failures with proper exception
logging rather than silently failing.
- Around line 207-215: Replace the hardcoded hex colors in the subscription
section with NativeWind theme tokens to ensure dark mode compatibility and
consistency with the design system. In the View component styled with inline
backgroundColor and the Icon component with hardcoded color props, convert the
hex color values (`#f59e0b`, `#f59e0b20`, `#6b728020`) to use NativeWind utility
classes or theme-based color variables instead of inline style objects. Apply
this refactor to both occurrences mentioned (lines 207-215 and 235-246 in the
settings index file) by removing the style prop and replacing hardcoded color
values with appropriate theme-aware styling approach.

In `@apps/expo/app/`(app)/trail-conditions.tsx:
- Around line 165-222: The useTrailConditionReports hook is being called at line
44 before the ProGate check, which means non-Pro users still execute the paid
data fetch even though they shouldn't have access. Add an entitlement-aware
enabled parameter to the useTrailConditionReports hook call that conditionally
disables the query based on whether the user has Pro access. This way the hook
will skip the data fetch entirely for non-Pro users rather than allowing the
query to execute regardless of the ProGate wrapper.

In `@apps/expo/features/packs/components/AddPackItemActions.tsx`:
- Around line 52-56: The presentPaywallIfNeeded() call is not guarded by
exception handling, and if the RevenueCat call throws, the press handler will
reject without proper error handling or telemetry. Wrap the
presentPaywallIfNeeded() call in a try-catch block at both locations mentioned
(around line 52-56 and 115-120). Import Sentry instrumentation from
`@sentry/react-native` at the top of the file, and in each catch block, call the
appropriate Sentry method to capture the exception before gracefully handling
the error (e.g., returning early or showing a user-friendly error message) so
that the UI flow remains controlled with proper telemetry.

In `@apps/expo/features/purchases/components/CustomerCenter.tsx`:
- Around line 22-25: The async function presentCustomerCenter is passed directly
to the onPress prop in the CustomerCenterButton component, leaving any rejected
promise unhandled and triggering warnings in React Native. Wrap the
presentCustomerCenter call in an arrow function within the onPress handler to
properly handle promise rejections. You can either use an async arrow function
with try-catch or chain a .catch() handler to the presentCustomerCenter() call
to ensure all rejection paths are handled.

In `@apps/expo/features/purchases/components/ProGate.tsx`:
- Around line 56-58: In the ProGate.tsx component, replace the inline style prop
containing flex layout and opacity values on both View elements with NativeWind
className utilities. Specifically, move the flex property and opacity property
from the style object to their corresponding Tailwind class equivalents in
className, and also convert the pointerEvents inline prop to its NativeWind
class utility. This ensures consistency with the app's styling contract and
removes inline StyleSheet objects from the layout/color styling.
- Around line 26-37: The presentPaywall() promise chain in the ProGate component
is missing a .catch() handler, which can result in unhandled promise rejections.
Add a .catch() handler to the promise chain between the existing .then() call
and the .finally() call to properly handle any errors that may be thrown by
presentPaywall(). The .catch() handler should accept the error and handle it
appropriately (such as logging it or performing necessary cleanup).

In `@apps/expo/features/purchases/hooks/useCustomerInfo.ts`:
- Line 18: The cleanup function returned by the useEffect in useCustomerInfo.ts
is currently returning the boolean result from
Purchases.removeCustomerInfoUpdateListener(handler), but React requires cleanup
functions to return void. Modify the cleanup function to call
Purchases.removeCustomerInfoUpdateListener(handler) without returning its value,
either by simply calling the function on its own line or by wrapping it in
parentheses without a return statement.

In `@apps/expo/features/purchases/hooks/useEntitlement.ts`:
- Around line 7-9: The useEntitlement hook currently returns isProMember = false
when customer info fails to load or is still loading, which causes ProGate to
show the paywall for users with transient network errors. Instead of collapsing
all non-success states into "not Pro", return an additional flag (such as
isEntitlementResolved) that explicitly tracks whether the entitlement status has
been successfully resolved. Set this flag to false when isLoading is true or
error exists, and only compute isProMember based on the actual entitlements when
the fetch succeeds. Update the return statement to include this new flag so that
downstream components like ProGate can avoid showing the paywall until the
entitlement status is actually resolved.

In `@apps/expo/features/purchases/hooks/usePurchase.ts`:
- Line 3: The type import at the top of the usePurchase hook file is incorrect.
Replace the import statement that brings in `Package` from
'react-native-purchases' with an import of `PurchasesPackage` instead, as
`Package` is not exported by the react-native-purchases SDK. Additionally,
update any usage of the `Package` type throughout the file (such as type
annotations for parameters in the usePurchase hook or related functions) to use
`PurchasesPackage` instead.

In `@apps/expo/features/purchases/hooks/useRevenueCatUser.ts`:
- Around line 13-19: The useEffect hook in useRevenueCatUser is making
fire-and-forget calls to identifyRevenueCatUser and resetRevenueCatUser without
awaiting them, which can cause race conditions when auth state changes rapidly.
Implement a Promise queue mechanism to serialize these calls so they execute
sequentially in order. Create a queue variable outside the useEffect that stores
pending operations, modify the useEffect to add identifyRevenueCatUser and
resetRevenueCatUser calls to this queue instead of calling them directly, and
ensure each operation waits for the previous one to complete before starting.
This prevents RevenueCat from binding to the wrong user when login/logout events
occur in quick succession.

In `@apps/expo/features/purchases/lib/revenueCat.ts`:
- Line 5: The REVENUECAT_API_KEY constant is hardcoded with a test key value
which causes production builds to be routed to the wrong RevenueCat project.
Replace the hardcoded string value with an environment variable reference (such
as process.env.REVENUECAT_API_KEY or the appropriate environment variable
accessor for your runtime). This allows different environments to use their
respective API keys without code changes, ensuring proper entitlement resolution
across development, staging, and production builds.
- Around line 34-37: The Sentry.captureException call in revenueCat.ts is
sending raw userId as user-identifying data in the extra object, which creates
privacy and compliance risks. Replace the raw userId value in the extra property
with either a hashed/redacted version of the userId or remove the userId from
the extra object entirely. Ensure the replacement approach maintains enough
debugging information while protecting user privacy in telemetry payloads.

In `@apps/expo/package.json`:
- Around line 152-153: Replace the wildcard version specifiers for the
react-native-purchases and react-native-purchases-ui dependencies in
apps/expo/package.json with explicit version constraints that match the root
package.json. Change both dependencies from using "*" to "^10.4.0" to ensure
deterministic installs and prevent unexpected major version upgrades.

In `@packages/config/src/config.ts`:
- Line 77: The EnableRevenueCat feature flag in the FeatureFlag configuration is
currently set to true by default, but according to coding guidelines new feature
flags must default to false for safe rollout and opt-in enablement. Change the
value of FeatureFlag.EnableRevenueCat from true to false in the configuration
object to make it opt-in rather than enabled by default.

---

Outside diff comments:
In `@apps/expo/features/packs/screens/PackDetailScreen.tsx`:
- Around line 189-204: The handleAnalyzeGapsPress function is awaiting
presentPaywallIfNeeded() on line 200 without a try/catch block, which means any
thrown errors will propagate unhandled and skip controlled fallback behavior.
Wrap the presentPaywallIfNeeded() call in a try/catch block, import Sentry
instrumentation from `@sentry/react-native`, and call Sentry methods in the catch
block to capture any errors, ensuring proper error logging and telemetry as per
the coding guidelines for apps/expo.
🪄 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: 1ef43a50-64aa-43f4-8afc-1f2d099f8a32

📥 Commits

Reviewing files that changed from the base of the PR and between ee25c31 and db9d282.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock, !bun.lock
📒 Files selected for processing (67)
  • apps/expo/app/(app)/(tabs)/catalog/index.tsx
  • apps/expo/app/(app)/(tabs)/feed/index.tsx
  • apps/expo/app/(app)/(tabs)/trips/index.tsx
  • apps/expo/app/(app)/_layout.tsx
  • apps/expo/app/(app)/ai-chat.tsx
  • apps/expo/app/(app)/catalog/[id].tsx
  • apps/expo/app/(app)/catalog/add-to-pack/details.tsx
  • apps/expo/app/(app)/catalog/add-to-pack/index.tsx
  • apps/expo/app/(app)/feed/[id].tsx
  • apps/expo/app/(app)/feed/create.tsx
  • apps/expo/app/(app)/guides/[id].tsx
  • apps/expo/app/(app)/guides/index.tsx
  • apps/expo/app/(app)/messages/chat.android.tsx
  • apps/expo/app/(app)/messages/chat.tsx
  • apps/expo/app/(app)/messages/conversations.android.tsx
  • apps/expo/app/(app)/messages/conversations.tsx
  • apps/expo/app/(app)/pack-stats/[id].tsx
  • apps/expo/app/(app)/pack-templates/[id]/edit.tsx
  • apps/expo/app/(app)/pack-templates/[id]/index.tsx
  • apps/expo/app/(app)/pack-templates/index.tsx
  • apps/expo/app/(app)/pack-templates/items-scan.tsx
  • apps/expo/app/(app)/pack-templates/new.tsx
  • apps/expo/app/(app)/pack/items-scan.tsx
  • apps/expo/app/(app)/reported-ai-content.tsx
  • apps/expo/app/(app)/season-suggestions-results.tsx
  • apps/expo/app/(app)/season-suggestions.tsx
  • apps/expo/app/(app)/settings/index.tsx
  • apps/expo/app/(app)/shared-packs.tsx
  • apps/expo/app/(app)/shopping-list.tsx
  • apps/expo/app/(app)/templateItem/[id]/edit.tsx
  • apps/expo/app/(app)/templateItem/[id]/index.tsx
  • apps/expo/app/(app)/templateItem/new.tsx
  • apps/expo/app/(app)/trail-conditions.tsx
  • apps/expo/app/(app)/trip/[id]/edit.tsx
  • apps/expo/app/(app)/trip/[id]/index.tsx
  • apps/expo/app/(app)/trip/location-search.tsx
  • apps/expo/app/(app)/trip/new.tsx
  • apps/expo/app/(app)/upcoming-trips.tsx
  • apps/expo/app/(app)/weather-alert-preferences.tsx
  • apps/expo/app/(app)/weather-alerts.tsx
  • apps/expo/app/(app)/weather/[id].tsx
  • apps/expo/app/(app)/weather/geo.tsx
  • apps/expo/app/(app)/weather/index.tsx
  • apps/expo/app/(app)/weather/preview.tsx
  • apps/expo/app/(app)/weather/search.tsx
  • apps/expo/app/(app)/wildlife/[id].tsx
  • apps/expo/app/(app)/wildlife/identify.tsx
  • apps/expo/app/(app)/wildlife/index.tsx
  • apps/expo/app/_layout.tsx
  • apps/expo/features/packs/components/AddPackItemActions.tsx
  • apps/expo/features/packs/screens/PackDetailScreen.tsx
  • apps/expo/features/purchases/components/CustomerCenter.tsx
  • apps/expo/features/purchases/components/ProGate.tsx
  • apps/expo/features/purchases/hooks/index.ts
  • apps/expo/features/purchases/hooks/useCustomerInfo.ts
  • apps/expo/features/purchases/hooks/useEntitlement.ts
  • apps/expo/features/purchases/hooks/useOfferings.ts
  • apps/expo/features/purchases/hooks/usePresentPaywall.ts
  • apps/expo/features/purchases/hooks/usePurchase.ts
  • apps/expo/features/purchases/hooks/useRestorePurchases.ts
  • apps/expo/features/purchases/hooks/useRevenueCatUser.ts
  • apps/expo/features/purchases/index.ts
  • apps/expo/features/purchases/lib/revenueCat.ts
  • apps/expo/features/purchases/types.ts
  • apps/expo/package.json
  • package.json
  • packages/config/src/config.ts
💤 Files with no reviewable changes (1)
  • apps/expo/app/(app)/(tabs)/trips/index.tsx

Comment thread apps/expo/app/_layout.tsx
Comment on lines +24 to +25
configureRevenueCat();

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 | 🟡 Minor | ⚡ Quick win

Initialize Sentry before calling configureRevenueCat().

Line 24 runs RevenueCat setup before Sentry.init, so early configure failures may not be captured.

Suggested ordering change
-configureRevenueCat();
-
 Sentry.init({
   dsn: clientEnvs.EXPO_PUBLIC_SENTRY_DSN,
   enabled: clientEnvs.NODE_ENV !== 'development' && !!clientEnvs.EXPO_PUBLIC_SENTRY_DSN,
@@
 });
+
+configureRevenueCat();
🤖 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/app/_layout.tsx` around lines 24 - 25, The configureRevenueCat()
function is being called before Sentry initialization, which means any early
failures in RevenueCat setup will not be captured by Sentry. Reorder the
initialization sequence by moving the Sentry.init() call to execute before the
configureRevenueCat() call to ensure all errors are properly captured and
monitored.

export default function AppLayout() {
const isLoading = useAuthInit();
const isAuthedValue = use$(isAuthed);
useRevenueCatUser();

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

echo "Inspect useRevenueCatUser implementation:"
fd -i 'useRevenueCatUser.ts' apps/expo | xargs -I{} sed -n '1,220p' {}

echo
echo "Inspect useAuthInit implementation:"
fd -i 'useAuthInit.ts' apps/expo | xargs -I{} sed -n '1,240p' {}

echo
echo "Inspect auth store defaults/hydration:"
fd -i 'store.ts' apps/expo/features/auth | xargs -I{} sed -n '1,260p' {}

Repository: PackRat-AI/PackRat

Length of output: 7959


🏁 Script executed:

cat -n apps/expo/app/\(app\)/_layout.tsx | head -60

Repository: PackRat-AI/PackRat

Length of output: 3618


Add a guard to prevent RevenueCat sync during auth hydration.

Line 37 calls useRevenueCatUser() unconditionally. Since useAuthInit() awaits cache hydration asynchronously, useRevenueCatUser's effect may run before userStore is populated, causing it to call resetRevenueCatUser() with a null user, then identifyRevenueCatUser() moments later when hydration completes—creating churn during cold start.

Either:

  1. Wrap the call with a conditional that defers to after isLoading is false, or
  2. Modify useRevenueCatUser() to internally skip its effect until hydration is complete.

Since hooks cannot be conditionally invoked, option 2 requires passing isLoading state to the hook or checking userSyncState.isPersistLoaded internally.

🤖 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/app/`(app)/_layout.tsx at line 37, The useRevenueCatUser() hook is
being called unconditionally before auth hydration from useAuthInit() completes,
causing its effect to run prematurely with a null user and trigger both
resetRevenueCatUser() followed immediately by identifyRevenueCatUser() on next
render. Modify the useRevenueCatUser() hook to defer its synchronization effects
until hydration is complete by either: (1) accepting an isLoading parameter from
the calling component and skipping the effect when isLoading is true, or (2)
checking userSyncState.isPersistLoaded internally before calling
resetRevenueCatUser() and identifyRevenueCatUser(). This ensures the hook only
runs its effects after the user store is populated.

Comment on lines +438 to +562
<ProGate>
<>
<Stack.Screen
options={{
header: () => <AiChatHeader onClear={handleClear} />,
}}
/>
<KeyboardAvoidingView
style={[
ROOT_STYLE,
{
backgroundColor: isDarkColorScheme ? colors.background : colors.card,
},
]}
behavior="padding"
>
<View>
<View
style={{
height: Platform.OS === 'ios' ? insets.top + 52 : HEADER_HEIGHT + insets.top,
}}
/>
<LocationContext location={location} onSetLocation={setLocation} />
<DateSeparator
date={new Date().toLocaleDateString('en-US', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
})}
/>
</View>

{messages.map((item, index) => {
let userQuery: TextUIPart['text'] | undefined;
if (item.role === 'assistant' && index > 1) {
const userMessage = messages[index - 1];
userQuery = userMessage?.parts.find((p) => p.type === 'text')?.text;
}
<ScrollView
ref={scrollViewRef}
onLayout={onLayout}
onScroll={onScroll}
onContentSizeChange={onContentSizeChange}
scrollIndicatorInsets={{
bottom: HEADER_HEIGHT + 10,
top: insets.bottom + 2,
}}
>
<View>
<View
style={{
height: Platform.OS === 'ios' ? insets.top + 52 : HEADER_HEIGHT + insets.top,
}}
/>
<LocationContext location={location} onSetLocation={setLocation} />
<DateSeparator
date={new Date().toLocaleDateString('en-US', {
weekday: 'short',
day: '2-digit',
month: 'short',
year: 'numeric',
})}
/>
</View>

return (
<ChatBubble
key={item.id}
item={item}
userQuery={userQuery}
isLast={index === messages.length - 1}
status={status}
testID={
item.role === 'assistant' ? testIds.aiChat.assistantMessage(item.id) : undefined
}
{messages.map((item, index) => {
let userQuery: TextUIPart['text'] | undefined;
if (item.role === 'assistant' && index > 1) {
const userMessage = messages[index - 1];
userQuery = userMessage?.parts.find((p) => p.type === 'text')?.text;
}

return (
<ChatBubble
key={item.id}
item={item}
userQuery={userQuery}
isLast={index === messages.length - 1}
status={status}
testID={
item.role === 'assistant' ? testIds.aiChat.assistantMessage(item.id) : undefined
}
/>
);
})}

{status === 'submitted' && (
<ActivityIndicator
size="small"
color={colors.primary}
className="self-start ml-4 mb-8"
/>
);
})}

{status === 'submitted' && (
<ActivityIndicator
size="small"
color={colors.primary}
className="self-start ml-4 mb-8"
/>
)}
{status === 'error' && (
<ErrorState error={error} onRetry={() => handleRetry()} onClear={handleClear} />
)}
{messages.length < 2 && (
<View className="pl-4 pr-16">
<Text className="mb-2 text-xs text-muted-foreground mt-0">{t('ai.suggestions')}</Text>
<View className="flex-row flex-wrap gap-2">
{getContextualSuggestions({ context, isAuthenticated }).map((suggestion) => (
<TouchableOpacity
key={suggestion}
onPress={() => handleSubmit(suggestion)}
className="mb-2 rounded-3xl border border-border bg-card px-3 py-2"
>
<Text className="text-sm text-foreground">{suggestion}</Text>
</TouchableOpacity>
))}
)}
{status === 'error' && (
<ErrorState error={error} onRetry={() => handleRetry()} onClear={handleClear} />
)}
{messages.length < 2 && (
<View className="pl-4 pr-16">
<Text className="mb-2 text-xs text-muted-foreground mt-0">
{t('ai.suggestions')}
</Text>
<View className="flex-row flex-wrap gap-2">
{getContextualSuggestions({ context, isAuthenticated }).map((suggestion) => (
<TouchableOpacity
key={suggestion}
onPress={() => handleSubmit(suggestion)}
className="mb-2 rounded-3xl border border-border bg-card px-3 py-2"
>
<Text className="text-sm text-foreground">{suggestion}</Text>
</TouchableOpacity>
))}
</View>
</View>
</View>
)}
<Animated.View style={[toolbarHeightStyle, { marginBottom: 20 }]} />
</ScrollView>
</KeyboardAvoidingView>

<KeyboardStickyView offset={{ opened: insets.bottom }}>
<Composer
textInputHeight={textInputHeight}
input={input}
handleInputChange={setInput}
handleSubmit={() => {
handleSubmit();
}}
stop={stop}
isLoading={isLoading}
placeholder={
context.contextType === 'general'
? t('ai.askAnythingOutdoors')
: context.contextType === 'item'
? t('ai.askAboutItem')
: t('ai.askAboutPack')
}
/>
</KeyboardStickyView>
{isArrowButtonVisible && status === 'ready' && (
<TouchableOpacity
onPress={scrollToBottom}
className="absolute bottom-20 right-4 rounded-full bg-gray-200 p-3 mb-5 shadow-lg"
>
<Icon name="arrow-down" size={20} color="black" />
</TouchableOpacity>
)}
</>
)}
<Animated.View style={[toolbarHeightStyle, { marginBottom: 20 }]} />
</ScrollView>
</KeyboardAvoidingView>

<KeyboardStickyView offset={{ opened: insets.bottom }}>
<Composer
textInputHeight={textInputHeight}
input={input}
handleInputChange={setInput}
handleSubmit={() => {
handleSubmit();
}}
stop={stop}
isLoading={isLoading}
placeholder={
context.contextType === 'general'
? t('ai.askAnythingOutdoors')
: context.contextType === 'item'
? t('ai.askAboutItem')
: t('ai.askAboutPack')
}
/>
</KeyboardStickyView>
{isArrowButtonVisible && status === 'ready' && (
<TouchableOpacity
onPress={scrollToBottom}
className="absolute bottom-20 right-4 rounded-full bg-gray-200 p-3 mb-5 shadow-lg"
>
<Icon name="arrow-down" size={20} color="black" />
</TouchableOpacity>
)}
</>
</ProGate>

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

Avoid mounting the full AI chat subtree behind the non-Pro invisible render path.

At Line 438, the full screen is wrapped in ProGate; for non-Pro users, ProGate still mounts children invisibly. That still executes the chat screen’s expensive effects/state setup before access is granted. Keep only header registration mounted for locked users and gate the heavy subtree by entitlement.

Suggested direction
+ // outside this block (near other hooks)
+ // const { isProMember } = useEntitlement();

   return (
     <ProGate>
       <>
         <Stack.Screen
           options={{
             header: () => <AiChatHeader onClear={handleClear} />,
           }}
         />
-        <KeyboardAvoidingView ...>
-          ...
-        </KeyboardAvoidingView>
+        {isProMember ? (
+          <KeyboardAvoidingView ...>
+            ...
+          </KeyboardAvoidingView>
+        ) : null}
       </>
     </ProGate>
   );
🤖 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/app/`(app)/ai-chat.tsx around lines 438 - 562, The entire AI chat
screen is wrapped in ProGate, which still mounts children invisibly for non-Pro
users, causing expensive effects and state setup to execute unnecessarily. Move
the Stack.Screen component with the header outside of ProGate so it always
renders, then wrap only the heavy subtree (the KeyboardAvoidingView containing
ScrollView, messages rendering, Composer, and the arrow button) inside ProGate
to prevent expensive state initialization and effect execution for non-Pro users
who don't have access.

Comment on lines +40 to +44
return (
<ProGate>
<PostDetailScreen post={post} currentUserId={currentUserId} />
</ProGate>
);

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

Gate data fetching, not only rendering.

Wrapping only the return path at Lines 40-44 does not prevent the post query (Line 14) from running for non-Pro users. This undermines route gating and does unnecessary API work.

Minimal fix sketch
+ import { ProGate, useEntitlement } from 'expo-app/features/purchases';

 export default function PostDetailRoute() {
   const { id } = useLocalSearchParams<{ id: string }>();
+  const { isProMember } = useEntitlement();
   const currentUserId = userStore.id.peek() as string | undefined;

   const { data: post, isLoading } = useQuery({
     queryKey: ['feed', Number(id)],
     queryFn: async () => {
       const { data, error } = await apiClient.feed({ postId: id }).get();
       if (error) throw new Error(`Failed to fetch post: ${error.value}`);
       return data;
     },
-    enabled: !!id,
+    enabled: !!id && isProMember,
   });

   return (
     <ProGate>
-      <PostDetailScreen post={post} currentUserId={currentUserId} />
+      {isProMember && post ? <PostDetailScreen post={post} currentUserId={currentUserId} /> : null}
     </ProGate>
   );
 }
🤖 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/app/`(app)/feed/[id].tsx around lines 40 - 44, The ProGate
component only gates the rendering of PostDetailScreen but does not prevent the
post data fetching that occurs earlier in the component. Move the post query
logic that happens before the return statement inside the ProGate component so
that non-Pro users do not trigger unnecessary API calls. This ensures the gate
controls both data fetching and rendering, not just the render output.

Comment on lines 4 to +9
export default function GuidesRoute() {
return <GuidesListScreen />;
return (
<ProGate>
<GuidesListScreen />
</ProGate>
);

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 | 🏗️ Heavy lift

Avoid mounting premium screen trees for non‑Pro users.

At Line 6, GuidesListScreen is wrapped by ProGate, but ProGate’s non‑Pro path still mounts children invisibly. That means guides hooks/queries still run for free users, which can fetch/cache gated content and add avoidable API load. Gate flow should preserve header setup without mounting the feature screen until entitlement is Pro (or purchase completes).

🤖 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/app/`(app)/guides/index.tsx around lines 4 - 9, The
GuidesListScreen component is being mounted as a child of ProGate regardless of
the user's Pro status, causing its hooks and queries to execute unnecessarily
for non-Pro users. Move the conditional rendering logic so that GuidesListScreen
is only mounted when the user has Pro entitlement. This means either checking
the Pro status before rendering GuidesListScreen or restructuring the component
so that ProGate renders only the gate/upgrade flow without mounting the feature
screen until Pro access is confirmed. Ensure the header setup is preserved in
the flow but GuidesListScreen itself should not be instantiated for non-Pro
users.

Comment on lines +13 to +19
useEffect(() => {
if (user?.id) {
identifyRevenueCatUser(user.id);
} else {
resetRevenueCatUser();
}
}, [user?.id]);

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:

# First, verify the file exists and check its current state
fd -t f "useRevenueCatUser.ts" apps/expo/

Repository: PackRat-AI/PackRat

Length of output: 118


🏁 Script executed:

# Read the file to see the actual implementation
cat apps/expo/features/purchases/hooks/useRevenueCatUser.ts

Repository: PackRat-AI/PackRat

Length of output: 649


🏁 Script executed:

# Find and examine the revenueCat lib functions
fd -t f "revenueCat" apps/expo/features/purchases/

Repository: PackRat-AI/PackRat

Length of output: 109


🏁 Script executed:

# Search for identifyRevenueCatUser and resetRevenueCatUser definitions
rg "identifyRevenueCatUser|resetRevenueCatUser" apps/expo/features/purchases/

Repository: PackRat-AI/PackRat

Length of output: 721


🏁 Script executed:

# Read the revenueCat.ts implementation to understand what these async functions do
cat apps/expo/features/purchases/lib/revenueCat.ts

Repository: PackRat-AI/PackRat

Length of output: 1451


Serialize RevenueCat identity sync to prevent login/logout races.

The async identifyRevenueCatUser and resetRevenueCatUser calls are fire-and-forget, so if auth state changes rapidly, operations can complete out of order and bind RevenueCat to the wrong user. Add serialization using a Promise queue:

Proposed fix
 import { use$ } from '`@legendapp/state/react`';
 import { userStore } from 'expo-app/features/auth/store';
-import { useEffect } from 'react';
+import { useEffect, useRef } from 'react';
 import { identifyRevenueCatUser, resetRevenueCatUser } from '../lib/revenueCat';
 
 export function useRevenueCatUser() {
   const user = use$(userStore);
+  const syncQueueRef = useRef(Promise.resolve());
 
   useEffect(() => {
-    if (user?.id) {
-      identifyRevenueCatUser(user.id);
-    } else {
-      resetRevenueCatUser();
-    }
+    syncQueueRef.current = syncQueueRef.current
+      .catch(() => undefined)
+      .then(() => (user?.id ? identifyRevenueCatUser(user.id) : resetRevenueCatUser()));
   }, [user?.id]);
 }
🤖 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/useRevenueCatUser.ts` around lines 13 -
19, The useEffect hook in useRevenueCatUser is making fire-and-forget calls to
identifyRevenueCatUser and resetRevenueCatUser without awaiting them, which can
cause race conditions when auth state changes rapidly. Implement a Promise queue
mechanism to serialize these calls so they execute sequentially in order. Create
a queue variable outside the useEffect that stores pending operations, modify
the useEffect to add identifyRevenueCatUser and resetRevenueCatUser calls to
this queue instead of calling them directly, and ensure each operation waits for
the previous one to complete before starting. This prevents RevenueCat from
binding to the wrong user when login/logout events occur in quick succession.

Comment thread apps/expo/features/purchases/lib/revenueCat.ts Outdated
Comment on lines +34 to +37
Sentry.captureException(error, {
tags: { feature: 'purchases', action: 'logIn' },
extra: { userId },
});

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

Do not send raw userId to Sentry extras.

Line 36 includes userId in telemetry payloads. That is user-identifying data and increases privacy/compliance exposure. Prefer a hashed/redacted value (or omit it).

🤖 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/revenueCat.ts` around lines 34 - 37, The
Sentry.captureException call in revenueCat.ts is sending raw userId as
user-identifying data in the extra object, which creates privacy and compliance
risks. Replace the raw userId value in the extra property with either a
hashed/redacted version of the userId or remove the userId from the extra object
entirely. Ensure the replacement approach maintains enough debugging information
while protecting user privacy in telemetry payloads.

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

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

echo "apps/expo/package.json"
jq -r '.dependencies["react-native-purchases"], .dependencies["react-native-purchases-ui"]' apps/expo/package.json

echo "root package.json"
jq -r '.dependencies["react-native-purchases"], .dependencies["react-native-purchases-ui"]' package.json

Repository: PackRat-AI/PackRat

Length of output: 123


Specify explicit versions for RevenueCat dependencies in apps/expo/package.json.

Lines 152-153 use wildcard versions ("*"), which causes non-deterministic installs and risks unexpected major version upgrades. Align with the root package.json, which pins both dependencies to ^10.4.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 152 - 153, Replace the wildcard version
specifiers for the react-native-purchases and react-native-purchases-ui
dependencies in apps/expo/package.json with explicit version constraints that
match the root package.json. Change both dependencies from using "*" to
"^10.4.0" to ensure deterministic installs and prevent unexpected major version
upgrades.

[FeatureFlag.EnableWildlifeIdentification]: false,
[FeatureFlag.EnableLocalAI]: true,
[FeatureFlag.EnableTrails]: false,
[FeatureFlag.EnableRevenueCat]: 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

Default new feature flags to false for safe rollout.

Line 77 enables RevenueCat by default. This should be opt-in first to prevent unintended broad rollout.

As per coding guidelines, "New feature flags must be added to apps/expo/config.ts and default to false."

🤖 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 `@packages/config/src/config.ts` at line 77, The EnableRevenueCat feature flag
in the FeatureFlag configuration is currently set to true by default, but
according to coding guidelines new feature flags must default to false for safe
rollout and opt-in enablement. Change the value of FeatureFlag.EnableRevenueCat
from true to false in the configuration object to make it opt-in rather than
enabled by default.

Source: Coding guidelines

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