-
Notifications
You must be signed in to change notification settings - Fork 10
feat: rework how subscriptions are handled #454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughThis 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
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this 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 (likelyundefined), 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.keyswould 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.
setEmailandsetAttributescan run concurrently afterlogIncompletes, reducing total wait time. All callers already pass theUserobject 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
📒 Files selected for processing (15)
src/app/_layout.tsxsrc/domain/contexts/authContext/AuthContextProvider.tsxsrc/domain/contexts/subscriptionContext/SubscriptionContext.tssrc/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsxsrc/domain/contexts/subscriptionContext/index.tssrc/domain/contexts/subscriptionContext/useSubscriptionContext.tssrc/domain/subscription/index.tssrc/domain/subscription/utils/hasActiveEntitlements.tssrc/domain/subscription/utils/index.tssrc/infra/analytics/analytics.tssrc/infra/analytics/analytics.types.tssrc/infra/featureFlags/defaultFlags.tssrc/infra/featureFlags/featureFlags.types.tssrc/infra/purchase/purchase.tssrc/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 withimportstatements instead ofrequire
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
Runyarn lint,yarn lint:ts, andyarn prettifyand fix potential errors before finishing a task
Files:
src/domain/subscription/index.tssrc/domain/subscription/utils/hasActiveEntitlements.tssrc/infra/featureFlags/featureFlags.types.tssrc/domain/contexts/authContext/AuthContextProvider.tsxsrc/domain/contexts/subscriptionContext/SubscriptionContext.tssrc/app/_layout.tsxsrc/domain/subscription/utils/index.tssrc/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsxsrc/infra/purchase/purchase.tssrc/infra/featureFlags/defaultFlags.tssrc/domain/contexts/subscriptionContext/index.tssrc/shared/hooks/useRunOnMount.tssrc/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 useReact.FCtype for components; prefer interfaces to types for Props
UseComponentProps<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 atsrc/domain/theme/unistyles.ts
Files:
src/domain/subscription/index.tssrc/domain/subscription/utils/hasActiveEntitlements.tssrc/infra/featureFlags/featureFlags.types.tssrc/domain/contexts/authContext/AuthContextProvider.tsxsrc/domain/contexts/subscriptionContext/SubscriptionContext.tssrc/app/_layout.tsxsrc/domain/subscription/utils/index.tssrc/domain/contexts/subscriptionContext/SubscriptionContextProvider.tsxsrc/infra/purchase/purchase.tssrc/infra/featureFlags/defaultFlags.tssrc/domain/contexts/subscriptionContext/index.tssrc/shared/hooks/useRunOnMount.tssrc/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 Xsyntax
Files:
src/domain/subscription/index.tssrc/domain/subscription/utils/index.tssrc/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.tssrc/domain/subscription/utils/index.tssrc/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
OfferingFlagTypeinterface follows the established pattern ofVersionFlagTypeand 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 Xsyntax.src/domain/contexts/authContext/AuthContextProvider.tsx (1)
43-43: LGTM!Passing the full
Userobject aligns with the updatedPurchase.setUsersignature 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
AuthContextProvideris appropriate since subscription data likely depends on an authenticated user. The nestedStack.Protectedguards 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
VersionFlagTypeandOfferingFlagType. 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
isPayingUsermethod correctly delegates tohasActiveEntitlements, 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
useMemowith correct dependencies prevents unnecessary re-renders of consumers when neitherisPayingUsernorofferingToDisplaychanges.
There was a problem hiding this 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
📒 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 withimportstatements instead ofrequire
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
Runyarn lint,yarn lint:ts, andyarn prettifyand 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 useReact.FCtype for components; prefer interfaces to types for Props
UseComponentProps<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 atsrc/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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
Summary by CodeRabbit
New Features
Removals
✏️ Tip: You can customize this high-level summary in your review settings.