diff --git a/Sources/SuperwallKit/Config/ConfigManager.swift b/Sources/SuperwallKit/Config/ConfigManager.swift index 94635374f..7211527d1 100644 --- a/Sources/SuperwallKit/Config/ConfigManager.swift +++ b/Sources/SuperwallKit/Config/ConfigManager.swift @@ -287,12 +287,16 @@ class ConfigManager { presentationSourceType: nil, retryCount: 6 ) - _ = try? await self.paywallManager.getPaywallViewController( - from: request, - isForPresentation: true, - isPreloading: true, - delegate: nil - ) + + let shouldSkip = try? await self.paywallManager.preloadViaPaywallArchivalAndShouldSkipViewControllerCache(form: request) + if (shouldSkip != nil && shouldSkip == true) { + _ = try? await self.paywallManager.getPaywallViewController( + from: request, + isForPresentation: true, + isPreloading: true, + delegate: nil + ) + } } } } diff --git a/Sources/SuperwallKit/Debug/DebugViewController.swift b/Sources/SuperwallKit/Debug/DebugViewController.swift index 2b09d7181..4c3ce5895 100644 --- a/Sources/SuperwallKit/Debug/DebugViewController.swift +++ b/Sources/SuperwallKit/Debug/DebugViewController.swift @@ -268,6 +268,7 @@ final class DebugViewController: UIViewController { let child = factory.makePaywallViewController( for: paywall, withCache: nil, + withPaywallArchivalManager: nil, delegate: nil ) addChild(child) diff --git a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift index 64fe4656d..7bd1587da 100644 --- a/Sources/SuperwallKit/Dependencies/DependencyContainer.swift +++ b/Sources/SuperwallKit/Dependencies/DependencyContainer.swift @@ -40,11 +40,14 @@ final class DependencyContainer { var purchaseController: PurchaseController! // swiftlint:enable implicitly_unwrapped_optional let productsFetcher = ProductsFetcherSK1() + + let paywallArchivalManager: PaywallArchivalManager init( purchaseController controller: PurchaseController? = nil, options: SuperwallOptions? = nil ) { + paywallArchivalManager = PaywallArchivalManager() purchaseController = controller ?? AutomaticPurchaseController(factory: self) receiptManager = ReceiptManager( delegate: productsFetcher, @@ -161,6 +164,14 @@ extension DependencyContainer: CacheFactory { } } + +// MARK - PaywallArchivalManager +extension DependencyContainer: PaywallArchivalManagerFactory { + func makePaywallArchivalManager() -> PaywallArchivalManager { + return self.paywallArchivalManager + } +} + // MARK: - DeviceInfofactory extension DependencyContainer: DeviceHelperFactory { func makeDeviceInfo() -> DeviceInfo { @@ -202,6 +213,7 @@ extension DependencyContainer: ViewControllerFactory { func makePaywallViewController( for paywall: Paywall, withCache cache: PaywallViewControllerCache?, + withPaywallArchivalManager archivalManager: PaywallArchivalManager?, delegate: PaywallViewControllerDelegateAdapter? ) -> PaywallViewController { let messageHandler = PaywallMessageHandler( @@ -221,7 +233,8 @@ extension DependencyContainer: ViewControllerFactory { factory: self, storage: storage, webView: webView, - cache: cache + cache: cache, + paywallArchivalManager: paywallArchivalManager ) webView.delegate = paywallViewController diff --git a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift index c95c22f32..b1a3fb99e 100644 --- a/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift +++ b/Sources/SuperwallKit/Dependencies/FactoryProtocols.swift @@ -15,6 +15,7 @@ protocol ViewControllerFactory: AnyObject { func makePaywallViewController( for paywall: Paywall, withCache cache: PaywallViewControllerCache?, + withPaywallArchivalManager archivalManager: PaywallArchivalManager?, delegate: PaywallViewControllerDelegateAdapter? ) -> PaywallViewController @@ -25,6 +26,10 @@ protocol CacheFactory: AnyObject { func makeCache() -> PaywallViewControllerCache } +protocol PaywallArchivalManagerFactory: AnyObject { + func makePaywallArchivalManager() -> PaywallArchivalManager +} + protocol VariablesFactory: AnyObject { func makeJsonVariables( products: [ProductVariable]?, diff --git a/Sources/SuperwallKit/Misc/RequestCoalescence.swift b/Sources/SuperwallKit/Misc/RequestCoalescence.swift new file mode 100644 index 000000000..5d1f0310c --- /dev/null +++ b/Sources/SuperwallKit/Misc/RequestCoalescence.swift @@ -0,0 +1,40 @@ +// +// RequestCoalleser.swift +// PaywallArchiveBuilder +// +// Created by Brian Anglin on 4/26/24. +// + +import Foundation + +public actor RequestCoalescence { + private var tasks: [Int: [(Output) -> Void]] = [:] + + public init() {} + + public func get(input: Input, request: @escaping (Input) async -> Output) async -> Output { + if tasks[input.id.hashValue] != nil { + // If there's already a task in progress, wait for it to finish + return await withCheckedContinuation { continuation in + appendCompletion(for: input.id.hashValue) { output in + continuation.resume(returning: output) + } + } + } else { + // Start a new task if one isn't already in progress + tasks[input.id.hashValue] = [] + let output = await request(input) + completeTasks(for: input.id.hashValue, with: output) + return output + } + } + + private func appendCompletion(for hashValue: Int, completion: @escaping (Output) -> Void) { + tasks[hashValue]?.append(completion) + } + + private func completeTasks(for hashValue: Int, with output: Output) { + tasks[hashValue]?.forEach { $0(output) } + tasks[hashValue] = nil + } +} diff --git a/Sources/SuperwallKit/Models/Paywall/Paywall.swift b/Sources/SuperwallKit/Models/Paywall/Paywall.swift index cb7bc3850..4d6efee55 100644 --- a/Sources/SuperwallKit/Models/Paywall/Paywall.swift +++ b/Sources/SuperwallKit/Models/Paywall/Paywall.swift @@ -110,6 +110,10 @@ struct Paywall: Decodable { /// The local notifications for the paywall, e.g. to notify the user of free trial expiry. var localNotifications: [LocalNotification] + + // A listing of all the filtes referenced in a paywall + // to be able to preload the whole paywall into a web archive + var manifest: ArchivalManifest? enum CodingKeys: String, CodingKey { case id @@ -127,6 +131,7 @@ struct Paywall: Decodable { case localNotifications case computedPropertyRequests = "computedProperties" case surveys + case manifest case responseLoadStartTime case responseLoadCompleteTime @@ -219,6 +224,8 @@ struct Paywall: Decodable { forKey: .computedPropertyRequests ) ?? [] computedPropertyRequests = throwableComputedPropertyRequests.compactMap { try? $0.result.get() } + + manifest = try? values.decodeIfPresent(ArchivalManifest.self, forKey: .manifest) } private static func makeProducts(from productItems: [ProductItem]) -> [Product] { @@ -269,7 +276,8 @@ struct Paywall: Decodable { onDeviceCache: OnDeviceCaching = .disabled, localNotifications: [LocalNotification] = [], computedPropertyRequests: [ComputedPropertyRequest] = [], - surveys: [Survey] = [] + surveys: [Survey] = [], + manifest: ArchivalManifest? = nil ) { self.databaseId = databaseId self.identifier = identifier @@ -297,6 +305,7 @@ struct Paywall: Decodable { self.computedPropertyRequests = computedPropertyRequests self.surveys = surveys self.products = Self.makeProducts(from: productItems) + self.manifest = manifest } func getInfo( diff --git a/Sources/SuperwallKit/Models/Paywall/PaywallManifest.swift b/Sources/SuperwallKit/Models/Paywall/PaywallManifest.swift new file mode 100644 index 000000000..3b3b898b2 --- /dev/null +++ b/Sources/SuperwallKit/Models/Paywall/PaywallManifest.swift @@ -0,0 +1,84 @@ +// +// File.swift +// +// +// Created by Brian Anglin on 4/27/24. +// + +import Foundation + +// What we get back from the API + +public enum ArchivalManifestUsage: Codable { + case always + case never + case ifAvailableOnPaywallOpen + + enum CodingKeys: String, CodingKey { + case always = "ALWAYS" + case never = "NEVER" + case ifAvailableOnPaywallOpen = "IF_AVAILABLE_ON_PAYWALL_OPEN" + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + let gatingType = CodingKeys(rawValue: rawValue) ?? .ifAvailableOnPaywallOpen + switch gatingType { + case .always: + self = .always + case .never: + self = .never + case .ifAvailableOnPaywallOpen: + self = .ifAvailableOnPaywallOpen + } + } +} + +public struct ArchivalManifest: Codable { + public var use: ArchivalManifestUsage + public var document: ArchivalManifestItem + public var resources: [ArchivalManifestItem] + public init(document: ArchivalManifestItem, resources: [ArchivalManifestItem], use: ArchivalManifestUsage) { + self.document = document + self.resources = resources + self.use = use + } +} + +public struct ArchivalManifestItem: Codable, Identifiable { + public var id: String { + url.absoluteString + } + let url: URL + let mimeType: String + public init(url: URL, mimeType: String) { + self.url = url + self.mimeType = mimeType + } +} + +// What we return when the item is downloaded + +struct ArchivalManifestDownloaded: Codable { + let document: ArchivalManifestItemDownloaded + let items: [ArchivalManifestItemDownloaded] + func toWebArchive() -> WebArchive { + var webArchive = WebArchive(resource: document.toWebArchiveResource()) + for item in items { + webArchive.addSubresource(item.toWebArchiveResource()) + } + return webArchive + } +} + + +public struct ArchivalManifestItemDownloaded: Codable { + let url: URL + let mimeType: String + let data: Data + let isMainDocument: Bool + func toWebArchiveResource() -> WebArchiveResource { + return WebArchiveResource(url: url, data: data, mimeType: mimeType) + } +} diff --git a/Sources/SuperwallKit/Models/Paywall/WebArchive.swift b/Sources/SuperwallKit/Models/Paywall/WebArchive.swift new file mode 100644 index 000000000..17c11387e --- /dev/null +++ b/Sources/SuperwallKit/Models/Paywall/WebArchive.swift @@ -0,0 +1,68 @@ +// +// File.swift +// +// +// Created by Brian Anglin on 4/27/24. +// + +import Foundation + +struct WebArchive: Encodable { + + enum CodingKeys: String, CodingKey { + case mainResource = "WebMainResource" + case webSubresources = "WebSubresources" + } + + let mainResource: WebArchiveMainResource + var webSubresources: [WebArchiveResource] + + init(resource: WebArchiveResource) { + self.mainResource = WebArchiveMainResource(baseResource: resource) + self.webSubresources = [] + } + + mutating func addSubresource(_ subresource: WebArchiveResource) { + self.webSubresources.append(subresource) + } +} +struct WebArchiveResource: Encodable { + + enum CodingKeys: String, CodingKey { + case url = "WebResourceURL" + case data = "WebResourceData" + case mimeType = "WebResourceMIMEType" + } + + let url: URL + let data: Data + let mimeType: String + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(url.absoluteString, forKey: .url) + try container.encode(data, forKey: .data) + try container.encode(mimeType, forKey: .mimeType) + } +} +struct WebArchiveMainResource: Encodable { + + enum CodingKeys: String, CodingKey { + case url = "WebResourceURL" + case data = "WebResourceData" + case mimeType = "WebResourceMIMEType" + case textEncodingName = "WebResourceTextEncodingName" + case frameName = "WebResourceFrameName" + } + + let baseResource: WebArchiveResource + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(baseResource.url.absoluteString, forKey: .url) + try container.encode(baseResource.data, forKey: .data) + try container.encode(baseResource.mimeType, forKey: .mimeType) + try container.encode("UTF-8", forKey: .textEncodingName) + try container.encode("", forKey: .frameName) + } +} diff --git a/Sources/SuperwallKit/Paywall/Archival/PaywallArchivalManager.swift b/Sources/SuperwallKit/Paywall/Archival/PaywallArchivalManager.swift new file mode 100644 index 000000000..86d74c067 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Archival/PaywallArchivalManager.swift @@ -0,0 +1,87 @@ +// +// File.swift +// +// +// Created by Brian Anglin on 4/27/24. +// + +import Foundation +class PaywallArchivalManager { + + private let webArchiveManager: WebArchiveManager? + init( + baseDirectory: URL? = nil, + webArchiveManager: WebArchiveManager? = nil + ) { + let _baseDirectory = baseDirectory ?? (try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + .appendingPathComponent("paywalls")) + + guard let baseDirectory = _baseDirectory else { + self.webArchiveManager = nil + return + } + self.webArchiveManager = webArchiveManager ?? WebArchiveManager(baseURL: baseDirectory) + } + + // Should + func preloadArchiveAndShouldSkipViewControllerCache(paywall: Paywall) -> Bool { + if let webArchiveManager = self.webArchiveManager { + if let manifest = paywall.manifest { + if manifest.use == .never { + return false + } + Task(priority: .background) { + await webArchiveManager.archiveForManifest(manifest:manifest) + } + return true + } + } + return false + } + + + // If we should be really agressive and wait for the archival to finsih + // before we load + func shouldWaitForWebArchiveToLoad(paywall: Paywall) -> Bool { + if let webArchiveManager = self.webArchiveManager { + if let manifest = paywall.manifest { + if manifest.use == .always { + return true + } + } + } + return false + } + + // 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 + func cachedArchiveForPaywallImmediately(paywall: Paywall) -> URL? { + if let webArchiveManager = self.webArchiveManager { + if let manifest = paywall.manifest { + if manifest.use == .never { + return nil + } + return webArchiveManager.archiveForManifestImmediately(manifest: manifest) + } + } + return nil + } + + func cachedArchiveForPaywall(paywall: Paywall) async -> URL? { + if let webArchiveManager = self.webArchiveManager { + if let manifest = paywall.manifest { + if manifest.use == .never { + return nil + } + let result = await webArchiveManager.archiveForManifest(manifest: manifest) + switch result { + case .success(let url): + return url + case .failure: + return nil + } + } + } + return nil + } +} diff --git a/Sources/SuperwallKit/Paywall/Archival/WebArchivalManager.swift b/Sources/SuperwallKit/Paywall/Archival/WebArchivalManager.swift new file mode 100644 index 000000000..8c6ddd606 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Archival/WebArchivalManager.swift @@ -0,0 +1,192 @@ +// +// File.swift +// +// +// Created by Brian Anglin on 4/27/24. +// + +import Foundation + +public enum ArchivingError: LocalizedError { + case unknownError + case unsupportedUrl + case requestFailed(resource: URL, error: Error) + case invalidResponse(resource: URL) + case unsupportedEncoding + case invalidReferenceUrl(string: String) + + public var errorDescription: String? { + switch self { + case .unknownError: return "Uknown error" + case .unsupportedUrl: return "Unsupported URL" + case .requestFailed(let res, _): return "Failed to load " + res.absoluteString + case .invalidResponse(let res): return "Invalid response for " + res.absoluteString + case .unsupportedEncoding: return "Unsupported encoding" + case .invalidReferenceUrl(let string): return "Invalid reference URL: " + string + } + } +} + +public struct ArchivalRequest: Identifiable { + public var id: String { + return manifest.document.url.absoluteString + } + let manifest: ArchivalManifest +} + +public class WebArchiveManager { + private var encoder: PropertyListEncoder = { + let plistEncoder = PropertyListEncoder() + plistEncoder.outputFormat = .binary + return plistEncoder + }() + private let cachePolicy = URLRequest.CachePolicy.returnCacheDataElseLoad + private let urlSession: URLSession + private let baseURL: URL + private let requestCoalescence: RequestCoalescence> + private let archivalCoalescence: RequestCoalescence> + private let archivalFileSystemManager: WebArchiveFileSytemManager + public init( + baseURL: URL, + requestCoalescence: RequestCoalescence> = RequestCoalescence(), + archivalCoalescence: RequestCoalescence> = RequestCoalescence(), + archivalFileSystemManager: WebArchiveFileSytemManager? = nil + ) { + self.baseURL = baseURL + self.requestCoalescence = requestCoalescence + self.archivalCoalescence = archivalCoalescence + self.urlSession = URLSession(configuration: .default, delegate: nil, delegateQueue: nil) + self.archivalFileSystemManager = archivalFileSystemManager ?? WebArchiveFileSytemManager(archiveURL: self.baseURL) + } + + public func archiveForManifestImmediately(manifest: ArchivalManifest) -> URL? { + let archivePath = self.fsPath(forURL: manifest.document.url) + let fsManager = WebArchiveFileSytemManager(archiveURL: archivePath) + if (fsManager.checkArchiveExists()) { + return archivePath + } + return nil + } + + public func archiveForManifest(manifest: ArchivalManifest) async -> Result { + // let webArchiveFile + let archivePath = self.fsPath(forURL: manifest.document.url) + let fsManager = WebArchiveFileSytemManager(archiveURL: archivePath) + if (fsManager.checkArchiveExists()) { + return .success(archivePath) + } + let archivalRequest = ArchivalRequest(manifest: manifest) + return await self.archivalCoalescence.get(input: archivalRequest) { request in + return await self._archiveForManifest(manifest: request.manifest) + } + } + + private func _archiveForManifest(manifest: ArchivalManifest) async -> Result { + do { + let downloadedManifest = try await self.downloadManifest(manifest: manifest) + let targetPath = self.fsPath(forURL: manifest.document.url) + try await self.writeManifest(manifest: downloadedManifest, path: targetPath) + return .success(targetPath) + } catch { + return .failure(.unknownError) + } + } + + // Consistent way to look up the appropriate directory + // for a given url + private func fsPath(forURL: URL) -> URL { + let hostDashed = forURL.host?.split(separator: ".").joined(separator: "-") ?? "unknown" + var path = baseURL.appendingPathComponent(hostDashed.replacingOccurrences(of: "/", with: "")) + for item in forURL.pathComponents.filter({ str in + return str != "/" + }) { + path = path.appendingPathComponent(item) + } + path = path.appendingPathComponent("cached").appendingPathExtension("webarchive") + + return path + } + + + private func downloadManifest(manifest: ArchivalManifest) async throws -> ArchivalManifestDownloaded { + let results = await withTaskGroup(of: Result.self, returning: [Result].self) { + group in + var results: [Result] = []; + + group.addTask { + return await self.requestCoalescence.get(input: manifest.document) { (item) in + let result = await self.fetchDataForManifest(manifest: item, isMainDocument: true) + return result + } + } + for item in manifest.resources { + group.addTask { + return await self.requestCoalescence.get(input: item) { item in + return await self.fetchDataForManifest(manifest: item, isMainDocument: false) + } + } + } + + do { + for try await result in group { + results.append(result) + } + return results + } catch { + return [] + } + } + let successfulResults = try results + .filter({ item in + if case .success = item { + return true + } + return false + }).map { item in + return try item.get() + } + if (successfulResults.isEmpty) { + throw NSError(domain: "com.example.error", code: 0, userInfo: [NSLocalizedDescriptionKey: "No results found"]) + } + let document = successfulResults + .first { item in + return item.isMainDocument + } + + guard let document = document else { + throw NSError(domain: "com.example.error", code: 0, userInfo: [NSLocalizedDescriptionKey: "Couldn't load document"]) + } + let restOfItems = successfulResults.filter { + item in return !item.isMainDocument + } + return ArchivalManifestDownloaded(document: document, items: restOfItems) + } + + // Helper to write manifest + private func writeManifest(manifest: ArchivalManifestDownloaded, path: URL) async throws -> Void { + // Write it to disk + // WebArchiver(archiveURL: <#T##URL#>) + let fsManager = WebArchiveFileSytemManager(archiveURL: self.fsPath(forURL: manifest.document.url)) + let result = fsManager.writeArchive(archive: manifest.toWebArchive()) + switch result { + case .failure(let error): + throw error + case .success: + return + } + } + + + // Helper to actually fetch the manifes + + private func fetchDataForManifest(manifest: ArchivalManifestItem, isMainDocument: Bool) async -> Result { + let request = URLRequest(url: manifest.url) + do { + let (data, _) = try await self.urlSession.data(for: request) + return .success(ArchivalManifestItemDownloaded(url: manifest.url, mimeType: manifest.mimeType, data: data, isMainDocument: isMainDocument)) + } catch { + print("Error", error ) + return .failure(error) + } + } +} diff --git a/Sources/SuperwallKit/Paywall/Archival/WebArchiveFileSytemManager.swift b/Sources/SuperwallKit/Paywall/Archival/WebArchiveFileSytemManager.swift new file mode 100644 index 000000000..ad6586439 --- /dev/null +++ b/Sources/SuperwallKit/Paywall/Archival/WebArchiveFileSytemManager.swift @@ -0,0 +1,60 @@ +// +// File.swift +// +// +// Created by Brian Anglin on 4/27/24. +// + +import Foundation + +public struct ArchivingResult { + public let plistData: Data? + public let errors: [Error] +} + + +public class WebArchiveFileSytemManager { + var encoder: PropertyListEncoder = { + let plistEncoder = PropertyListEncoder() + plistEncoder.outputFormat = .binary + return plistEncoder + }() + + private let archiveURL: URL + init(archiveURL: URL) { + self.archiveURL = archiveURL + } + + func checkArchiveExists() -> Bool { + return FileManager.default.fileExists(atPath: archiveURL.path) + } + + func writeArchive(archive: WebArchive) -> Result { + guard let plistData = try? self.encoder.encode(archive) else { + return .failure(ArchivingError.unknownError) + } + do { + guard let directory = self.directoryURL(of: self.archiveURL) else { + throw ArchivingError.unknownError + } + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + try plistData.write(to: self.archiveURL) + return .success(self.archiveURL) + } catch { + return .failure(error) + } + } + + func directoryURL(of url: URL) -> URL? { + var pathComponents = url.pathComponents + guard !pathComponents.isEmpty else { return nil } + + // Remove the last component if it's a file, not a directory + if pathComponents.last != "/", let _ = pathComponents.last?.split(separator: ".").last { + pathComponents.removeLast() + } + + let directoryURL = url.deletingLastPathComponent() + return directoryURL + } +} diff --git a/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift b/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift index cf9c552d1..c80ac6c75 100644 --- a/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift +++ b/Sources/SuperwallKit/Paywall/Manager/PaywallManager.swift @@ -14,15 +14,21 @@ class PaywallManager { } private let queue = DispatchQueue(label: "com.superwall.paywallmanager") private unowned let paywallRequestManager: PaywallRequestManager - private unowned let factory: ViewControllerFactory & CacheFactory & DeviceHelperFactory + private unowned let factory: ViewControllerFactory & CacheFactory & DeviceHelperFactory & PaywallArchivalManagerFactory private var cache: PaywallViewControllerCache { return queue.sync { _cache ?? createCache() } } private var _cache: PaywallViewControllerCache? + + private var paywallArchivalManager: PaywallArchivalManager { + return queue.sync { _paywallArchivalManager ?? createPaywallArchivalManager() } + } + private var _paywallArchivalManager: PaywallArchivalManager? + init( - factory: ViewControllerFactory & CacheFactory & DeviceHelperFactory, + factory: ViewControllerFactory & CacheFactory & DeviceHelperFactory & PaywallArchivalManagerFactory, paywallRequestManager: PaywallRequestManager ) { self.factory = factory @@ -34,6 +40,12 @@ class PaywallManager { _cache = cache return cache } + + private func createPaywallArchivalManager() -> PaywallArchivalManager { + let paywallArchivalManager = factory.makePaywallArchivalManager() + _paywallArchivalManager = paywallArchivalManager + return paywallArchivalManager + } func removePaywallViewController(forKey key: String) { cache.removePaywallViewController(forKey: key) @@ -42,6 +54,19 @@ class PaywallManager { func resetCache() { cache.removeAll() } + + /// 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. + func preloadViaPaywallArchivalAndShouldSkipViewControllerCache( + form request: PaywallRequest + ) async throws -> Bool { + let paywall = try await paywallRequestManager.getPaywall(from: request) + return paywallArchivalManager.preloadArchiveAndShouldSkipViewControllerCache(paywall: paywall) + } /// First, this gets the paywall response for a specified paywall identifier or trigger event. /// It then creates the paywall view controller from that response, and caches it. @@ -68,7 +93,7 @@ class PaywallManager { identifier: paywall.identifier, locale: deviceInfo.locale ) - + if !request.isDebuggerLaunched, let viewController = self.cache.getPaywallViewController(forKey: cacheKey) { if !isPreloading { @@ -81,6 +106,7 @@ class PaywallManager { let paywallViewController = factory.makePaywallViewController( for: paywall, withCache: cache, + withPaywallArchivalManager: paywallArchivalManager, delegate: delegate ) cache.save(paywallViewController, forKey: cacheKey) diff --git a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift index b0c1365d8..fd45cd8bf 100644 --- a/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift +++ b/Sources/SuperwallKit/Paywall/View Controller/PaywallViewController.swift @@ -145,6 +145,7 @@ public class PaywallViewController: UIViewController, LoadingDelegate { private unowned let storage: Storage private unowned let deviceHelper: DeviceHelper private weak var cache: PaywallViewControllerCache? + private weak var paywallArchivalManager: PaywallArchivalManager? // MARK: - View Lifecycle @@ -156,9 +157,11 @@ public class PaywallViewController: UIViewController, LoadingDelegate { factory: TriggerSessionManagerFactory & TriggerFactory, storage: Storage, webView: SWWebView, - cache: PaywallViewControllerCache? + cache: PaywallViewControllerCache?, + paywallArchivalManager: PaywallArchivalManager? ) { self.cache = cache + self.paywallArchivalManager = paywallArchivalManager self.cacheKey = PaywallCacheLogic.key( identifier: paywall.identifier, locale: deviceHelper.locale @@ -273,7 +276,52 @@ public class PaywallViewController: UIViewController, LoadingDelegate { state: .start ) } + + // + // The web archival method for loading a paywall can be + // always, never, ifAvailableOnPaywallOpen + // + // This will return true if always, otherwise we'll just try and + // use the archive if it's availble when we need it. + // + if let paywallArchivalManager = self.paywallArchivalManager { + if paywallArchivalManager.shouldWaitForWebArchiveToLoad(paywall: self.paywall) { + Task { + // + // There is still a chance something goes wrong so we can fall back to the + // other loading method if we really need to + // + if let webArchiveURL = await paywallArchivalManager.cachedArchiveForPaywall(paywall: self.paywall) { + DispatchQueue.main.async { + self.loadWebViewFromArchivalPath(webArchiveURL: webArchiveURL) + } + } else { + DispatchQueue.main.async { + self.loadWebViewFromURL(url: url) + } + } + } + + loadingState = .loadingURL + return + } + } + if let webArchiveURL = self.paywallArchivalManager?.cachedArchiveForPaywallImmediately(paywall: self.paywall) { + self.loadWebViewFromArchivalPath(webArchiveURL: webArchiveURL) + } else { + loadWebViewFromURL(url: url) + } + + loadingState = .loadingURL + } + + func loadWebViewFromArchivalPath(webArchiveURL: URL) { + webView.loadFileURL(webArchiveURL, allowingReadAccessTo: webArchiveURL) + } + + + func loadWebViewFromURL(url: URL) { if paywall.onDeviceCache == .enabled { let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad) webView.load(request) @@ -281,8 +329,6 @@ public class PaywallViewController: UIViewController, LoadingDelegate { let request = URLRequest(url: url) webView.load(request) } - - loadingState = .loadingURL } @objc private func reloadWebView() {