Skip to content
Merged
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
12 changes: 10 additions & 2 deletions libraries/flutter_inapp_purchase/lib/enums.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ enum AndroidReplacementMode {
chargeFullPrice,
}

/// Integer values for the legacy
/// `BillingFlowParams.SubscriptionUpdateParams.ReplacementMode` API, which is
/// what `setSubscriptionReplacementMode(int)` consumes on the Android side.
///
/// Prefer the new string-keyed API via `SubscriptionProductReplacementParamsAndroid`
/// + `SubscriptionReplacementModeAndroid` (see `lib/types.dart`). That path
/// avoids hand-typed integers entirely — the Kotlin side resolves the enum
/// against the live Billing Library constants.
extension AndroidReplacementModeValue on AndroidReplacementMode {
int get value {
switch (this) {
Expand All @@ -167,10 +175,10 @@ extension AndroidReplacementModeValue on AndroidReplacementMode {
return 2;
case AndroidReplacementMode.withoutProration:
return 3;
case AndroidReplacementMode.deferred:
return 4;
case AndroidReplacementMode.chargeFullPrice:
return 5;
case AndroidReplacementMode.deferred:
return 6;
}
}
}
23 changes: 19 additions & 4 deletions libraries/flutter_inapp_purchase/test/enums_unit_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,23 @@ void main() {
expect(AndroidPurchaseState.Unknown.value, 0);
});

test('AndroidReplacementModeValue returns mapped integers', () {
expect(AndroidReplacementMode.withTimeProration.value, 1);
expect(AndroidReplacementMode.chargeFullPrice.value, 5);
});
test(
'AndroidReplacementModeValue returns legacy SubscriptionUpdateParams.ReplacementMode integers',
() {
// Reference (Billing Library 8.x):
// BillingFlowParams.SubscriptionUpdateParams.ReplacementMode
// - UNKNOWN_REPLACEMENT_MODE = 0
// - WITH_TIME_PRORATION = 1
// - CHARGE_PRORATED_PRICE = 2
// - WITHOUT_PRORATION = 3
// - CHARGE_FULL_PRICE = 5
// - DEFERRED = 6
expect(AndroidReplacementMode.unknownReplacementMode.value, 0);
expect(AndroidReplacementMode.withTimeProration.value, 1);
expect(AndroidReplacementMode.chargeProratedPrice.value, 2);
expect(AndroidReplacementMode.withoutProration.value, 3);
expect(AndroidReplacementMode.chargeFullPrice.value, 5);
expect(AndroidReplacementMode.deferred.value, 6);
},
);
}
3 changes: 3 additions & 0 deletions packages/google/openiap/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ android {
named("horizon") {
java.srcDirs("src/horizon/java")
}
named("testPlay") {
java.srcDirs("src/testPlay/java")
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
package dev.hyo.openiap

import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode

/**
* Extension function to convert SubscriptionReplacementModeAndroid enum to
* BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode constants.
*
* Reference (Billing Library 8.1.0+):
* https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode
*
* Values are sourced from the native Billing Library constants so the mapping
* tracks Google's library, not a hardcoded copy that can drift.
*
* Note: These constants differ from the legacy SubscriptionUpdateParams.ReplacementMode API.
* See: https://github.com/hyodotdev/openiap/issues/71
*
* @return The integer constant for SubscriptionProductReplacementParams.ReplacementMode
*/
internal fun SubscriptionReplacementModeAndroid.toReplacementModeConstant(): Int {
return when (this) {
SubscriptionReplacementModeAndroid.UnknownReplacementMode -> 0
SubscriptionReplacementModeAndroid.WithTimeProration -> 1
SubscriptionReplacementModeAndroid.ChargeProratedPrice -> 2
SubscriptionReplacementModeAndroid.WithoutProration -> 3
SubscriptionReplacementModeAndroid.ChargeFullPrice -> 4
SubscriptionReplacementModeAndroid.Deferred -> 5
SubscriptionReplacementModeAndroid.KeepExisting -> 6
SubscriptionReplacementModeAndroid.UnknownReplacementMode -> ReplacementMode.UNKNOWN_REPLACEMENT_MODE
SubscriptionReplacementModeAndroid.WithTimeProration -> ReplacementMode.WITH_TIME_PRORATION
SubscriptionReplacementModeAndroid.ChargeProratedPrice -> ReplacementMode.CHARGE_PRORATED_PRICE
SubscriptionReplacementModeAndroid.WithoutProration -> ReplacementMode.WITHOUT_PRORATION
SubscriptionReplacementModeAndroid.ChargeFullPrice -> ReplacementMode.CHARGE_FULL_PRICE
SubscriptionReplacementModeAndroid.Deferred -> ReplacementMode.DEFERRED
SubscriptionReplacementModeAndroid.KeepExisting -> ReplacementMode.KEEP_EXISTING
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package dev.hyo.openiap

import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test

/**
* Tests for SubscriptionReplacementModeAndroid enum and its mapping to
* BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode constants.
*
* These tests reference the native Billing Library constants directly so the
* mapping cannot drift if Google ever renumbers the values.
*
* Reference (Billing Library 8.1.0+):
* https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProductDetailsParams.SubscriptionProductReplacementParams.ReplacementMode
*
Expand All @@ -16,114 +19,70 @@ import org.junit.Test
*/
class SubscriptionReplacementModeTest {

// MARK: - SubscriptionProductReplacementParams.ReplacementMode constants (8.1.0+)

@Test
fun `UNKNOWN_REPLACEMENT_MODE constant is 0`() {
assertEquals(0, SubscriptionReplacementModeAndroid.UnknownReplacementMode.toReplacementModeConstant())
}

@Test
fun `WITH_TIME_PRORATION constant is 1`() {
assertEquals(1, SubscriptionReplacementModeAndroid.WithTimeProration.toReplacementModeConstant())
fun `UnknownReplacementMode maps to native UNKNOWN_REPLACEMENT_MODE`() {
assertEquals(
ReplacementMode.UNKNOWN_REPLACEMENT_MODE,
SubscriptionReplacementModeAndroid.UnknownReplacementMode.toReplacementModeConstant()
)
}

@Test
fun `CHARGE_PRORATED_PRICE constant is 2`() {
assertEquals(2, SubscriptionReplacementModeAndroid.ChargeProratedPrice.toReplacementModeConstant())
fun `WithTimeProration maps to native WITH_TIME_PRORATION`() {
assertEquals(
ReplacementMode.WITH_TIME_PRORATION,
SubscriptionReplacementModeAndroid.WithTimeProration.toReplacementModeConstant()
)
}

@Test
fun `WITHOUT_PRORATION constant is 3`() {
assertEquals(3, SubscriptionReplacementModeAndroid.WithoutProration.toReplacementModeConstant())
fun `ChargeProratedPrice maps to native CHARGE_PRORATED_PRICE`() {
assertEquals(
ReplacementMode.CHARGE_PRORATED_PRICE,
SubscriptionReplacementModeAndroid.ChargeProratedPrice.toReplacementModeConstant()
)
}

@Test
fun `CHARGE_FULL_PRICE constant is 4 for SubscriptionProductReplacementParams`() {
// This was incorrectly mapped to 5 (legacy value) before the fix
// Correct value for SubscriptionProductReplacementParams.ReplacementMode is 4
assertEquals(4, SubscriptionReplacementModeAndroid.ChargeFullPrice.toReplacementModeConstant())
fun `WithoutProration maps to native WITHOUT_PRORATION`() {
assertEquals(
ReplacementMode.WITHOUT_PRORATION,
SubscriptionReplacementModeAndroid.WithoutProration.toReplacementModeConstant()
)
}

@Test
fun `DEFERRED constant is 5 for SubscriptionProductReplacementParams`() {
// This was incorrectly mapped to 6 (legacy value) before the fix
// Correct value for SubscriptionProductReplacementParams.ReplacementMode is 5
assertEquals(5, SubscriptionReplacementModeAndroid.Deferred.toReplacementModeConstant())
fun `ChargeFullPrice maps to native CHARGE_FULL_PRICE`() {
assertEquals(
ReplacementMode.CHARGE_FULL_PRICE,
SubscriptionReplacementModeAndroid.ChargeFullPrice.toReplacementModeConstant()
)
}

@Test
fun `KEEP_EXISTING constant is 6 for SubscriptionProductReplacementParams`() {
// This was incorrectly mapped to 7 before the fix
// Correct value for SubscriptionProductReplacementParams.ReplacementMode is 6
// Note: KEEP_EXISTING is only available in Billing Library 8.1.0+
assertEquals(6, SubscriptionReplacementModeAndroid.KeepExisting.toReplacementModeConstant())
}

// MARK: - Verify all enum values are covered

@Test
fun `all SubscriptionReplacementModeAndroid values have correct constants`() {
val expectedMappings = mapOf(
SubscriptionReplacementModeAndroid.UnknownReplacementMode to 0,
SubscriptionReplacementModeAndroid.WithTimeProration to 1,
SubscriptionReplacementModeAndroid.ChargeProratedPrice to 2,
SubscriptionReplacementModeAndroid.WithoutProration to 3,
SubscriptionReplacementModeAndroid.ChargeFullPrice to 4,
SubscriptionReplacementModeAndroid.Deferred to 5,
SubscriptionReplacementModeAndroid.KeepExisting to 6
fun `Deferred maps to native DEFERRED`() {
assertEquals(
ReplacementMode.DEFERRED,
SubscriptionReplacementModeAndroid.Deferred.toReplacementModeConstant()
)

// Verify all enum values are tested
assertEquals(7, SubscriptionReplacementModeAndroid.entries.size)
assertEquals(7, expectedMappings.size)

// Verify each mapping
for ((mode, expectedConstant) in expectedMappings) {
assertEquals(
"Mapping for $mode should be $expectedConstant",
expectedConstant,
mode.toReplacementModeConstant()
)
}
}

// MARK: - Document the difference between legacy and new API

@Test
fun `CHARGE_FULL_PRICE differs between legacy and new API`() {
// Legacy SubscriptionUpdateParams.ReplacementMode: CHARGE_FULL_PRICE = 5
// New SubscriptionProductReplacementParams.ReplacementMode: CHARGE_FULL_PRICE = 4
val legacyValue = 5
val newValue = 4

assertEquals(newValue, SubscriptionReplacementModeAndroid.ChargeFullPrice.toReplacementModeConstant())
// Document that legacy value is different
assertNotEquals(
"Legacy and new API values should differ for CHARGE_FULL_PRICE",
legacyValue,
newValue
fun `KeepExisting maps to native KEEP_EXISTING`() {
assertEquals(
ReplacementMode.KEEP_EXISTING,
SubscriptionReplacementModeAndroid.KeepExisting.toReplacementModeConstant()
)
}

@Test
fun `DEFERRED differs between legacy and new API`() {
// Legacy SubscriptionUpdateParams.ReplacementMode: DEFERRED = 6
// New SubscriptionProductReplacementParams.ReplacementMode: DEFERRED = 5
val legacyValue = 6
val newValue = 5

assertEquals(newValue, SubscriptionReplacementModeAndroid.Deferred.toReplacementModeConstant())
// Document that legacy value is different
assertNotEquals(
"Legacy and new API values should differ for DEFERRED",
legacyValue,
newValue
)
fun `all enum values are covered by toReplacementModeConstant`() {
assertEquals(7, SubscriptionReplacementModeAndroid.entries.size)
for (mode in SubscriptionReplacementModeAndroid.entries) {
mode.toReplacementModeConstant()
}
}

// MARK: - Enum JSON serialization tests

@Test
fun `SubscriptionReplacementModeAndroid fromJson parses correctly`() {
assertEquals(SubscriptionReplacementModeAndroid.UnknownReplacementMode, SubscriptionReplacementModeAndroid.fromJson("unknown-replacement-mode"))
Expand Down
Loading