Skip to content

Commit ebd386c

Browse files
committed
Adds support for web archive apis
1 parent e290f47 commit ebd386c

13 files changed

+649
-14
lines changed

Sources/SuperwallKit/Config/ConfigManager.swift

+10-6
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,16 @@ class ConfigManager {
287287
presentationSourceType: nil,
288288
retryCount: 6
289289
)
290-
_ = try? await self.paywallManager.getPaywallViewController(
291-
from: request,
292-
isForPresentation: true,
293-
isPreloading: true,
294-
delegate: nil
295-
)
290+
291+
let shouldSkip = try? await self.paywallManager.preloadViaPaywallArchivalAndShouldSkipViewControllerCache(form: request)
292+
if (shouldSkip != nil && shouldSkip == true) {
293+
_ = try? await self.paywallManager.getPaywallViewController(
294+
from: request,
295+
isForPresentation: true,
296+
isPreloading: true,
297+
delegate: nil
298+
)
299+
}
296300
}
297301
}
298302
}

Sources/SuperwallKit/Debug/DebugViewController.swift

+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ final class DebugViewController: UIViewController {
268268
let child = factory.makePaywallViewController(
269269
for: paywall,
270270
withCache: nil,
271+
withPaywallArchivalManager: nil,
271272
delegate: nil
272273
)
273274
addChild(child)

Sources/SuperwallKit/Dependencies/DependencyContainer.swift

+14-1
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@ final class DependencyContainer {
4040
var purchaseController: PurchaseController!
4141
// swiftlint:enable implicitly_unwrapped_optional
4242
let productsFetcher = ProductsFetcherSK1()
43+
44+
let paywallArchivalManager: PaywallArchivalManager
4345

4446
init(
4547
purchaseController controller: PurchaseController? = nil,
4648
options: SuperwallOptions? = nil
4749
) {
50+
paywallArchivalManager = PaywallArchivalManager()
4851
purchaseController = controller ?? AutomaticPurchaseController(factory: self)
4952
receiptManager = ReceiptManager(
5053
delegate: productsFetcher,
@@ -161,6 +164,14 @@ extension DependencyContainer: CacheFactory {
161164
}
162165
}
163166

167+
168+
// MARK - PaywallArchivalManager
169+
extension DependencyContainer: PaywallArchivalManagerFactory {
170+
func makePaywallArchivalManager() -> PaywallArchivalManager {
171+
return self.paywallArchivalManager
172+
}
173+
}
174+
164175
// MARK: - DeviceInfofactory
165176
extension DependencyContainer: DeviceHelperFactory {
166177
func makeDeviceInfo() -> DeviceInfo {
@@ -202,6 +213,7 @@ extension DependencyContainer: ViewControllerFactory {
202213
func makePaywallViewController(
203214
for paywall: Paywall,
204215
withCache cache: PaywallViewControllerCache?,
216+
withPaywallArchivalManager archivalManager: PaywallArchivalManager?,
205217
delegate: PaywallViewControllerDelegateAdapter?
206218
) -> PaywallViewController {
207219
let messageHandler = PaywallMessageHandler(
@@ -221,7 +233,8 @@ extension DependencyContainer: ViewControllerFactory {
221233
factory: self,
222234
storage: storage,
223235
webView: webView,
224-
cache: cache
236+
cache: cache,
237+
paywallArchivalManager: paywallArchivalManager
225238
)
226239

227240
webView.delegate = paywallViewController

Sources/SuperwallKit/Dependencies/FactoryProtocols.swift

+5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ protocol ViewControllerFactory: AnyObject {
1515
func makePaywallViewController(
1616
for paywall: Paywall,
1717
withCache cache: PaywallViewControllerCache?,
18+
withPaywallArchivalManager archivalManager: PaywallArchivalManager?,
1819
delegate: PaywallViewControllerDelegateAdapter?
1920
) -> PaywallViewController
2021

@@ -25,6 +26,10 @@ protocol CacheFactory: AnyObject {
2526
func makeCache() -> PaywallViewControllerCache
2627
}
2728

29+
protocol PaywallArchivalManagerFactory: AnyObject {
30+
func makePaywallArchivalManager() -> PaywallArchivalManager
31+
}
32+
2833
protocol VariablesFactory: AnyObject {
2934
func makeJsonVariables(
3035
products: [ProductVariable]?,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// RequestCoalleser.swift
3+
// PaywallArchiveBuilder
4+
//
5+
// Created by Brian Anglin on 4/26/24.
6+
//
7+
8+
import Foundation
9+
10+
public actor RequestCoalescence<Input: Identifiable, Output> {
11+
private var tasks: [Int: [(Output) -> Void]] = [:]
12+
13+
public init() {}
14+
15+
public func get(input: Input, request: @escaping (Input) async -> Output) async -> Output {
16+
if tasks[input.id.hashValue] != nil {
17+
// If there's already a task in progress, wait for it to finish
18+
return await withCheckedContinuation { continuation in
19+
appendCompletion(for: input.id.hashValue) { output in
20+
continuation.resume(returning: output)
21+
}
22+
}
23+
} else {
24+
// Start a new task if one isn't already in progress
25+
tasks[input.id.hashValue] = []
26+
let output = await request(input)
27+
completeTasks(for: input.id.hashValue, with: output)
28+
return output
29+
}
30+
}
31+
32+
private func appendCompletion(for hashValue: Int, completion: @escaping (Output) -> Void) {
33+
tasks[hashValue]?.append(completion)
34+
}
35+
36+
private func completeTasks(for hashValue: Int, with output: Output) {
37+
tasks[hashValue]?.forEach { $0(output) }
38+
tasks[hashValue] = nil
39+
}
40+
}

Sources/SuperwallKit/Models/Paywall/Paywall.swift

+10-1
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ struct Paywall: Decodable {
110110

111111
/// The local notifications for the paywall, e.g. to notify the user of free trial expiry.
112112
var localNotifications: [LocalNotification]
113+
114+
// A listing of all the filtes referenced in a paywall
115+
// to be able to preload the whole paywall into a web archive
116+
var manifest: ArchivalManifest?
113117

114118
enum CodingKeys: String, CodingKey {
115119
case id
@@ -127,6 +131,7 @@ struct Paywall: Decodable {
127131
case localNotifications
128132
case computedPropertyRequests = "computedProperties"
129133
case surveys
134+
case manifest
130135

131136
case responseLoadStartTime
132137
case responseLoadCompleteTime
@@ -219,6 +224,8 @@ struct Paywall: Decodable {
219224
forKey: .computedPropertyRequests
220225
) ?? []
221226
computedPropertyRequests = throwableComputedPropertyRequests.compactMap { try? $0.result.get() }
227+
228+
manifest = try? values.decodeIfPresent(ArchivalManifest.self, forKey: .manifest)
222229
}
223230

224231
private static func makeProducts(from productItems: [ProductItem]) -> [Product] {
@@ -269,7 +276,8 @@ struct Paywall: Decodable {
269276
onDeviceCache: OnDeviceCaching = .disabled,
270277
localNotifications: [LocalNotification] = [],
271278
computedPropertyRequests: [ComputedPropertyRequest] = [],
272-
surveys: [Survey] = []
279+
surveys: [Survey] = [],
280+
manifest: ArchivalManifest? = nil
273281
) {
274282
self.databaseId = databaseId
275283
self.identifier = identifier
@@ -297,6 +305,7 @@ struct Paywall: Decodable {
297305
self.computedPropertyRequests = computedPropertyRequests
298306
self.surveys = surveys
299307
self.products = Self.makeProducts(from: productItems)
308+
self.manifest = manifest
300309
}
301310

302311
func getInfo(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Brian Anglin on 4/27/24.
6+
//
7+
8+
import Foundation
9+
10+
// What we get back from the API
11+
12+
public enum ArchivalManifestUsage: Codable {
13+
case always
14+
case never
15+
case ifAvailableOnPaywallOpen
16+
17+
enum CodingKeys: String, CodingKey {
18+
case always = "ALWAYS"
19+
case never = "NEVER"
20+
case ifAvailableOnPaywallOpen = "IF_AVAILABLE_ON_PAYWALL_OPEN"
21+
}
22+
23+
public init(from decoder: any Decoder) throws {
24+
let container = try decoder.singleValueContainer()
25+
let rawValue = try container.decode(String.self)
26+
let gatingType = CodingKeys(rawValue: rawValue) ?? .ifAvailableOnPaywallOpen
27+
switch gatingType {
28+
case .always:
29+
self = .always
30+
case .never:
31+
self = .never
32+
case .ifAvailableOnPaywallOpen:
33+
self = .ifAvailableOnPaywallOpen
34+
}
35+
}
36+
}
37+
38+
public struct ArchivalManifest: Codable {
39+
public var use: ArchivalManifestUsage
40+
public var document: ArchivalManifestItem
41+
public var resources: [ArchivalManifestItem]
42+
public init(document: ArchivalManifestItem, resources: [ArchivalManifestItem], use: ArchivalManifestUsage) {
43+
self.document = document
44+
self.resources = resources
45+
self.use = use
46+
}
47+
}
48+
49+
public struct ArchivalManifestItem: Codable, Identifiable {
50+
public var id: String {
51+
url.absoluteString
52+
}
53+
let url: URL
54+
let mimeType: String
55+
public init(url: URL, mimeType: String) {
56+
self.url = url
57+
self.mimeType = mimeType
58+
}
59+
}
60+
61+
// What we return when the item is downloaded
62+
63+
struct ArchivalManifestDownloaded: Codable {
64+
let document: ArchivalManifestItemDownloaded
65+
let items: [ArchivalManifestItemDownloaded]
66+
func toWebArchive() -> WebArchive {
67+
var webArchive = WebArchive(resource: document.toWebArchiveResource())
68+
for item in items {
69+
webArchive.addSubresource(item.toWebArchiveResource())
70+
}
71+
return webArchive
72+
}
73+
}
74+
75+
76+
public struct ArchivalManifestItemDownloaded: Codable {
77+
let url: URL
78+
let mimeType: String
79+
let data: Data
80+
let isMainDocument: Bool
81+
func toWebArchiveResource() -> WebArchiveResource {
82+
return WebArchiveResource(url: url, data: data, mimeType: mimeType)
83+
}
84+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// File.swift
3+
//
4+
//
5+
// Created by Brian Anglin on 4/27/24.
6+
//
7+
8+
import Foundation
9+
10+
struct WebArchive: Encodable {
11+
12+
enum CodingKeys: String, CodingKey {
13+
case mainResource = "WebMainResource"
14+
case webSubresources = "WebSubresources"
15+
}
16+
17+
let mainResource: WebArchiveMainResource
18+
var webSubresources: [WebArchiveResource]
19+
20+
init(resource: WebArchiveResource) {
21+
self.mainResource = WebArchiveMainResource(baseResource: resource)
22+
self.webSubresources = []
23+
}
24+
25+
mutating func addSubresource(_ subresource: WebArchiveResource) {
26+
self.webSubresources.append(subresource)
27+
}
28+
}
29+
struct WebArchiveResource: Encodable {
30+
31+
enum CodingKeys: String, CodingKey {
32+
case url = "WebResourceURL"
33+
case data = "WebResourceData"
34+
case mimeType = "WebResourceMIMEType"
35+
}
36+
37+
let url: URL
38+
let data: Data
39+
let mimeType: String
40+
41+
func encode(to encoder: Encoder) throws {
42+
var container = encoder.container(keyedBy: CodingKeys.self)
43+
try container.encode(url.absoluteString, forKey: .url)
44+
try container.encode(data, forKey: .data)
45+
try container.encode(mimeType, forKey: .mimeType)
46+
}
47+
}
48+
struct WebArchiveMainResource: Encodable {
49+
50+
enum CodingKeys: String, CodingKey {
51+
case url = "WebResourceURL"
52+
case data = "WebResourceData"
53+
case mimeType = "WebResourceMIMEType"
54+
case textEncodingName = "WebResourceTextEncodingName"
55+
case frameName = "WebResourceFrameName"
56+
}
57+
58+
let baseResource: WebArchiveResource
59+
60+
func encode(to encoder: Encoder) throws {
61+
var container = encoder.container(keyedBy: CodingKeys.self)
62+
try container.encode(baseResource.url.absoluteString, forKey: .url)
63+
try container.encode(baseResource.data, forKey: .data)
64+
try container.encode(baseResource.mimeType, forKey: .mimeType)
65+
try container.encode("UTF-8", forKey: .textEncodingName)
66+
try container.encode("", forKey: .frameName)
67+
}
68+
}

0 commit comments

Comments
 (0)