Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Feature/Add support for loading webarchive files from manifest #105

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[versions]
billing_version = "6.1.0"
browser_version = "1.5.0"
webkit = "1.11.0"
gradle_plugin_version = "7.4.2"
mockk = "1.13.8"
revenue_cat_version = "7.7.1"
Expand Down Expand Up @@ -33,6 +34,7 @@ revenue_cat = { module = "com.revenuecat.purchases:purchases", version.ref = "re

# Browser
browser = { module = "androidx.browser:browser", version.ref = "browser_version" }
webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }

# Compose
compose_bom = { module = "androidx.compose:compose-bom", version.ref = "compose_version" }
Expand Down
2 changes: 1 addition & 1 deletion superwall/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ dependencies {

// Browser
implementation(libs.browser)

implementation(libs.webkit)

// Core
implementation(libs.core)
Expand Down
49 changes: 39 additions & 10 deletions superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.superwall.sdk.config

import android.content.Context
import android.webkit.WebView
import com.superwall.sdk.billing.GoogleBillingWrapper
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.config.models.getConfig
import com.superwall.sdk.config.options.SuperwallOptions
Expand All @@ -17,6 +16,8 @@ import com.superwall.sdk.misc.awaitFirstValidConfig
import com.superwall.sdk.models.assignment.AssignmentPostback
import com.superwall.sdk.models.assignment.ConfirmableAssignment
import com.superwall.sdk.models.config.Config
import com.superwall.sdk.models.config.WebArchiveManifest
import com.superwall.sdk.models.paywall.Paywall
import com.superwall.sdk.models.triggers.Experiment
import com.superwall.sdk.models.triggers.ExperimentID
import com.superwall.sdk.models.triggers.Trigger
Expand All @@ -27,14 +28,18 @@ import com.superwall.sdk.paywall.request.ResponseIdentifiers
import com.superwall.sdk.storage.DisableVerboseEvents
import com.superwall.sdk.storage.Storage
import com.superwall.sdk.store.StoreKitManager
import com.superwall.sdk.webarchive.archive.WebArchiveLibrary
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch

// TODO: Re-enable those params
Expand All @@ -45,9 +50,11 @@ open class ConfigManager(
private val network: Network,
options: SuperwallOptions? = null,
private val paywallManager: PaywallManager,
private val webArchiveLibrary: WebArchiveLibrary,
private val factory: Factory
) {
interface Factory: RequestFactory, DeviceInfoFactory, RuleAttributesFactory {}
interface Factory : RequestFactory, DeviceInfoFactory, RuleAttributesFactory {}

var options = SuperwallOptions()

// The configuration of the Superwall dashboard
Expand Down Expand Up @@ -127,7 +134,17 @@ open class ConfigManager(

// TODO: Re-enable those params
// storeKitManager.loadPurchasedProducts()
CoroutineScope(Dispatchers.IO).launch { preloadPaywalls() }
with(CoroutineScope(Dispatchers.IO)) {
launch {
cachePaywallsFromManifest(config.paywalls)
}
launch {
//Preload paywalls that do not need to be archived
preloadPaywalls(excludeIds = config.paywalls.filter {
it.manifest != null && it.manifest.use != WebArchiveManifest.Usage.NEVER
}.map { it.identifier })
}
}
} catch (e: Throwable) {
configState.emit(Result.Failure(e))
Logger.debug(
Expand Down Expand Up @@ -223,17 +240,28 @@ open class ConfigManager(
}


private suspend fun cachePaywallsFromManifest(paywalls: List<Paywall>) {
paywalls
.filter {
it.manifest != null
}
.forEach {
webArchiveLibrary.downloadManifest(it.identifier, it.url.toString(), it.manifest!!)
}
}

// Preloads paywalls.
private suspend fun preloadPaywalls() {
private suspend fun preloadPaywalls(excludeIds: List<String> = emptyList()) {
if (!options.paywalls.shouldPreload) return
preloadAllPaywalls()
preloadAllPaywalls(excludeIds)
}

// Preloads paywalls referenced by triggers.
suspend fun preloadAllPaywalls() {
suspend fun preloadAllPaywalls(excludeIds: List<String> = emptyList()) {
if (currentPreloadingTask != null) {
return
}

currentPreloadingTask = CoroutineScope(Dispatchers.IO).launch {
val config = configState.awaitFirstValidConfig() ?: return@launch

Expand All @@ -253,7 +281,8 @@ open class ConfigManager(
unconfirmedAssignments = unconfirmedAssignments,
expressionEvaluator = expressionEvaluator
)
preloadPaywalls(paywallIdentifiers = paywallIds)
preloadPaywallsWithIds(paywallIdentifiers = paywallIds.filterNot { excludeIds.contains(it) }
.toSet())

currentPreloadingTask = null
}
Expand All @@ -264,11 +293,11 @@ open class ConfigManager(
val config = configState.awaitFirstValidConfig() ?: return
val triggersToPreload = config.triggers.filter { eventNames.contains(it.eventName) }
val triggerPaywallIdentifiers = getTreatmentPaywallIds(triggersToPreload.toSet())
preloadPaywalls(triggerPaywallIdentifiers)
preloadPaywallsWithIds(triggerPaywallIdentifiers)
}

// Preloads paywalls referenced by triggers.
private suspend fun preloadPaywalls(paywallIdentifiers: Set<String>) {
private suspend fun preloadPaywallsWithIds(paywallIdentifiers: Set<String>) {
val webviewExists = WebView.getCurrentWebViewPackage() != null
if (webviewExists) {
coroutineScope {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ import com.superwall.sdk.store.StoreKitManager
import com.superwall.sdk.store.abstractions.transactions.GoogleBillingPurchaseTransaction
import com.superwall.sdk.store.abstractions.transactions.StoreTransaction
import com.superwall.sdk.store.transactions.TransactionManager
import com.superwall.sdk.webarchive.ManifestDownloader
import com.superwall.sdk.webarchive.archive.ArchiveCompressor
import com.superwall.sdk.webarchive.archive.WebArchiveLibrary
import com.superwall.sdk.webarchive.archive.Base64ArchiveEncoder
import com.superwall.sdk.webarchive.archive.CachedArchiveLibrary
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -95,6 +100,7 @@ class DependencyContainer(
var storeKitManager: StoreKitManager
val transactionManager: TransactionManager
val googleBillingWrapper: GoogleBillingWrapper
val webArchiveLibrary: WebArchiveLibrary

init {
// TODO: Add delegate adapter
Expand Down Expand Up @@ -142,14 +148,26 @@ class DependencyContainer(
factory = this,
)

val manifestDownloader = ManifestDownloader(
coroutineScope = CoroutineScope(Dispatchers.IO),
network = network,
)

webArchiveLibrary = CachedArchiveLibrary(
storage = storage,
manifestDownloader = manifestDownloader,
archiveCompressor = ArchiveCompressor(encoder = Base64ArchiveEncoder())
)

configManager = ConfigManager(
context = context,
storeKitManager = storeKitManager,
storage = storage,
network = network,
options = options,
factory = this,
paywallManager = paywallManager
paywallManager = paywallManager,
webArchiveLibrary = webArchiveLibrary,
)

api = Api(networkEnvironment = configManager.options.networkEnvironment)
Expand Down Expand Up @@ -278,7 +296,8 @@ class DependencyContainer(
paywallManager = paywallManager,
storage = storage,
webView = webView,
eventDelegate = Superwall.instance
eventDelegate = Superwall.instance,
webArchiveLibrary = webArchiveLibrary
)
webView.delegate = paywallViewController
messageHandler.delegate = paywallViewController
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ enum class LogScope {
paywallViewController,
nativePurchaseController,
cache,
webarchive,
all;

override fun toString(): String {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.superwall.sdk.models.config

import com.superwall.sdk.models.serialization.URLSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URL

@Serializable
data class WebArchiveManifest(
@SerialName("use") val use: Usage,
@SerialName("document") val document: Document,
@SerialName("resources") val resources: List<Resource>
) {
sealed interface ManifestPart {
val url: URL
val mimeType: String
}

@Serializable
enum class Usage {
@SerialName("IF_AVAILABLE_ON_PAYWALL_OPEN")
IF_AVAILABLE_ON_PAYWALL_OPEN,

@SerialName("NEVER")
NEVER,

@SerialName("ALWAYS")
ALWAYS
}

@Serializable
data class Document(
@SerialName("url")
override val url: @Serializable(with = URLSerializer::class) URL,
@SerialName("mime_type")
override val mimeType: String
) : ManifestPart

@Serializable
data class Resource(
@SerialName("url")
override val url: @Serializable(with = URLSerializer::class) URL,
@SerialName("mime_type")
override val mimeType: String
) : ManifestPart
}
43 changes: 28 additions & 15 deletions superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package com.superwall.sdk.models.paywall

import ComputedPropertyRequest
import android.graphics.Color
import com.superwall.sdk.config.models.OnDeviceCaching
import com.superwall.sdk.config.models.Survey
import com.superwall.sdk.dependencies.TriggerSessionManagerFactory
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.logger.Logger
import com.superwall.sdk.models.SerializableEntity
import com.superwall.sdk.models.config.FeatureGatingBehavior
import com.superwall.sdk.models.config.WebArchiveManifest
import com.superwall.sdk.models.events.EventData
import com.superwall.sdk.models.product.Product
import com.superwall.sdk.models.product.ProductItem
import com.superwall.sdk.models.product.ProductItemsDeserializer
import com.superwall.sdk.models.product.ProductType
import com.superwall.sdk.models.product.ProductVariable
import com.superwall.sdk.models.serialization.DateSerializer
import com.superwall.sdk.models.serialization.URLSerializer
Expand All @@ -17,17 +25,10 @@ import com.superwall.sdk.paywall.presentation.PaywallInfo
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.net.URL
import java.util.*
import android.graphics.Color
import com.superwall.sdk.logger.LogLevel
import com.superwall.sdk.logger.LogScope
import com.superwall.sdk.logger.Logger
import com.superwall.sdk.models.product.ProductItem
import com.superwall.sdk.models.product.ProductItemsDeserializer
import com.superwall.sdk.models.product.ProductType
import java.util.Date

@Serializable
data class Paywalls(val paywalls: List<Paywall>): SerializableEntity
data class Paywalls(val paywalls: List<Paywall>) : SerializableEntity

@Serializable
data class Paywall(
Expand Down Expand Up @@ -85,7 +86,7 @@ data class Paywall(

/**
* Indicates whether the caching of the paywall is enabled or not.
*/
*/
var onDeviceCache: OnDeviceCaching = OnDeviceCaching.Disabled,

@kotlinx.serialization.Transient()
Expand All @@ -98,19 +99,24 @@ data class Paywall(
var closeReason: PaywallCloseReason = PaywallCloseReason.None,

/**
Surveys to potentially show when an action happens in the paywall.
Surveys to potentially show when an action happens in the paywall.
*/
var surveys: List<Survey> = emptyList()
var surveys: List<Survey> = emptyList(),

) : SerializableEntity {
// Manifest for webarchive files
@SerialName("manifest") val manifest: WebArchiveManifest? = null,


) : SerializableEntity {
// Public getter for productItems
var productItems: List<ProductItem>
get() = _productItems
set(value) {
_productItems = value
// Automatically update related properties when productItems is set
productIds = value.map { it.fullProductId }
_products = makeProducts(value) // Assuming makeProducts is a function that generates products based on product items
_products =
makeProducts(value) // Assuming makeProducts is a function that generates products based on product items
}

// Public getter for products to allow access but not direct modification
Expand Down Expand Up @@ -207,9 +213,11 @@ data class Paywall(
"primary" -> output.add(
Product(type = ProductType.PRIMARY, id = productItem.fullProductId)
)

"secondary" -> output.add(
Product(type = ProductType.SECONDARY, id = productItem.fullProductId)
)

"tertiary" -> output.add(
Product(type = ProductType.TERTIARY, id = productItem.fullProductId)
)
Expand Down Expand Up @@ -244,7 +252,12 @@ data class Paywall(
paywalljsVersion = "",
isFreeTrialAvailable = false,
featureGating = FeatureGatingBehavior.NonGated,
localNotifications = arrayListOf()
localNotifications = arrayListOf(),
manifest = WebArchiveManifest(
WebArchiveManifest.Usage.ALWAYS,
WebArchiveManifest.Document(URL("http://google.com"), "text/html"),
emptyList()
)
)

}
Expand Down
Loading