Skip to content

Commit

Permalink
Paywalls: Fix purchasing regression by providing real activity (#1467)
Browse files Browse the repository at this point in the history
### Description
With #1418, we are overriding the context in order to provide our own
locale to make sure the screen has the same locale in the whole screen.
However, with the current implementation, the context becomes a
`ContextImpl` which can't be converted to the original activity. This
was causing a crash when starting the purchase process

This PR fixes the issue by providing a new `LocalActivity` local
composition that is performed before the overriden context takes place
so we can access the activity in our composable tree.
  • Loading branch information
tonidero authored Nov 14, 2023
1 parent b176d99 commit ce8f627
Show file tree
Hide file tree
Showing 9 changed files with 48 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.revenuecat.purchases.ui.revenuecatui

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
Expand Down Expand Up @@ -37,6 +39,7 @@ import com.revenuecat.purchases.ui.revenuecatui.data.isInFullScreenMode
import com.revenuecat.purchases.ui.revenuecatui.data.processed.PaywallTemplate
import com.revenuecat.purchases.ui.revenuecatui.extensions.conditional
import com.revenuecat.purchases.ui.revenuecatui.fonts.PaywallTheme
import com.revenuecat.purchases.ui.revenuecatui.helpers.LocalActivity
import com.revenuecat.purchases.ui.revenuecatui.helpers.isInPreviewMode
import com.revenuecat.purchases.ui.revenuecatui.helpers.toResourceProvider
import com.revenuecat.purchases.ui.revenuecatui.templates.Template1
Expand Down Expand Up @@ -125,6 +128,7 @@ private fun LoadedPaywall(state: PaywallState.Loaded, viewModel: PaywallViewMode
val configuration = state.configurationWithOverriddenLocale()

CompositionLocalProvider(
LocalActivity provides LocalContext.current.getActivity(),
LocalContext provides state.contextWithConfiguration(configuration),
LocalConfiguration provides configuration,
) {
Expand Down Expand Up @@ -205,3 +209,19 @@ private fun ErrorDialog(
},
)
}

/**
* Returns the activity from a given context. Most times, the context itself will be
* an activity, but in the case it's not, it will iterate through the context wrappers until it
* finds one that is an activity.
*/
private fun Context.getActivity(): Activity? {
var currentContext = this
while (currentContext is ContextWrapper) {
if (currentContext is Activity) {
return currentContext
}
currentContext = currentContext.baseContext
}
return null
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.revenuecat.purchases.ui.revenuecatui

import android.content.Context
import android.app.Activity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ColorScheme
Expand Down Expand Up @@ -186,7 +186,7 @@ private class LoadingViewModel(
error("Not supported")
}

override fun purchaseSelectedPackage(context: Context) {
override fun purchaseSelectedPackage(activity: Activity?) {
error("Can't purchase loading view model")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.text
Expand All @@ -40,6 +39,7 @@ import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfigura
import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockViewModel
import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
import com.revenuecat.purchases.ui.revenuecatui.extensions.introEligibility
import com.revenuecat.purchases.ui.revenuecatui.helpers.LocalActivity

@Composable
internal fun PurchaseButton(
Expand Down Expand Up @@ -74,7 +74,7 @@ private fun PurchaseButton(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val context = LocalContext.current
val activity = LocalActivity.current

val labelOpacity by animateFloatAsState(
targetValue = if (viewModel.actionInProgress.value) 0.0f else 1.0f,
Expand All @@ -100,7 +100,7 @@ private fun PurchaseButton(
brush = buttonBrush(colors),
shape = ButtonDefaults.shape,
),
onClick = { viewModel.purchaseSelectedPackage(context) },
onClick = { viewModel.purchaseSelectedPackage(activity) },
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent, // color set on background
contentColor = colors.callToActionForeground,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.revenuecat.purchases.ui.revenuecatui.data

import android.app.Activity
import android.content.Context
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
Expand All @@ -25,7 +24,6 @@ import com.revenuecat.purchases.ui.revenuecatui.PaywallMode
import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfiguration
import com.revenuecat.purchases.ui.revenuecatui.data.processed.VariableDataProvider
import com.revenuecat.purchases.ui.revenuecatui.extensions.getActivity
import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger
import com.revenuecat.purchases.ui.revenuecatui.helpers.ResourceProvider
import com.revenuecat.purchases.ui.revenuecatui.helpers.toPaywallState
Expand Down Expand Up @@ -55,7 +53,7 @@ internal interface PaywallViewModel {
* Purchase the selected package
* Note: This method requires the context to be an activity or to allow reaching an activity
*/
fun purchaseSelectedPackage(context: Context)
fun purchaseSelectedPackage(activity: Activity?)

fun restorePurchases()

Expand Down Expand Up @@ -142,8 +140,11 @@ internal class PaywallViewModelImpl(
options.dismissRequest()
}

override fun purchaseSelectedPackage(context: Context) {
val activity = context.getActivity() ?: error("Activity not found")
override fun purchaseSelectedPackage(activity: Activity?) {
if (activity == null) {
Logger.e("Activity is null, not initiating package purchase")
return
}
when (val currentState = _state.value) {
is PaywallState.Loaded -> {
val selectedPackage = currentState.selectedPackage.value
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.revenuecat.purchases.ui.revenuecatui.data.testdata

import android.content.Context
import android.app.Activity
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -326,7 +326,7 @@ internal class MockViewModel(
error("Not supported")
}

override fun purchaseSelectedPackage(context: Context) {
override fun purchaseSelectedPackage(activity: Activity?) {
if (allowsPurchases) {
simulateActionInProgress()
} else {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.revenuecat.purchases.ui.revenuecatui.helpers

import android.app.Activity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalInspectionMode
import com.revenuecat.purchases.CustomerInfo
Expand All @@ -11,6 +14,12 @@ import com.revenuecat.purchases.getCustomerInfoWith
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

/**
* CompositionLocal containing the current Activity.
* Needs to be provided, it's null by default.
*/
internal val LocalActivity: ProvidableCompositionLocal<Activity?> = compositionLocalOf { null }

@Composable
@ReadOnlyComposable
internal fun isInPreviewMode() = LocalInspectionMode.current
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@ package com.revenuecat.purchases.ui.revenuecatui.helpers

import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalContext
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import androidx.window.layout.WindowMetricsCalculator
import com.revenuecat.purchases.ui.revenuecatui.extensions.getActivity

@Composable
@ReadOnlyComposable
internal fun computeWindowWidthSizeClass(): WindowWidthSizeClass? {
val activity = LocalContext.current.getActivity() ?: return null
val activity = LocalActivity.current ?: return null
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val density = LocalContext.current.resources.displayMetrics.density
val density = activity.resources.displayMetrics.density
return WindowSizeClass.compute(width / density, height / density).windowWidthSizeClass
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions
import com.revenuecat.purchases.ui.revenuecatui.data.processed.TemplateConfiguration
import com.revenuecat.purchases.ui.revenuecatui.data.testdata.MockResourceProvider
import com.revenuecat.purchases.ui.revenuecatui.data.testdata.TestData
import com.revenuecat.purchases.ui.revenuecatui.extensions.getActivity
import io.mockk.Runs
import io.mockk.clearAllMocks
import io.mockk.coEvery
Expand Down Expand Up @@ -84,11 +83,6 @@ class PaywallViewModelTest {

dismissInvoked = false

// Allows mocking Context.getActivity
mockkStatic("com.revenuecat.purchases.ui.revenuecatui.extensions.ContextExtensionsKt")

every { context.getActivity() } returns activity

coEvery { purchases.awaitOfferings() } returns offerings
coEvery { purchases.awaitCustomerInfo(any()) } returns customerInfo

Expand Down Expand Up @@ -243,7 +237,7 @@ class PaywallViewModelTest {

assertThat(dismissInvoked).isFalse

model.purchaseSelectedPackage(context)
model.purchaseSelectedPackage(activity)

coVerify {
purchases.awaitPurchase(any())
Expand Down Expand Up @@ -275,7 +269,7 @@ class PaywallViewModelTest {
purchases.awaitPurchase(any())
} throws PurchasesException(expectedError)

model.purchaseSelectedPackage(context)
model.purchaseSelectedPackage(activity)

coVerify {
purchases.awaitPurchase(any())
Expand Down Expand Up @@ -449,7 +443,7 @@ class PaywallViewModelTest {
purchases.awaitPurchase(any())
} throws PurchasesException(expectedError)

model.purchaseSelectedPackage(context)
model.purchaseSelectedPackage(activity)

verifyEventTracked(PaywallEventType.CANCEL, 1)
}
Expand All @@ -463,7 +457,7 @@ class PaywallViewModelTest {
purchases.awaitPurchase(any())
} throws PurchasesException(expectedError)

model.purchaseSelectedPackage(context)
model.purchaseSelectedPackage(activity)

verifyNoEventsOfTypeTracked(PaywallEventType.CANCEL)
}
Expand Down

0 comments on commit ce8f627

Please sign in to comment.