Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -873,12 +873,12 @@ jobs:
install: true
- android/start_emulator:
avd_name: test-e2e-tester
post_emulator_launch_assemble_command: ./gradlew test-apps:e2etests:assembleDebug
post_emulator_launch_assemble_command: ./gradlew test-apps:e2etests:assembleRelease
- android/restore_gradle_cache
- run:
name: Install E2E Tester on emulator
command: |
adb install test-apps/e2etests/build/outputs/apk/debug/e2etests-debug.apk
adb install test-apps/e2etests/build/outputs/apk/release/e2etests-release.apk
- revenuecat/install-maestro
- run:
name: Run Maestro tests
Expand Down
7 changes: 7 additions & 0 deletions feature/galaxy/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# GalaxyBillingWrapper is instantiated reflectively from the purchases module
# (GalaxyBillingWrapperFactory) via Class.forName + getDeclaredConstructor(...). Keep all its
# constructors so the reflective lookup resolves under minification, regardless of which
# constructor signature the factory uses (so changing it doesn't require updating this rule).
-keep class com.revenuecat.purchases.galaxy.GalaxyBillingWrapper {
<init>(...);
}
54 changes: 10 additions & 44 deletions purchases/consumer-rules.pro
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
-keep class com.revenuecat.** { *; }
# EnumDeserializerWithDefault matches JSON against enum CONSTANT NAMES read reflectively via
# Class.enumConstants (value.name.lowercase()) and silently falls back to a default on mismatch.
# Keep enum constant names so an obfuscated name can't turn into a silent wrong-value bug.
-keepclassmembers enum com.revenuecat.** {
<fields>;
public static **[] values();
public static ** valueOf(java.lang.String);
}

-keep class androidx.lifecycle.DefaultLifecycleObserver
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
-dontwarn com.google.errorprone.annotations.Immutable
Expand All @@ -7,53 +15,11 @@
# Adding temporarily to fix issue after adding kotlin serialization
-dontwarn java.lang.ClassValue

# START Keep kotlinx.serialization annotations.

# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}

# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}

# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}

# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault

# Serializer for classes with named companion objects are retrieved using `getDeclaredClasses`.
# If you have any, uncomment and replace classes with those containing named companion objects.
#-keepattributes InnerClasses # Needed for `getDeclaredClasses`.
#-if @kotlinx.serialization.Serializable class
#com.example.myapplication.HasNamedCompanion, # <-- List serializable classes with named companions.
#com.example.myapplication.HasNamedCompanion2
#{
# static **$* *;
#}
#-keepnames class <1>$$serializer { # -keepnames suffices; class is kept when serializer() is kept.
# static <1>$$serializer INSTANCE;
#}

# These rules target kotlinx.serialization 1.5.1 (Kotlin 2.0.21).
# Adding these because when target Android is 14 but compile version is lower than 14 there are r8 issues
# https://github.com/RevenueCat/purchases-android/pull/1606
-keep class kotlinx.serialization.internal.ClassValueParametrizedCache$initClassValue$1 { ** computeValue(java.lang.Class); }
-keep class kotlinx.serialization.internal.ClassValueCache$initClassValue$1 { ** computeValue(java.lang.Class); }
# END Keep kotlinx.serialization annotations.

# The org.json package is part of the Android framework, so the classes are always available.
# However, some apps add it to their classpath, either explicitly or transitively. When
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ public class DangerousSettings internal constructor(
internal val uiPreviewMode: Boolean = false,

internal val applyObfuscatedAccountIdToSubscriptionChanges: Boolean = false,

internal val allowTestStoreInReleaseBuild: Boolean = false,
) : Parcelable {
public constructor(autoSyncPurchases: Boolean = true) : this(autoSyncPurchases, false, false, false)
public constructor(autoSyncPurchases: Boolean = true) : this(autoSyncPurchases, false, false, false, false)

public companion object {
/**
Expand All @@ -39,5 +41,17 @@ public class DangerousSettings internal constructor(
uiPreviewMode = true,
applyObfuscatedAccountIdToSubscriptionChanges = false,
)

/**
* Creates a [DangerousSettings] that allows configuring the SDK with a Test Store API key
* in a release (non-debuggable) build, bypassing the safety check that otherwise blocks it.
*
* For RevenueCat-internal end-to-end testing only. Do not use in production apps.
*/
@InternalRevenueCatAPI
@JvmStatic
public fun forTestStoreInReleaseBuild(): DangerousSettings = DangerousSettings(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I do wonder if we would want to end up making this public... But for now, kept it internal

allowTestStoreInReleaseBuild = true,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ public enum class Store {
}

internal object StoreSerializer : EnumDeserializerWithDefault<Store>(
serialName = "com.revenuecat.purchases.Store",
defaultValue = Store.UNKNOWN_STORE,
typeForValue = { value -> value.stringValue },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,10 +518,14 @@ internal class PurchasesFactory(

val apiKeyValidationResult = apiKeyValidator.validateAndLog(apiKey, store)

if (!isDebugBuild() &&
apiKeyValidationResult == APIKeyValidator.ValidationResult.SIMULATED_STORE &&
!dangerousSettings.uiPreviewMode
) {
// Test Store keys are only meant for development. uiPreviewMode and
// allowTestStoreInReleaseBuild are internal opt-ins that intentionally bypass this guard.
val isTestStoreKeyInReleaseBuild = !isDebugBuild() &&
apiKeyValidationResult == APIKeyValidator.ValidationResult.SIMULATED_STORE
val testStoreReleaseBuildAllowed = dangerousSettings.uiPreviewMode ||
dangerousSettings.allowTestStoreInReleaseBuild

if (isTestStoreKeyInReleaseBuild && !testStoreReleaseBuildAllowed) {
val redactedApiKey = apiKeyValidator.redactApiKey(apiKey)
errorLog(
error = PurchasesError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum class WorkflowTriggerType {
}

internal object WorkflowTriggerTypeDeserializer : EnumDeserializerWithDefault<WorkflowTriggerType>(
serialName = "com.revenuecat.purchases.common.workflows.WorkflowTriggerType",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

So basically we need each deserializer to have a different name, and after minifiying, the previous strategy didn't work so I'm now passing the FQN as a parameter... In theory, any different string could work, but with the FQN we make sure to avoid duplicates.

defaultValue = WorkflowTriggerType.UNKNOWN,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,14 +295,17 @@ private enum class DestinationSurrogate {
private class UrlSurrogate(val url_lid: LocalizationKey, val method: UrlMethod)

private object ActionTypeSurrogateDeserializer : EnumDeserializerWithDefault<ActionTypeSurrogate> (
serialName = "com.revenuecat.purchases.paywalls.components.ActionTypeSurrogate",
defaultValue = ActionTypeSurrogate.unknown,
)

private object DestinationSurrogateDeserializer : EnumDeserializerWithDefault<DestinationSurrogate> (
serialName = "com.revenuecat.purchases.paywalls.components.DestinationSurrogate",
defaultValue = DestinationSurrogate.unknown,
)

@OptIn(InternalRevenueCatAPI::class)
private object UrlMethodDeserializer : EnumDeserializerWithDefault<UrlMethod> (
serialName = "com.revenuecat.purchases.paywalls.components.ButtonComponent.UrlMethod",
defaultValue = UrlMethod.UNKNOWN,
)
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,14 @@ public class PartialCarouselComponent(

@OptIn(InternalRevenueCatAPI::class)
internal object CarouselPageControlPositionDeserializer : EnumDeserializerWithDefault<PageControl.Position>(
serialName = "com.revenuecat.purchases.paywalls.components.CarouselComponent.PageControl.Position",
defaultValue = PageControl.Position.BOTTOM,
)

@OptIn(InternalRevenueCatAPI::class)
internal object CarouselTransitionTypeDeserializer :
EnumDeserializerWithDefault<CarouselComponent.AutoAdvancePages.TransitionType>(
serialName = "com.revenuecat.purchases.paywalls.components." +
"CarouselComponent.AutoAdvancePages.TransitionType",
defaultValue = CarouselComponent.AutoAdvancePages.TransitionType.SLIDE,
)
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public class PaywallAnimation(

@OptIn(InternalRevenueCatAPI::class)
internal object AnimationTypeSerializer : EnumDeserializerWithDefault<PaywallAnimation.AnimationType>(
serialName = "com.revenuecat.purchases.paywalls.components.PaywallAnimation.AnimationType",
defaultValue = PaywallAnimation.AnimationType.EASE_IN_OUT,
typeForValue = { value ->
when (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class PaywallTransition(

@OptIn(InternalRevenueCatAPI::class)
internal object DisplacementStrategyDeserializer : EnumDeserializerWithDefault<PaywallTransition.DisplacementStrategy>(
serialName = "com.revenuecat.purchases.paywalls.components.PaywallTransition.DisplacementStrategy",
defaultValue = PaywallTransition.DisplacementStrategy.GREEDY,
typeForValue = { value ->
when (value) {
Expand All @@ -64,6 +65,7 @@ internal object DisplacementStrategyDeserializer : EnumDeserializerWithDefault<P

@OptIn(InternalRevenueCatAPI::class)
internal object TransitionTypeSerializer : EnumDeserializerWithDefault<PaywallTransition.TransitionType>(
serialName = "com.revenuecat.purchases.paywalls.components.PaywallTransition.TransitionType",
defaultValue = PaywallTransition.TransitionType.FADE,
typeForValue = { value ->
when (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public class PurchaseButtonComponent(

@OptIn(InternalRevenueCatAPI::class)
private object ActionDeserializer : EnumDeserializerWithDefault<PurchaseButtonComponent.Action> (
serialName = "com.revenuecat.purchases.paywalls.components.PurchaseButtonComponent.Action",
defaultValue = PurchaseButtonComponent.Action.IN_APP_CHECKOUT,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,6 @@ public class PartialStackComponent(

@OptIn(InternalRevenueCatAPI::class)
internal object StackOverflowDeserializer : EnumDeserializerWithDefault<StackComponent.Overflow>(
serialName = "com.revenuecat.purchases.paywalls.components.StackComponent.Overflow",
defaultValue = StackComponent.Overflow.NONE,
)
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public class PartialTimelineComponentItem(

@OptIn(InternalRevenueCatAPI::class)
internal object TimelineIconAlignmentDeserializer : EnumDeserializerWithDefault<IconAlignment>(
serialName = "com.revenuecat.purchases.paywalls.components.TimelineComponent.IconAlignment",
defaultValue = TimelineComponent.IconAlignment.Title,
typeForValue = { value ->
when (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,18 @@ public enum class TwoDimensionalAlignment {

@OptIn(InternalRevenueCatAPI::class)
internal object HorizontalAlignmentDeserializer : EnumDeserializerWithDefault<HorizontalAlignment>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.HorizontalAlignment",
defaultValue = HorizontalAlignment.LEADING,
)

@OptIn(InternalRevenueCatAPI::class)
internal object VerticalAlignmentDeserializer : EnumDeserializerWithDefault<VerticalAlignment>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.VerticalAlignment",
defaultValue = VerticalAlignment.TOP,
)

@OptIn(InternalRevenueCatAPI::class)
internal object TwoDimensionalAlignmentDeserializer : EnumDeserializerWithDefault<TwoDimensionalAlignment>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.TwoDimensionalAlignment",
defaultValue = TwoDimensionalAlignment.TOP,
)
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public class Badge(

@OptIn(InternalRevenueCatAPI::class)
internal object BadgeStyleSerializer : EnumDeserializerWithDefault<Badge.Style>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.Badge.Style",
defaultValue = Badge.Style.Overlay,
typeForValue = { style ->
when (style) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ public enum class FitMode {

@OptIn(InternalRevenueCatAPI::class)
internal object FitModeDeserializer : EnumDeserializerWithDefault<FitMode>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.FitMode",
defaultValue = FitMode.FIT,
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ public enum class FlexDistribution {

@OptIn(InternalRevenueCatAPI::class)
internal object FlexDistributionDeserializer : EnumDeserializerWithDefault<FlexDistribution>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.FlexDistribution",
defaultValue = FlexDistribution.START,
)
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public enum class FontSize {

@OptIn(InternalRevenueCatAPI::class)
internal object FontWeightDeserializer : EnumDeserializerWithDefault<FontWeight>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.FontWeight",
defaultValue = FontWeight.REGULAR,
typeForValue = { value ->
when (value) {
Expand All @@ -88,6 +89,7 @@ internal object FontWeightDeserializer : EnumDeserializerWithDefault<FontWeight>

@OptIn(InternalRevenueCatAPI::class)
internal object FontStyleDeserializer : EnumDeserializerWithDefault<FontStyle>(
serialName = "com.revenuecat.purchases.paywalls.components.properties.FontStyle",
defaultValue = FontStyle.NORMAL,
typeForValue = { value ->
when (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,23 @@ import kotlinx.serialization.encoding.Encoder
* Deserializer for enums with a default value.
*/
internal abstract class EnumDeserializerWithDefault<T : Enum<T>>(
private val serialName: String,
private val valuesByType: Map<String, T>,
private val defaultValue: T,
) : KSerializer<T> {

constructor(
serialName: String,
defaultValue: T,
typeForValue: (T) -> String = { value -> value.name.lowercase() },
) : this(
serialName = serialName,
valuesByType = defaultValue::class.java.enumConstants.associateBy(typeForValue),
defaultValue = defaultValue,
)

private val enumName = defaultValue.javaClass.simpleName

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(enumName, PrimitiveKind.STRING)
PrimitiveSerialDescriptor(serialName, PrimitiveKind.STRING)

override fun deserialize(decoder: Decoder): T {
val key = decoder.decodeString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,36 @@ class PurchasesFactoryTest {
verify(exactly = 0) { applicationMock.startActivity(any()) }
}

@OptIn(InternalRevenueCatAPI::class)
@Test
fun `configuring SDK with simulated store api key in release mode and allowTestStoreInReleaseBuild does not show error activity`() {
// Arrange
purchasesFactory = PurchasesFactory(
isDebugBuild = { false },
apiKeyValidator = apiKeyValidatorMock,
)
val applicationContextMock = mockk<Application>()
every {
applicationMock.checkCallingOrSelfPermission(Manifest.permission.INTERNET)
} returns PackageManager.PERMISSION_GRANTED
every {
applicationMock.applicationContext
} returns applicationContextMock
every {
apiKeyValidatorMock.validateAndLog("fakeApiKey", Store.PLAY_STORE)
} returns APIKeyValidator.ValidationResult.SIMULATED_STORE

// Act
purchasesFactory.validateConfiguration(
createConfiguration(
dangerousSettings = DangerousSettings.forTestStoreInReleaseBuild(),
),
)

// Assert
verify(exactly = 0) { applicationMock.startActivity(any()) }
}

// region shouldInitializeDiagnostics

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,8 @@ class AppConfigTest {
"autoSyncPurchases=true, " +
"customEntitlementComputation=false, " +
"uiPreviewMode=false, " +
"applyObfuscatedAccountIdToSubscriptionChanges=false), " +
"applyObfuscatedAccountIdToSubscriptionChanges=false, " +
"allowTestStoreInReleaseBuild=false), " +
"languageTag='', " +
"versionName='', " +
"packageName='', " +
Expand Down
Loading