Skip to content

Conversation

@tsyirvo
Copy link
Owner

@tsyirvo tsyirvo commented Jan 7, 2026

Summary by CodeRabbit

  • New Features

    • Added dedicated login screen with enhanced authentication flow
    • Implemented subscription status management with real-time customer updates
    • Added remote configuration support to customize subscription offerings display
  • Removals

    • Removed legacy revenue tracking method from analytics

✏️ Tip: You can customize this high-level summary in your review settings.

@tsyirvo tsyirvo self-assigned this Jan 7, 2026
@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

Walkthrough

This PR introduces a comprehensive subscription management system. A new SubscriptionContext and SubscriptionContextProvider are created to manage subscription state, including paying user status and offering to display. The app layout is updated to wrap the navigation stack with the new context provider. The Purchase service is modified to accept User objects in setUser and a new isPayingUser method is added to determine paying status via entitlements. Remote configuration support is added to specify which offerings to display via a new 'offering-to-display' remote config entry. Analytics revenue tracking methods and related types are removed. A utility function hasActiveEntitlements is introduced to check for active customer entitlements.

Sequence Diagram(s)

sequenceDiagram
    participant App as App Layout
    participant SCP as SubscriptionContextProvider
    participant Purchase as Purchase Service
    participant RC as Remote Config
    participant RevenueCat as RevenueCat SDK
    
    App->>SCP: Mount with children
    activate SCP
    
    Note over SCP: useRunOnMount Effect
    par Concurrent Initialization
        SCP->>Purchase: isPayingUser()
        Purchase->>RevenueCat: getCustomerInfo()
        RevenueCat-->>Purchase: CustomerInfo
        Purchase->>Purchase: hasActiveEntitlements(customerInfo)
        Purchase-->>SCP: boolean
        
        SCP->>RC: Get "offering-to-display" flag
        activate RC
        RC-->>SCP: { type: 'offering', offering: string }
        deactivate RC
        
        SCP->>Purchase: getOfferings()
        Purchase->>RevenueCat: getOfferings()
        RevenueCat-->>Purchase: Offerings
        Purchase-->>SCP: Offerings
        
        SCP->>SCP: Resolve offeringToDisplay<br/>(remote config or current)
    and
        SCP->>Purchase: customerListener(callback)
        Purchase->>RevenueCat: addCustomerInfoUpdateListener()
        RevenueCat-->>Purchase: cleanup function
    end
    
    SCP->>SCP: setIsPayingUser(boolean)<br/>setOfferingToDisplay(offering)
    SCP->>SCP: Memoize context value
    SCP->>App: Provider with context value
    
    Note over RevenueCat: Real-time Update
    RevenueCat->>Purchase: customerInfoUpdateListener(info)
    Purchase->>SCP: callback(customerInfo)
    SCP->>SCP: Update isPayingUser state
    
    deactivate SCP
Loading
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: rework how subscriptions are handled' directly corresponds to the main objective of the PR, which introduces a SubscriptionContextProvider and reorganizes how subscription state is managed throughout the codebase.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx:
- Around line 44-50: Replace the inline entitlement check inside the
useRunOnMount > Purchase.customerListener callback by calling the existing
hasActiveEntitlements utility and passing customerInfo.entitlements to it;
update the setIsPayingUser call to use the boolean returned by
hasActiveEntitlements. If hasActiveEntitlements is not yet imported into
SubscriptionContextProvider, add an import for it at the top. Keep the
surrounding useRunOnMount, Purchase.customerListener, and setIsPayingUser usage
unchanged—only swap the Object.entries(...) check for
hasActiveEntitlements(customerInfo.entitlements).

In @src/infra/purchase/purchase.ts:
- Around line 22-26: init() currently calls the async method
setLogLevel(PURCHASES_LOG_LEVEL.ERROR) with void which can cause race
conditions; change init() to await this.setLogLevel(PURCHASES_LOG_LEVEL.ERROR)
(mark init as async if not already) so the log level is set before other
Purchase methods run, or explicitly document/return the Promise from init() so
callers know to await initialization.

In @src/shared/hooks/useRunOnMount.ts:
- Around line 7-20: The effect currently depends on callback so its cleanup runs
whenever callback identity changes even though the setup only ran once; to fix,
stop re-running the effect: create a ref for the incoming callback (e.g.
callbackRef.current = callback in a separate small effect) and change the main
effect in useRunOnMount to run only once (useEffect(..., [])), call
callbackRef.current() on mount, store any cleanup in cleanupRef (as you already
do) and return a cleanup that invokes cleanupRef.current(); keep hasRunRef logic
optional or remove it since the effect now executes only on mount.
🧹 Nitpick comments (4)
src/domain/contexts/subscriptionContext/useSubscriptionContext.ts (1)

5-9: Consider adding a guard for usage outside provider.

If this hook is called outside a SubscriptionContextProvider, it will silently return the default context value (likely undefined), which can cause confusing runtime errors downstream.

Optional: Add context validation
 export const useSubscriptionContext = () => {
   const value = useContext(SubscriptionContext);

+  if (value === undefined) {
+    throw new Error(
+      'useSubscriptionContext must be used within a SubscriptionContextProvider',
+    );
+  }
+
   return value;
 };
src/domain/subscription/utils/hasActiveEntitlements.ts (1)

3-5: LGTM - Simple and correct utility function.

The logic correctly checks for active entitlements. Minor suggestion: Object.keys would be slightly more efficient since you only need the count, not the values.

💡 Optional: Use Object.keys for slightly better performance
 export const hasActiveEntitlements = (customerInfo: CustomerInfo): boolean => {
-  return Object.entries(customerInfo.entitlements.active).length > 0;
+  return Object.keys(customerInfo.entitlements.active).length > 0;
 };
src/domain/contexts/subscriptionContext/SubscriptionContext.ts (1)

4-12: LGTM - Well-structured context definition.

The context shape with nullable states correctly represents loading/uninitialized states. Consider extracting the context value type to a named interface for reusability.

💡 Optional: Extract context value type for reusability
 import { createContext } from 'react';
 import { PurchasesOffering } from 'react-native-purchases';

+export interface SubscriptionContextValue {
+  isPayingUser: boolean | null;
+  offeringToDisplay: PurchasesOffering | null;
+}
+
-const SubscriptionContext = createContext<{
-  isPayingUser: boolean | null;
-  offeringToDisplay: PurchasesOffering | null;
-}>({
+const SubscriptionContext = createContext<SubscriptionContextValue>({
   isPayingUser: null,
   offeringToDisplay: null,
 });

 export default SubscriptionContext;
src/infra/purchase/purchase.ts (1)

34-40: Consider parallelizing independent async calls for efficiency.

setEmail and setAttributes can run concurrently after logIn completes, reducing total wait time. All callers already pass the User object correctly.

♻️ Suggested refactor
   async setUser(user: User) {
     await RevenueCat.logIn(user.id);
-    await RevenueCat.setEmail(user.email);
-    await this.setAttributes({
+    await Promise.all([
+      RevenueCat.setEmail(user.email),
+      this.setAttributes({
-      $posthogUserId: user.id,
-    });
+        $posthogUserId: user.id,
+      }),
+    ]);
   }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dc783cf and 7fde5dc.

📒 Files selected for processing (15)
  • src/app/_layout.tsx
  • src/domain/contexts/authContext/AuthContextProvider.tsx
  • src/domain/contexts/subscriptionContext/SubscriptionContext.ts
  • src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
  • src/domain/contexts/subscriptionContext/index.ts
  • src/domain/contexts/subscriptionContext/useSubscriptionContext.ts
  • src/domain/subscription/index.ts
  • src/domain/subscription/utils/hasActiveEntitlements.ts
  • src/domain/subscription/utils/index.ts
  • src/infra/analytics/analytics.ts
  • src/infra/analytics/analytics.types.ts
  • src/infra/featureFlags/defaultFlags.ts
  • src/infra/featureFlags/featureFlags.types.ts
  • src/infra/purchase/purchase.ts
  • src/shared/hooks/useRunOnMount.ts
💤 Files with no reviewable changes (2)
  • src/infra/analytics/analytics.types.ts
  • src/infra/analytics/analytics.ts
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{ts,tsx}: Always use TypeScript for code files
Use ES modules with import statements instead of require
Always wrap arrow function arguments in parentheses
Prefer async/await for promises and always handle error cases
Always fix ESLint warnings and errors before finishing a task
Run yarn lint, yarn lint:ts, and yarn prettify and fix potential errors before finishing a task

Files:

  • src/domain/subscription/index.ts
  • src/domain/subscription/utils/hasActiveEntitlements.ts
  • src/infra/featureFlags/featureFlags.types.ts
  • src/domain/contexts/authContext/AuthContextProvider.tsx
  • src/domain/contexts/subscriptionContext/SubscriptionContext.ts
  • src/app/_layout.tsx
  • src/domain/subscription/utils/index.ts
  • src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
  • src/infra/purchase/purchase.ts
  • src/infra/featureFlags/defaultFlags.ts
  • src/domain/contexts/subscriptionContext/index.ts
  • src/shared/hooks/useRunOnMount.ts
  • src/domain/contexts/subscriptionContext/useSubscriptionContext.ts
src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{tsx,ts}: Always use arrow functions with extracted Props interfaces for React components
Don't use React.FC type for components; prefer interfaces to types for Props
Use ComponentProps<typeof MyComponent> to extract Props type instead of manually defining them
Never define inline styles; always use Unistyles's StyleSheet API for styling
Maintain theme consistency using Unistyles theme file located at src/domain/theme/unistyles.ts

Files:

  • src/domain/subscription/index.ts
  • src/domain/subscription/utils/hasActiveEntitlements.ts
  • src/infra/featureFlags/featureFlags.types.ts
  • src/domain/contexts/authContext/AuthContextProvider.tsx
  • src/domain/contexts/subscriptionContext/SubscriptionContext.ts
  • src/app/_layout.tsx
  • src/domain/subscription/utils/index.ts
  • src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
  • src/infra/purchase/purchase.ts
  • src/infra/featureFlags/defaultFlags.ts
  • src/domain/contexts/subscriptionContext/index.ts
  • src/shared/hooks/useRunOnMount.ts
  • src/domain/contexts/subscriptionContext/useSubscriptionContext.ts
src/**/index.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Create an index.ts file for components, hooks, or utils used outside the current folder using export * from X syntax

Files:

  • src/domain/subscription/index.ts
  • src/domain/subscription/utils/index.ts
  • src/domain/contexts/subscriptionContext/index.ts
🧠 Learnings (1)
📚 Learning: 2026-01-07T07:41:02.477Z
Learnt from: CR
Repo: tsyirvo/react-native-starter PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-07T07:41:02.477Z
Learning: Applies to src/**/index.ts : Create an index.ts file for components, hooks, or utils used outside the current folder using `export * from X` syntax

Applied to files:

  • src/domain/subscription/index.ts
  • src/domain/subscription/utils/index.ts
  • src/domain/contexts/subscriptionContext/index.ts
🧬 Code graph analysis (5)
src/domain/contexts/authContext/AuthContextProvider.tsx (1)
src/infra/purchase/purchase.ts (1)
  • Purchase (93-93)
src/app/_layout.tsx (1)
src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx (1)
  • SubscriptionContextProvider (15-98)
src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx (4)
src/infra/purchase/purchase.ts (2)
  • isPayingUser (58-62)
  • Purchase (93-93)
src/shared/hooks/useRunOnMount.ts (1)
  • useRunOnMount (3-21)
src/infra/logger/logger.ts (2)
  • error (51-71)
  • Logger (81-81)
src/infra/featureFlags/featureFlags.types.ts (1)
  • OfferingFlagType (13-16)
src/infra/purchase/purchase.ts (1)
src/domain/subscription/utils/hasActiveEntitlements.ts (1)
  • hasActiveEntitlements (3-5)
src/infra/featureFlags/defaultFlags.ts (1)
src/infra/featureFlags/featureFlags.types.ts (2)
  • VersionFlagType (18-21)
  • OfferingFlagType (13-16)
🔇 Additional comments (12)
src/infra/featureFlags/featureFlags.types.ts (1)

13-16: LGTM!

The new OfferingFlagType interface follows the established pattern of VersionFlagType and properly extends the remote config system.

src/domain/subscription/utils/index.ts (1)

1-1: LGTM!

Barrel export follows the coding guidelines using export * from X syntax.

src/domain/contexts/authContext/AuthContextProvider.tsx (1)

43-43: LGTM!

Passing the full User object aligns with the updated Purchase.setUser signature and enables richer user tracking with attributes like email.

src/domain/subscription/index.ts (1)

1-1: LGTM!

Barrel export correctly re-exports utilities following the coding guidelines.

src/domain/contexts/subscriptionContext/index.ts (1)

1-2: LGTM!

Clean barrel export following the coding guidelines. Exposing only the provider and hook keeps the API surface minimal.

src/app/_layout.tsx (1)

85-113: LGTM - SubscriptionContextProvider integration looks correct.

The provider placement inside AuthContextProvider is appropriate since subscription data likely depends on an authenticated user. The nested Stack.Protected guards correctly handle conditional screen rendering.

src/infra/featureFlags/defaultFlags.ts (1)

15-22: LGTM - Remote config extension for offering selection.

The type union correctly accommodates both VersionFlagType and OfferingFlagType. The default value of 'default' is a sensible fallback for the offering identifier.

src/infra/purchase/purchase.ts (2)

58-62: LGTM - Clean implementation using the new utility.

The isPayingUser method correctly delegates to hasActiveEntitlements, maintaining consistency with the utility function.


84-90: LGTM - Proper cleanup pattern for listener.

Returning the cleanup function enables proper teardown in React effects, preventing memory leaks from orphaned listeners.

src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx (3)

24-42: LGTM - Proper error handling with fallback.

The try-catch with setIsPayingUser(false) as fallback ensures graceful degradation when the subscription status cannot be fetched.


52-83: LGTM - Well-structured offering resolution with remote config override.

The fallback chain (remote config → default offering → null on error) provides resilient offering selection. The type guard payload?.type === 'offering' correctly validates the flag payload structure.


85-91: LGTM - Properly memoized context value.

The useMemo with correct dependencies prevents unnecessary re-renders of consumers when neither isPayingUser nor offeringToDisplay changes.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx:
- Line 38: The error branch should set isPayingUser to null instead of false to
match the typed union and mirror offeringToDisplay's null-on-error pattern;
update the error handling in SubscriptionContextProvider (replace the
setIsPayingUser(false) call with setIsPayingUser(null)) so the "unknown/error"
state is clearly distinguished from a definite non-paying user, and ensure any
downstream checks that rely on isPayingUser handle null accordingly.
- Around line 25-43: The async fetchIsPayingUser can call setIsPayingUser after
unmount; wrap the effect body returned to useRunOnMount with an isMounted flag
(e.g., let isMounted = true) and return a cleanup that sets isMounted = false,
then check isMounted before calling setIsPayingUser in both the try and catch
paths (when awaiting Purchase.isPayingUser()). This prevents state updates after
unmount while keeping the same functions (useRunOnMount, fetchIsPayingUser,
Purchase.isPayingUser, setIsPayingUser).
- Around line 51-82: The async fetchOfferingToDisplay in useRunOnMount may call
setOfferingToDisplay after the component unmounts; fix by adding a mounted flag
(e.g., let mounted = true) inside useRunOnMount, check mounted before every
setOfferingToDisplay and before Logger/error handling if needed, and return a
cleanup function that sets mounted = false so pending Purchase.getOfferings()
results are ignored after unmount; update references in fetchOfferingToDisplay
and the enclosing useRunOnMount accordingly.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7fde5dc and 6f986c9.

📒 Files selected for processing (1)
  • src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{ts,tsx}: Always use TypeScript for code files
Use ES modules with import statements instead of require
Always wrap arrow function arguments in parentheses
Prefer async/await for promises and always handle error cases
Always fix ESLint warnings and errors before finishing a task
Run yarn lint, yarn lint:ts, and yarn prettify and fix potential errors before finishing a task

Files:

  • src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{tsx,ts}: Always use arrow functions with extracted Props interfaces for React components
Don't use React.FC type for components; prefer interfaces to types for Props
Use ComponentProps<typeof MyComponent> to extract Props type instead of manually defining them
Never define inline styles; always use Unistyles's StyleSheet API for styling
Maintain theme consistency using Unistyles theme file located at src/domain/theme/unistyles.ts

Files:

  • src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
🧬 Code graph analysis (1)
src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx (5)
src/infra/purchase/purchase.ts (2)
  • isPayingUser (58-62)
  • Purchase (93-93)
src/shared/hooks/useRunOnMount.ts (1)
  • useRunOnMount (3-7)
src/infra/logger/logger.ts (2)
  • error (51-71)
  • Logger (81-81)
src/domain/subscription/utils/hasActiveEntitlements.ts (1)
  • hasActiveEntitlements (3-5)
src/infra/featureFlags/featureFlags.types.ts (1)
  • OfferingFlagType (13-16)
🔇 Additional comments (4)
src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx (4)

1-10: LGTM!

Imports are clean, organized, and all dependencies are used appropriately.


12-21: LGTM!

Component follows coding guidelines: arrow function with extracted Props interface, proper TypeScript typing, and appropriate state initialization.


45-49: LGTM!

The customer listener correctly uses hasActiveEntitlements(customerInfo) for consistent entitlement checking, and cleanup is properly handled via the returned function. The concern from the past review has been addressed.


84-96: LGTM!

The memoization pattern correctly prevents unnecessary re-renders, and the context provider follows standard React patterns.

Comment on lines +25 to +43
useRunOnMount(() => {
const fetchIsPayingUser = async () => {
try {
const isPayingUser = await Purchase.isPayingUser();

setIsPayingUser(isPayingUser);
} catch (error) {
Logger.error({
error,
level: 'warning',
message: 'Failed to fetch user subscription status',
});

setIsPayingUser(false);
}
};

void fetchIsPayingUser();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add cleanup to prevent state updates after unmount.

The async operation in fetchIsPayingUser can complete after the component unmounts, causing React to attempt setting state on an unmounted component. This leads to memory leaks and console warnings.

🔧 Proposed fix with mounted flag
  useRunOnMount(() => {
+   let mounted = true;
+
    const fetchIsPayingUser = async () => {
      try {
        const isPayingUser = await Purchase.isPayingUser();

-       setIsPayingUser(isPayingUser);
+       if (mounted) {
+         setIsPayingUser(isPayingUser);
+       }
      } catch (error) {
        Logger.error({
          error,
          level: 'warning',
          message: 'Failed to fetch user subscription status',
        });

-       setIsPayingUser(false);
+       if (mounted) {
+         setIsPayingUser(false);
+       }
      }
    };

    void fetchIsPayingUser();
+
+   return () => {
+     mounted = false;
+   };
  });
🤖 Prompt for AI Agents
In @src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
around lines 25 - 43, The async fetchIsPayingUser can call setIsPayingUser after
unmount; wrap the effect body returned to useRunOnMount with an isMounted flag
(e.g., let isMounted = true) and return a cleanup that sets isMounted = false,
then check isMounted before calling setIsPayingUser in both the try and catch
paths (when awaiting Purchase.isPayingUser()). This prevents state updates after
unmount while keeping the same functions (useRunOnMount, fetchIsPayingUser,
Purchase.isPayingUser, setIsPayingUser).

message: 'Failed to fetch user subscription status',
});

setIsPayingUser(false);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Consider using null instead of false for error state consistency.

Line 38 sets isPayingUser to false on error, while line 77 sets offeringToDisplay to null on error. Since isPayingUser is typed as boolean | null, setting it to null on error would be more consistent and better distinguish "error/unknown" state from "definitely not a paying user."

♻️ Suggested change for consistency
-       setIsPayingUser(false);
+       setIsPayingUser(null);
📝 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
setIsPayingUser(false);
setIsPayingUser(null);
🤖 Prompt for AI Agents
In @src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx at
line 38, The error branch should set isPayingUser to null instead of false to
match the typed union and mirror offeringToDisplay's null-on-error pattern;
update the error handling in SubscriptionContextProvider (replace the
setIsPayingUser(false) call with setIsPayingUser(null)) so the "unknown/error"
state is clearly distinguished from a definite non-paying user, and ensure any
downstream checks that rely on isPayingUser handle null accordingly.

Comment on lines +51 to +82
useRunOnMount(() => {
const fetchOfferingToDisplay = async () => {
try {
const offering = await Purchase.getOfferings();
const payload = getFlagPayloadSync<OfferingFlagType>(
'offering-to-display',
);

if (payload?.type === 'offering' && payload.offering) {
const remotelySelectedOffering = offering.all[payload.offering];

if (remotelySelectedOffering) {
setOfferingToDisplay(remotelySelectedOffering);
} else {
setOfferingToDisplay(offering.current);
}
} else {
setOfferingToDisplay(offering.current);
}
} catch (error) {
Logger.error({
error,
level: 'warning',
message: 'Failed to fetch offering to display',
});

setOfferingToDisplay(null);
}
};

void fetchOfferingToDisplay();
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add cleanup to prevent state updates after unmount.

Similar to the first useRunOnMount, the async operation in fetchOfferingToDisplay can complete after unmount, causing state updates on an unmounted component.

🔧 Proposed fix with mounted flag
  useRunOnMount(() => {
+   let mounted = true;
+
    const fetchOfferingToDisplay = async () => {
      try {
        const offering = await Purchase.getOfferings();
        const payload = getFlagPayloadSync<OfferingFlagType>(
          'offering-to-display',
        );

        if (payload?.type === 'offering' && payload.offering) {
          const remotelySelectedOffering = offering.all[payload.offering];

          if (remotelySelectedOffering) {
-           setOfferingToDisplay(remotelySelectedOffering);
+           if (mounted) {
+             setOfferingToDisplay(remotelySelectedOffering);
+           }
          } else {
-           setOfferingToDisplay(offering.current);
+           if (mounted) {
+             setOfferingToDisplay(offering.current);
+           }
          }
        } else {
-         setOfferingToDisplay(offering.current);
+         if (mounted) {
+           setOfferingToDisplay(offering.current);
+         }
        }
      } catch (error) {
        Logger.error({
          error,
          level: 'warning',
          message: 'Failed to fetch offering to display',
        });

-       setOfferingToDisplay(null);
+       if (mounted) {
+         setOfferingToDisplay(null);
+       }
      }
    };

    void fetchOfferingToDisplay();
+
+   return () => {
+     mounted = false;
+   };
  });
📝 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
useRunOnMount(() => {
const fetchOfferingToDisplay = async () => {
try {
const offering = await Purchase.getOfferings();
const payload = getFlagPayloadSync<OfferingFlagType>(
'offering-to-display',
);
if (payload?.type === 'offering' && payload.offering) {
const remotelySelectedOffering = offering.all[payload.offering];
if (remotelySelectedOffering) {
setOfferingToDisplay(remotelySelectedOffering);
} else {
setOfferingToDisplay(offering.current);
}
} else {
setOfferingToDisplay(offering.current);
}
} catch (error) {
Logger.error({
error,
level: 'warning',
message: 'Failed to fetch offering to display',
});
setOfferingToDisplay(null);
}
};
void fetchOfferingToDisplay();
});
useRunOnMount(() => {
let mounted = true;
const fetchOfferingToDisplay = async () => {
try {
const offering = await Purchase.getOfferings();
const payload = getFlagPayloadSync<OfferingFlagType>(
'offering-to-display',
);
if (payload?.type === 'offering' && payload.offering) {
const remotelySelectedOffering = offering.all[payload.offering];
if (remotelySelectedOffering) {
if (mounted) {
setOfferingToDisplay(remotelySelectedOffering);
}
} else {
if (mounted) {
setOfferingToDisplay(offering.current);
}
}
} else {
if (mounted) {
setOfferingToDisplay(offering.current);
}
}
} catch (error) {
Logger.error({
error,
level: 'warning',
message: 'Failed to fetch offering to display',
});
if (mounted) {
setOfferingToDisplay(null);
}
}
};
void fetchOfferingToDisplay();
return () => {
mounted = false;
};
});
🤖 Prompt for AI Agents
In @src/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsx
around lines 51 - 82, The async fetchOfferingToDisplay in useRunOnMount may call
setOfferingToDisplay after the component unmounts; fix by adding a mounted flag
(e.g., let mounted = true) inside useRunOnMount, check mounted before every
setOfferingToDisplay and before Logger/error handling if needed, and return a
cleanup function that sets mounted = false so pending Purchase.getOfferings()
results are ignored after unmount; update references in fetchOfferingToDisplay
and the enclosing useRunOnMount accordingly.

@tsyirvo tsyirvo merged commit 4d1aefd into develop Jan 7, 2026
2 checks passed
@tsyirvo tsyirvo deleted the feat/subscriptionContext branch January 7, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants