Skip to content

Commit 4cf269d

Browse files
NickSxticlaude
andcommitted
refactor: match iOS coexistence model for deferred purchases listener
Remove adapter pattern, restore independent listener coexistence to match iOS SDK PR #648 and tech task DEV-643: - Restore entitlementsUpdateListener as separate property on InternalConfig and QonversionConfig (both listeners coexist independently) - Both listeners fire when deferred purchase completes (matching iOS behavior where purchasesDelegate and deferredPurchasesListener both fire) - Delete EntitlementsUpdateListenerAdapter (no longer needed) - Update setEntitlementsUpdateListener to store directly, not wrap in adapter - Update tests for coexistence model (setter, independence, both-set) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3320845 commit 4cf269d

7 files changed

Lines changed: 73 additions & 75 deletions

File tree

sdk/src/main/java/com/qonversion/android/sdk/QonversionConfig.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import com.qonversion.android.sdk.dto.QLaunchMode
77
import android.content.Context
88
import androidx.annotation.RawRes
99
import com.qonversion.android.sdk.dto.entitlements.QEntitlementsCacheLifetime
10-
import com.qonversion.android.sdk.internal.EntitlementsUpdateListenerAdapter
1110
import com.qonversion.android.sdk.internal.dto.config.CacheConfig
1211
import com.qonversion.android.sdk.internal.dto.config.PrimaryConfig
1312
import com.qonversion.android.sdk.internal.application
@@ -28,6 +27,7 @@ class QonversionConfig internal constructor(
2827
internal val application: Application,
2928
internal val primaryConfig: PrimaryConfig,
3029
internal val cacheConfig: CacheConfig,
30+
internal val entitlementsUpdateListener: QEntitlementsUpdateListener? = null,
3131
internal val deferredPurchasesListener: QDeferredPurchasesListener? = null
3232
) {
3333

@@ -49,6 +49,7 @@ class QonversionConfig internal constructor(
4949
) {
5050
internal var environment = QEnvironment.Production
5151
internal var entitlementsCacheLifetime = QEntitlementsCacheLifetime.Month
52+
internal var entitlementsUpdateListener: QEntitlementsUpdateListener? = null
5253
internal var deferredPurchasesListener: QDeferredPurchasesListener? = null
5354
internal var proxyUrl: String? = null
5455
internal var isKidsMode: Boolean = false
@@ -106,9 +107,7 @@ class QonversionConfig internal constructor(
106107
*/
107108
@Deprecated("Use setDeferredPurchasesListener instead", ReplaceWith("setDeferredPurchasesListener(listener)"))
108109
fun setEntitlementsUpdateListener(entitlementsUpdateListener: QEntitlementsUpdateListener): Builder = apply {
109-
// Adapter pattern: wrap the legacy listener so the SDK internally works
110-
// with a single listener type (QDeferredPurchasesListener).
111-
this.deferredPurchasesListener = EntitlementsUpdateListenerAdapter(entitlementsUpdateListener)
110+
this.entitlementsUpdateListener = entitlementsUpdateListener
112111
}
113112

114113
/**
@@ -186,6 +185,7 @@ class QonversionConfig internal constructor(
186185
context.application,
187186
primaryConfig,
188187
cacheConfig,
188+
entitlementsUpdateListener,
189189
deferredPurchasesListener
190190
)
191191
}

sdk/src/main/java/com/qonversion/android/sdk/internal/EntitlementsUpdateListenerAdapter.kt

Lines changed: 0 additions & 24 deletions
This file was deleted.

sdk/src/main/java/com/qonversion/android/sdk/internal/InternalConfig.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import com.qonversion.android.sdk.internal.provider.EnvironmentProvider
1010
import com.qonversion.android.sdk.internal.provider.PrimaryConfigProvider
1111
import com.qonversion.android.sdk.internal.provider.UidProvider
1212
import com.qonversion.android.sdk.listeners.QDeferredPurchasesListener
13+
import com.qonversion.android.sdk.listeners.QEntitlementsUpdateListener
1314

1415
internal class InternalConfig(
1516
override var primaryConfig: PrimaryConfig,
1617
override val cacheConfig: CacheConfig,
18+
var entitlementsUpdateListener: QEntitlementsUpdateListener? = null,
1719
var deferredPurchasesListener: QDeferredPurchasesListener? = null
1820
) : EnvironmentProvider,
1921
PrimaryConfigProvider,
@@ -33,6 +35,7 @@ internal class InternalConfig(
3335
constructor(qonversionConfig: QonversionConfig) : this(
3436
qonversionConfig.primaryConfig,
3537
qonversionConfig.cacheConfig,
38+
qonversionConfig.entitlementsUpdateListener,
3639
qonversionConfig.deferredPurchasesListener
3740
)
3841

sdk/src/main/java/com/qonversion/android/sdk/internal/QProductCenterManager.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ internal class QProductCenterManager internal constructor(
467467
}
468468

469469
fun setEntitlementsUpdateListener(entitlementsUpdateListener: QEntitlementsUpdateListener) {
470-
internalConfig.deferredPurchasesListener = EntitlementsUpdateListenerAdapter(entitlementsUpdateListener)
470+
internalConfig.entitlementsUpdateListener = entitlementsUpdateListener
471471
}
472472

473473
fun setDeferredPurchasesListener(listener: QDeferredPurchasesListener) {
@@ -572,6 +572,9 @@ internal class QProductCenterManager internal constructor(
572572
if (callback != null) {
573573
callback.onResult(purchaseResult)
574574
} else {
575+
// Both listeners fire independently (matching iOS behavior)
576+
@Suppress("DEPRECATION")
577+
internalConfig.entitlementsUpdateListener?.onEntitlementsUpdated(entitlements)
575578
internalConfig.deferredPurchasesListener?.deferredPurchaseCompleted(purchaseResult)
576579
}
577580
}
@@ -1068,6 +1071,9 @@ internal class QProductCenterManager internal constructor(
10681071
removePurchaseOptions(product?.storeId)
10691072

10701073
if (purchaseCallback == null) {
1074+
// Both listeners fire independently (matching iOS behavior)
1075+
@Suppress("DEPRECATION")
1076+
internalConfig.entitlementsUpdateListener?.onEntitlementsUpdated(entitlements)
10711077
val purchaseResult = QPurchaseResult.success(entitlements, purchase)
10721078
internalConfig.deferredPurchasesListener?.deferredPurchaseCompleted(purchaseResult)
10731079
} else {

sdk/src/test/java/com/qonversion/android/sdk/QonversionConfigTest.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.qonversion.android.sdk.internal.dto.config.CacheConfig
1111
import com.qonversion.android.sdk.internal.dto.config.PrimaryConfig
1212
import com.qonversion.android.sdk.internal.isDebuggable
1313
import com.qonversion.android.sdk.listeners.QDeferredPurchasesListener
14+
import com.qonversion.android.sdk.listeners.QEntitlementsUpdateListener
1415
import io.mockk.every
1516
import io.mockk.mockk
1617
import io.mockk.mockkStatic
@@ -32,6 +33,7 @@ internal class QonversionConfigTest {
3233
private val projectKey = "some project key"
3334
private val mockLaunchMode = mockk<QLaunchMode>()
3435
private val mockEnvironment = mockk<QEnvironment>()
36+
private val mockEntitlementsUpdateListener = mockk<QEntitlementsUpdateListener>()
3537
private val mockDeferredPurchasesListener = mockk<QDeferredPurchasesListener>()
3638
private val mockEntitlementsCacheLifetime = mockk<QEntitlementsCacheLifetime>()
3739
private val mockProxyUrl = "mock url"
@@ -53,13 +55,15 @@ internal class QonversionConfigTest {
5355
mockApplication,
5456
mockPrimaryConfig,
5557
mockCacheConfig,
58+
mockEntitlementsUpdateListener,
5659
mockDeferredPurchasesListener
5760
)
5861

5962
// then
6063
assertThat(config.application).isSameAs(mockApplication)
6164
assertThat(config.primaryConfig).isSameAs(mockPrimaryConfig)
6265
assertThat(config.cacheConfig).isSameAs(mockCacheConfig)
66+
assertThat(config.entitlementsUpdateListener).isSameAs(mockEntitlementsUpdateListener)
6367
assertThat(config.deferredPurchasesListener).isSameAs(mockDeferredPurchasesListener)
6468
}
6569
}
@@ -164,13 +168,15 @@ internal class QonversionConfigTest {
164168
.apply {
165169
environment = mockEnvironment
166170
entitlementsCacheLifetime = mockEntitlementsCacheLifetime
171+
entitlementsUpdateListener = mockEntitlementsUpdateListener
167172
deferredPurchasesListener = mockDeferredPurchasesListener
168173
proxyUrl = mockProxyUrl
169174
}
170175
val expResult = QonversionConfig(
171176
mockApplication,
172177
mockPrimaryConfig,
173178
mockCacheConfig,
179+
mockEntitlementsUpdateListener,
174180
mockDeferredPurchasesListener
175181
)
176182

sdk/src/test/java/com/qonversion/android/sdk/internal/DeferredPurchasesListenerTest.kt

Lines changed: 44 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,81 +17,80 @@ import org.robolectric.annotation.Config
1717
@Config(sdk = [Build.VERSION_CODES.O_MR1])
1818
internal class DeferredPurchasesListenerTest {
1919

20-
private val mockConfig = mockk<InternalConfig>(relaxed = true)
20+
private lateinit var internalConfig: InternalConfig
2121

2222
@Before
2323
fun setUp() {
2424
clearAllMocks()
25+
internalConfig = InternalConfig(
26+
mockk(relaxed = true),
27+
mockk(relaxed = true)
28+
)
2529
}
2630

27-
// -- Adapter pattern tests --
31+
// -- Setter tests --
2832

2933
@Test
30-
fun `adapter forwards entitlements from purchaseResult to legacy listener`() {
31-
// The adapter extracts entitlements from QPurchaseResult and passes them
32-
// to the wrapped QEntitlementsUpdateListener - this is the core adapter behavior.
33-
val legacyListener = mockk<QEntitlementsUpdateListener>()
34-
every { legacyListener.onEntitlementsUpdated(any()) } just Runs
35-
36-
val adapter = EntitlementsUpdateListenerAdapter(legacyListener)
37-
38-
val entitlements = mapOf("premium" to mockk<QEntitlement>())
39-
val purchaseResult = mockk<QPurchaseResult>()
40-
every { purchaseResult.entitlements } returns entitlements
34+
fun `setDeferredPurchasesListener stores listener`() {
35+
val listener = mockk<QDeferredPurchasesListener>()
4136

42-
adapter.deferredPurchaseCompleted(purchaseResult)
37+
internalConfig.deferredPurchasesListener = listener
4338

44-
verify(exactly = 1) { legacyListener.onEntitlementsUpdated(entitlements) }
39+
assertEquals(listener, internalConfig.deferredPurchasesListener)
4540
}
4641

4742
@Test
48-
fun `adapter forwards empty entitlements for consumable products`() {
49-
// Consumable products may have no entitlements - the adapter should still
50-
// forward the empty map to the legacy listener.
51-
val legacyListener = mockk<QEntitlementsUpdateListener>()
52-
every { legacyListener.onEntitlementsUpdated(any()) } just Runs
43+
fun `setDeferredPurchasesListener to null clears listener`() {
44+
val listener = mockk<QDeferredPurchasesListener>()
45+
internalConfig.deferredPurchasesListener = listener
5346

54-
val adapter = EntitlementsUpdateListenerAdapter(legacyListener)
47+
internalConfig.deferredPurchasesListener = null
5548

56-
val purchaseResult = mockk<QPurchaseResult>()
57-
every { purchaseResult.entitlements } returns emptyMap()
49+
assertNull(internalConfig.deferredPurchasesListener)
50+
}
5851

59-
adapter.deferredPurchaseCompleted(purchaseResult)
52+
@Test
53+
fun `setEntitlementsUpdateListener stores listener`() {
54+
val listener = mockk<QEntitlementsUpdateListener>()
6055

61-
verify(exactly = 1) { legacyListener.onEntitlementsUpdated(emptyMap()) }
56+
internalConfig.entitlementsUpdateListener = listener
57+
58+
assertEquals(listener, internalConfig.entitlementsUpdateListener)
6259
}
6360

61+
// -- Coexistence tests (match iOS behavior) --
62+
6463
@Test
65-
fun `adapter implements QDeferredPurchasesListener interface`() {
66-
// The adapter must be a valid QDeferredPurchasesListener so it can replace
67-
// the entitlementsUpdateListener everywhere internally.
68-
val legacyListener = mockk<QEntitlementsUpdateListener>()
69-
val adapter = EntitlementsUpdateListenerAdapter(legacyListener)
64+
fun `both listeners can be set independently`() {
65+
val deferredListener = mockk<QDeferredPurchasesListener>()
66+
val entitlementsListener = mockk<QEntitlementsUpdateListener>()
7067

71-
assertTrue(adapter is QDeferredPurchasesListener)
72-
}
68+
internalConfig.deferredPurchasesListener = deferredListener
69+
internalConfig.entitlementsUpdateListener = entitlementsListener
7370

74-
// -- Config tests --
71+
assertEquals(deferredListener, internalConfig.deferredPurchasesListener)
72+
assertEquals(entitlementsListener, internalConfig.entitlementsUpdateListener)
73+
}
7574

7675
@Test
77-
fun `setDeferredPurchasesListener stores listener in config`() {
78-
val listener = mockk<QDeferredPurchasesListener>()
79-
every { mockConfig.deferredPurchasesListener = any() } just Runs
80-
every { mockConfig.deferredPurchasesListener } returns listener
76+
fun `setting deferred listener does not affect entitlements listener`() {
77+
val entitlementsListener = mockk<QEntitlementsUpdateListener>()
78+
internalConfig.entitlementsUpdateListener = entitlementsListener
8179

82-
mockConfig.deferredPurchasesListener = listener
80+
val deferredListener = mockk<QDeferredPurchasesListener>()
81+
internalConfig.deferredPurchasesListener = deferredListener
8382

84-
assertEquals(listener, mockConfig.deferredPurchasesListener)
85-
verify { mockConfig.deferredPurchasesListener = listener }
83+
assertEquals(entitlementsListener, internalConfig.entitlementsUpdateListener)
8684
}
8785

8886
@Test
89-
fun `setDeferredPurchasesListener to null clears listener`() {
90-
every { mockConfig.deferredPurchasesListener = any() } just Runs
91-
every { mockConfig.deferredPurchasesListener } returns null
87+
fun `setting entitlements listener does not affect deferred listener`() {
88+
val deferredListener = mockk<QDeferredPurchasesListener>()
89+
internalConfig.deferredPurchasesListener = deferredListener
9290

93-
mockConfig.deferredPurchasesListener = null
91+
val entitlementsListener = mockk<QEntitlementsUpdateListener>()
92+
internalConfig.entitlementsUpdateListener = entitlementsListener
9493

95-
assertNull(mockConfig.deferredPurchasesListener)
94+
assertEquals(deferredListener, internalConfig.deferredPurchasesListener)
9695
}
9796
}

sdk/src/test/java/com/qonversion/android/sdk/internal/InternalConfigTest.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.qonversion.android.sdk.internal.dto.config.CacheConfig
77
import com.qonversion.android.sdk.internal.dto.config.PrimaryConfig
88
import com.qonversion.android.sdk.internal.provider.EnvironmentProvider
99
import com.qonversion.android.sdk.listeners.QDeferredPurchasesListener
10+
import com.qonversion.android.sdk.listeners.QEntitlementsUpdateListener
1011
import io.mockk.mockk
1112
import org.assertj.core.api.Assertions.assertThat
1213
import org.junit.jupiter.api.BeforeEach
@@ -18,13 +19,15 @@ internal class InternalConfigTest {
1819

1920
private val mockPrimaryConfig = mockk<PrimaryConfig>()
2021
private val mockCacheConfig = mockk<CacheConfig>()
22+
private val mockEntitlementsUpdateListener = mockk<QEntitlementsUpdateListener>()
2123
private val mockDeferredPurchasesListener = mockk<QDeferredPurchasesListener>()
2224

2325
@BeforeEach
2426
fun setUp() {
2527
internalConfig = InternalConfig(
2628
mockPrimaryConfig,
2729
mockCacheConfig,
30+
mockEntitlementsUpdateListener,
2831
mockDeferredPurchasesListener
2932
)
3033
}
@@ -44,23 +47,26 @@ internal class InternalConfigTest {
4447
// then
4548
assertThat(internalConfig.primaryConfig).isSameAs(mockPrimaryConfig)
4649
assertThat(internalConfig.cacheConfig).isSameAs(mockCacheConfig)
50+
assertThat(internalConfig.entitlementsUpdateListener).isNull()
4751
assertThat(internalConfig.deferredPurchasesListener).isNull()
4852
}
4953

5054
@Test
51-
fun `primary constructor with deferred purchases listener`() {
55+
fun `primary constructor with both listeners`() {
5256
// given
5357

5458
// when
5559
val internalConfig = InternalConfig(
5660
mockPrimaryConfig,
5761
mockCacheConfig,
62+
mockEntitlementsUpdateListener,
5863
mockDeferredPurchasesListener
5964
)
6065

6166
// then
6267
assertThat(internalConfig.primaryConfig).isSameAs(mockPrimaryConfig)
6368
assertThat(internalConfig.cacheConfig).isSameAs(mockCacheConfig)
69+
assertThat(internalConfig.entitlementsUpdateListener).isSameAs(mockEntitlementsUpdateListener)
6470
assertThat(internalConfig.deferredPurchasesListener).isSameAs(mockDeferredPurchasesListener)
6571
}
6672

@@ -71,6 +77,7 @@ internal class InternalConfigTest {
7177
mockk(),
7278
mockPrimaryConfig,
7379
mockCacheConfig,
80+
mockEntitlementsUpdateListener,
7481
mockDeferredPurchasesListener
7582
)
7683

@@ -80,6 +87,7 @@ internal class InternalConfigTest {
8087
// then
8188
assertThat(internalConfig.primaryConfig).isSameAs(mockPrimaryConfig)
8289
assertThat(internalConfig.cacheConfig).isSameAs(mockCacheConfig)
90+
assertThat(internalConfig.entitlementsUpdateListener).isSameAs(mockEntitlementsUpdateListener)
8391
assertThat(internalConfig.deferredPurchasesListener).isSameAs(mockDeferredPurchasesListener)
8492
}
8593
}

0 commit comments

Comments
 (0)