Skip to content

Commit

Permalink
Add awaitRestore to customEntitlementComputation library (#1275)
Browse files Browse the repository at this point in the history
### Description
We moved the restore purchases functionality to the
customEntitlementComputation library in #1211 but we didn't make the
coroutines version available as well. This fixes that.
  • Loading branch information
tonidero committed Sep 21, 2023
1 parent b6c491e commit 28ba6b0
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 136 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,33 +74,6 @@ suspend fun Purchases.awaitLogOut(): CustomerInfo {
}
}

/**
* Restores purchases made with the current Play Store account for the current user.
* This method will post all purchases associated with the current Play Store account to
* RevenueCat and become associated with the current `appUserID`. If the receipt token is being
* used by an existing user, the current `appUserID` will be aliased together with the
* `appUserID` of the existing user. Going forward, either `appUserID` will be able to reference
* the same user.
*
* You shouldn't use this method if you have your own account system. In that case
* "restoration" is provided by your app passing the same `appUserId` used to purchase originally.
*
* Coroutine friendly version of [Purchases.restorePurchases].
*
* @throws [PurchasesException] with a [PurchasesError] if there's an error login out the user.
* @return The [CustomerInfo] with the restored purchases.
*/
@JvmSynthetic
@Throws(PurchasesTransactionException::class)
suspend fun Purchases.awaitRestore(): CustomerInfo {
return suspendCoroutine { continuation ->
restorePurchasesWith(
onSuccess = { continuation.resume(it) },
onError = { continuation.resumeWithException(PurchasesException(it)) },
)
}
}

/**
* This method will send all the purchases to the RevenueCat backend. Call this when using your own implementation
* for subscriptions anytime a sync is needed, such as when migrating existing users to RevenueCat.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,30 @@ suspend fun Purchases.awaitGetProducts(
)
}
}

/**
* Restores purchases made with the current Play Store account for the current user.
* This method will post all purchases associated with the current Play Store account to
* RevenueCat and become associated with the current `appUserID`. If the receipt token is being
* used by an existing user, the current `appUserID` will be aliased together with the
* `appUserID` of the existing user. Going forward, either `appUserID` will be able to reference
* the same user.
*
* You shouldn't use this method if you have your own account system. In that case
* "restoration" is provided by your app passing the same `appUserId` used to purchase originally.
*
* Coroutine friendly version of [Purchases.restorePurchases].
*
* @throws [PurchasesException] with a [PurchasesError] if there's an error login out the user.
* @return The [CustomerInfo] with the restored purchases.
*/
@JvmSynthetic
@Throws(PurchasesTransactionException::class)
suspend fun Purchases.awaitRestore(): CustomerInfo {
return suspendCoroutine { continuation ->
restorePurchasesWith(
onSuccess = { continuation.resume(it) },
onError = { continuation.resumeWithException(PurchasesException(it)) },
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package com.revenuecat.purchases

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.revenuecat.purchases.models.StoreProduct
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.utils.STUB_PRODUCT_IDENTIFIER
import com.revenuecat.purchases.utils.stubStoreProduct
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
Expand Down Expand Up @@ -229,4 +231,113 @@ internal class PurchasesCoroutinesCommonTest : BasePurchasesTest() {
}

// endregion

// region awaitRestore

@Test
fun `restore - Success`() = runTest {
every {
mockBillingAbstract.queryAllPurchases(
appUserId,
captureLambda(),
any()
)
} answers {
lambda<(List<StoreTransaction>) -> Unit>().captured.also {
it.invoke(listOf(mockk(relaxed = true)))
}
}

val result: CustomerInfo = purchases.awaitRestore()

verify(exactly = 1) {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
any(),
)
}
assertThat(result).isNotNull
}

@Test
fun `restore - Success - customer info matches expectations`() = runTest {
val afterRestoreCustomerInfo = mockk<CustomerInfo>()
val storeTransaction = mockk<StoreTransaction>(relaxed = true)
every {
mockPostReceiptHelper.postTransactionAndConsumeIfNeeded(
storeTransaction,
any(),
true,
appUserId,
PostReceiptInitiationSource.RESTORE,
onSuccess = captureLambda(),
any()
)
} answers {
lambda<(StoreTransaction, CustomerInfo) -> Unit>().captured.also {
it.invoke(storeTransaction, afterRestoreCustomerInfo)
}
}
every {
mockBillingAbstract.queryAllPurchases(
appUserId,
captureLambda(),
any()
)
} answers {
lambda<(List<StoreTransaction>) -> Unit>().captured.also {
it.invoke(listOf(storeTransaction))
}
}

val result: CustomerInfo = purchases.awaitRestore()

verify(exactly = 1) {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
any(),
)
}
assertThat(result).isNotNull
assertThat(result).isEqualTo(afterRestoreCustomerInfo)
}

@Test
fun `restore - CustomerInfoError`() = runTest {
val error = PurchasesError(PurchasesErrorCode.CustomerInfoError, "Customer info error")
every {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
onReceivePurchaseHistoryError = captureLambda(),
)
} answers {
lambda<(PurchasesError?) -> Unit>().captured.invoke(error)
}

var result: CustomerInfo? = null
var exception: Throwable? = null
runCatching {
result = purchases.awaitRestore()
}.onFailure {
exception = it
}

verify(exactly = 1) {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
any(),
)
}

assertThat(result).isNull()
assertThat(exception).isNotNull
assertThat(exception).isInstanceOf(PurchasesException::class.java)
assertThat((exception as PurchasesException).code).isEqualTo(PurchasesErrorCode.CustomerInfoError)
}

// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -245,115 +245,6 @@ internal class PurchasesCoroutinesTest : BasePurchasesTest() {

// endregion

// region awaitRestore

@Test
fun `restore - Success`() = runTest {
every {
mockBillingAbstract.queryAllPurchases(
appUserId,
captureLambda(),
any()
)
} answers {
lambda<(List<StoreTransaction>) -> Unit>().captured.also {
it.invoke(listOf(mockk(relaxed = true)))
}
}

val result: CustomerInfo = purchases.awaitRestore()

verify(exactly = 1) {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
any(),
)
}
assertThat(result).isNotNull
}

@Test
fun `restore - Success - customer info matches expectations`() = runTest {
val afterRestoreCustomerInfo = mockk<CustomerInfo>()
val storeTransaction = mockk<StoreTransaction>(relaxed = true)
every {
mockPostReceiptHelper.postTransactionAndConsumeIfNeeded(
storeTransaction,
any(),
true,
appUserId,
PostReceiptInitiationSource.RESTORE,
onSuccess = captureLambda(),
any()
)
} answers {
lambda<(StoreTransaction, CustomerInfo) -> Unit>().captured.also {
it.invoke(storeTransaction, afterRestoreCustomerInfo)
}
}
every {
mockBillingAbstract.queryAllPurchases(
appUserId,
captureLambda(),
any()
)
} answers {
lambda<(List<StoreTransaction>) -> Unit>().captured.also {
it.invoke(listOf(storeTransaction))
}
}

val result: CustomerInfo = purchases.awaitRestore()

verify(exactly = 1) {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
any(),
)
}
assertThat(result).isNotNull
assertThat(result).isEqualTo(afterRestoreCustomerInfo)
}

@Test
fun `restore - CustomerInfoError`() = runTest {
val error = PurchasesError(PurchasesErrorCode.CustomerInfoError, "Customer info error")
every {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
onReceivePurchaseHistoryError = captureLambda(),
)
} answers {
lambda<(PurchasesError?) -> Unit>().captured.invoke(error)
}

var result: CustomerInfo? = null
var exception: Throwable? = null
runCatching {
result = purchases.awaitRestore()
}.onFailure {
exception = it
}

verify(exactly = 1) {
mockBillingAbstract.queryAllPurchases(
appUserId,
any(),
any(),
)
}

assertThat(result).isNull()
assertThat(exception).isNotNull
assertThat(exception).isInstanceOf(PurchasesException::class.java)
assertThat((exception as PurchasesException).code).isEqualTo(PurchasesErrorCode.CustomerInfoError)
}

// endregion

// region awaitSyncPurchases

@Test
Expand Down

0 comments on commit 28ba6b0

Please sign in to comment.