Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/apple/Sources/Helpers/IapState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,7 @@ actor IapState {
func snapshotSubscriptionBillingIssue() -> [SubscriptionBillingIssueListener] {
subscriptionBillingIssueListeners.map { $0.listener }
}
func hasSubscriptionBillingIssueListeners() -> Bool {
!subscriptionBillingIssueListeners.isEmpty
}
}
285 changes: 285 additions & 0 deletions packages/apple/Sources/Helpers/OpenIapConnectionLifecycle.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
import Foundation

/// Owns the mutable connection resources that must move together during
/// init/end races. OpenIapModule keeps StoreKit behavior; this helper keeps the
/// lock, generation checks, and task handles in one place.
@available(iOS 15.0, macOS 14.0, tvOS 16.0, watchOS 8.0, *)
final class OpenIapConnectionLifecycle {
// MARK: - Resource Snapshots

struct CleanupResources {
let updateListenerTask: Task<Void, Error>?
let messageListenerTask: Task<Void, Never>?
let unfinishedTransactionTask: Task<Void, Never>?
let productManager: ProductManager?
let didRegisterPaymentQueueObserver: Bool
}

struct DeinitResources {
let initTask: Task<Bool, Error>?
let endTask: Task<Void, Never>?
let updateListenerTask: Task<Void, Error>?
let messageListenerTask: Task<Void, Never>?
let unfinishedTransactionTask: Task<Void, Never>?
}

private let lock = NSLock()
private var connectionGeneration: UInt64 = 0
private var initTask: Task<Bool, Error>?
private var initTaskGeneration: UInt64?
private var endTask: Task<Void, Never>?
private var endTaskGeneration: UInt64?
private var updateListenerTask: Task<Void, Error>?
private var messageListenerTask: Task<Void, Never>?
private var unfinishedTransactionTask: Task<Void, Never>?
private var productManager: ProductManager?

#if os(iOS)
private var didRegisterPaymentQueueObserver = false
#endif

// MARK: - Init / End Tasks

func currentEndTask() -> Task<Void, Never>? {
withLock { endTask }
}

func makeInitTask(
operation: @escaping (UInt64) async throws -> Bool
) -> (task: Task<Bool, Error>, generation: UInt64)? {
withLock {
guard endTask == nil else {
return nil
}

if let initTask, initTaskGeneration == connectionGeneration {
return (initTask, connectionGeneration)
}

connectionGeneration += 1
let generation = connectionGeneration
let task = Task<Bool, Error> {
try await operation(generation)
}
initTask = task
initTaskGeneration = generation
return (task, generation)
}
}

func makeEndTask(cleanup: @escaping () async -> Void) -> Task<Void, Never> {
withLock {
if let endTask {
return endTask
}

connectionGeneration += 1
let generation = connectionGeneration
let taskToCancel = initTask
initTask = nil
initTaskGeneration = nil

let task = Task { [weak self] in
taskToCancel?.cancel()
if let taskToCancel {
await Self.awaitCancelledInitTask(taskToCancel)
}

await cleanup()
self?.clearEndTask(generation: generation)
}
Comment thread
hyochan marked this conversation as resolved.
endTask = task
endTaskGeneration = generation
return task
}
}

func clearInitTask(generation: UInt64) {
withLock {
if initTaskGeneration == generation {
initTask = nil
initTaskGeneration = nil
}
}
}

func clearUnfinishedTransactionTask(generation: UInt64) {
withLock {
if connectionGeneration == generation {
unfinishedTransactionTask = nil
}
}
}

func ensureCurrent(_ generation: UInt64) throws {
try withLock {
guard connectionGeneration == generation else {
throw CancellationError()
}
}
}

// MARK: - Resource Access

func currentProductManager() -> ProductManager? {
withLock { productManager }
}

func getOrCreateProductManager(generation: UInt64) throws -> ProductManager {
try withLock {
guard connectionGeneration == generation else {
throw CancellationError()
}

if let productManager {
return productManager
}

let productManager = ProductManager()
self.productManager = productManager
return productManager
}
}

#if os(iOS)
func markPaymentQueueObserverRegisteredIfNeeded(generation: UInt64) throws -> Bool {
try withLock {
guard connectionGeneration == generation else {
throw CancellationError()
}

if didRegisterPaymentQueueObserver {
return false
}

didRegisterPaymentQueueObserver = true
return true
}
}
#endif

// MARK: - Listener Tasks

func startTransactionListenerTask(
generation: UInt64,
makeTask: () -> Task<Void, Error>
) throws {
try withLock {
guard connectionGeneration == generation else {
throw CancellationError()
}
guard updateListenerTask == nil else {
return
}

updateListenerTask = makeTask()
}
}

func startUnfinishedTransactionTask(
generation: UInt64,
makeTask: () -> Task<Void, Never>
) throws {
try withLock {
guard connectionGeneration == generation else {
throw CancellationError()
}
guard unfinishedTransactionTask == nil else {
return
}

unfinishedTransactionTask = makeTask()
}
}

func startMessageListenerTask(
generation: UInt64?,
makeTask: () -> Task<Void, Never>
) throws {
try withLock {
if let generation, connectionGeneration != generation {
throw CancellationError()
}
guard endTask == nil, messageListenerTask == nil else {
return
}

messageListenerTask = makeTask()
}
}

// MARK: - Cleanup

func detachResourcesForCleanup() -> CleanupResources {
withLock {
#if os(iOS)
let wasRegistered = didRegisterPaymentQueueObserver
didRegisterPaymentQueueObserver = false
#else
let wasRegistered = false
#endif

let resources = CleanupResources(
updateListenerTask: updateListenerTask,
messageListenerTask: messageListenerTask,
unfinishedTransactionTask: unfinishedTransactionTask,
productManager: productManager,
didRegisterPaymentQueueObserver: wasRegistered
)

updateListenerTask = nil
messageListenerTask = nil
unfinishedTransactionTask = nil
productManager = nil
return resources
}
}

func detachTasksForDeinit() -> DeinitResources {
withLock {
let resources = DeinitResources(
initTask: initTask,
endTask: endTask,
updateListenerTask: updateListenerTask,
messageListenerTask: messageListenerTask,
unfinishedTransactionTask: unfinishedTransactionTask
)

initTask = nil
initTaskGeneration = nil
endTask = nil
endTaskGeneration = nil
updateListenerTask = nil
messageListenerTask = nil
unfinishedTransactionTask = nil
return resources
}
}

// MARK: - Locking

private func clearEndTask(generation: UInt64) {
withLock {
if endTaskGeneration == generation {
endTask = nil
endTaskGeneration = nil
}
}
}

private func withLock<T>(_ body: () throws -> T) rethrows -> T {
lock.lock()
defer { lock.unlock() }
return try body()
}

private static func awaitCancelledInitTask(_ task: Task<Bool, Error>) async {
do {
_ = try await task.value
} catch is CancellationError {
// Expected when endConnection cancels an in-flight initConnection.
} catch {
OpenIapLog.warn("initConnection failed while endConnection was cancelling it: \(error)")
}
}
}
Loading