mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-09 22:35:56 +05:45
* address crash * add test * update comment * update compile flags used around new function * Apply suggestion from @JZDesign
140 lines
4.5 KiB
Swift
140 lines
4.5 KiB
Swift
//
|
|
// Copyright RevenueCat Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the MIT License (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// https://opensource.org/licenses/MIT
|
|
//
|
|
// WebViewWebsiteDataStoreSweeper.swift
|
|
//
|
|
// Created by Jacob Zivan Rakidzich on 8/12/26.
|
|
//
|
|
|
|
import Foundation
|
|
@_spi(Internal) import RevenueCat
|
|
|
|
#if !os(tvOS) && !os(watchOS) && canImport(WebKit)
|
|
|
|
import WebKit
|
|
|
|
#endif
|
|
|
|
protocol WebViewDataStoreSweeping: AnyObject {
|
|
|
|
@MainActor
|
|
func sweepStores() async
|
|
|
|
}
|
|
|
|
final class WebViewWebsiteDataStoreSweeper: WebViewDataStoreSweeping {
|
|
|
|
private let idStore: WebViewDataStoreIdentifierStore
|
|
private let existingIdentifiers: @MainActor () async -> Set<UUID>
|
|
private let remove: @MainActor (UUID) async -> Bool
|
|
|
|
init(store: WebViewDataStoreIdentifierStore = .init()) {
|
|
self.idStore = store
|
|
self.existingIdentifiers = Self.loadExistingIdentifiers
|
|
self.remove = Self.removeStore(for:)
|
|
}
|
|
|
|
#if DEBUG
|
|
|
|
// Test initializer - for mocking and spying WebKit APIs
|
|
init(
|
|
idStore: WebViewDataStoreIdentifierStore = .init(),
|
|
existingIdentifiers: (@MainActor () async -> Set<UUID>)? = nil,
|
|
remove: (@MainActor (UUID) async -> Bool)? = nil
|
|
) {
|
|
self.idStore = idStore
|
|
self.existingIdentifiers = existingIdentifiers ?? Self.loadExistingIdentifiers
|
|
self.remove = remove ?? Self.removeStore(for:)
|
|
}
|
|
#endif
|
|
|
|
@MainActor
|
|
func sweepStores() async {
|
|
let pending = self.idStore.pendingRemovalIdentifiers()
|
|
guard !pending.isEmpty else { return }
|
|
|
|
let existing = await self.existingIdentifiers()
|
|
let remaining = await Self.sweep(
|
|
pending: pending,
|
|
existing: existing,
|
|
remove: self.remove
|
|
)
|
|
// Subtract only IDs this pass cleared. Replacing the set would drop IDs
|
|
// retired during the WebKit awaits (logout / overlapping sweeps).
|
|
self.idStore.removeFromPending(pending.subtracting(remaining))
|
|
}
|
|
|
|
/// Drops pending IDs whose stores are already gone. Only calls `remove` for IDs that still exist.
|
|
/// Failed removals stay pending so they can be retried later.
|
|
static func sweep(
|
|
pending: Set<UUID>,
|
|
existing: Set<UUID>,
|
|
remove: @MainActor (UUID) async -> Bool
|
|
) async -> Set<UUID> {
|
|
var remaining = pending
|
|
|
|
for identifier in pending {
|
|
if !existing.contains(identifier) {
|
|
remaining.remove(identifier)
|
|
continue
|
|
}
|
|
|
|
if await remove(identifier) {
|
|
remaining.remove(identifier)
|
|
}
|
|
}
|
|
|
|
return remaining
|
|
}
|
|
|
|
@MainActor
|
|
private static func loadExistingIdentifiers() async -> Set<UUID> {
|
|
#if compiler(>=5.9) && !os(tvOS) && !os(watchOS) && canImport(WebKit)
|
|
// Compiler 5.9 is beyond Xcode 14. Even with the following guard 👇 Xcode 14 fails to compile
|
|
if #available(iOS 17.0, macOS 14.0, *) {
|
|
ensureWebKitIsReady()
|
|
return Set(await WKWebsiteDataStore.allDataStoreIdentifiers)
|
|
}
|
|
#endif
|
|
|
|
return []
|
|
}
|
|
|
|
@MainActor
|
|
private static func removeStore(for identifier: UUID) async -> Bool {
|
|
#if compiler(>=5.9) && !os(tvOS) && !os(watchOS) && canImport(WebKit)
|
|
// Compiler 5.9 is beyond Xcode 14. Even with the following guard 👇 Xcode 14 fails to compile
|
|
if #available(iOS 17.0, macOS 14.0, *) {
|
|
do {
|
|
ensureWebKitIsReady()
|
|
try await WKWebsiteDataStore.remove(forIdentifier: identifier)
|
|
return true
|
|
} catch {
|
|
Logger.debug(Strings.web_view_data_store_removal_failed(identifier, error))
|
|
return false
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return true
|
|
}
|
|
|
|
#if compiler(>=5.9) && !os(tvOS) && !os(watchOS) && canImport(WebKit)
|
|
// There are certain cases where a consuming application will invoke the cache clearing path in our SDK
|
|
// before a data store is initialized. In those cases, our SDK can crash with a EXC_BAD_ACCESS when attempting
|
|
// to interact with the WebKit API. By creating one, we can ensure that the crash never happens, and we can
|
|
// clear the retired store.
|
|
// reported in: https://github.com/RevenueCat/purchases-ios/issues/7830
|
|
@MainActor
|
|
private static func ensureWebKitIsReady() {
|
|
_ = WKWebsiteDataStore.default()
|
|
}
|
|
#endif
|
|
}
|