Files
purchases-ios/RevenueCatUI/Cache/WebViewWebsiteDataStoreSweeper.swift
Jacob Rakidzich 6df6cd672b Fix(paywalls) Crash on logout (#7836)
* address crash

* add test

* update comment

* update compile flags used around new function

* Apply suggestion from @JZDesign
2026-09-29 14:07:15 +00:00

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
}