Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -28,6 +28,8 @@ public class DangerousSettings internal constructor(
*/
@InternalRevenueCatAPI
public val useWorkflows: Boolean = false,

internal val allowTestStoreInReleaseBuild: Boolean = false,
) : Parcelable {
@OptIn(InternalRevenueCatAPI::class)
public constructor(autoSyncPurchases: Boolean = true) : this(
Expand All @@ -36,6 +38,7 @@ public class DangerousSettings internal constructor(
uiPreviewMode = false,
applyObfuscatedAccountIdToSubscriptionChanges = false,
useWorkflows = false,
allowTestStoreInReleaseBuild = false,
)

public companion object {
Expand Down Expand Up @@ -66,5 +69,11 @@ public class DangerousSettings internal constructor(
applyObfuscatedAccountIdToSubscriptionChanges = false,
useWorkflows = true,
)

@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 @@ -517,10 +517,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 @@ -508,7 +508,8 @@ class AppConfigTest {
"customEntitlementComputation=false, " +
"uiPreviewMode=false, " +
"applyObfuscatedAccountIdToSubscriptionChanges=false, " +
"useWorkflows=false), " +
"useWorkflows=false, " +
"allowTestStoreInReleaseBuild=false), " +
"languageTag='', " +
"versionName='', " +
"packageName='', " +
Expand Down
Loading