RevenueCat / RevenueCat/react-native-purchases
Android: RevenueCatUI.Paywall crashes with "No ViewModelStoreOwner was provided" when its view detaches during recomposition
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 1.2k
- Forks
- 117
- Avg merge
- 14h 40m
- Merged PRs (30d)
- 40
Description
Environment
react-native-purchases/react-native-purchases-ui: 10.0.1 (source also checked through 10.4.1)- React Native 0.83, New Architecture (Fabric) enabled, Expo SDK 55
- Android only. iOS is unaffected.
What happens
RevenueCatUI.Paywall crashes on Android when its host view is detached from the window while a Compose recomposition is pending (for example when the paywall is unmounted, or when it lives inside a container that detaches, such as react-native-pager-view or a @shopify/flash-list cell):
Fatal Exception: java.lang.IllegalStateException: No ViewModelStoreOwner was provided via LocalViewModelStoreOwner
at com.revenuecat.purchases.ui.revenuecatui.InternalPaywallKt.getPaywallViewModel(InternalPaywall.kt:473)
at com.revenuecat.purchases.ui.revenuecatui.InternalPaywallKt.InternalPaywall(InternalPaywall.kt:64)
at com.revenuecat.purchases.ui.revenuecatui.InternalPaywallKt$InternalPaywall$8.invoke(:14)
at com.revenuecat.purchases.ui.revenuecatui.InternalPaywallKt$InternalPaywall$8.invoke(:8)
at androidx.compose.runtime.RecomposeScopeImpl.compose(RecomposeScopeImpl.kt:201)
at androidx.compose.runtime.ComposerImpl.recomposeToGroupEnd(ComposerImpl.kt:1690)
at androidx.compose.runtime.ComposerImpl.skipCurrentGroup(ComposerImpl.kt:2026)
at androidx.compose.runtime.ComposerImpl.doCompose-aFTiNEg(ComposerImpl.kt:2659)
at androidx.compose.runtime.ComposerImpl.recompose-aFTiNEg$runtime(ComposerImpl.kt:2583)
at androidx.compose.runtime.CompositionImpl.recompose(Composition.kt:1080)
at androidx.compose.runtime.Recomposer.performRecompose(Recomposer.kt:1406)
at androidx.compose.runtime.Recomposer.access$performRecompose(Recomposer.kt:159)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2.invokeSuspend$lambda$2(Recomposer.kt:638)
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2.$r8$lambda$sdKIQuFT6MpOW8QdHT9yWSawFoM()
at androidx.compose.runtime.Recomposer$runRecomposeAndApplyChanges$2$$ExternalSyntheticLambda0.invoke(D8$$SyntheticClass)
at androidx.compose.ui.platform.AndroidUiFrameClock$withFrameNanos$2$callback$1.doFrame(AndroidUiFrameClock.android.kt:39)
at androidx.compose.ui.platform.AndroidUiDispatcher.performFrameDispatch(AndroidUiDispatcher.android.kt:108)
at androidx.compose.ui.platform.AndroidUiDispatcher.access$performFrameDispatch(AndroidUiDispatcher.android.kt:41)
at androidx.compose.ui.platform.AndroidUiDispatcher$dispatchCallback$1.doFrame(AndroidUiDispatcher.android.kt:69)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1959)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java:1970)
at android.view.Choreographer.doCallbacks(Choreographer.java:1423)
at android.view.Choreographer.doFrame(Choreographer.java:1338)
at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:1930)
at android.os.Handler.handleCallback(Handler.java:1070)
at android.os.Handler.dispatchMessage(Handler.java:125)
at android.os.Looper.dispatchMessage(Looper.java:358)
at android.os.Looper.loopOnce(Looper.java:288)
at android.os.Looper.loop(Looper.java:392)
at android.app.ActivityThread.main(ActivityThread.java:10346)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:638)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:972)
Root cause
WrappedPaywallComposeView extends ComposeViewWrapper, and neither sets a ViewTreeViewModelStoreOwner (nor the lifecycle / saved-state owners) on the host view. The Compose PaywallView resolves LocalViewModelStoreOwner via ViewTreeViewModelStoreOwner.get(view), which walks up the attached view tree. React Native does not set one, so it depends on an ancestor providing it. When the view detaches while a recompose is in flight, that lookup returns null and Compose throws.
This is the same class of bug as #994, but that fix only covered PaywallFooterContainerView. The full Paywall path still sets no owner.
Reproduction
Mount RevenueCatUI.Paywall inside a container that detaches while a recomposition is pending on Android New Architecture (e.g. a react-native-pager-view page or a FlashList cell), then swipe or unmount it. It crashes intermittently. iOS does not.
Suggested fix
Set the host Activity (a ComponentActivity, which is a ViewModelStoreOwner / LifecycleOwner / SavedStateRegistryOwner) as the ViewTree*Owner on the wrapper when it is created, so the lookup succeeds even while detached. This is presumably what the #994 footer fix did; extending it to the paywall wrapper would resolve it.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with WrappedPaywallComposeView and ComposeViewWrapper, then compare their owner setup with the PaywallFooterContainerView fix in issue #994. Reproduce the Android New Architecture crash by detaching or unmounting a Paywall during recomposition, and verify that the paywall no longer crashes when detached and that normal rendering still works.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- android, kotlin, react-native
- Domain
- mobile
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 66/100