Skip to content

Commit

Permalink
Adds support for web archive apis
Browse files Browse the repository at this point in the history
  • Loading branch information
anglinb committed Apr 27, 2024
1 parent e290f47 commit ebd386c
Show file tree
Hide file tree
Showing 13 changed files with 649 additions and 14 deletions.
16 changes: 10 additions & 6 deletions Sources/SuperwallKit/Config/ConfigManager.swift
Expand Up @@ -287,12 +287,16 @@ class ConfigManager {
presentationSourceType: nil,
retryCount: 6
)
_ = try? await self.paywallManager.getPaywallViewController(
from: request,
isForPresentation: true,
isPreloading: true,
delegate: nil
)

Check warning on line 290 in Sources/SuperwallKit/Config/ConfigManager.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
let shouldSkip = try? await self.paywallManager.preloadViaPaywallArchivalAndShouldSkipViewControllerCache(form: request)

Check warning on line 291 in Sources/SuperwallKit/Config/ConfigManager.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Line Length Violation: Line should be 120 characters or less; currently it has 130 characters (line_length)
if (shouldSkip != nil && shouldSkip == true) {

Check warning on line 292 in Sources/SuperwallKit/Config/ConfigManager.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Control Statement Violation: `if`, `for`, `guard`, `switch`, `while`, and `catch` statements shouldn't unnecessarily wrap their conditionals or arguments in parentheses (control_statement)
_ = try? await self.paywallManager.getPaywallViewController(
from: request,
isForPresentation: true,
isPreloading: true,
delegate: nil
)
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions Sources/SuperwallKit/Debug/DebugViewController.swift
Expand Up @@ -268,6 +268,7 @@ final class DebugViewController: UIViewController {
let child = factory.makePaywallViewController(
for: paywall,
withCache: nil,
withPaywallArchivalManager: nil,
delegate: nil
)
addChild(child)
Expand Down
15 changes: 14 additions & 1 deletion Sources/SuperwallKit/Dependencies/DependencyContainer.swift
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -202,6 +213,7 @@ extension DependencyContainer: ViewControllerFactory {
func makePaywallViewController(
for paywall: Paywall,
withCache cache: PaywallViewControllerCache?,
withPaywallArchivalManager archivalManager: PaywallArchivalManager?,
delegate: PaywallViewControllerDelegateAdapter?
) -> PaywallViewController {
let messageHandler = PaywallMessageHandler(
Expand All @@ -221,7 +233,8 @@ extension DependencyContainer: ViewControllerFactory {
factory: self,
storage: storage,
webView: webView,
cache: cache
cache: cache,
paywallArchivalManager: paywallArchivalManager
)

webView.delegate = paywallViewController
Expand Down
5 changes: 5 additions & 0 deletions Sources/SuperwallKit/Dependencies/FactoryProtocols.swift
Expand Up @@ -15,6 +15,7 @@ protocol ViewControllerFactory: AnyObject {
func makePaywallViewController(
for paywall: Paywall,
withCache cache: PaywallViewControllerCache?,
withPaywallArchivalManager archivalManager: PaywallArchivalManager?,
delegate: PaywallViewControllerDelegateAdapter?
) -> PaywallViewController

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

protocol PaywallArchivalManagerFactory: AnyObject {
func makePaywallArchivalManager() -> PaywallArchivalManager
}

protocol VariablesFactory: AnyObject {
func makeJsonVariables(
products: [ProductVariable]?,
Expand Down
40 changes: 40 additions & 0 deletions 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<Input: Identifiable, Output> {
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
}
}
11 changes: 10 additions & 1 deletion Sources/SuperwallKit/Models/Paywall/Paywall.swift
Expand Up @@ -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
Expand All @@ -127,6 +131,7 @@ struct Paywall: Decodable {
case localNotifications
case computedPropertyRequests = "computedProperties"
case surveys
case manifest

case responseLoadStartTime
case responseLoadCompleteTime
Expand Down Expand Up @@ -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] {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -297,6 +305,7 @@ struct Paywall: Decodable {
self.computedPropertyRequests = computedPropertyRequests
self.surveys = surveys
self.products = Self.makeProducts(from: productItems)
self.manifest = manifest
}

func getInfo(
Expand Down
84 changes: 84 additions & 0 deletions 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

Check warning on line 16 in Sources/SuperwallKit/Models/Paywall/PaywallManifest.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
enum CodingKeys: String, CodingKey {
case always = "ALWAYS"
case never = "NEVER"
case ifAvailableOnPaywallOpen = "IF_AVAILABLE_ON_PAYWALL_OPEN"
}

Check warning on line 22 in Sources/SuperwallKit/Models/Paywall/PaywallManifest.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
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)
}
}
68 changes: 68 additions & 0 deletions 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 {

Check warning on line 11 in Sources/SuperwallKit/Models/Paywall/WebArchive.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
enum CodingKeys: String, CodingKey {
case mainResource = "WebMainResource"
case webSubresources = "WebSubresources"
}

Check warning on line 16 in Sources/SuperwallKit/Models/Paywall/WebArchive.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
let mainResource: WebArchiveMainResource
var webSubresources: [WebArchiveResource]

Check warning on line 19 in Sources/SuperwallKit/Models/Paywall/WebArchive.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
init(resource: WebArchiveResource) {
self.mainResource = WebArchiveMainResource(baseResource: resource)
self.webSubresources = []
}

Check warning on line 24 in Sources/SuperwallKit/Models/Paywall/WebArchive.swift

View workflow job for this annotation

GitHub Actions / Package-SwiftLint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
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)
}
}

0 comments on commit ebd386c

Please sign in to comment.