Skip to content

Commit

Permalink
Feature/2.6.5 (#90)
Browse files Browse the repository at this point in the history
* implement preferredTimeout in paywalls and placements fetching
* internal improvements
* added new loadFallbackPaywalls method
  • Loading branch information
ren6 authored May 4, 2024
1 parent d164b49 commit a84b6fd
Show file tree
Hide file tree
Showing 15 changed files with 275 additions and 120 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.8.0"
ext.nav_version = '2.7.6'
repositories {
maven { url "https://plugins.gradle.org/m2/" }
google()
Expand Down
2 changes: 1 addition & 1 deletion demo/src/main/assets/apphud_paywalls_fallback.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{ertet
{
"data": {
"results": [
{
Expand Down
45 changes: 44 additions & 1 deletion demo/src/main/java/com/apphud/demo/ApphudApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import android.content.Context
import android.util.Log
import androidx.lifecycle.lifecycleScope
import com.apphud.sdk.Apphud
import com.apphud.sdk.ApphudError
import com.apphud.sdk.ApphudUtils
import com.apphud.sdk.client.ApiClient
import com.apphud.sdk.domain.ApphudPaywall
import com.apphud.sdk.domain.ApphudPlacement
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch

class ApphudApplication : Application() {
Expand All @@ -37,9 +40,49 @@ class ApphudApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
ApphudUtils.enableAllLogs()
ApphudUtils.enableDebugLogs()
}
// Apphud.invalidatePaywallsCache()
Apphud.start(this, API_KEY, observerMode = false)
Apphud.collectDeviceIdentifiers()

fetchPlacements()
}

fun fetchPlacements() {
Log.d("ApphudLogsDemo", "Fetching Placements Started")
Apphud.fetchPlacements { apphudPlacements, apphudError ->

val hasInternet = ApphudUtils.hasInternetConnection(this)
Log.d("ApphudLogsDemo", "Internet connected: $hasInternet")

if (apphudPlacements.isNotEmpty() && apphudError == null) {
Log.d("ApphudLogsDemo", "Placements are loaded, all good.")
} else if (apphudError?.billingErrorTitle() != null) {
Log.d("ApphudLogsDemo", "Placements are loaded, however there is Google Billing Issue (${apphudError.billingErrorTitle()}): ask user to sign in to Google Play and try again later.")
// Developer can retry fetchPlacements() immediately or after user taps "Try again" button in your custom UI.
fetchPlacements()
} else if (apphudError?.networkIssue() == true) {
Log.d("ApphudLogsDemo", "Failed to load placements due to Internet connection issue, ask user to connect to the Internet and try again later.")
// Developer can retry fetchPlacements() immediately or after user taps "Try again" button in your custom UI.
fetchPlacements()
} else {
// unknown or server-side error, try to load fallback paywalls
Apphud.loadFallbackPaywalls { paywalls, fallbackError ->
if (!paywalls.isNullOrEmpty() && fallbackError?.billingErrorTitle() == null) {
Log.d("ApphudLogsDemo", "Fallback paywalls are loaded from JSON, use them instead of placements")
// Grab the paywall and display it
} else if (fallbackError?.billingErrorTitle() != null) {
Log.d("ApphudLogsDemo", "Fallback paywalls are loaded, however there is Google Billing Issue (${fallbackError.billingErrorTitle()}): ask user to sign in to Google Play and try again later.")
// Developer can retry fetchPlacements() immediately or after user taps "Try again" button in your custom UI.
fetchPlacements()
} else {
Log.d("ApphudLogsDemo", "Fallback paywalls JSON is missing or invalid.")
// Developer can retry fetchPlacements() immediately or after user taps "Try again" button in your custom UI.
fetchPlacements()
}
}
}
}
}
}
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
sdkVersion=2.6.0
sdkVersion=2.6.5
46 changes: 34 additions & 12 deletions sdk/src/main/java/com/apphud/sdk/Apphud.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,15 @@ object Apphud {
* If you want to obtain placements without waiting for `ProductDetails`
* from Google Play, you can use `rawPlacements()` method.
*
* @param maxAttempts Number of request attempts before throwing an error. Must be between 1 and 10. Default value is 3.
* @param preferredTimeout The approximate duration, in seconds, after which the SDK will cease
* retry attempts to Apphud backend in case of failures and return an error.
* The default and minimum value for this parameter is 10.0 seconds.
* This parameter doesn't affect fetching products from Google Play.
* @return The list of `ApphudPlacement` objects.
*/
suspend fun placements(maxAttempts: Int? = null): List<ApphudPlacement> =
suspend fun placements(preferredTimeout: Double = APPHUD_DEFAULT_MAX_TIMEOUT): List<ApphudPlacement> =
suspendCancellableCoroutine { continuation ->
fetchPlacements(maxAttempts = maxAttempts) { _, _ ->
fetchPlacements(preferredTimeout = preferredTimeout) { _, _ ->
/* Error is not returned is suspending function.
If you want to handle error, use `fetchPlacements` method.
*/
Expand Down Expand Up @@ -184,14 +187,17 @@ object Apphud {
* an error will be returned along with the raw placements array.
* This allows for handling situations where partial data is available.
*
* @param maxAttempts Number of request attempts before throwing an error. Must be between 1 and 10. Default value is 3.
* @param preferredTimeout The approximate duration, in seconds, after which the SDK will cease
* retry attempts to Apphud backend in case of failures and return an error.
* The default and minimum value for this parameter is 10.0 seconds.
* This parameter doesn't affect fetching products from Google Play.
* @param callback The callback function that is invoked with the list of `ApphudPlacement` objects.
* Second parameter in callback represents optional error, which may be
* on Google (BillingClient issue) or Apphud side.
*
*/
fun fetchPlacements(maxAttempts: Int? = null, callback: (List<ApphudPlacement>, ApphudError?) -> Unit) {
ApphudInternal.performWhenOfferingsPrepared(maxAttempts = maxAttempts) { callback(ApphudInternal.placements, it) }
fun fetchPlacements(preferredTimeout: Double = APPHUD_DEFAULT_MAX_TIMEOUT, callback: (List<ApphudPlacement>, ApphudError?) -> Unit) {
ApphudInternal.performWhenOfferingsPrepared(preferredTimeout = preferredTimeout) { callback(ApphudInternal.placements, it) }
}
@Deprecated(
message = "This method has been renamed to fetchPlacements",
Expand Down Expand Up @@ -230,16 +236,19 @@ object Apphud {
*
* If you want to obtain paywalls without waiting for `ProductDetails` from
* Google Play, you can use `rawPaywalls()` method.
* @param maxAttempts Number of request attempts before throwing an error. Must be between 1 and 10. Default value is 3.
* @param preferredTimeout The approximate duration, in seconds, after which the SDK will cease
* retry attempts to Apphud backend in case of failures and return an error.
* The default and minimum value for this parameter is 10.0 seconds.
* This parameter doesn't affect fetching products from Google Play.
* @return The list of `ApphudPaywall` objects.
*/
@Deprecated(
"Deprecated in favor of Placements",
ReplaceWith("this.placements()"),
)
suspend fun paywalls(maxAttempts: Int? = null): List<ApphudPaywall> =
suspend fun paywalls(preferredTimeout: Double = APPHUD_DEFAULT_MAX_TIMEOUT): List<ApphudPaywall> =
suspendCancellableCoroutine { continuation ->
ApphudInternal.performWhenOfferingsPrepared(maxAttempts = maxAttempts) {
ApphudInternal.performWhenOfferingsPrepared(preferredTimeout = preferredTimeout) {
/* Error is not returned is suspending function.
If you want to handle error, use `paywallsDidLoadCallback` method. */
continuation.resume(ApphudInternal.paywalls)
Expand Down Expand Up @@ -288,7 +297,10 @@ object Apphud {
* an error will be returned along with the raw paywalls array.
* This allows for handling situations where partial data is available.
*
* @param maxAttempts Number of request attempts before throwing an error. Must be between 1 and 10. Default value is 3.
* @param preferredTimeout The approximate duration, in seconds, after which the SDK will cease
* retry attempts to Apphud backend in case of failures and return an error.
* The default and minimum value for this parameter is 10.0 seconds.
* This parameter doesn't affect fetching products from Google Play.
* @param callback The callback function that is invoked with the list of `ApphudPaywall` objects.
* Second parameter in callback represents optional error, which may be
* on Google (BillingClient issue) or Apphud side.
Expand All @@ -297,8 +309,8 @@ object Apphud {
"Deprecated in favor of Placements",
ReplaceWith("this.placementsDidLoadCallback(callback)"),
)
fun paywallsDidLoadCallback(maxAttempts: Int? = null, callback: (List<ApphudPaywall>, ApphudError?) -> Unit) {
ApphudInternal.performWhenOfferingsPrepared(maxAttempts = maxAttempts) { callback(ApphudInternal.paywalls, it) }
fun paywallsDidLoadCallback(preferredTimeout: Double = APPHUD_DEFAULT_MAX_TIMEOUT, callback: (List<ApphudPaywall>, ApphudError?) -> Unit) {
ApphudInternal.performWhenOfferingsPrepared(preferredTimeout = preferredTimeout) { callback(ApphudInternal.paywalls, it) }
}

/** Returns:
Expand Down Expand Up @@ -722,6 +734,16 @@ object Apphud {
return ApphudInternal.fallbackMode
}

/**
* Explicitly loads fallback paywalls from the json file, if it was added to the project assets.
* By default, SDK automatically tries to load paywalls from the JSON file, if possible.
* However, developer can also call this method directly for more control.
* For more details, visit https://docs.apphud.com/docs/paywalls#set-up-fallback-mode
*/
fun loadFallbackPaywalls(callback: (List<ApphudPaywall>?, ApphudError?) -> Unit) {
ApphudInternal.processFallbackData(callback)
}

/**
* Must be called before SDK initialization.
* Will make SDK to disregard cache and force refresh paywalls and placements.
Expand Down
11 changes: 6 additions & 5 deletions sdk/src/main/java/com/apphud/sdk/ApphudError.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ data class ApphudError(

companion object {
fun from(exception: Exception): ApphudError {
ApphudLog.log("Apphud Error from Exception: ${exception}")
var message = exception.message
var errorCode: Int? = null
if (exception.message == APPHUD_NO_TIME_TO_RETRY || (exception is InterruptedIOException)) {
Expand All @@ -32,7 +33,7 @@ data class ApphudError(
errorCode = APPHUD_ERROR_MAX_TIMEOUT_REACHED
}
} else {
errorCodeFrom(exception)
errorCode = errorCodeFrom(exception)
}

return ApphudError(message ?: "Undefined Error", null, errorCode)
Expand All @@ -53,7 +54,7 @@ data class ApphudError(
* Returns true if given error is due to Internet connectivity issues.
*/
fun networkIssue(): Boolean {
return errorCode == APPHUD_ERROR_NO_INTERNET || errorCode == APPHUD_ERROR_TIMEOUT
return errorCode == APPHUD_ERROR_NO_INTERNET
}

/*
Expand Down Expand Up @@ -102,6 +103,6 @@ const val APPHUD_NO_REQUEST = -998
const val APPHUD_PURCHASE_PENDING = -997
const val APPHUD_DEFAULT_RETRIES: Int = 3
const val APPHUD_INFINITE_RETRIES: Int = 999_999
const val APPHUD_DEFAULT_HTTP_TIMEOUT: Long = 7L
const val APPHUD_DEFAULT_HTTP_CONNECT_TIMEOUT: Long = 6L
const val APPHUD_DEFAULT_MAX_TIMEOUT: Long = 10L
const val APPHUD_DEFAULT_HTTP_TIMEOUT: Long = 6L
const val APPHUD_DEFAULT_HTTP_CONNECT_TIMEOUT: Long = 5L
const val APPHUD_DEFAULT_MAX_TIMEOUT: Double = 10.0
27 changes: 21 additions & 6 deletions sdk/src/main/java/com/apphud/sdk/ApphudInternal+Fallback.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.apphud.sdk

import android.content.Context
import android.content.SharedPreferences
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.apphud.sdk.client.ApiClient
import com.apphud.sdk.domain.ApphudUser
import com.apphud.sdk.domain.FallbackJsonObject
Expand Down Expand Up @@ -35,12 +36,12 @@ internal fun ApphudInternal.processFallbackError(request: Request, isTimeout: Bo
&& !processedFallbackData) {

if (fallbackHost?.withRemovedScheme() == request.url.host) {
processFallbackData()
processFallbackData { _, _ -> }
} else {
coroutineScope.launch {
tryFallbackHost()
if (fallbackHost == null || fallbackHost?.withRemovedScheme() == request.url.host) {
processFallbackData()
processFallbackData { _, _ -> }
}
}
}
Expand Down Expand Up @@ -70,7 +71,7 @@ fun isValidUrl(urlString: String): Boolean {
}
}

private fun ApphudInternal.processFallbackData() {
internal fun ApphudInternal.processFallbackData(callback: PaywallCallback) {
try {
if (currentUser == null) {
currentUser =
Expand All @@ -84,7 +85,7 @@ private fun ApphudInternal.processFallbackData() {
processedFallbackData = true

// read paywalls from cache
var ids = paywalls.map { it.products?.map { it.productId } ?: listOf() }.flatten()
var ids = getPaywalls().map { it.products?.map { it.productId } ?: listOf() }.flatten()
if (ids.isEmpty()) {
// read from json file
val jsonFileString = getJsonDataFromAsset(context, "apphud_paywalls_fallback.json")
Expand All @@ -96,24 +97,37 @@ private fun ApphudInternal.processFallbackData() {
cachePaywalls(paywallToParse)
}

if (ids.isEmpty()) { return }
if (ids.isEmpty()) {
val error = ApphudError("Invalid Paywalls Fallback File")
mainScope.launch {
callback(null, error)
}
return
}

fallbackMode = true
didRegisterCustomerAtThisLaunch = false
isRegisteringUser = false
ApphudLog.log("Fallback: ENABLED")
coroutineScope.launch {
fetchDetails(ids)
val responseCode = fetchDetails(ids)
val error = if (responseCode == BillingResponseCode.OK) null else (if (responseCode == APPHUD_NO_REQUEST) ApphudError("Paywalls load error", errorCode = responseCode) else ApphudError("Google Billing error", errorCode = responseCode))
mainScope.launch {
notifyLoadingCompleted(
customerLoaded = currentUser,
productDetailsLoaded = productDetails,
fromFallback = true,
fromCache = true
)
callback(getPaywalls(), error)
}
}
} catch (ex: Exception) {
val error = ApphudError("Fallback Mode Failed: ${ex.message}")
ApphudLog.logE("Fallback Mode Failed: ${ex.message}")
mainScope.launch {
callback(null, error)
}
}
}

Expand All @@ -132,6 +146,7 @@ private fun getJsonDataFromAsset(
}

internal fun ApphudInternal.disableFallback() {
if (currentUser?.isTemporary == true) { return }
fallbackMode = false
processedFallbackData = false
ApphudLog.log("Fallback: DISABLED")
Expand Down
Loading

0 comments on commit a84b6fd

Please sign in to comment.