-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #96 from superwall-me/develop
v1.0.1
- Loading branch information
Showing
16 changed files
with
767 additions
and
360 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ plugins { | |
id("maven-publish") | ||
} | ||
|
||
version = "1.0.0" | ||
version = "1.0.1" | ||
|
||
android { | ||
compileSdk = 33 | ||
|
37 changes: 37 additions & 0 deletions
37
superwall/src/main/java/com/superwall/sdk/billing/BillingClientParamBuilders.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package com.superwall.sdk.billing | ||
|
||
import com.android.billingclient.api.BillingClient | ||
import com.android.billingclient.api.QueryProductDetailsParams | ||
import com.android.billingclient.api.QueryPurchaseHistoryParams | ||
import com.android.billingclient.api.QueryPurchasesParams | ||
|
||
internal fun @receiver:BillingClient.ProductType String.buildQueryPurchaseHistoryParams(): QueryPurchaseHistoryParams? { | ||
return when (this) { | ||
BillingClient.ProductType.INAPP, | ||
BillingClient.ProductType.SUBS, | ||
-> QueryPurchaseHistoryParams.newBuilder().setProductType(this).build() | ||
else -> null | ||
} | ||
} | ||
|
||
internal fun @receiver:BillingClient.ProductType String.buildQueryPurchasesParams(): QueryPurchasesParams? { | ||
return when (this) { | ||
BillingClient.ProductType.INAPP, | ||
BillingClient.ProductType.SUBS, | ||
-> QueryPurchasesParams.newBuilder().setProductType(this).build() | ||
else -> null | ||
} | ||
} | ||
|
||
internal fun @receiver:BillingClient.ProductType String.buildQueryProductDetailsParams( | ||
productIds: Set<String>, | ||
): QueryProductDetailsParams { | ||
val productList = productIds.map { productId -> | ||
QueryProductDetailsParams.Product.newBuilder() | ||
.setProductId(productId) | ||
.setProductType(this) | ||
.build() | ||
} | ||
return QueryProductDetailsParams.newBuilder() | ||
.setProductList(productList).build() | ||
} |
166 changes: 166 additions & 0 deletions
166
superwall/src/main/java/com/superwall/sdk/billing/BillingClientUseCase.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package com.superwall.sdk.billing | ||
|
||
import com.android.billingclient.api.BillingClient | ||
import com.android.billingclient.api.BillingResult | ||
import com.superwall.sdk.logger.LogLevel | ||
import com.superwall.sdk.logger.LogScope | ||
import com.superwall.sdk.logger.Logger | ||
import java.io.PrintWriter | ||
import java.io.StringWriter | ||
import kotlin.math.min | ||
|
||
internal typealias ExecuteRequestOnUIThreadFunction = (delayInMillis: Long, onError: (BillingError?) -> Unit) -> Unit | ||
|
||
private const val MAX_RETRIES_DEFAULT = 3 | ||
private const val RETRY_TIMER_START_MILLISECONDS = 878L // So it gets close to 15 minutes in last retry | ||
internal const val RETRY_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L // 15 minutes | ||
|
||
internal interface UseCaseParams { | ||
val appInBackground: Boolean | ||
} | ||
|
||
/** | ||
* A superclass that is used to interact with the billing client. It coordinates the request on the | ||
* UI thread. Deals with error handling such as retries and returning errors. | ||
*/ | ||
internal abstract class BillingClientUseCase<T>( | ||
private val useCaseParams: UseCaseParams, | ||
private val onError: (BillingError) -> Unit, | ||
val executeRequestOnUIThread: ExecuteRequestOnUIThreadFunction, | ||
) { | ||
protected open val backoffForNetworkErrors = false | ||
|
||
private val maxRetries: Int = MAX_RETRIES_DEFAULT | ||
private var retryAttempt: Int = 0 | ||
private var retryBackoffMilliseconds = RETRY_TIMER_START_MILLISECONDS | ||
|
||
fun run( | ||
delayMilliseconds: Long = 0, | ||
) { | ||
executeRequestOnUIThread(delayMilliseconds) { connectionError -> | ||
if (connectionError == null) { | ||
this.executeAsync() | ||
} else { | ||
onError(connectionError) | ||
} | ||
} | ||
} | ||
|
||
abstract fun executeAsync() | ||
abstract fun onOk(received: T) | ||
|
||
fun processResult( | ||
billingResult: BillingResult, | ||
response: T, | ||
onSuccess: (T) -> Unit = ::onOk, | ||
onError: (BillingResult) -> Unit = ::forwardError, | ||
) { | ||
when (billingResult.responseCode) { | ||
BillingClient.BillingResponseCode.OK -> { | ||
retryBackoffMilliseconds = RETRY_TIMER_START_MILLISECONDS | ||
onSuccess(response) | ||
} | ||
|
||
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> { | ||
Logger.debug( | ||
logLevel = LogLevel.error, | ||
scope = LogScope.productsManager, | ||
message = "Billing Service disconnected." | ||
) | ||
run() | ||
} | ||
|
||
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> { | ||
backoffOrErrorIfUseInSession(onError, billingResult) | ||
} | ||
|
||
BillingClient.BillingResponseCode.NETWORK_ERROR, | ||
BillingClient.BillingResponseCode.ERROR, | ||
-> { | ||
backoffOrRetryNetworkError(onError, billingResult) | ||
} | ||
|
||
else -> { | ||
onError(billingResult) | ||
} | ||
} | ||
} | ||
|
||
protected fun BillingClient?.withConnectedClient(receivingFunction: BillingClient.() -> Unit) { | ||
this?.takeIf { it.isReady }?.let { | ||
it.receivingFunction() | ||
} ?: Logger.debug( | ||
logLevel = LogLevel.warn, | ||
scope = LogScope.productsManager, | ||
message = "Billing Service disconnected. Stack trace: ${getStackTrace()}" | ||
) | ||
} | ||
|
||
private fun getStackTrace(): String { | ||
val stringWriter = StringWriter() | ||
val printWriter = PrintWriter(stringWriter) | ||
Throwable().printStackTrace(printWriter) | ||
return stringWriter.toString() | ||
} | ||
|
||
private fun forwardError(billingResult: BillingResult) { | ||
val underlyingErrorMessage = "Error loading products - DebugMessage: ${billingResult.debugMessage} " + | ||
"ErrorCode: ${billingResult.responseCode}." | ||
val error = BillingError.BillingNotAvailable(underlyingErrorMessage) | ||
Logger.debug( | ||
logLevel = LogLevel.error, | ||
scope = LogScope.productsManager, | ||
message = underlyingErrorMessage | ||
) | ||
onError(error) | ||
} | ||
|
||
private fun backoffOrRetryNetworkError( | ||
onError: (BillingResult) -> Unit, | ||
billingResult: BillingResult, | ||
) { | ||
if (backoffForNetworkErrors && retryBackoffMilliseconds < RETRY_TIMER_MAX_TIME_MILLISECONDS) { | ||
retryWithBackoff() | ||
} else if (!backoffForNetworkErrors && retryAttempt < maxRetries) { | ||
retryAttempt++ | ||
executeAsync() | ||
} else { | ||
onError(billingResult) | ||
} | ||
} | ||
|
||
private fun backoffOrErrorIfUseInSession( | ||
onError: (BillingResult) -> Unit, | ||
billingResult: BillingResult, | ||
) { | ||
if (useCaseParams.appInBackground) { | ||
Logger.debug( | ||
logLevel = LogLevel.warn, | ||
scope = LogScope.productsManager, | ||
message = "Billing is unavailable. App is in background, will retry with backoff." | ||
) | ||
if (retryBackoffMilliseconds < RETRY_TIMER_MAX_TIME_MILLISECONDS) { | ||
retryWithBackoff() | ||
} else { | ||
onError(billingResult) | ||
} | ||
} else { | ||
Logger.debug( | ||
logLevel = LogLevel.error, | ||
scope = LogScope.productsManager, | ||
message = "Billing is unavailable. App is in foreground. Won't retry." | ||
) | ||
onError(billingResult) | ||
} | ||
} | ||
|
||
private fun retryWithBackoff() { | ||
retryBackoffMilliseconds.let { currentDelayMilliseconds -> | ||
retryBackoffMilliseconds = min( | ||
retryBackoffMilliseconds * 2, | ||
RETRY_TIMER_MAX_TIME_MILLISECONDS, | ||
) | ||
run(currentDelayMilliseconds) | ||
} | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
superwall/src/main/java/com/superwall/sdk/billing/BillingError.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package com.superwall.sdk.billing | ||
|
||
sealed class BillingError(val code: Int, val description: String) : Exception(description) { | ||
object UnknownError : BillingError(0, "Unknown error.") | ||
object IllegalStateException : BillingError(1, "IllegalStateException when connecting to billing client") | ||
// Define a class for custom errors where you can pass a message | ||
class BillingNotAvailable(description: String) : BillingError(2, description) | ||
} |
32 changes: 32 additions & 0 deletions
32
superwall/src/main/java/com/superwall/sdk/billing/DecomposedProductIds.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.superwall.sdk.billing | ||
|
||
import com.superwall.sdk.store.abstractions.product.OfferType | ||
|
||
data class DecomposedProductIds( | ||
val subscriptionId: String, | ||
val basePlanId: String?, | ||
val offerType: OfferType?, | ||
val fullId: String | ||
) { | ||
companion object { | ||
fun from(productId: String): DecomposedProductIds { | ||
val components = productId.split(":") | ||
val subscriptionId = components.getOrNull(0) ?: "" | ||
val basePlanId = components.getOrNull(1) | ||
val offerId = components.getOrNull(2) | ||
var offerType: OfferType? = null | ||
|
||
if (offerId == "sw-auto") { | ||
offerType = OfferType.Auto | ||
} else if (offerId != null) { | ||
offerType = OfferType.Offer(id = offerId) | ||
} | ||
return DecomposedProductIds( | ||
subscriptionId = subscriptionId, | ||
basePlanId = basePlanId, | ||
offerType = offerType, | ||
fullId = productId | ||
) | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
superwall/src/main/java/com/superwall/sdk/billing/GetStoreProductsCallback.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package com.superwall.sdk.billing | ||
|
||
import com.superwall.sdk.store.abstractions.product.StoreProduct | ||
|
||
interface GetStoreProductsCallback { | ||
/** | ||
* Will be called after products have been fetched successfully | ||
* | ||
* @param [storeProducts] The list of [StoreProduct] that have been able to be successfully fetched from the store. | ||
* Not found products will be ignored. | ||
*/ | ||
@JvmSuppressWildcards | ||
fun onReceived(storeProducts: Set<StoreProduct>) | ||
|
||
/** | ||
* Will be called after the purchase has completed with error | ||
* | ||
* @param error A [Error] containing the reason for the failure when fetching the [StoreProduct] | ||
*/ | ||
fun onError(error: BillingError) | ||
} |
Oops, something went wrong.