diff --git a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt
index 9158a883..035ea812 100644
--- a/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt
+++ b/superwall/src/main/java/com/superwall/sdk/config/ConfigManager.kt
@@ -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
@@ -34,7 +33,10 @@ 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
@@ -47,7 +49,8 @@ open class ConfigManager(
     private val paywallManager: PaywallManager,
     private val factory: Factory
 ) {
-    interface Factory: RequestFactory, DeviceInfoFactory, RuleAttributesFactory {}
+    interface Factory : RequestFactory, DeviceInfoFactory, RuleAttributesFactory {}
+
     var options = SuperwallOptions()
 
     // The configuration of the Superwall dashboard
@@ -289,15 +292,19 @@ open class ConfigManager(
                             presentationSourceType = null,
                             retryCount = 6
                         )
-                        try {
-                            paywallManager.getPaywallViewController(
-                                request = request,
-                                isForPresentation = true,
-                                isPreloading = true,
-                                delegate = null
-                            )
-                        } catch (e: Exception) {
-                            // Handle exception
+                        val shouldSkip = paywallManager
+                            .preloadViaPaywallArchivalAndShouldSkipViewControllerCache(request = request)
+                        if (!shouldSkip) {
+                            try {
+                                paywallManager.getPaywallViewController(
+                                    request = request,
+                                    isForPresentation = true,
+                                    isPreloading = true,
+                                    delegate = null
+                                )
+                            } catch (e: Exception) {
+                                // Handle exception
+                            }
                         }
                     }
                     tasks.add(task)
diff --git a/superwall/src/main/java/com/superwall/sdk/debug/DebugViewController.kt b/superwall/src/main/java/com/superwall/sdk/debug/DebugViewController.kt
index 5f490f45..b2cabc2b 100644
--- a/superwall/src/main/java/com/superwall/sdk/debug/DebugViewController.kt
+++ b/superwall/src/main/java/com/superwall/sdk/debug/DebugViewController.kt
@@ -440,7 +440,8 @@ class DebugViewController(
         val paywallVc = factory.makePaywallViewController(
             paywall = paywall,
             cache = null,
-            delegate = null
+            delegate = null,
+            paywallArchivalManager = null
         )
         previewContainerView.addView(paywallVc)
         previewViewContent = paywallVc
diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt
index da45f7da..8cc9f502 100644
--- a/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt
+++ b/superwall/src/main/java/com/superwall/sdk/dependencies/DependencyContainer.kt
@@ -24,9 +24,9 @@ import com.superwall.sdk.delegate.SuperwallDelegateAdapter
 import com.superwall.sdk.delegate.subscription_controller.PurchaseController
 import com.superwall.sdk.identity.IdentityInfo
 import com.superwall.sdk.identity.IdentityManager
-import com.superwall.sdk.misc.CurrentActivityTracker
 import com.superwall.sdk.misc.ActivityProvider
 import com.superwall.sdk.misc.AppLifecycleObserver
+import com.superwall.sdk.misc.CurrentActivityTracker
 import com.superwall.sdk.models.config.FeatureFlags
 import com.superwall.sdk.models.events.EventData
 import com.superwall.sdk.models.paywall.Paywall
@@ -35,6 +35,7 @@ import com.superwall.sdk.network.Api
 import com.superwall.sdk.network.Network
 import com.superwall.sdk.network.device.DeviceHelper
 import com.superwall.sdk.network.device.DeviceInfo
+import com.superwall.sdk.paywall.archival.PaywallArchivalManager
 import com.superwall.sdk.paywall.manager.PaywallManager
 import com.superwall.sdk.paywall.manager.PaywallViewControllerCache
 import com.superwall.sdk.paywall.presentation.internal.PresentationRequest
@@ -52,6 +53,7 @@ import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandler
 import com.superwall.sdk.paywall.vc.web_view.templating.models.JsonVariables
 import com.superwall.sdk.paywall.vc.web_view.templating.models.Variables
 import com.superwall.sdk.storage.EventsQueue
+import com.superwall.sdk.storage.SearchPathDirectory
 import com.superwall.sdk.storage.Storage
 import com.superwall.sdk.store.InternalPurchaseController
 import com.superwall.sdk.store.StoreKitManager
@@ -76,7 +78,8 @@ class DependencyContainer(
     StoreTransactionFactory, Storage.Factory, InternalSuperwallEvent.PresentationRequest.Factory,
     ViewControllerFactory, PaywallManager.Factory, OptionsFactory, TriggerFactory,
     TransactionVerifierFactory, TransactionManager.Factory, PaywallViewController.Factory,
-    ConfigManager.Factory, AppSessionManager.Factory, DebugViewController.Factory {
+    ConfigManager.Factory, AppSessionManager.Factory, DebugViewController.Factory,
+    PaywallArchivalManagerFactory {
 
     var network: Network
     override lateinit var api: Api
@@ -95,7 +98,8 @@ class DependencyContainer(
     var storeKitManager: StoreKitManager
     val transactionManager: TransactionManager
     val googleBillingWrapper: GoogleBillingWrapper
-
+    val paywallArchivalManager: PaywallArchivalManager =
+        PaywallArchivalManager(baseDirectory = SearchPathDirectory.USER_SPECIFIC_DOCUMENTS.fileDirectory(context))
     init {
         // TODO: Add delegate adapter
 
@@ -247,6 +251,7 @@ class DependencyContainer(
     override suspend fun makePaywallViewController(
         paywall: Paywall,
         cache: PaywallViewControllerCache?,
+        paywallArchivalManager: PaywallArchivalManager?,
         delegate: PaywallViewControllerDelegateAdapter?
     ): PaywallViewController {
         return withContext(Dispatchers.Main) {
@@ -278,7 +283,8 @@ class DependencyContainer(
                 paywallManager = paywallManager,
                 storage = storage,
                 webView = webView,
-                eventDelegate = Superwall.instance
+                eventDelegate = Superwall.instance,
+                paywallArchivalManager = paywallArchivalManager
             )
             webView.delegate = paywallViewController
             messageHandler.delegate = paywallViewController
@@ -500,4 +506,9 @@ class DependencyContainer(
     override suspend fun makeTriggers(): Set<String> {
         return configManager.triggersByEventName.keys
     }
+
+    override fun makePaywallArchivalManager(): PaywallArchivalManager {
+        return this.paywallArchivalManager
+    }
+
 }
\ No newline at end of file
diff --git a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt
index 21e1d814..a9dbc0d6 100644
--- a/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt
+++ b/superwall/src/main/java/com/superwall/sdk/dependencies/FactoryProtocols.kt
@@ -20,6 +20,7 @@ import com.superwall.sdk.models.product.ProductVariable
 import com.superwall.sdk.network.Api
 import com.superwall.sdk.network.device.DeviceHelper
 import com.superwall.sdk.network.device.DeviceInfo
+import com.superwall.sdk.paywall.archival.PaywallArchivalManager
 import com.superwall.sdk.paywall.manager.PaywallViewControllerCache
 import com.superwall.sdk.paywall.presentation.internal.PresentationRequest
 import com.superwall.sdk.paywall.presentation.internal.PresentationRequestType
@@ -133,6 +134,7 @@ interface ViewControllerFactory {
     suspend fun makePaywallViewController(
         paywall: Paywall,
         cache: PaywallViewControllerCache?,
+        paywallArchivalManager: PaywallArchivalManager?,
         delegate: PaywallViewControllerDelegateAdapter?
     ): PaywallViewController
 
@@ -157,6 +159,9 @@ interface ViewControllerFactory {
 //    fun makeCache(): PaywallViewControllerCache
 //}
 
+interface PaywallArchivalManagerFactory {
+    fun makePaywallArchivalManager(): PaywallArchivalManager
+}
 
 interface CacheFactory {
     fun makeCache(): PaywallViewControllerCache
diff --git a/superwall/src/main/java/com/superwall/sdk/misc/RequestCoalescence.kt b/superwall/src/main/java/com/superwall/sdk/misc/RequestCoalescence.kt
new file mode 100644
index 00000000..79de0198
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/misc/RequestCoalescence.kt
@@ -0,0 +1,24 @@
+package com.superwall.sdk.misc
+
+import kotlinx.coroutines.CompletableDeferred
+import java.util.concurrent.ConcurrentHashMap
+
+class RequestCoalescence<Input : Any, Output> {
+    private val tasks = ConcurrentHashMap<Input, CompletableDeferred<Output>>()
+
+    suspend fun get(input: Input, request: suspend (Input) -> Output): Output {
+        val existingTask = tasks[input]
+        return if (existingTask != null) {
+            // If there's already a task in progress, wait for it to finish
+            existingTask.await()
+        } else {
+            // Start a new task if one isn't already in progress
+            val newTask = CompletableDeferred<Output>()
+            tasks[input] = newTask
+            val output = request(input)
+            newTask.complete(output)
+            tasks.remove(input)
+            output
+        }
+    }
+}
\ No newline at end of file
diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt
index 22b4dc2e..2adbd40c 100644
--- a/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt
+++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/Paywall.kt
@@ -100,7 +100,11 @@ data class Paywall(
     /**
      Surveys to potentially show when an action happens in the paywall.
      */
-    var surveys: List<Survey> = emptyList()
+    var surveys: List<Survey> = emptyList(),
+
+    // A listing of all the files referenced in a paywall
+    // to be able to preload the whole paywall into a web archive
+    val manifest: ArchivalManifest? = null
 
 ) : SerializableEntity {
     // Public getter for productItems
@@ -244,7 +248,8 @@ data class Paywall(
                 paywalljsVersion = "",
                 isFreeTrialAvailable = false,
                 featureGating = FeatureGatingBehavior.NonGated,
-                localNotifications = arrayListOf()
+                localNotifications = arrayListOf(),
+                manifest = null
             )
 
         }
diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallManifest.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallManifest.kt
new file mode 100644
index 00000000..07cc2503
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/PaywallManifest.kt
@@ -0,0 +1,23 @@
+package com.superwall.sdk.models.paywall
+
+import com.superwall.sdk.models.serialization.URLSerializer
+import kotlinx.serialization.Serializable
+import java.net.URL
+
+@Serializable
+enum class ArchivalManifestUsage {
+    ALWAYS, NEVER, IF_AVAILABLE_ON_PAYWALL_OPEN
+}
+
+@Serializable
+data class ArchivalManifest(
+    val use: ArchivalManifestUsage,
+    val document: ArchivalManifestItem,
+    val resources: List<ArchivalManifestItem>
+)
+
+@Serializable
+data class ArchivalManifestItem(
+    val url: @Serializable(with = URLSerializer::class) URL,
+    val mimeType: String
+)
diff --git a/superwall/src/main/java/com/superwall/sdk/models/paywall/WebArchive.kt b/superwall/src/main/java/com/superwall/sdk/models/paywall/WebArchive.kt
new file mode 100644
index 00000000..81c00c38
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/models/paywall/WebArchive.kt
@@ -0,0 +1,14 @@
+package com.superwall.sdk.models.paywall
+
+import java.net.URL
+
+data class WebArchive(
+    val mainResource: WebArchiveResource,
+    val subResources: List<WebArchiveResource>
+)
+
+class WebArchiveResource(
+    val url: URL,
+    val data: ByteArray,
+    val mimeType: String
+)
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/archival/PaywallArchivalManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/archival/PaywallArchivalManager.kt
new file mode 100644
index 00000000..cd3a0aa1
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/archival/PaywallArchivalManager.kt
@@ -0,0 +1,77 @@
+package com.superwall.sdk.paywall.archival
+
+import com.superwall.sdk.misc.Result
+import com.superwall.sdk.models.paywall.ArchivalManifestUsage
+import com.superwall.sdk.models.paywall.Paywall
+import com.superwall.sdk.models.paywall.WebArchive
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import java.io.File
+
+class PaywallArchivalManager(
+    private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+    private val ioCoroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
+    private var webArchiveManager: WebArchiveManager? = null,
+    baseDirectory: File? = null,
+) {
+
+    init {
+        if (webArchiveManager == null && baseDirectory != null) {
+            webArchiveManager = WebArchiveManager(baseDirectory = baseDirectory.resolve("paywalls"))
+        }
+    }
+
+    fun preloadArchiveAndShouldSkipViewControllerCache(paywall: Paywall): Boolean {
+        webArchiveManager?.let { webArchiveManager ->
+            if (paywall.manifest != null) {
+                if (paywall.manifest.use == ArchivalManifestUsage.NEVER) {
+                    return false
+                }
+                coroutineScope.launch(ioCoroutineDispatcher) {
+                    webArchiveManager.archiveForManifest(manifest = paywall.manifest)
+                }
+                return true
+            }
+        }
+        return false
+    }
+
+    // If we should be really aggressive and wait for the archival to finish
+    // before we load
+    fun shouldWaitForWebArchiveToLoad(paywall: Paywall): Boolean {
+        return webArchiveManager != null && paywall.manifest?.use == ArchivalManifestUsage.ALWAYS
+    }
+
+    // We'll try to see if it's cached, if not we'll just
+    // skip it and fall back to the normal method of loading
+    fun cachedArchiveForPaywallImmediately(paywall: Paywall): WebArchive? {
+        webArchiveManager?.let { webArchiveManager ->
+            if (paywall.manifest != null) {
+                if (paywall.manifest.use == ArchivalManifestUsage.NEVER) {
+                    return null
+                }
+                return webArchiveManager.archiveForManifestImmediately(manifest = paywall.manifest)
+            }
+        }
+        return null
+    }
+
+    suspend fun cachedArchiveForPaywall(paywall: Paywall): WebArchive? {
+        webArchiveManager?.let { webArchiveManager ->
+            if (paywall.manifest != null) {
+                if (paywall.manifest.use == ArchivalManifestUsage.NEVER) {
+                    return null
+                }
+                val result = webArchiveManager.archiveForManifest(manifest = paywall.manifest)
+                return when (result) {
+                    is Result.Success -> result.value
+                    is Result.Failure -> null
+                }
+            }
+        }
+        return null
+    }
+
+}
\ No newline at end of file
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/archival/WebArchiveFile.kt b/superwall/src/main/java/com/superwall/sdk/paywall/archival/WebArchiveFile.kt
new file mode 100644
index 00000000..25f0e222
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/archival/WebArchiveFile.kt
@@ -0,0 +1,64 @@
+package com.superwall.sdk.paywall.archival
+
+import com.superwall.sdk.models.paywall.WebArchive
+import com.superwall.sdk.models.paywall.WebArchiveResource
+import java.io.BufferedReader
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileReader
+import java.io.FileWriter
+import java.net.URL
+import java.util.Base64
+
+fun WebArchive.writeToFile(file: File) {
+    BufferedWriter(FileWriter(file)).use { writer ->
+        mainResource.write(writer)
+        subResources.forEach { resource ->
+            resource.write(writer)
+        }
+    }
+}
+
+private fun WebArchiveResource.write(bufferedWriter: BufferedWriter) {
+    with(bufferedWriter) {
+        appendLine(url.toString())
+        appendLine(mimeType)
+        appendLine(Base64.getEncoder().encodeToString(data))
+    }
+}
+
+fun File.readWebArchive(): WebArchive? {
+    return BufferedReader(FileReader(this)).use { reader ->
+        val mainResource = reader.readWebArchiveResource()
+        if (mainResource != null) {
+            val subResources = mutableListOf<WebArchiveResource>()
+            var resource = reader.readWebArchiveResource()
+            while (resource != null) {
+                subResources.add(resource)
+                resource = reader.readWebArchiveResource()
+            }
+            WebArchive(
+                mainResource = mainResource,
+                subResources = subResources
+            )
+        } else {
+            null
+        }
+    }
+}
+
+private fun BufferedReader.readWebArchiveResource(): WebArchiveResource? {
+    val url = readLine()
+    val mimeType = readLine()
+    val data = readLine()
+    return if (url != null && mimeType != null && data != null) {
+        WebArchiveResource(
+            url = URL(url),
+            mimeType = mimeType,
+            data = Base64.getDecoder().decode(data)
+
+        )
+    } else {
+        null
+    }
+}
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/archival/WebArchiveManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/archival/WebArchiveManager.kt
new file mode 100644
index 00000000..90bf3eaa
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/archival/WebArchiveManager.kt
@@ -0,0 +1,127 @@
+package com.superwall.sdk.paywall.archival
+
+import com.superwall.sdk.misc.RequestCoalescence
+import com.superwall.sdk.misc.Result
+import com.superwall.sdk.models.paywall.ArchivalManifest
+import com.superwall.sdk.models.paywall.ArchivalManifestItem
+import com.superwall.sdk.models.paywall.WebArchive
+import com.superwall.sdk.models.paywall.WebArchiveResource
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.net.HttpURLConnection
+import java.net.URL
+
+class WebArchiveManager(
+    private val baseDirectory: File,
+    private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
+    private val ioCoroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
+    private val requestCoalescence: RequestCoalescence<ArchivalManifestItem, WebArchiveResource> = RequestCoalescence(),
+    private val archivalCoalescence: RequestCoalescence<ArchivalManifest, Result<WebArchive>> = RequestCoalescence()
+) {
+
+    fun archiveForManifestImmediately(manifest: ArchivalManifest): WebArchive? {
+        val archiveFile = fsPath(forUrl = manifest.document.url)
+        return if (archiveFile.exists()) {
+            readArchiveFromFile(archiveFile)
+        } else {
+            null
+        }
+    }
+
+    suspend fun archiveForManifest(manifest: ArchivalManifest): Result<WebArchive> =
+        withContext(ioCoroutineDispatcher) {
+            val archiveFile = fsPath(forUrl = manifest.document.url)
+            if (archiveFile.exists()) {
+                val archive = readArchiveFromFile(archiveFile)
+                if (archive != null) {
+                    Result.Success(archive)
+                } else {
+                    Result.Failure(Exception("Reading failed"))
+                }
+            } else {
+                archivalCoalescence.get(input = manifest) {
+                    createArchiveForManifest(manifest = it)
+                }
+            }
+        }
+
+    private suspend fun createArchiveForManifest(manifest: ArchivalManifest): Result<WebArchive> {
+        return try {
+            val archive = downloadManifest(manifest = manifest)
+            val targetFile = fsPath(forUrl = manifest.document.url)
+            writeArchiveToFile(archive = archive, file = targetFile)
+            Result.Success(archive)
+        } catch (exception: Exception) {
+            Result.Failure(exception)
+        }
+    }
+
+    // Consistent way to look up the appropriate directory for a given url
+    private fun fsPath(forUrl: URL): File {
+        val hostDashed = forUrl.host?.split(".")?.joinToString(separator = "-") ?: "unknown"
+        var path = baseDirectory.resolve(hostDashed.replace(oldValue = "/", newValue = ""))
+        forUrl.path.split("/").filter { it.isNotEmpty() }.forEach { item ->
+            path = path.resolve(item)
+        }
+        path = File("${path.path}/cached.custom_web_archive")
+
+        return path
+    }
+
+    private suspend fun downloadManifest(manifest: ArchivalManifest): WebArchive {
+        val mainResourceDeferred = coroutineScope.async {
+            requestCoalescence.get(manifest.document) {
+                fetchDataForManifest(manifestItem = it)
+            }
+        }
+        val subResourcesDeferred = manifest.resources.map { resource ->
+            coroutineScope.async {
+                requestCoalescence.get(resource) {
+                    fetchDataForManifest(manifestItem = it)
+                }
+            }
+        }
+        val mainResource = mainResourceDeferred.await()
+        val subResources = subResourcesDeferred.map { it.await() }
+        return WebArchive(mainResource = mainResource, subResources = subResources)
+    }
+
+    private fun writeArchiveToFile(archive: WebArchive, file: File) {
+        if (file.parentFile?.mkdirs() != false) {
+            archive.writeToFile(file)
+        } else {
+            throw Exception("Cannot create directory")
+        }
+    }
+
+    private fun readArchiveFromFile(file: File): WebArchive? {
+        return try {
+            file.readWebArchive()
+        } catch (exception: Exception) {
+            null
+        }
+    }
+
+    private fun fetchDataForManifest(manifestItem: ArchivalManifestItem): WebArchiveResource {
+        val connection = manifestItem.url.openConnection() as HttpURLConnection
+        connection.requestMethod = "GET"
+        connection.useCaches = true
+        val responseCode = connection.responseCode
+        return if (responseCode == HttpURLConnection.HTTP_OK) {
+            val data = connection.inputStream.readBytes()
+            connection.disconnect()
+            WebArchiveResource(
+                url = manifestItem.url,
+                mimeType = manifestItem.mimeType,
+                data = data,
+            )
+        } else {
+            connection.disconnect()
+            throw Exception("Invalid response for resource: ${manifestItem.url}")
+        }
+    }
+}
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt
index 436e0bcb..614dccd1 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/manager/PaywallManager.kt
@@ -2,7 +2,9 @@ package com.superwall.sdk.paywall.manager
 
 import com.superwall.sdk.dependencies.CacheFactory
 import com.superwall.sdk.dependencies.DeviceHelperFactory
+import com.superwall.sdk.dependencies.PaywallArchivalManagerFactory
 import com.superwall.sdk.dependencies.ViewControllerFactory
+import com.superwall.sdk.paywall.archival.PaywallArchivalManager
 import com.superwall.sdk.paywall.request.PaywallRequest
 import com.superwall.sdk.paywall.request.PaywallRequestManager
 import com.superwall.sdk.paywall.vc.PaywallViewController
@@ -14,10 +16,11 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
 class PaywallManager(
-    private val factory: PaywallManager.Factory,
+    private val factory: Factory,
     private val paywallRequestManager: PaywallRequestManager
 ) {
-    interface Factory: ViewControllerFactory, CacheFactory, DeviceHelperFactory {}
+    interface Factory : ViewControllerFactory, CacheFactory, DeviceHelperFactory,
+        PaywallArchivalManagerFactory
 
     var presentedViewController: PaywallViewController? = null
         get() = cache.activePaywallViewController
@@ -32,6 +35,9 @@ class PaywallManager(
             return _cache!!
         }
 
+    private val paywallArchivalManager: PaywallArchivalManager =
+        factory.makePaywallArchivalManager()
+
     private fun createCache(): PaywallViewControllerCache {
         val cache: PaywallViewControllerCache = factory.makeCache()
         _cache = cache
@@ -52,6 +58,19 @@ class PaywallManager(
         }
     }
 
+    /// First, this gets the paywall response for a specified paywall identifier or trigger event.
+    /// It then checks with the archival manager to tell us if we should still eagerly create the
+    /// view controller or not.
+    ///
+    /// - Parameters:
+    ///   - request: The request to get the paywall.
+    suspend fun preloadViaPaywallArchivalAndShouldSkipViewControllerCache(
+        request: PaywallRequest
+    ): Boolean {
+        val paywall = paywallRequestManager.getPaywall(request = request)
+        return paywallArchivalManager.preloadArchiveAndShouldSkipViewControllerCache(paywall = paywall)
+    }
+
     suspend fun getPaywallViewController(
         request: PaywallRequest,
         isForPresentation: Boolean,
@@ -79,7 +98,8 @@ class PaywallManager(
         val paywallViewController = factory.makePaywallViewController(
             paywall = paywall,
             cache = cache,
-            delegate = delegate
+            delegate = delegate,
+            paywallArchivalManager = paywallArchivalManager
         )
         cache.save(paywallViewController, cacheKey)
 
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/PaywallViewController.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/PaywallViewController.kt
index 02b480eb..4906f6d8 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/PaywallViewController.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/PaywallViewController.kt
@@ -52,6 +52,7 @@ import com.superwall.sdk.models.paywall.Paywall
 import com.superwall.sdk.models.paywall.PaywallPresentationStyle
 import com.superwall.sdk.models.triggers.TriggerRuleOccurrence
 import com.superwall.sdk.network.device.DeviceHelper
+import com.superwall.sdk.paywall.archival.PaywallArchivalManager
 import com.superwall.sdk.paywall.manager.PaywallCacheLogic
 import com.superwall.sdk.paywall.manager.PaywallManager
 import com.superwall.sdk.paywall.manager.PaywallViewControllerCache
@@ -75,13 +76,16 @@ import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandlerDele
 import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallWebEvent
 import com.superwall.sdk.storage.Storage
 import com.superwall.sdk.store.transactions.notifications.NotificationScheduler
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import java.net.MalformedURLException
 import java.net.URL
-import java.util.*
+import java.util.Date
+import java.util.UUID
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
 
@@ -97,9 +101,14 @@ class PaywallViewController(
     val paywallManager: PaywallManager,
     override val webView: SWWebView,
     val cache: PaywallViewControllerCache?,
-    private val loadingViewController: LoadingViewController = LoadingViewController(context)
-) : FrameLayout(context), PaywallMessageHandlerDelegate, SWWebViewDelegate, ActivityEncapsulatable, GameControllerDelegate {
-    interface Factory: TriggerSessionManagerFactory, TriggerFactory {}
+    private val loadingViewController: LoadingViewController = LoadingViewController(context),
+    val paywallArchivalManager: PaywallArchivalManager?,
+    private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main),
+    private val ioCoroutineDispatcher: CoroutineDispatcher = Dispatchers.IO,
+    private val mainCoroutineDispatcher: CoroutineDispatcher = Dispatchers.Main
+) : FrameLayout(context), PaywallMessageHandlerDelegate, SWWebViewDelegate, ActivityEncapsulatable,
+    GameControllerDelegate {
+    interface Factory : TriggerSessionManagerFactory, TriggerFactory {}
     //region Public properties
 
     // MUST be set prior to presentation
@@ -663,23 +672,51 @@ class PaywallViewController(
             paywall.webviewLoadingInfo.startAt = Date()
         }
 
-        CoroutineScope(Dispatchers.IO).launch {
+        coroutineScope.launch {
             val trackedEvent = InternalSuperwallEvent.PaywallWebviewLoad(
                 state = InternalSuperwallEvent.PaywallWebviewLoad.State.Start(),
                 paywallInfo = this@PaywallViewController.info
             )
             Superwall.instance.track(trackedEvent)
         }
+        coroutineScope.launch(ioCoroutineDispatcher) {
+            if (paywallArchivalManager?.shouldWaitForWebArchiveToLoad(paywall = paywall) == true) {
+                //
+                // There is still a chance something goes wrong so we can fall back to the
+                // other loading method if we really need to
+                //
+                val webArchive = paywallArchivalManager.cachedArchiveForPaywall(paywall = paywall)
+                withContext(mainCoroutineDispatcher) {
+                    if (webArchive != null) {
+                        webView.loadWebArchive(webArchive = webArchive)
+                    } else {
+                        loadWebViewFromUrl(url = url)
+                    }
+                }
 
-        if (paywall.onDeviceCache is OnDeviceCaching.Enabled) {
-            webView.settings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
-        } else {
-            webView.settings.cacheMode = WebSettings.LOAD_DEFAULT
+            } else {
+                val webArchive =
+                    paywallArchivalManager?.cachedArchiveForPaywallImmediately(paywall = paywall)
+                if (webArchive != null) {
+                    webView.loadWebArchive(webArchive = webArchive)
+                } else {
+                    loadWebViewFromUrl(url = url)
+                }
+            }
         }
+        loadingState = PaywallLoadingState.LoadingURL()
+    }
 
-        webView.loadUrl(url.toString())
+    private suspend fun loadWebViewFromUrl(url: URL) {
+        withContext(mainCoroutineDispatcher) {
+            if (paywall.onDeviceCache is OnDeviceCaching.Enabled) {
+                webView.settings.cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK
+            } else {
+                webView.settings.cacheMode = WebSettings.LOAD_DEFAULT
+            }
 
-        loadingState = PaywallLoadingState.LoadingURL()
+            webView.loadUrl(url.toString())
+        }
     }
 
     //endregion
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt
index 1855d8b3..bff095f1 100644
--- a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/SWWebView.kt
@@ -9,21 +9,28 @@ import android.view.MotionEvent
 import android.view.inputmethod.BaseInputConnection
 import android.view.inputmethod.EditorInfo
 import android.view.inputmethod.InputConnection
-import android.webkit.*
+import android.webkit.ConsoleMessage
+import android.webkit.WebChromeClient
+import android.webkit.WebResourceError
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
 import com.superwall.sdk.Superwall
 import com.superwall.sdk.analytics.SessionEventsManager
 import com.superwall.sdk.analytics.internal.track
 import com.superwall.sdk.analytics.internal.trackable.InternalSuperwallEvent
-import com.superwall.sdk.analytics.trigger_session.LoadState
 import com.superwall.sdk.game.dispatchKeyEvent
 import com.superwall.sdk.game.dispatchMotionEvent
+import com.superwall.sdk.models.paywall.WebArchive
 import com.superwall.sdk.paywall.presentation.PaywallInfo
 import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandler
 import com.superwall.sdk.paywall.vc.web_view.messaging.PaywallMessageHandlerDelegate
+import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
-import java.util.*
+import kotlinx.coroutines.withContext
+import java.util.Date
 
 
 interface _SWWebViewDelegate {
@@ -35,7 +42,9 @@ interface SWWebViewDelegate : _SWWebViewDelegate, PaywallMessageHandlerDelegate
 class SWWebView(
     context: Context,
     private val sessionEventsManager: SessionEventsManager,
-    val messageHandler: PaywallMessageHandler
+    val messageHandler: PaywallMessageHandler,
+    private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main),
+    private val mainCoroutineDispatcher: CoroutineDispatcher = Dispatchers.Main
 ) : WebView(context) {
     var delegate: SWWebViewDelegate? = null
 
@@ -83,7 +92,7 @@ class SWWebView(
                 request: WebResourceRequest?,
                 error: WebResourceError?
             ) {
-                CoroutineScope(Dispatchers.Main).launch {
+                coroutineScope.launch {
                     trackPaywallError()
                 }
             }
@@ -131,6 +140,27 @@ class SWWebView(
         super.loadUrl(urlString)
     }
 
+    suspend fun loadWebArchive(webArchive: WebArchive) {
+        withContext(mainCoroutineDispatcher) {
+            webViewClient = object : WebArchiveWebViewClient(webArchive = webArchive) {
+
+                override fun onReceivedError(
+                    view: WebView?,
+                    request: WebResourceRequest?,
+                    error: WebResourceError?
+                ) {
+                    coroutineScope.launch {
+                        trackPaywallError()
+                    }
+                }
+
+            }
+            //use the url of the main resource so the relative paths of references are prepended
+            //with the same base url WebViewClient's shouldInterceptRequest()
+            loadUrl(webArchive.mainResource.url.toString())
+        }
+    }
+
     private suspend fun trackPaywallError() {
         delegate?.paywall?.webviewLoadingInfo?.failAt = Date()
 
diff --git a/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebArchiveWebViewClient.kt b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebArchiveWebViewClient.kt
new file mode 100644
index 00000000..272264d5
--- /dev/null
+++ b/superwall/src/main/java/com/superwall/sdk/paywall/vc/web_view/WebArchiveWebViewClient.kt
@@ -0,0 +1,34 @@
+package com.superwall.sdk.paywall.vc.web_view
+
+import android.webkit.WebResourceRequest
+import android.webkit.WebResourceResponse
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import com.superwall.sdk.models.paywall.WebArchive
+import com.superwall.sdk.models.paywall.WebArchiveResource
+
+open class WebArchiveWebViewClient(
+    val webArchive: WebArchive
+) : WebViewClient() {
+
+    override fun shouldInterceptRequest(
+        view: WebView,
+        request: WebResourceRequest
+    ): WebResourceResponse? {
+        val url = request.url.toString()
+        return if (url.contains(webArchive.mainResource.url.toString())) {
+            webArchive.mainResource.toWebResourceResponse()
+        } else {
+            //if no subresource for url exists the method returns null and the subresource is loaded from network
+            webArchive.subResources.find { it.url.toString() == url }?.toWebResourceResponse()
+        }
+    }
+
+    override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
+        return true
+    }
+
+    private fun WebArchiveResource.toWebResourceResponse() =
+        WebResourceResponse(mimeType, Charsets.UTF_8.name(), data.inputStream())
+
+}
\ No newline at end of file