Skip to content

Fix IllegalStateException: No ViewModelStoreOwner was provided via LocalViewModelStoreOwner in CompatComposeView#2912

Merged
vegaro merged 2 commits into
mainfrom
cesar/fix-expo-54-modals
Dec 11, 2025
Merged

Fix IllegalStateException: No ViewModelStoreOwner was provided via LocalViewModelStoreOwner in CompatComposeView#2912
vegaro merged 2 commits into
mainfrom
cesar/fix-expo-54-modals

Conversation

@vegaro

@vegaro vegaro commented Dec 9, 2025

Copy link
Copy Markdown
Member

We got a report that in Expo 54 the app crashes with this exception when presenting a paywall in a Modal:

java.lang.IllegalStateException: No ViewModelStoreOwner was provided via LocalViewModelStoreOwner
	at com.revenuecat.purchases.ui.revenuecatui.InternalPaywallKt.getPaywallViewModel(InternalPaywall.kt:380)
	at com.revenuecat.purchases.ui.revenuecatui.InternalPaywallKt.InternalPaywall(InternalPaywall.kt:62)
	at com.revenuecat.purchases.ui.revenuecatui.PaywallKt.Paywall(Paywall.kt:11)
	at com.revenuecat.purchases.ui.revenuecatui.views.PaywallView.Content(PaywallView.kt:180)
	at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:259)
	at androidx.compose.ui.platform.AbstractComposeView$ensureCompositionCreated$1.invoke(ComposeView.android.kt:258)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:380)
	at androidx.compose.ui.platform.CompositionLocalsKt.ProvideCommonCompositionLocals(CompositionLocals.kt:216)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:132)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt$ProvideAndroidCompositionLocals$3.invoke(AndroidCompositionLocals.android.kt:131)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:380)
	at androidx.compose.ui.platform.AndroidCompositionLocals_androidKt.ProvideAndroidCompositionLocals(AndroidCompositionLocals.android.kt:121)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:155)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1$3.invoke(Wrapper.android.kt:154)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
	at androidx.compose.runtime.CompositionLocalKt.CompositionLocalProvider(CompositionLocal.kt:401)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:154)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1$1.invoke(Wrapper.android.kt:133)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:109)
	at androidx.compose.runtime.internal.ComposableLambdaImpl.invoke(ComposableLambda.jvm.kt:35)
	at androidx.compose.runtime.ActualJvm_jvmKt.invokeComposable(ActualJvm.jvm.kt:97)
	at androidx.compose.runtime.ComposerImpl.doCompose(Composer.kt:3595)
	at androidx.compose.runtime.ComposerImpl.composeContent$runtime_release(Composer.kt:3522)
	at androidx.compose.runtime.CompositionImpl.composeContent(Composition.kt:743)
	at androidx.compose.runtime.Recomposer.composeInitial$runtime_release(Recomposer.kt:1122)
	at androidx.compose.runtime.CompositionImpl.composeInitial(Composition.kt:649)
	at androidx.compose.runtime.CompositionImpl.setContent(Composition.kt:635)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:133)
	at androidx.compose.ui.platform.WrappedComposition$setContent$1.invoke(Wrapper.android.kt:124)
	at androidx.compose.ui.platform.AndroidComposeView.setOnViewTreeOwnersAvailable(AndroidComposeView.android.kt:1631)
	at androidx.compose.ui.platform.WrappedComposition.setContent(Wrapper.android.kt:124)
	at androidx.compose.ui.platform.WrappedComposition.onStateChanged(Wrapper.android.kt:180)
	at androidx.lifecycle.LifecycleRegistry$ObserverWithState.dispatchEvent(LifecycleRegistry.jvm.kt:320)

I was debugging and noticed that when it hits this line:

if (windowRoot == null || windowRoot.findViewTreeLifecycleOwner() != null) return

in Expo < 54, findViewTreeLifecycleOwner, findViewTreeLifecycleOwner, findViewTreeSavedStateRegistryOwner all null.

in Expo 54, windowRoot.findViewTreeLifecycleOwner() returns != null, so it skips the rest

@vegaro vegaro requested a review from a team as a code owner December 9, 2025 23:23
@vegaro vegaro changed the title Fix IllegalStateException: No ViewModelStoreOwner was provided via LocalViewModelStoreOwner in CompatComposeView Fix IllegalStateException: No ViewModelStoreOwner was provided via LocalViewModelStoreOwner in CompatComposeView Dec 9, 2025
@codecov

codecov Bot commented Dec 9, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.45%. Comparing base (6f7400c) to head (da53033).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #2912   +/-   ##
=======================================
  Coverage   78.45%   78.45%           
=======================================
  Files         332      332           
  Lines       12884    12884           
  Branches     1749     1749           
=======================================
  Hits        10108    10108           
  Misses       2042     2042           
  Partials      734      734           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@vegaro vegaro requested a review from a team December 9, 2025 23:54

@tonidero tonidero 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.

I think it might be good documenting a bit more the reason of the crash, but if we've tested this changes seem reasonable

@JayShortway JayShortway left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome job fixing this! 💪

}
if (windowRoot.findViewTreeSavedStateRegistryOwner() == null) {
windowRoot.setViewTreeSavedStateRegistryOwner(this)
isManagingViewTree = true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe we should set an additional boolean here (isManagingSavedState), and use it in these places:

    override fun onSaveInstanceState(): Parcelable? {
        val state = super.onSaveInstanceState()
        if (isManagingSavedState) performSave(state)
        return state
    }

    override fun onRestoreInstanceState(state: Parcelable?) {
        super.onRestoreInstanceState(state)
        if (isManagingSavedState) performRestore(state)
    }

    override fun onAttachedToWindow() {
        initViewTreeOwners()
        if (isManagingSavedState) {
            savedStateRegistryController.performAttach()
            performRestore(null)
        }
        // ...
    }

Otherwise we might be "managing" the saved state even if the view tree has a SavedStateRegistryOwner already.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

right! I think that makes sense

}
if (windowRoot.findViewTreeViewModelStoreOwner() == null) {
windowRoot.setViewTreeViewModelStoreOwner(this)
isManagingViewTree = true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here, maybe we should set an additional boolean (isManagingViewTreeViewModelStore), and use it here:

    override fun onDetachedFromWindow() {
        // ...
        if (isManagingViewTreeViewModelStore) viewModelStore.clear()
        // ...
    }

Although I don't think it can hurt much if we're clearing a ViewModelStore that isn't actually being used.

@JZDesign JZDesign 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.

👑

@vegaro vegaro requested review from a team December 11, 2025 14:50

@tonidero tonidero 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.

Makes sense, thanks again for the fix!!

@vegaro vegaro added this pull request to the merge queue Dec 11, 2025
Merged via the queue into main with commit c4c3696 Dec 11, 2025
23 checks passed
@vegaro vegaro deleted the cesar/fix-expo-54-modals branch December 11, 2025 15:41
tonidero pushed a commit that referenced this pull request Dec 15, 2025
**This is an automatic release.**

> [!WARNING]  
> If you don't have any login system in your app, please make sure your
one-time purchase products have been correctly configured in the
RevenueCat dashboard as either consumable or non-consumable. If they're
incorrectly configured as consumables, RevenueCat will consume these
purchases. This means that users won't be able to restore them from
version 9.0.0 onward.
> Non-consumables are products that are meant to be bought only once,
for example, lifetime subscriptions.


## RevenueCat SDK
### 📦 Dependency Updates
* [RENOVATE] Update build-dependencies to v8.13.2 (#2929) via RevenueCat
Git Bot (@RCGitBot)

## RevenueCatUI SDK
### 🐞 Bugfixes
* Fix `IllegalStateException`: No `ViewModelStoreOwner` was provided via
`LocalViewModelStoreOwner` in `CompatComposeView` (#2912) via Cesar de
la Vega (@vegaro)

### 🔄 Other Changes
* Change AdFailedToLoad mediatorErrorCode type from Long to Int (#2924)
via Pol Miro (@polmiro)
* Change paywall preview recorder name template (#2923) via Toni Rico
(@tonidero)
* Don't use reflection to instantiate AmazonDeviceIdentifiersFetcher
(#2919) via Will Taylor (@fire-at-will)

Co-authored-by: revenuecat-ops <ops@revenuecat.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants