From fcf54b2eea100cd45514f54255c4ff17c4801a1e Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Fri, 18 Oct 2024 14:14:10 +0900 Subject: [PATCH 01/12] Update the project to Swift 6 --- .swiftlint.yml | 2 + Package.swift | 2 +- Package@swift-5.10.swift | 76 +++++++++++++++++++ Package@swift-5.9.swift | 76 +++++++++++++++++++ Sources/Flare/Classes/Common/Logger.swift | 2 +- .../Extensions/SKRequest+Identifier.swift | 4 +- Sources/Flare/Classes/Flare.swift | 4 +- .../AsyncSequence/AsyncSequence+Stream.swift | 2 +- .../Helpers/PaymentQueue/PaymentQueue.swift | 4 +- .../PaymentTransaction.swift | 4 +- .../Helpers/ScenesHolder/IScenesHolder.swift | 4 +- .../TransactionListenerDelegate.swift | 2 +- .../Protocols/IStoreTransaction.swift | 2 +- .../Classes/Models/StoreTransaction.swift | 2 +- .../RedeemCodeProvider.swift | 2 +- .../RefundProvider/RefundProvider.swift | 4 +- .../ISystemInfoProvider.swift | 7 +- .../SystemInfoProvider.swift | 22 ++++-- 18 files changed, 192 insertions(+), 29 deletions(-) create mode 100644 Package@swift-5.10.swift create mode 100644 Package@swift-5.9.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index e0943c198..d3c7d26bc 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -3,6 +3,8 @@ excluded: - Package.swift - Package@swift-5.7.swift - Package@swift-5.8.swift + - Package@swift-5.9.swift + - Package@swift-5.10.swift - Sources/Flare/Classes/Generated/Strings.swift - Sources/FlareUI/Classes/Generated/Strings.swift - Sources/FlareUI/Classes/Generated/Colors.swift diff --git a/Package.swift b/Package.swift index 4e637807f..6914c6467 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.9 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. // swiftlint:disable all diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift new file mode 100644 index 000000000..c62e4e19d --- /dev/null +++ b/Package@swift-5.10.swift @@ -0,0 +1,76 @@ +// swift-tools-version: 5.10 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable all + +import PackageDescription + +let visionOSSetting: SwiftSetting = .define("VISION_OS", .when(platforms: [.visionOS])) + +let package = Package( + name: "Flare", + defaultLocalization: "en", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v7), + .tvOS(.v13), + .visionOS(.v1), + ], + products: [ + .library(name: "Flare", targets: ["Flare"]), + .library(name: "FlareUI", targets: ["FlareUI"]), + ], + dependencies: [ + .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), + .package(url: "https://github.com/space-code/log.git", .upToNextMajor(from: "1.1.0")), + .package(url: "https://github.com/space-code/atomic.git", .upToNextMajor(from: "1.0.0")), + .package( + url: "https://github.com/pointfreeco/swift-snapshot-testing", + from: "1.15.3" + ), + ], + targets: [ + .target( + name: "Flare", + dependencies: [ + .product(name: "Atomic", package: "atomic"), + .product(name: "Concurrency", package: "concurrency"), + .product(name: "Log", package: "log"), + ], + resources: [.process("Resources")], + swiftSettings: [visionOSSetting] + ), + .target( + name: "FlareUI", + dependencies: ["Flare"], + resources: [.process("Resources")] + ), + .target(name: "FlareMock", dependencies: ["Flare"]), + .target(name: "FlareUIMock", dependencies: ["FlareMock", "FlareUI"]), + .testTarget( + name: "FlareTests", + dependencies: [ + "Flare", + "FlareMock", + .product(name: "TestConcurrency", package: "concurrency"), + ] + ), + .testTarget( + name: "FlareUITests", + dependencies: [ + "FlareUI", + "FlareMock", + "FlareUIMock", + ] + ), + .testTarget( + name: "SnapshotTests", + dependencies: [ + "Flare", + "FlareUIMock", + .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), + ] + ), + ] +) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift new file mode 100644 index 000000000..4e637807f --- /dev/null +++ b/Package@swift-5.9.swift @@ -0,0 +1,76 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. +// swiftlint:disable all + +import PackageDescription + +let visionOSSetting: SwiftSetting = .define("VISION_OS", .when(platforms: [.visionOS])) + +let package = Package( + name: "Flare", + defaultLocalization: "en", + platforms: [ + .macOS(.v10_15), + .iOS(.v13), + .watchOS(.v7), + .tvOS(.v13), + .visionOS(.v1), + ], + products: [ + .library(name: "Flare", targets: ["Flare"]), + .library(name: "FlareUI", targets: ["FlareUI"]), + ], + dependencies: [ + .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), + .package(url: "https://github.com/space-code/log.git", .upToNextMajor(from: "1.1.0")), + .package(url: "https://github.com/space-code/atomic.git", .upToNextMajor(from: "1.0.0")), + .package( + url: "https://github.com/pointfreeco/swift-snapshot-testing", + from: "1.15.3" + ), + ], + targets: [ + .target( + name: "Flare", + dependencies: [ + .product(name: "Atomic", package: "atomic"), + .product(name: "Concurrency", package: "concurrency"), + .product(name: "Log", package: "log"), + ], + resources: [.process("Resources")], + swiftSettings: [visionOSSetting] + ), + .target( + name: "FlareUI", + dependencies: ["Flare"], + resources: [.process("Resources")] + ), + .target(name: "FlareMock", dependencies: ["Flare"]), + .target(name: "FlareUIMock", dependencies: ["FlareMock", "FlareUI"]), + .testTarget( + name: "FlareTests", + dependencies: [ + "Flare", + "FlareMock", + .product(name: "TestConcurrency", package: "concurrency"), + ] + ), + .testTarget( + name: "FlareUITests", + dependencies: [ + "FlareUI", + "FlareMock", + "FlareUIMock", + ] + ), + .testTarget( + name: "SnapshotTests", + dependencies: [ + "Flare", + "FlareUIMock", + .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), + ] + ), + ] +) diff --git a/Sources/Flare/Classes/Common/Logger.swift b/Sources/Flare/Classes/Common/Logger.swift index 05d887ddf..4ecfa8cbd 100644 --- a/Sources/Flare/Classes/Common/Logger.swift +++ b/Sources/Flare/Classes/Common/Logger.swift @@ -4,7 +4,7 @@ // import Foundation -import Log +@preconcurrency import Log // MARK: - Logger diff --git a/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift b/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift index 37983ce72..6952db402 100644 --- a/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift +++ b/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift @@ -1,11 +1,11 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit -private var requestIdKey: UInt = 0 +private nonisolated(unsafe) var requestIdKey: UInt = 0 extension SKRequest { var id: String { diff --git a/Sources/Flare/Classes/Flare.swift b/Sources/Flare/Classes/Flare.swift index f3b3b7336..00e7d1a19 100644 --- a/Sources/Flare/Classes/Flare.swift +++ b/Sources/Flare/Classes/Flare.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import struct Log.LogLevel @@ -23,7 +23,7 @@ public final class Flare { private let configurationProvider: IConfigurationProvider /// The singleton instance. - private static let flare: Flare = .init() + private nonisolated(unsafe) static let flare: Flare = .init() /// Returns a shared `Flare` object. public static var shared: IFlare { flare } diff --git a/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift b/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift index aa630ddc5..f0c62373b 100644 --- a/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift +++ b/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift @@ -5,7 +5,7 @@ import Foundation -extension AsyncSequence { +extension AsyncSequence where Element: Sendable { func toAsyncStream() -> AsyncStream { var asyncIterator = makeAsyncIterator() return AsyncStream { diff --git a/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift b/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift index 7376a825d..e99ceeef3 100644 --- a/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift +++ b/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift @@ -1,12 +1,12 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit /// `PaymentQueue` interacts with the server-side payment queue -public protocol PaymentQueue: AnyObject { +public protocol PaymentQueue: AnyObject, Sendable { /// `False` if this device is not able or allowed to make payments var canMakePayments: Bool { get } diff --git a/Sources/Flare/Classes/Helpers/PaymentTransaction/PaymentTransaction.swift b/Sources/Flare/Classes/Helpers/PaymentTransaction/PaymentTransaction.swift index 32b660d60..13162fe67 100644 --- a/Sources/Flare/Classes/Helpers/PaymentTransaction/PaymentTransaction.swift +++ b/Sources/Flare/Classes/Helpers/PaymentTransaction/PaymentTransaction.swift @@ -1,11 +1,11 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit -public struct PaymentTransaction: Equatable { +public struct PaymentTransaction: Equatable, Sendable { // MARK: Lifecycle init(_ skTransaction: SKPaymentTransaction) { diff --git a/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift b/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift index dba0202de..ac000bcc5 100644 --- a/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift +++ b/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // #if canImport(UIKit) @@ -10,7 +10,7 @@ // MARK: - IScenesHolder /// A type that holds all connected scenes. -protocol IScenesHolder { +protocol IScenesHolder: Sendable { #if os(iOS) || VISION_OS /// The scenes that are connected to the app. var connectedScenes: Set { get } diff --git a/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListenerDelegate.swift b/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListenerDelegate.swift index 10bb0880b..a08ac32be 100644 --- a/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListenerDelegate.swift +++ b/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListenerDelegate.swift @@ -5,7 +5,7 @@ import Foundation -protocol TransactionListenerDelegate: AnyObject { +protocol TransactionListenerDelegate: AnyObject, Sendable { func transactionListener( _ transactionListener: ITransactionListener, transactionDidUpdate result: Result diff --git a/Sources/Flare/Classes/Models/Internal/Protocols/IStoreTransaction.swift b/Sources/Flare/Classes/Models/Internal/Protocols/IStoreTransaction.swift index 5372af55a..e10b49e03 100644 --- a/Sources/Flare/Classes/Models/Internal/Protocols/IStoreTransaction.swift +++ b/Sources/Flare/Classes/Models/Internal/Protocols/IStoreTransaction.swift @@ -8,7 +8,7 @@ import Foundation // MARK: - IStoreTransaction /// A type that represents a store transaction. -protocol IStoreTransaction { +protocol IStoreTransaction: Sendable { /// The unique identifier for the product. var productIdentifier: String { get } /// The date when the transaction occurred. diff --git a/Sources/Flare/Classes/Models/StoreTransaction.swift b/Sources/Flare/Classes/Models/StoreTransaction.swift index 7d1c7d662..c99b75e6e 100644 --- a/Sources/Flare/Classes/Models/StoreTransaction.swift +++ b/Sources/Flare/Classes/Models/StoreTransaction.swift @@ -9,7 +9,7 @@ import StoreKit // MARK: - StoreTransaction /// A class represent a StoreKit transaction. -public final class StoreTransaction { +public final class StoreTransaction: Sendable { // MARK: Properties /// The StoreKit transaction. diff --git a/Sources/Flare/Classes/Providers/RedeemCodeProvider/RedeemCodeProvider.swift b/Sources/Flare/Classes/Providers/RedeemCodeProvider/RedeemCodeProvider.swift index 6358aeb69..7714560b1 100644 --- a/Sources/Flare/Classes/Providers/RedeemCodeProvider/RedeemCodeProvider.swift +++ b/Sources/Flare/Classes/Providers/RedeemCodeProvider/RedeemCodeProvider.swift @@ -36,7 +36,7 @@ extension RedeemCodeProvider: IRedeemCodeProvider { @available(tvOS, unavailable) @MainActor func presentOfferCodeRedeemSheet() async throws { - let windowScene = try systemInfoProvider.currentScene + let windowScene = try await systemInfoProvider.currentScene do { Logger.debug(message: L10n.Redeem.presentingOfferCodeRedeemSheet) try await AppStore.presentOfferCodeRedeemSheet(in: windowScene) diff --git a/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift b/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift index 794db1066..5c08a6ab1 100644 --- a/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift +++ b/Sources/Flare/Classes/Providers/RefundProvider/RefundProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // #if canImport(UIKit) @@ -77,7 +77,7 @@ extension RefundProvider: IRefundProvider { @available(tvOS, unavailable) @MainActor func beginRefundRequest(productID: String) async throws -> RefundRequestStatus { - let windowScene = try systemInfoProvider.currentScene + let windowScene = try await systemInfoProvider.currentScene let transactionID = try await refundRequestProvider.verifyTransaction(productID: productID) return try await initRefundRequest(transactionID: transactionID, windowScene: windowScene) } diff --git a/Sources/Flare/Classes/Providers/SystemInfoProvider/ISystemInfoProvider.swift b/Sources/Flare/Classes/Providers/SystemInfoProvider/ISystemInfoProvider.swift index 0c30a97a0..85a588e59 100644 --- a/Sources/Flare/Classes/Providers/SystemInfoProvider/ISystemInfoProvider.swift +++ b/Sources/Flare/Classes/Providers/SystemInfoProvider/ISystemInfoProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // #if canImport(UIKit) @@ -10,9 +10,10 @@ // MARK: - ISystemInfoProvider /// A type that provides the system info. -protocol ISystemInfoProvider { +protocol ISystemInfoProvider: Sendable { #if os(iOS) || VISION_OS /// The current window scene. - var currentScene: UIWindowScene { get throws } + @MainActor + var currentScene: UIWindowScene { get async throws } #endif } diff --git a/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift b/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift index 1c04cb197..f6dd30ef7 100644 --- a/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift +++ b/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // #if canImport(UIKit) @@ -13,13 +13,20 @@ final class SystemInfoProvider { // MARK: Properties #if os(iOS) || VISION_OS - private let scenesHolder: IScenesHolder + private let scenesHolder: @Sendable () async -> IScenesHolder // MARK: Initialization - init(scenesHolder: IScenesHolder = UIApplication.shared) { - self.scenesHolder = scenesHolder + init(scenesHolder: IScenesHolder? = nil) { + if let scenesHolder { + self.scenesHolder = { scenesHolder } + } else { + self.scenesHolder = { + await MainActor.run { UIApplication.shared } + } + } } + #endif } @@ -33,13 +40,14 @@ extension SystemInfoProvider: ISystemInfoProvider { @available(tvOS, unavailable) @MainActor var currentScene: UIWindowScene { - get throws { - var scenes = scenesHolder.connectedScenes + get async throws { + let holder = await scenesHolder() + var scenes = holder.connectedScenes .filter { $0.activationState == .foregroundActive } #if DEBUG && targetEnvironment(simulator) if scenes.isEmpty, ProcessInfo.isRunningUnitTests { - scenes = scenesHolder.connectedScenes + scenes = holder.connectedScenes } #endif From 7928c0730c4a93e93b7cfb070cb181ad4f05c2ed Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 10:32:29 +0100 Subject: [PATCH 02/12] Add full Swift concurrency support with Sendable requirements --- .gitignore | 3 +- Package.resolved | 68 ------------------- Package.swift | 13 ++-- Package@swift-5.10.swift | 10 +-- Package@swift-5.7.swift | 10 +-- Package@swift-5.8.swift | 10 +-- Package@swift-5.9.swift | 10 +-- Sources/Flare/Classes/Flare.swift | 12 ++-- .../Classes/Helpers/Async/AsyncHandler.swift | 8 +-- .../Helpers/PaymentQueue/PaymentQueue.swift | 12 ++++ Sources/Flare/Classes/IFlare.swift | 16 ++--- .../Internal/Protocols/ISKProduct.swift | 2 +- .../Internal/Protocols/IStorePayment.swift | 12 ++++ .../Models/Internal/SK1StorePayment.swift | 29 ++++++++ .../Models/Internal/SK1StoreProduct.swift | 3 +- .../Models/Internal/SK2StoreProduct.swift | 3 +- .../Models/Internal/SK2StoreTransaction.swift | 2 +- .../Providers/IAPProvider/IAPProvider.swift | 22 +++--- .../Providers/IAPProvider/IIAPProvider.swift | 16 ++--- .../PaymentProvider/PaymentProvider.swift | 9 ++- .../CachingProductsProviderDecorator.swift | 12 ++-- .../SortingProductsProviderDecorator.swift | 6 ++ .../ProductProvider/IProductProvider.swift | 10 +-- .../ProductProvider/ProductProvider.swift | 15 +++- .../PurchaseProvider/IPurchaseProvider.swift | 4 +- .../PurchaseProvider/PurchaseProvider.swift | 6 +- .../Factories/IReceiptRefreshRequest.swift | 4 +- .../ReceiptRefreshProvider.swift | 11 +-- .../Mocks/PaymentTransactionMock.swift | 2 +- Sources/FlareMock/Mocks/ProductMock.swift | 2 +- .../TestHelpers/Mocks/PaymentQueueMock.swift | 4 +- .../Mocks/ProductResponseMock.swift | 4 +- .../Mocks/ProductsRequestMock.swift | 4 +- .../Mocks/PurchaseProviderMock.swift | 12 ++-- .../Mocks/ReceiptRefreshRequestMock.swift | 4 +- .../TestHelpers/Mocks/SKProductMock.swift | 6 +- .../Mocks/StoreTransactionMock.swift | 2 +- .../Stubs/StoreTransactionStub.swift | 2 +- 38 files changed, 202 insertions(+), 178 deletions(-) delete mode 100644 Package.resolved create mode 100644 Sources/Flare/Classes/Models/Internal/Protocols/IStorePayment.swift create mode 100644 Sources/Flare/Classes/Models/Internal/SK1StorePayment.swift diff --git a/.gitignore b/.gitignore index a47bd1257..73b1212d8 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,5 @@ fastlane/test_output iOSInjectionProject/ *.xcodeproj -Example/ \ No newline at end of file +Example/ +Package.resolved diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 5149092ec..000000000 --- a/Package.resolved +++ /dev/null @@ -1,68 +0,0 @@ -{ - "pins" : [ - { - "identity" : "atomic", - "kind" : "remoteSourceControl", - "location" : "https://github.com/space-code/atomic.git", - "state" : { - "revision" : "6a1473440c31c6debf1de2404265949ed7892b14", - "version" : "1.0.1" - } - }, - { - "identity" : "concurrency", - "kind" : "remoteSourceControl", - "location" : "https://github.com/space-code/concurrency", - "state" : { - "revision" : "f9611694f77f64e43d9467a16b2f5212cd04099b", - "version" : "0.0.1" - } - }, - { - "identity" : "log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/space-code/log", - "state" : { - "revision" : "d99fff5656c31ef7e604965b90a50ec10539c98f", - "version" : "1.1.0" - } - }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin", - "state" : { - "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", - "version" : "1.4.3" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, - { - "identity" : "swift-snapshot-testing", - "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-snapshot-testing", - "state" : { - "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", - "version" : "1.17.6" - } - }, - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax", - "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift index 6914c6467..5023bf58b 100644 --- a/Package.swift +++ b/Package.swift @@ -21,13 +21,13 @@ let package = Package( .library(name: "FlareUI", targets: ["FlareUI"]), ], dependencies: [ - .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), - .package(url: "https://github.com/space-code/log.git", .upToNextMajor(from: "1.1.0")), - .package(url: "https://github.com/space-code/atomic.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/space-code/concurrency.git", exact: "0.1.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.3.0"), + .package(url: "https://github.com/space-code/log.git", exact: "1.2.0"), + .package(url: "https://github.com/space-code/atomic.git", exact: "1.1.0"), .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", - from: "1.15.3" + exact: "1.15.3" ), ], targets: [ @@ -72,5 +72,6 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ] + ], + swiftLanguageModes: [.v5] ) diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift index c62e4e19d..9842c79a8 100644 --- a/Package@swift-5.10.swift +++ b/Package@swift-5.10.swift @@ -21,13 +21,13 @@ let package = Package( .library(name: "FlareUI", targets: ["FlareUI"]), ], dependencies: [ - .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), - .package(url: "https://github.com/space-code/log.git", .upToNextMajor(from: "1.1.0")), - .package(url: "https://github.com/space-code/atomic.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/space-code/concurrency.git", exact: "0.1.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.3.0"), + .package(url: "https://github.com/space-code/log.git", exact: "1.2.0"), + .package(url: "https://github.com/space-code/atomic.git", exact: "1.1.0"), .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", - from: "1.15.3" + exact: "1.15.3" ), ], targets: [ diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 045522079..ef15de429 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -18,13 +18,13 @@ let package = Package( .library(name: "FlareUI", targets: ["FlareUI"]), ], dependencies: [ - .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), - .package(url: "https://github.com/space-code/log.git", .upToNextMajor(from: "1.1.0")), - .package(url: "https://github.com/space-code/atomic.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/space-code/concurrency.git", exact: "0.1.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.3.0"), + .package(url: "https://github.com/space-code/log.git", exact: "1.2.0"), + .package(url: "https://github.com/space-code/atomic.git", exact: "1.1.0"), .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", - from: "1.15.3" + exact: "1.15.3" ), ], targets: [ diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index adca19830..1946fdb3b 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -18,13 +18,13 @@ let package = Package( .library(name: "FlareUI", targets: ["FlareUI"]), ], dependencies: [ - .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), - .package(url: "https://github.com/space-code/log.git", .upToNextMajor(from: "1.1.0")), - .package(url: "https://github.com/space-code/atomic.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/space-code/concurrency.git", exact: "0.1.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.3.0"), + .package(url: "https://github.com/space-code/log.git", exact: "1.2.0"), + .package(url: "https://github.com/space-code/atomic.git", exact: "1.1.0"), .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", - from: "1.15.3" + exact: "1.15.3" ), ], targets: [ diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 4e637807f..0f0bc05d2 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -21,13 +21,13 @@ let package = Package( .library(name: "FlareUI", targets: ["FlareUI"]), ], dependencies: [ - .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), - .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), - .package(url: "https://github.com/space-code/log.git", .upToNextMajor(from: "1.1.0")), - .package(url: "https://github.com/space-code/atomic.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/space-code/concurrency.git", exact: "0.1.0"), + .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.3.0"), + .package(url: "https://github.com/space-code/log.git", exact: "1.2.0"), + .package(url: "https://github.com/space-code/atomic.git", exact: "1.1.0"), .package( url: "https://github.com/pointfreeco/swift-snapshot-testing", - from: "1.15.3" + exact: "1.15.3" ), ], targets: [ diff --git a/Sources/Flare/Classes/Flare.swift b/Sources/Flare/Classes/Flare.swift index 00e7d1a19..2a7d44a80 100644 --- a/Sources/Flare/Classes/Flare.swift +++ b/Sources/Flare/Classes/Flare.swift @@ -60,7 +60,7 @@ public final class Flare { // MARK: IFlare extension Flare: IFlare { - public func fetch(productIDs: some Collection, completion: @escaping Closure>) { + public func fetch(productIDs: some Collection, completion: @escaping SendableClosure>) { iapProvider.fetch(productIDs: productIDs, completion: completion) } @@ -71,7 +71,7 @@ extension Flare: IFlare { public func purchase( product: StoreProduct, promotionalOffer: PromotionalOffer?, - completion: @escaping Closure> + completion: @escaping SendableClosure> ) { guard checkIfUserCanMakePayments() else { completion(.failure(.paymentNotAllowed)) @@ -117,7 +117,7 @@ extension Flare: IFlare { return try await iapProvider.purchase(product: product, options: options, promotionalOffer: promotionalOffer) } - public func receipt(completion: @escaping Closure>) { + public func receipt(completion: @escaping SendableClosure>) { iapProvider.refreshReceipt { result in switch result { case let .success(receipt): @@ -140,7 +140,7 @@ extension Flare: IFlare { await iapProvider.finish(transaction: transaction) } - public func addTransactionObserver(fallbackHandler: Closure>?) { + public func addTransactionObserver(fallbackHandler: SendableClosure>?) { iapProvider.addTransactionObserver(fallbackHandler: fallbackHandler) } @@ -157,7 +157,7 @@ extension Flare: IFlare { try await iapProvider.restore() } - public func restore(_ completion: @escaping (Result) -> Void) { + public func restore(_ completion: @escaping @Sendable (Result) -> Void) { iapProvider.restore(completion) } @@ -165,7 +165,7 @@ extension Flare: IFlare { try await iapProvider.refreshReceipt(updateTransactions: updateTransactions) } - public func receipt(updateTransactions: Bool, completion: @escaping (Result) -> Void) { + public func receipt(updateTransactions: Bool, completion: @escaping @Sendable (Result) -> Void) { iapProvider.refreshReceipt(updateTransactions: updateTransactions, completion: completion) } diff --git a/Sources/Flare/Classes/Helpers/Async/AsyncHandler.swift b/Sources/Flare/Classes/Helpers/Async/AsyncHandler.swift index 8ffb43c1f..2db012f27 100644 --- a/Sources/Flare/Classes/Helpers/Async/AsyncHandler.swift +++ b/Sources/Flare/Classes/Helpers/Async/AsyncHandler.swift @@ -9,10 +9,10 @@ import Foundation @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) enum AsyncHandler { - static func call( + static func call( strategy: Strategy = .default, - completion: @escaping (Result) -> Void, - asyncMethod method: @escaping () async throws -> T + completion: @escaping @Sendable (Result) -> Void, + asyncMethod method: @escaping @Sendable () async throws -> T ) { Task { do { @@ -26,7 +26,7 @@ enum AsyncHandler { // MARK: Private - private static func execute(strategy: Strategy, block: @escaping () -> Void) async { + private static func execute(strategy: Strategy, block: @escaping @Sendable () -> Void) async { switch strategy { case .runOnMain: await MainActor.run { block() } diff --git a/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift b/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift index e99ceeef3..06ed0e847 100644 --- a/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift +++ b/Sources/Flare/Classes/Helpers/PaymentQueue/PaymentQueue.swift @@ -5,6 +5,8 @@ import StoreKit +// MARK: - PaymentQueue + /// `PaymentQueue` interacts with the server-side payment queue public protocol PaymentQueue: AnyObject, Sendable { /// `False` if this device is not able or allowed to make payments @@ -42,3 +44,13 @@ public protocol PaymentQueue: AnyObject, Sendable { func presentCodeRedemptionSheet() #endif } + +extension PaymentQueue { + func add(_ payment: IStorePayment) { + if let payment = payment as? SK1StorePayment { + self.add(payment.underlyingProduct) + } else { + fatalError("Incompatible type: PaymentQueue works only with SK1StorePayment.") + } + } +} diff --git a/Sources/Flare/Classes/IFlare.swift b/Sources/Flare/Classes/IFlare.swift index 20a4b0ac8..c185ebc82 100644 --- a/Sources/Flare/Classes/IFlare.swift +++ b/Sources/Flare/Classes/IFlare.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import Foundation @@ -19,7 +19,7 @@ public protocol IFlare { /// - Parameters: /// - productIDs: The list of product identifiers for which you wish to retrieve descriptions. /// - completion: The completion containing the response of retrieving products. - func fetch(productIDs: some Collection, completion: @escaping Closure>) + func fetch(productIDs: some Collection, completion: @escaping SendableClosure>) /// Retrieves localized information from the App Store about a specified list of products. /// @@ -43,7 +43,7 @@ public protocol IFlare { func purchase( product: StoreProduct, promotionalOffer: PromotionalOffer?, - completion: @escaping Closure> + completion: @escaping SendableClosure> ) /// Purchases a product. @@ -116,7 +116,7 @@ public protocol IFlare { /// - On failure, it returns a `Result` with an `IAPError` describing the issue. /// /// - Note: Use this method to handle asynchronous receipt refreshing and transaction updates with completion handler feedback. - func receipt(updateTransactions: Bool, completion: @escaping (Result) -> Void) + func receipt(updateTransactions: Bool, completion: @escaping @Sendable (Result) -> Void) /// Refreshes the receipt and optionally updates transactions. /// @@ -149,7 +149,7 @@ public protocol IFlare { /// The transactions array will only be synchronized with the server while the queue has observers. /// /// - Note: This may require that the user authenticate. - func addTransactionObserver(fallbackHandler: Closure>?) + func addTransactionObserver(fallbackHandler: SendableClosure>?) /// Removes transaction observer from the payment queue. /// The transactions array will only be synchronized with the server while the queue has observers. @@ -187,7 +187,7 @@ public protocol IFlare { /// /// - Note: Use this method when you need to handle the restoration process asynchronously and provide feedback through the completion /// handler. - func restore(_ completion: @escaping (Result) -> Void) + func restore(_ completion: @escaping @Sendable (Result) -> Void) #if os(iOS) || VISION_OS /// Present the refund request sheet for the specified transaction in a window scene. @@ -231,7 +231,7 @@ public extension IFlare { /// - completion: The closure to be executed once the purchase is complete. func purchase( product: StoreProduct, - completion: @escaping Closure> + completion: @escaping SendableClosure> ) { purchase(product: product, promotionalOffer: nil, completion: completion) } @@ -298,7 +298,7 @@ public extension IFlare { /// Refreshes the receipt, representing the user's transactions with your app. /// /// - Parameter completion: The closure to be executed when the refresh operation ends. - func receipt(completion: @escaping Closure>) { + func receipt(completion: @escaping SendableClosure>) { receipt(updateTransactions: false, completion: completion) } diff --git a/Sources/Flare/Classes/Models/Internal/Protocols/ISKProduct.swift b/Sources/Flare/Classes/Models/Internal/Protocols/ISKProduct.swift index 00ad42451..7bdb13507 100644 --- a/Sources/Flare/Classes/Models/Internal/Protocols/ISKProduct.swift +++ b/Sources/Flare/Classes/Models/Internal/Protocols/ISKProduct.swift @@ -6,7 +6,7 @@ import Foundation /// Protocol representing a Store Kit product. -protocol ISKProduct { +protocol ISKProduct: Sendable { /// A localized description of the product. var localizedDescription: String { get } diff --git a/Sources/Flare/Classes/Models/Internal/Protocols/IStorePayment.swift b/Sources/Flare/Classes/Models/Internal/Protocols/IStorePayment.swift new file mode 100644 index 000000000..243de7ab4 --- /dev/null +++ b/Sources/Flare/Classes/Models/Internal/Protocols/IStorePayment.swift @@ -0,0 +1,12 @@ +// +// Flare +// Copyright © 2024 Space Code. All rights reserved. +// + +import Foundation + +// MARK: - IStorePayment + +protocol IStorePayment: AnyObject, Sendable { + var productIdentifier: String { get } +} diff --git a/Sources/Flare/Classes/Models/Internal/SK1StorePayment.swift b/Sources/Flare/Classes/Models/Internal/SK1StorePayment.swift new file mode 100644 index 000000000..d965b21aa --- /dev/null +++ b/Sources/Flare/Classes/Models/Internal/SK1StorePayment.swift @@ -0,0 +1,29 @@ +// +// Flare +// Copyright © 2024 Space Code. All rights reserved. +// + +import Foundation +import StoreKit + +// MARK: - SK1StorePayment + +final class SK1StorePayment: @unchecked Sendable { + // MARK: Properties + + let underlyingProduct: SKPayment + + // MARK: Initialization + + init(underlyingProduct: SKPayment) { + self.underlyingProduct = underlyingProduct + } +} + +// MARK: IStorePayment + +extension SK1StorePayment: IStorePayment { + var productIdentifier: String { + underlyingProduct.productIdentifier + } +} diff --git a/Sources/Flare/Classes/Models/Internal/SK1StoreProduct.swift b/Sources/Flare/Classes/Models/Internal/SK1StoreProduct.swift index e692030a4..35b98550f 100644 --- a/Sources/Flare/Classes/Models/Internal/SK1StoreProduct.swift +++ b/Sources/Flare/Classes/Models/Internal/SK1StoreProduct.swift @@ -15,12 +15,13 @@ final class SK1StoreProduct { let product: SKProduct /// The price formatter. - private lazy var numberFormatter: NumberFormatter = .numberFormatter(with: self.product.priceLocale) + private let numberFormatter: NumberFormatter // MARK: Initialization init(_ product: SKProduct) { self.product = product + self.numberFormatter = .numberFormatter(with: self.product.priceLocale) } } diff --git a/Sources/Flare/Classes/Models/Internal/SK2StoreProduct.swift b/Sources/Flare/Classes/Models/Internal/SK2StoreProduct.swift index d1318c183..4929c5c37 100644 --- a/Sources/Flare/Classes/Models/Internal/SK2StoreProduct.swift +++ b/Sources/Flare/Classes/Models/Internal/SK2StoreProduct.swift @@ -21,12 +21,13 @@ final class SK2StoreProduct { } /// The price formatter. - private lazy var numberFormatter: NumberFormatter = .numberFormatter(with: self.currencyFormat.locale) + private let numberFormatter: NumberFormatter // MARK: Initialization init(_ product: StoreKit.Product) { self.product = product + self.numberFormatter = .numberFormatter(with: product.priceFormatStyle.locale) } } diff --git a/Sources/Flare/Classes/Models/Internal/SK2StoreTransaction.swift b/Sources/Flare/Classes/Models/Internal/SK2StoreTransaction.swift index 2fafe3f25..3e2c1e1d8 100644 --- a/Sources/Flare/Classes/Models/Internal/SK2StoreTransaction.swift +++ b/Sources/Flare/Classes/Models/Internal/SK2StoreTransaction.swift @@ -69,7 +69,7 @@ extension SK2StoreTransaction: IStoreTransaction { var price: Decimal? { #if swift(>=6.0) - underlyingRenewalInfo.price + transaction.price #else nil #endif diff --git a/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift b/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift index 3d42de619..13f9b2723 100644 --- a/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift +++ b/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift @@ -1,12 +1,12 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit /// A class that provides in-app purchase functionality. -final class IAPProvider: IIAPProvider { +final class IAPProvider: IIAPProvider, @unchecked Sendable { // MARK: Properties /// The queue of payment transactions to be processed by the App Store. @@ -60,15 +60,17 @@ final class IAPProvider: IIAPProvider { paymentQueue.canMakePayments } - func fetch(productIDs: some Collection, completion: @escaping Closure>) { + func fetch(productIDs: some Collection, completion: @escaping SendableClosure>) { if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) { + let productIDs = Array(productIDs) + AsyncHandler.call( strategy: .runOnMain, completion: { (result: Result<[StoreProduct], Error>) in switch result { case let .success(products): if products.isEmpty { - completion(.failure(.invalid(productIDs: Array(productIDs)))) + completion(.failure(.invalid(productIDs: productIDs))) } else { completion(.success(products)) } @@ -100,7 +102,7 @@ final class IAPProvider: IIAPProvider { func purchase( product: StoreProduct, promotionalOffer: PromotionalOffer?, - completion: @escaping Closure> + completion: @escaping SendableClosure> ) { purchaseProvider.purchase(product: product, promotionalOffer: promotionalOffer) { result in switch result { @@ -151,8 +153,8 @@ final class IAPProvider: IIAPProvider { } } - func refreshReceipt(updateTransactions: Bool, completion: @escaping (Result) -> Void) { - let refresh = { [weak self] in + func refreshReceipt(updateTransactions: Bool, completion: @escaping SendableClosure>) { + let refresh = { @Sendable [weak self] in self?.receiptRefreshProvider.refresh(requestID: UUID().uuidString) { [weak self] result in switch result { case .success: @@ -181,7 +183,7 @@ final class IAPProvider: IIAPProvider { } } - func refreshReceipt(completion: @escaping Closure>) { + func refreshReceipt(completion: @escaping SendableClosure>) { refreshReceipt(updateTransactions: false, completion: completion) } @@ -205,7 +207,7 @@ final class IAPProvider: IIAPProvider { } } - func addTransactionObserver(fallbackHandler: Closure>?) { + func addTransactionObserver(fallbackHandler: SendableClosure>?) { purchaseProvider.addTransactionObserver(fallbackHandler: fallbackHandler) } @@ -223,7 +225,7 @@ final class IAPProvider: IIAPProvider { try await purchaseProvider.restore() } - func restore(_ completion: @escaping (Result) -> Void) { + func restore(_ completion: @escaping @Sendable (Result) -> Void) { purchaseProvider.restore(completion) } diff --git a/Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift b/Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift index 4fe65b6d2..a27fcb7fe 100644 --- a/Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift +++ b/Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit @@ -17,7 +17,7 @@ public protocol IIAPProvider { /// - Parameters: /// - productIDs: The list of product identifiers for which you wish to retrieve descriptions. /// - completion: The completion containing the response of retrieving products. - func fetch(productIDs: some Collection, completion: @escaping Closure>) + func fetch(productIDs: some Collection, completion: @escaping SendableClosure>) /// Retrieves localized information from the App Store about a specified list of products. /// @@ -41,7 +41,7 @@ public protocol IIAPProvider { func purchase( product: StoreProduct, promotionalOffer: PromotionalOffer?, - completion: @escaping Closure> + completion: @escaping SendableClosure> ) /// Purchases a product. @@ -114,7 +114,7 @@ public protocol IIAPProvider { /// - On failure, it returns a `Result` with an `IAPError` describing the issue. /// /// - Note: Use this method to handle asynchronous receipt refreshing and transaction updates with completion handler feedback. - func refreshReceipt(updateTransactions: Bool, completion: @escaping (Result) -> Void) + func refreshReceipt(updateTransactions: Bool, completion: @escaping SendableClosure>) /// Refreshes the receipt and optionally updates transactions. /// @@ -132,7 +132,7 @@ public protocol IIAPProvider { /// Refreshes the receipt, representing the user's transactions with your app. /// /// - Parameter completion: The closure to be executed when the refresh operation ends. - func refreshReceipt(completion: @escaping Closure>) + func refreshReceipt(completion: @escaping SendableClosure>) /// Refreshes the receipt, representing the user's transactions with your app. /// @@ -160,7 +160,7 @@ public protocol IIAPProvider { /// The transactions array will only be synchronized with the server while the queue has observers. /// /// - Note: This may require that the user authenticate. - func addTransactionObserver(fallbackHandler: Closure>?) + func addTransactionObserver(fallbackHandler: SendableClosure>?) /// Removes transaction observer from the payment queue. /// The transactions array will only be synchronized with the server while the queue has observers. @@ -198,7 +198,7 @@ public protocol IIAPProvider { /// /// - Note: Use this method when you need to handle the restoration process asynchronously and provide feedback through the completion /// handler. - func restore(_ completion: @escaping (Result) -> Void) + func restore(_ completion: @escaping @Sendable (Result) -> Void) #if os(iOS) || VISION_OS /// Present the refund request sheet for the specified transaction in a window scene. @@ -242,7 +242,7 @@ extension IIAPProvider { /// - completion: The closure to be executed once the purchase is complete. func purchase( product: StoreProduct, - completion: @escaping Closure> + completion: @escaping SendableClosure> ) { purchase(product: product, promotionalOffer: nil, completion: completion) } diff --git a/Sources/Flare/Classes/Providers/PaymentProvider/PaymentProvider.swift b/Sources/Flare/Classes/Providers/PaymentProvider/PaymentProvider.swift index 0d681238e..dc834f916 100644 --- a/Sources/Flare/Classes/Providers/PaymentProvider/PaymentProvider.swift +++ b/Sources/Flare/Classes/Providers/PaymentProvider/PaymentProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import Concurrency @@ -61,6 +61,9 @@ extension PaymentProvider: IPaymentProvider { func add(payment: SKPayment, handler: @escaping PaymentHandler) { addPaymentHandler(productID: payment.productIdentifier, handler: handler) + + let payment = SK1StorePayment(underlyingProduct: payment) + dispatchQueueFactory.main().async { self.paymentQueue.add(payment) @@ -178,3 +181,7 @@ extension PaymentProvider: SKPaymentTransactionObserver { paymentQueue.transactions.map(PaymentTransaction.init(_:)) } } + +// MARK: Sendable + +extension PaymentProvider: @unchecked Sendable {} diff --git a/Sources/Flare/Classes/Providers/ProductProvider/Decorators/CachingProductsProviderDecorator/CachingProductsProviderDecorator.swift b/Sources/Flare/Classes/Providers/ProductProvider/Decorators/CachingProductsProviderDecorator/CachingProductsProviderDecorator.swift index 6f993172a..7f829c861 100644 --- a/Sources/Flare/Classes/Providers/ProductProvider/Decorators/CachingProductsProviderDecorator/CachingProductsProviderDecorator.swift +++ b/Sources/Flare/Classes/Providers/ProductProvider/Decorators/CachingProductsProviderDecorator/CachingProductsProviderDecorator.swift @@ -59,7 +59,7 @@ final class CachingProductsProviderDecorator { /// - completion: A closure to be called with the fetched products or an error. private func fetch( productIDs: some Collection, - fetcher: (any Collection, @escaping (Result<[StoreProduct], IAPError>) -> Void) -> Void, + fetcher: (any Collection, @escaping @Sendable (Result<[StoreProduct], IAPError>) -> Void) -> Void, completion: @escaping ProductsHandler ) { let cachedProducts = cachedProducts(ids: productIDs) @@ -90,7 +90,7 @@ final class CachingProductsProviderDecorator { private func fetch( fetchPolicy: FetchCachePolicy, productIDs: some Collection, - fetcher: (any Collection, @escaping (Result<[StoreProduct], IAPError>) -> Void) -> Void, + fetcher: (any Collection, @escaping @Sendable (Result<[StoreProduct], IAPError>) -> Void) -> Void, completion: @escaping ProductsHandler ) { switch fetchPolicy { @@ -107,7 +107,7 @@ final class CachingProductsProviderDecorator { /// - productIDs: The set of product IDs to check the cache for. /// - completion: A closure to be called with the fetched products or an error. @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) - private func fetchSK2Products(productIDs: some Collection, completion: @escaping ProductsHandler) { + private func fetchSK2Products(productIDs: [String], completion: @escaping ProductsHandler) { AsyncHandler.call( completion: { result in switch result { @@ -149,7 +149,7 @@ extension CachingProductsProviderDecorator: ICachingProductsProviderDecorator { fetchPolicy: self.configurationProvider.fetchCachePolicy, productIDs: productIDs, fetcher: { [weak self] _, completion in - self?.fetchSK2Products(productIDs: productIDs, completion: completion) + self?.fetchSK2Products(productIDs: Array(productIDs), completion: completion) }, completion: { result in continuation.resume(with: result) @@ -158,3 +158,7 @@ extension CachingProductsProviderDecorator: ICachingProductsProviderDecorator { } } } + +// MARK: Sendable + +extension CachingProductsProviderDecorator: @unchecked Sendable {} diff --git a/Sources/Flare/Classes/Providers/ProductProvider/Decorators/SortingProductsProviderDecorator/SortingProductsProviderDecorator.swift b/Sources/Flare/Classes/Providers/ProductProvider/Decorators/SortingProductsProviderDecorator/SortingProductsProviderDecorator.swift index b687790b8..fb58db20e 100644 --- a/Sources/Flare/Classes/Providers/ProductProvider/Decorators/SortingProductsProviderDecorator/SortingProductsProviderDecorator.swift +++ b/Sources/Flare/Classes/Providers/ProductProvider/Decorators/SortingProductsProviderDecorator/SortingProductsProviderDecorator.swift @@ -43,6 +43,8 @@ extension SortingProductsProviderDecorator: ISortingProductsProviderDecorator { requestID: String, completion: @escaping ProductsHandler ) { + let productIDs = Array(productIDs) + productProvider.fetch(productIDs: productIDs, requestID: requestID) { [weak self] result in guard let self = self else { return } @@ -70,3 +72,7 @@ private extension Array where Element: StoreProduct { first(where: { $0.productIdentifier == id }) } } + +// MARK: - SortingProductsProviderDecorator + Sendable + +extension SortingProductsProviderDecorator: @unchecked Sendable {} diff --git a/Sources/Flare/Classes/Providers/ProductProvider/IProductProvider.swift b/Sources/Flare/Classes/Providers/ProductProvider/IProductProvider.swift index 55955ecf4..1a930adc7 100644 --- a/Sources/Flare/Classes/Providers/ProductProvider/IProductProvider.swift +++ b/Sources/Flare/Classes/Providers/ProductProvider/IProductProvider.swift @@ -1,20 +1,20 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit -typealias PaymentHandler = (_ queue: PaymentQueue, _ result: Result) -> Void -typealias RestoreHandler = (_ queue: SKPaymentQueue, _ error: IAPError?) -> Void +typealias PaymentHandler = @Sendable (_ queue: PaymentQueue, _ result: Result) -> Void +typealias RestoreHandler = @Sendable (_ queue: SKPaymentQueue, _ error: IAPError?) -> Void typealias ShouldAddStorePaymentHandler = (_ queue: SKPaymentQueue, _ payment: SKPayment, _ product: SKProduct) -> Bool -typealias ReceiptRefreshHandler = (Result) -> Void +typealias ReceiptRefreshHandler = @Sendable (Result) -> Void // MARK: - IProductProvider /// A type that is responsible for retrieving StoreKit products. protocol IProductProvider { - typealias ProductsHandler = Closure> + typealias ProductsHandler = SendableClosure> /// Retrieves localized information from the App Store about a specified list of products. /// diff --git a/Sources/Flare/Classes/Providers/ProductProvider/ProductProvider.swift b/Sources/Flare/Classes/Providers/ProductProvider/ProductProvider.swift index 357229817..cdaadec8d 100644 --- a/Sources/Flare/Classes/Providers/ProductProvider/ProductProvider.swift +++ b/Sources/Flare/Classes/Providers/ProductProvider/ProductProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import Atomic @@ -136,6 +136,12 @@ extension ProductProvider: SKProductsRequestDelegate { } } +// MARK: Sendable + +// @unchecked because: +// - It has mutable state, but it's made thread-safe through `queue`. +extension ProductProvider: @unchecked Sendable {} + // MARK: - Helpers private extension SKRequest { @@ -143,3 +149,10 @@ private extension SKRequest { ProductsRequest(self) } } + +#if swift(>=5.8) + #if hasFeature(RetroactiveAttribute) + extension SKRequest: @unchecked @retroactive Sendable {} + extension SKProductsRequest: @unchecked @retroactive Sendable {} + #endif +#endif diff --git a/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift b/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift index f5951a3a6..e60455c74 100644 --- a/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift +++ b/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift @@ -23,7 +23,7 @@ protocol IPurchaseProvider { /// The transactions array will only be synchronized with the server while the queue has observers. /// /// - Note: This may require that the user authenticate. - func addTransactionObserver(fallbackHandler: Closure>?) + func addTransactionObserver(fallbackHandler: SendableClosure>?) /// Removes transaction observer from the payment queue. /// The transactions array will only be synchronized with the server while the queue has observers. @@ -80,7 +80,7 @@ protocol IPurchaseProvider { /// /// - Note: Use this method when you need to handle the restoration process asynchronously and provide feedback through the completion /// handler. - func restore(_ completion: @escaping (Result) -> Void) + func restore(_ completion: @escaping @Sendable (Result) -> Void) } extension IPurchaseProvider { diff --git a/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift b/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift index 2290c6871..77459d112 100644 --- a/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift +++ b/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift @@ -6,11 +6,11 @@ import Foundation import StoreKit -typealias FallbackHandler = Closure> +typealias FallbackHandler = SendableClosure> // MARK: - PurchaseProvider -final class PurchaseProvider { +final class PurchaseProvider: @unchecked Sendable { // MARK: Properties /// The provider is responsible for making in-app payments. @@ -242,7 +242,7 @@ extension PurchaseProvider: IPurchaseProvider { } } - func restore(_ completion: @escaping (Result) -> Void) { + func restore(_ completion: @escaping @Sendable (Result) -> Void) { paymentProvider.restoreCompletedTransactions { _, error in if let error = error { completion(.failure(error)) diff --git a/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/Factories/IReceiptRefreshRequest.swift b/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/Factories/IReceiptRefreshRequest.swift index a2c43c1aa..676b9a9a1 100644 --- a/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/Factories/IReceiptRefreshRequest.swift +++ b/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/Factories/IReceiptRefreshRequest.swift @@ -1,12 +1,12 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import Foundation /// A type that represents a receipt refresh request. -protocol IReceiptRefreshRequest { +protocol IReceiptRefreshRequest: Sendable { /// The request's identifier. var id: String { get set } diff --git a/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift b/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift index f5ec09957..2f800bc99 100644 --- a/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift +++ b/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import Concurrency @@ -10,7 +10,7 @@ import StoreKit // MARK: - ReceiptRefreshProvider /// A class that can refresh the bundle's App Store receipt. -final class ReceiptRefreshProvider: NSObject { +final class ReceiptRefreshProvider: NSObject, @unchecked Sendable { // MARK: Properties /// The dispatch queue factory. @@ -80,8 +80,9 @@ final class ReceiptRefreshProvider: NSObject { /// - request: The refresh request. /// - handler: The closure to be executed once the refresh is complete. private func fetch(request: IReceiptRefreshRequest, handler: @escaping ReceiptRefreshHandler) { + self.handlers[request.id] = handler + dispatchQueue.async { - self.handlers[request.id] = handler self.dispatchQueueFactory.main().async { request.start() } @@ -115,8 +116,8 @@ extension ReceiptRefreshProvider: SKRequestDelegate { Logger.error(message: L10n.Receipt.refreshingReceiptFailed(request.id, error.localizedDescription)) dispatchQueue.async { - let handler = self.handlers.removeValue(forKey: request.id) self.dispatchQueueFactory.main().async { + let handler = self.handlers.removeValue(forKey: request.id) handler?(.failure(IAPError(error: error))) } } @@ -126,8 +127,8 @@ extension ReceiptRefreshProvider: SKRequestDelegate { Logger.info(message: L10n.Receipt.refreshedReceipt(request.id)) dispatchQueue.async { - let handler = self.handlers.removeValue(forKey: request.id) self.dispatchQueueFactory.main().async { + let handler = self.handlers.removeValue(forKey: request.id) handler?(.success(())) } } diff --git a/Sources/FlareMock/Mocks/PaymentTransactionMock.swift b/Sources/FlareMock/Mocks/PaymentTransactionMock.swift index 9df171c64..bf0aa00a0 100644 --- a/Sources/FlareMock/Mocks/PaymentTransactionMock.swift +++ b/Sources/FlareMock/Mocks/PaymentTransactionMock.swift @@ -5,7 +5,7 @@ import StoreKit -public final class PaymentTransactionMock: SKPaymentTransaction { +public final class PaymentTransactionMock: SKPaymentTransaction, @unchecked Sendable { override public init() {} public var invokedTransactionState = false diff --git a/Sources/FlareMock/Mocks/ProductMock.swift b/Sources/FlareMock/Mocks/ProductMock.swift index bbb84b7ba..f2581250e 100644 --- a/Sources/FlareMock/Mocks/ProductMock.swift +++ b/Sources/FlareMock/Mocks/ProductMock.swift @@ -6,7 +6,7 @@ @testable import Flare import StoreKit -public final class ProductMock: ISKProduct { +public final class ProductMock: ISKProduct, @unchecked Sendable { public init() {} public var invokedLocalizedDescriptionGetter = false diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PaymentQueueMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PaymentQueueMock.swift index cca5ea543..4405abd5f 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PaymentQueueMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PaymentQueueMock.swift @@ -1,13 +1,13 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare import Foundation import StoreKit -final class PaymentQueueMock: SKPaymentQueue { +final class PaymentQueueMock: SKPaymentQueue, @unchecked Sendable { var invokedCanMakePayments = false var invokedCanMakePaymentsCount = 0 var stubbedCanMakePayments = false diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductResponseMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductResponseMock.swift index 379b12730..e7c81e42e 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductResponseMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductResponseMock.swift @@ -1,11 +1,11 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit -final class ProductResponseMock: SKProductsResponse { +final class ProductResponseMock: SKProductsResponse, @unchecked Sendable { var invokedInvalidProductsIdentifiers = false var invokedInvalidProductsIdentifiersCount = 0 var stubbedInvokedInvalidProductsIdentifiers: [String] = [] diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductsRequestMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductsRequestMock.swift index 07f69c448..b224535e1 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductsRequestMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ProductsRequestMock.swift @@ -1,11 +1,11 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit -final class ProductsRequestMock: SKProductsRequest { +final class ProductsRequestMock: SKProductsRequest, @unchecked Sendable { var invokedStart = false var invokedStartCount = 0 diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift index ec176852a..9f141f4d7 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift @@ -6,7 +6,7 @@ @testable import Flare import StoreKit -final class PurchaseProviderMock: IPurchaseProvider { +final class PurchaseProviderMock: IPurchaseProvider, @unchecked Sendable { var invokedFinish = false var invokedFinishCount = 0 var invokedFinishParameters: (transaction: StoreTransaction, Void)? @@ -46,14 +46,15 @@ final class PurchaseProviderMock: IPurchaseProvider { var invokedPurchaseParametersList = [(product: StoreProduct, promotionalOffer: PromotionalOffer?)]() var stubbedPurchaseCompletionResult: (Result, Void)? - @MainActor func purchase(product: StoreProduct, promotionalOffer: PromotionalOffer?, completion: @escaping PurchaseCompletionHandler) { invokedPurchase = true invokedPurchaseCount += 1 invokedPurchaseParameters = (product, promotionalOffer) invokedPurchaseParametersList.append((product, promotionalOffer)) if let result = stubbedPurchaseCompletionResult { - completion(result.0) + MainActor.assumeIsolated { + completion(result.0) + } } } @@ -64,7 +65,6 @@ final class PurchaseProviderMock: IPurchaseProvider { var stubbedinvokedPurchaseWithOptionsCompletionResult: (Result, Void)? @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) - @MainActor func purchase( product: StoreProduct, options: Set, @@ -77,7 +77,9 @@ final class PurchaseProviderMock: IPurchaseProvider { invokedPurchaseWithOptionsParametersList.append((product, options, promotionalOffer)) if let result = stubbedinvokedPurchaseWithOptionsCompletionResult { - completion(result.0) + MainActor.assumeIsolated { + completion(result.0) + } } } diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ReceiptRefreshRequestMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ReceiptRefreshRequestMock.swift index d5f5b31aa..a35c388a5 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ReceiptRefreshRequestMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ReceiptRefreshRequestMock.swift @@ -1,12 +1,12 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare import Foundation -final class ReceiptRefreshRequestMock: IReceiptRefreshRequest { +final class ReceiptRefreshRequestMock: IReceiptRefreshRequest, @unchecked Sendable { var invokedIdSetter = false var invokedIdSetterCount = 0 var invokedId: String? diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SKProductMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SKProductMock.swift index 74e27fcb4..1a8728408 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SKProductMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SKProductMock.swift @@ -1,11 +1,11 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit -final class SKProductMock: SKProduct { +final class SKProductMock: SKProduct, @unchecked Sendable { var invokedProductIdentifier = false var invokedProductIdentifierCount = 0 var stubbedProductIdentifier: String = "product_id" @@ -16,7 +16,7 @@ final class SKProductMock: SKProduct { return stubbedProductIdentifier } - var stubbedPriceLocale: Locale! + var stubbedPriceLocale: Locale = .autoupdatingCurrent override var priceLocale: Locale { stubbedPriceLocale diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/StoreTransactionMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/StoreTransactionMock.swift index 07dc333e6..144e17cdb 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/StoreTransactionMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/StoreTransactionMock.swift @@ -6,7 +6,7 @@ @testable import Flare import Foundation -final class StoreTransactionMock: IStoreTransaction { +final class StoreTransactionMock: IStoreTransaction, @unchecked Sendable { var invokedProductIdentifierGetter = false var invokedProductIdentifierGetterCount = 0 var stubbedProductIdentifier: String! = "" diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Stubs/StoreTransactionStub.swift b/Tests/FlareTests/UnitTests/TestHelpers/Stubs/StoreTransactionStub.swift index 066e9effd..6d2e9cb44 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Stubs/StoreTransactionStub.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Stubs/StoreTransactionStub.swift @@ -6,7 +6,7 @@ @testable import Flare import Foundation -final class StoreTransactionStub: IStoreTransaction { +final class StoreTransactionStub: IStoreTransaction, @unchecked Sendable { var stubbedProductIdentifier: String! = UUID().uuidString var productIdentifier: String { From e710ffe793bfef490eead3a95b9e499bbaa80fb3 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 10:36:54 +0100 Subject: [PATCH 03/12] Update packages --- Package@swift-5.10.swift | 3 ++- Package@swift-5.7.swift | 3 ++- Package@swift-5.8.swift | 3 ++- Package@swift-5.9.swift | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift index 9842c79a8..b8d3cae20 100644 --- a/Package@swift-5.10.swift +++ b/Package@swift-5.10.swift @@ -72,5 +72,6 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ] + ], + swiftLanguageVersions: [.v5] ) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index ef15de429..745a56bff 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -68,5 +68,6 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ] + ], + swiftLanguageVersions: [.v5] ) diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index 1946fdb3b..c81ec1d1f 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -68,5 +68,6 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ] + ], + swiftLanguageVersions: [.v5] ) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 0f0bc05d2..425feb053 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -72,5 +72,6 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ] + ], + swiftLanguageVersions: [.v5] ) From 3b7657b7421f622016f90b66760df2f4121c16cc Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 10:44:13 +0100 Subject: [PATCH 04/12] Update `CHANGELOG.md` --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e900ea4d7..df3b9d2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ All notable changes to this project will be documented in this file. ## Added +- Add full Swift concurrency support with Sendable requirements. + - Added in Pull Request [#92](https://github.com/space-code/flare/pull/92). - Implement locale for StoreProduct - Added in Pull Request [#82](https://github.com/space-code/flare/pull/82). From 0b7efd5909b46e62c12b4f7fba6662a2facece69 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 10:59:48 +0100 Subject: [PATCH 05/12] Update workflows --- .github/workflows/flare.yml | 66 +++++++++++++++---- .github/workflows/flare_ui.yml | 66 +++++++++++++++---- .../Extensions/SKRequest+Identifier.swift | 6 +- Sources/Flare/Classes/Flare.swift | 6 +- .../Mocks/PurchaseProviderMock.swift | 16 +++-- 5 files changed, 126 insertions(+), 34 deletions(-) diff --git a/.github/workflows/flare.yml b/.github/workflows/flare.yml index 9f23ef429..33179771d 100644 --- a/.github/workflows/flare.yml +++ b/.github/workflows/flare.yml @@ -26,18 +26,18 @@ jobs: fail-fast: false matrix: include: + - xcode: "Xcode_16.0" + runsOn: macOS-14 + name: "macOS 14, Xcode 16.0, Swift 6.0" + - xcode: "Xcode_15.4" + runsOn: macOS-14 + name: "macOS 14, Xcode 15.4, Swift 5.10" - xcode: "Xcode_15.0" runsOn: macos-13 name: "macOS 13, Xcode 15.0, Swift 5.9.0" - xcode: "Xcode_14.3.1" runsOn: macos-13 name: "macOS 13, Xcode 14.3.1, Swift 5.8.0" - - xcode: "Xcode_14.2" - runsOn: macOS-12 - name: "macOS 12, Xcode 14.2, Swift 5.7.2" - - xcode: "Xcode_14.1" - runsOn: macOS-12 - name: "macOS 12, Xcode 14.1, Swift 5.7.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -64,6 +64,18 @@ jobs: fail-fast: false matrix: include: + - destination: "OS=18.1,name=iPhone 16 Pro" + name: "iOS 18.1" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - destination: "OS=18.0,name=iPhone 16 Pro" + name: "iOS 18.0" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - destination: "OS=17.5,name=iPhone 15 Pro" + name: "iOS 17.5" + xcode: "Xcode_15.4" + runsOn: macOS-14 - destination: "OS=17.0.1,name=iPhone 14 Pro" name: "iOS 17.0.1" xcode: "Xcode_15.0" @@ -98,6 +110,18 @@ jobs: fail-fast: false matrix: include: + - destination: "OS=18.1,name=Apple TV" + name: "tvOS 18.1" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - destination: "OS=18.0,name=Apple TV" + name: "tvOS 18.0" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - destination: "OS=17.5,name=Apple TV" + name: "tvOS 17.5" + xcode: "Xcode_15.4" + runsOn: macOS-14 - destination: "OS=17.0,name=Apple TV" name: "tvOS 17.0" xcode: "Xcode_15.0" @@ -132,6 +156,18 @@ jobs: fail-fast: false matrix: include: + - destination: "OS=11.1,name=Apple Watch Series 10 (46mm)" + name: "watchOS 11.1" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - destination: "OS=11.0,name=Apple Watch Series 10 (46mm)" + name: "watchOS 11.0" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - destination: "OS=10.5,name=Apple Watch Series 9 (45mm)" + name: "watchOS 10.5" + xcode: "Xcode_15.4" + runsOn: macOS-14 - destination: "OS=10.0,name=Apple Watch Series 9 (45mm)" name: "watchOS 10.0" xcode: "Xcode_15.0" @@ -140,10 +176,6 @@ jobs: name: "watchOS 9.4" xcode: "Xcode_14.3.1" runsOn: macos-13 - - destination: "OS=8.5,name=Apple Watch Series 7 (45mm)" - name: "watchOS 8.5" - xcode: "Xcode_14.3.1" - runsOn: macos-13 steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -170,10 +202,16 @@ jobs: fail-fast: false matrix: include: - - name: "Xcode 15" + - name: "macOS 14, SPM 6.0.2 Test" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - name: "macOS 14, SPM 6.0.0 Test" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - name: "macOS 14, SPM 5.9.0 Test" xcode: "Xcode_15.0" - runsOn: macos-13 - - name: "Xcode 14" + runsOn: macos-14 + - name: "macOS 13, SPM 5.8.1 Test" xcode: "Xcode_14.3.1" runsOn: macos-13 steps: @@ -198,7 +236,7 @@ jobs: discover-typos: name: Discover Typos - runs-on: macOS-12 + runs-on: macOS-13 env: DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer steps: diff --git a/.github/workflows/flare_ui.yml b/.github/workflows/flare_ui.yml index d0fa04285..aae656810 100644 --- a/.github/workflows/flare_ui.yml +++ b/.github/workflows/flare_ui.yml @@ -26,18 +26,18 @@ jobs: fail-fast: false matrix: include: + - xcode: "Xcode_16.0" + runsOn: macOS-14 + name: "macOS 14, Xcode 16.0, Swift 6.0" + - xcode: "Xcode_15.4" + runsOn: macOS-14 + name: "macOS 14, Xcode 15.4, Swift 5.10" - xcode: "Xcode_15.0" runsOn: macos-13 name: "macOS 13, Xcode 15.0, Swift 5.9.0" - xcode: "Xcode_14.3.1" runsOn: macos-13 name: "macOS 13, Xcode 14.3.1, Swift 5.8.0" - - xcode: "Xcode_14.2" - runsOn: macOS-12 - name: "macOS 12, Xcode 14.2, Swift 5.7.2" - - xcode: "Xcode_14.1" - runsOn: macOS-12 - name: "macOS 12, Xcode 14.1, Swift 5.7.1" steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -64,6 +64,18 @@ jobs: fail-fast: false matrix: include: + - destination: "OS=18.1,name=iPhone 16 Pro" + name: "iOS 18.1" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - destination: "OS=18.0,name=iPhone 16 Pro" + name: "iOS 18.0" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - destination: "OS=17.5,name=iPhone 15 Pro" + name: "iOS 17.5" + xcode: "Xcode_15.4" + runsOn: macOS-14 - destination: "OS=17.0.1,name=iPhone 14 Pro" name: "iOS 17.0.1" xcode: "Xcode_15.0" @@ -98,6 +110,18 @@ jobs: fail-fast: false matrix: include: + - destination: "OS=18.1,name=Apple TV" + name: "tvOS 18.1" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - destination: "OS=18.0,name=Apple TV" + name: "tvOS 18.0" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - destination: "OS=17.5,name=Apple TV" + name: "tvOS 17.5" + xcode: "Xcode_15.4" + runsOn: macOS-14 - destination: "OS=17.0,name=Apple TV" name: "tvOS 17.0" xcode: "Xcode_15.0" @@ -132,6 +156,18 @@ jobs: fail-fast: false matrix: include: + - destination: "OS=11.1,name=Apple Watch Series 10 (46mm)" + name: "watchOS 11.1" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - destination: "OS=11.0,name=Apple Watch Series 10 (46mm)" + name: "watchOS 11.0" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - destination: "OS=10.5,name=Apple Watch Series 9 (45mm)" + name: "watchOS 10.5" + xcode: "Xcode_15.4" + runsOn: macOS-14 - destination: "OS=10.0,name=Apple Watch Series 9 (45mm)" name: "watchOS 10.0" xcode: "Xcode_15.0" @@ -140,10 +176,6 @@ jobs: name: "watchOS 9.4" xcode: "Xcode_14.3.1" runsOn: macos-13 - - destination: "OS=8.5,name=Apple Watch Series 7 (45mm)" - name: "watchOS 8.5" - xcode: "Xcode_14.3.1" - runsOn: macos-13 steps: - uses: actions/checkout@v4 - name: ${{ matrix.name }} @@ -170,10 +202,16 @@ jobs: fail-fast: false matrix: include: - - name: "Xcode 15" + - name: "macOS 14, SPM 6.0.2 Test" + xcode: "Xcode_16.1" + runsOn: macOS-14 + - name: "macOS 14, SPM 6.0.0 Test" + xcode: "Xcode_16.0" + runsOn: macOS-14 + - name: "macOS 14, SPM 5.9.0 Test" xcode: "Xcode_15.0" - runsOn: macos-13 - - name: "Xcode 14" + runsOn: macos-14 + - name: "macOS 13, SPM 5.8.1 Test" xcode: "Xcode_14.3.1" runsOn: macos-13 steps: @@ -236,7 +274,7 @@ jobs: discover-typos: name: Discover Typos - runs-on: macOS-12 + runs-on: macOS-13 env: DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer steps: diff --git a/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift b/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift index 6952db402..74c7140d7 100644 --- a/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift +++ b/Sources/Flare/Classes/Extensions/SKRequest+Identifier.swift @@ -5,7 +5,11 @@ import StoreKit -private nonisolated(unsafe) var requestIdKey: UInt = 0 +#if swift(>=6.0) + private nonisolated(unsafe) var requestIdKey: UInt = 0 +#else + private var requestIdKey: UInt = 0 +#endif extension SKRequest { var id: String { diff --git a/Sources/Flare/Classes/Flare.swift b/Sources/Flare/Classes/Flare.swift index 2a7d44a80..c766018f6 100644 --- a/Sources/Flare/Classes/Flare.swift +++ b/Sources/Flare/Classes/Flare.swift @@ -23,7 +23,11 @@ public final class Flare { private let configurationProvider: IConfigurationProvider /// The singleton instance. - private nonisolated(unsafe) static let flare: Flare = .init() + #if swift(>=6.0) + private nonisolated(unsafe) static let flare: Flare = .init() + #else + private static let flare: Flare = .init() + #endif /// Returns a shared `Flare` object. public static var shared: IFlare { flare } diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift index 9f141f4d7..df42fa8a0 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift @@ -52,9 +52,13 @@ final class PurchaseProviderMock: IPurchaseProvider, @unchecked Sendable { invokedPurchaseParameters = (product, promotionalOffer) invokedPurchaseParametersList.append((product, promotionalOffer)) if let result = stubbedPurchaseCompletionResult { - MainActor.assumeIsolated { + #if swift(>=6.0) + MainActor.assumeIsolated { + completion(result.0) + } + #else completion(result.0) - } + #endif } } @@ -77,9 +81,13 @@ final class PurchaseProviderMock: IPurchaseProvider, @unchecked Sendable { invokedPurchaseWithOptionsParametersList.append((product, options, promotionalOffer)) if let result = stubbedinvokedPurchaseWithOptionsCompletionResult { - MainActor.assumeIsolated { + #if swift(>=6.0) + MainActor.assumeIsolated { + completion(result.0) + } + #else completion(result.0) - } + #endif } } From 725ce3fe6114cde22bd0659d3db2047077a3bede Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 15:29:46 +0100 Subject: [PATCH 06/12] Fix the FlareCore unit tests --- Sources/Flare/Classes/Common/Logger.swift | 26 +++-- .../AsyncSequence/AsyncSequence+Stream.swift | 16 +-- .../Helpers/ScenesHolder/IScenesHolder.swift | 5 +- .../TransactionListener.swift | 6 +- .../PurchaseProvider/IPurchaseProvider.swift | 2 +- .../SystemInfoProvider.swift | 13 +-- Tests/FlareTests/UnitTests/FlareTests.swift | 58 ++++++---- .../Providers/IAPProviderTests.swift | 47 +++++--- .../Providers/PaymentProviderTests.swift | 103 ++++++++---------- .../Providers/ProductProviderTests.swift | 45 +++++--- .../ReceiptRefreshProviderTests.swift | 37 ++++--- .../Providers/RefundProviderTests.swift | 13 ++- ...ortingProductsProviderDecoratorTests.swift | 21 ++-- .../Providers/SystemInfoProviderTests.swift | 4 +- .../Helpers/WindowSceneFactory.swift | 1 + .../Mocks/PurchaseProviderMock.swift | 16 +-- .../TestHelpers/Mocks/ScenesHolderMock.swift | 4 +- .../Mocks/SystemInfoProviderMock.swift | 4 +- 18 files changed, 229 insertions(+), 192 deletions(-) diff --git a/Sources/Flare/Classes/Common/Logger.swift b/Sources/Flare/Classes/Common/Logger.swift index 4ecfa8cbd..63c315e56 100644 --- a/Sources/Flare/Classes/Common/Logger.swift +++ b/Sources/Flare/Classes/Common/Logger.swift @@ -4,7 +4,7 @@ // import Foundation -@preconcurrency import Log +import Log // MARK: - Logger @@ -19,13 +19,23 @@ enum Logger { #endif } - private static let `default`: Log.Logger = .init( - printers: [ - ConsolePrinter(formatters: Self.formatters), - OSPrinter(subsystem: .subsystem, category: .category, formatters: Self.formatters), - ], - logLevel: Self.defaultLogLevel - ) + #if swift(>=6.0) + private nonisolated(unsafe) static let `default`: Log.Logger = .init( + printers: [ + ConsolePrinter(formatters: Self.formatters), + OSPrinter(subsystem: .subsystem, category: .category, formatters: Self.formatters), + ], + logLevel: Self.defaultLogLevel + ) + #else + private static let `default`: Log.Logger = .init( + printers: [ + ConsolePrinter(formatters: Self.formatters), + OSPrinter(subsystem: .subsystem, category: .category, formatters: Self.formatters), + ], + logLevel: Self.defaultLogLevel + ) + #endif private static var formatters: [ILogFormatter] { [ diff --git a/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift b/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift index f0c62373b..4806afeb0 100644 --- a/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift +++ b/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift @@ -5,11 +5,11 @@ import Foundation -extension AsyncSequence where Element: Sendable { - func toAsyncStream() -> AsyncStream { - var asyncIterator = makeAsyncIterator() - return AsyncStream { - try? await asyncIterator.next() - } - } -} +// extension AsyncSequence where Element: Sendable { +// func toAsyncStream() -> AsyncStream { +// var asyncIterator = makeAsyncIterator() +// return AsyncStream { +// try? await asyncIterator.next() +// } +// } +// } diff --git a/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift b/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift index ac000bcc5..2df88d73a 100644 --- a/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift +++ b/Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift @@ -10,13 +10,10 @@ // MARK: - IScenesHolder /// A type that holds all connected scenes. +@MainActor protocol IScenesHolder: Sendable { #if os(iOS) || VISION_OS /// The scenes that are connected to the app. var connectedScenes: Set { get } #endif } - -#if os(iOS) || VISION_OS - extension UIApplication: IScenesHolder {} -#endif diff --git a/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListener.swift b/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListener.swift index c57f6c1b5..5a2e12c2d 100644 --- a/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListener.swift +++ b/Sources/Flare/Classes/Listeners/TransactionListener/TransactionListener.swift @@ -16,16 +16,16 @@ actor TransactionListener { // MARK: Private - private let updates: AsyncStream + private let updates: StoreKit.Transaction.Transactions private var task: Task? private weak var delegate: TransactionListenerDelegate? // MARK: Initialization - init(delegate: TransactionListenerDelegate? = nil, updates: S) where S.Element == TransactionResult { + init(delegate: TransactionListenerDelegate? = nil, updates: StoreKit.Transaction.Transactions) { self.delegate = delegate - self.updates = updates.toAsyncStream() + self.updates = updates // .toAsyncStream() } // MARK: Private diff --git a/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift b/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift index e60455c74..8ce0d9895 100644 --- a/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift +++ b/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift @@ -6,7 +6,7 @@ import Foundation import StoreKit -public typealias PurchaseCompletionHandler = @MainActor @Sendable (Result) -> Void +public typealias PurchaseCompletionHandler = @Sendable (Result) -> Void // MARK: - IPurchaseProvider diff --git a/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift b/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift index f6dd30ef7..753fe32ee 100644 --- a/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift +++ b/Sources/Flare/Classes/Providers/SystemInfoProvider/SystemInfoProvider.swift @@ -9,20 +9,20 @@ // MARK: - SystemInfoProvider -final class SystemInfoProvider { +final class SystemInfoProvider: @unchecked Sendable { // MARK: Properties #if os(iOS) || VISION_OS - private let scenesHolder: @Sendable () async -> IScenesHolder + private let scenesHolder: () async -> Set // MARK: Initialization init(scenesHolder: IScenesHolder? = nil) { if let scenesHolder { - self.scenesHolder = { scenesHolder } + self.scenesHolder = { await scenesHolder.connectedScenes } } else { self.scenesHolder = { - await MainActor.run { UIApplication.shared } + await UIApplication.shared.connectedScenes } } } @@ -41,13 +41,12 @@ extension SystemInfoProvider: ISystemInfoProvider { @MainActor var currentScene: UIWindowScene { get async throws { - let holder = await scenesHolder() - var scenes = holder.connectedScenes + var scenes = await scenesHolder() .filter { $0.activationState == .foregroundActive } #if DEBUG && targetEnvironment(simulator) if scenes.isEmpty, ProcessInfo.isRunningUnitTests { - scenes = holder.connectedScenes + scenes = await scenesHolder() } #endif diff --git a/Tests/FlareTests/UnitTests/FlareTests.swift b/Tests/FlareTests/UnitTests/FlareTests.swift index a116a97fc..a4bbb0026 100644 --- a/Tests/FlareTests/UnitTests/FlareTests.swift +++ b/Tests/FlareTests/UnitTests/FlareTests.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare @@ -88,17 +88,19 @@ class FlareTests: XCTestCase { XCTAssertFalse(iapProviderMock.invokedPurchase) } - func test_thatFlarePurchasesAProduct_whenRequestCompleted() { + func test_thatFlarePurchasesAProduct_whenRequestCompleted() async throws { // given let paymentTransaction = StoreTransaction(storeTransaction: StoreTransactionStub()) iapProviderMock.stubbedCanMakePayments = true iapProviderMock.stubbedPurchaseWithPromotionalOffer = .success(paymentTransaction) // when - var transaction: IStoreTransaction? - sut.purchase(product: .fake(productIdentifier: .productID), completion: { result in - transaction = result.success - }) + let transaction: IStoreTransaction? = try await withCheckedThrowingContinuation { continuation in + sut.purchase(product: .fake(productIdentifier: .productID), completion: { result in + continuation.resume(returning: result.success) + }) + } + iapProviderMock.invokedPurchaseParameters?.completion(.success(paymentTransaction)) // then @@ -106,22 +108,24 @@ class FlareTests: XCTestCase { XCTAssertEqual(transaction?.productIdentifier, paymentTransaction.productIdentifier) } - func test_thatFlareDoesNotPurchaseAProduct_whenPurchaseReturnsUnkownError() { + func test_thatFlareDoesNotPurchaseAProduct_whenPurchaseReturnsUnkownError() async throws { // given let errorMock = IAPError.paymentNotAllowed iapProviderMock.stubbedCanMakePayments = true iapProviderMock.stubbedPurchaseWithPromotionalOffer = .failure(errorMock) // when - var error: IAPError? - sut.purchase(product: .fake(productIdentifier: .productID), completion: { result in - error = result.error - }) + let result: Result = try await withCheckedThrowingContinuation { continuation in + sut.purchase(product: .fake(productIdentifier: .productID), completion: { result in + continuation.resume(returning: result) + }) + } + iapProviderMock.invokedPurchaseParameters?.completion(.failure(errorMock)) // then XCTAssertTrue(iapProviderMock.invokedPurchaseWithPromotionalOffer) - XCTAssertEqual(error, errorMock) + XCTAssertEqual(result.error, errorMock) } func test_thatFlareDoesNotPurchaseAProduct_whenUserCannotMakePayments() async { @@ -152,28 +156,34 @@ class FlareTests: XCTestCase { XCTAssertEqual(transaction?.productIdentifier, transactionMock.productIdentifier) } - func test_thatFlareFetchesReceipt_whenRequestCompleted() { + func test_thatFlareFetchesReceipt_whenRequestCompleted() async throws { // when - var receipt: String? - sut.receipt { receipt = $0.success } + let result: Result = try await withCheckedThrowingContinuation { continuation in + sut.receipt { result in + continuation.resume(returning: result) + } - iapProviderMock.invokedRefreshReceiptParameters?.completion(.success(.receipt)) + iapProviderMock.invokedRefreshReceiptParameters?.completion(.success(.receipt)) + } // then XCTAssertTrue(iapProviderMock.invokedRefreshReceipt) - XCTAssertEqual(receipt, .receipt) + XCTAssertEqual(result.success, .receipt) } - func test_thatFlareDoesNotFetchReceipt_whenRequestFailed() { + func test_thatFlareDoesNotFetchReceipt_whenRequestFailed() async throws { // when - var error: IAPError? - sut.receipt { error = $0.error } + let result = try await withCheckedThrowingContinuation { continuation in + sut.receipt { result in + continuation.resume(returning: result) + } - iapProviderMock.invokedRefreshReceiptParameters?.completion(.failure(.paymentNotAllowed)) + iapProviderMock.invokedRefreshReceiptParameters?.completion(.failure(.paymentNotAllowed)) + } // then XCTAssertTrue(iapProviderMock.invokedRefreshReceipt) - XCTAssertEqual(error, .paymentNotAllowed) + XCTAssertEqual(result.error, .paymentNotAllowed) } func test_thatFlareRemovesTransactionObserver() { @@ -184,7 +194,7 @@ class FlareTests: XCTestCase { XCTAssertTrue(iapProviderMock.invokedRemoveTransactionObserver) } - func test_thatFlareFetchesReceipt_whenRequestCompleted() async throws { + func test_thatFlareFetchesReceiptAsync_whenRequestCompleted() async throws { // given iapProviderMock.stubbedRefreshReceiptAsyncResult = .success(.receipt) @@ -195,7 +205,7 @@ class FlareTests: XCTestCase { XCTAssertEqual(receipt, .receipt) } - func test_thatFlareDoesNotFetchReceipt_whenRequestFailed() async throws { + func test_thatFlareDoesNotFetchReceiptAsync_whenRequestFailed() async throws { // given iapProviderMock.stubbedRefreshReceiptAsyncResult = .failure(.paymentNotAllowed) diff --git a/Tests/FlareTests/UnitTests/Providers/IAPProviderTests.swift b/Tests/FlareTests/UnitTests/Providers/IAPProviderTests.swift index 3db90c719..533c3cedb 100644 --- a/Tests/FlareTests/UnitTests/Providers/IAPProviderTests.swift +++ b/Tests/FlareTests/UnitTests/Providers/IAPProviderTests.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare @@ -155,65 +155,80 @@ class IAPProviderTests: XCTestCase { XCTAssertEqual(errorResult as? NSError, IAPError.unknown as NSError) } - func test_thatIAPProviderReturnsError_whenAddingPaymentFailed() { + func test_thatIAPProviderReturnsError_whenAddingPaymentFailed() async throws { // given productProviderMock.stubbedFetchResult = .success([StoreProduct(SK1StoreProduct(SKProductMock()))]) purchaseProvider.stubbedPurchaseCompletionResult = (.failure(.unknown), ()) // when - var error: Error? - sut.purchase(product: .fake(productIdentifier: .productID)) { error = $0.error } + let error: Error? = try await withCheckedThrowingContinuation { continuation in + sut.purchase(product: .fake(productIdentifier: .productID)) { + continuation.resume(returning: $0.error) + } + } // then XCTAssertEqual(error as? NSError, IAPError.unknown as NSError) } - func test_thatIAPProviderReturnsError_whenFetchRequestFailed() { + func test_thatIAPProviderReturnsError_whenFetchRequestFailed() async throws { // given purchaseProvider.stubbedPurchaseCompletionResult = (.failure(IAPError.unknown), ()) // when - var error: Error? - sut.purchase(product: .fake(productIdentifier: .productID)) { error = $0.error } + let error: Error? = try await withCheckedThrowingContinuation { continuation in + sut.purchase(product: .fake(productIdentifier: .productID)) { + continuation.resume(returning: $0.error) + } + } // then XCTAssertEqual(error as? NSError, IAPError.unknown as NSError) } - func test_thatIAPProviderRefreshesReceipt_whenReceiptExist() { + func test_thatIAPProviderRefreshesReceipt_whenReceiptExist() async throws { // given receiptRefreshProviderMock.stubbedReceipt = .receipt receiptRefreshProviderMock.stubbedRefreshResult = .success(()) // when - var receipt: String? - sut.refreshReceipt { receipt = $0.success } + let receipt: String? = try await withCheckedThrowingContinuation { continuation in + sut.refreshReceipt { + continuation.resume(returning: $0.success) + } + } // then XCTAssertEqual(receipt, .receipt) } - func test_thatIAPProviderDoesNotRefreshReceipt_whenRequestFailed() { + func test_thatIAPProviderDoesNotRefreshReceipt_whenRequestFailed() async throws { // given receiptRefreshProviderMock.stubbedReceipt = nil receiptRefreshProviderMock.stubbedRefreshResult = .failure(.receiptNotFound) // when - var error: Error? - sut.refreshReceipt { error = $0.error } + let error: Error? = try await withCheckedThrowingContinuation { continuation in + sut.refreshReceipt { + continuation.resume(returning: $0.error) + } + } // then XCTAssertEqual(error as? NSError, IAPError.receiptNotFound as NSError) } - func test_thatIAPProviderReturnsReceiptNotFoundError_whenReceiptIsNil() { + func test_thatIAPProviderReturnsReceiptNotFoundError_whenReceiptIsNil() async throws { // given receiptRefreshProviderMock.stubbedReceipt = nil receiptRefreshProviderMock.stubbedRefreshResult = .success(()) // when - var error: Error? - sut.refreshReceipt { error = $0.error } + let error: Error? = try await withCheckedThrowingContinuation { continuation in + sut.refreshReceipt { + continuation.resume(returning: $0.error) + } + } // then XCTAssertEqual(error as? NSError, IAPError.receiptNotFound as NSError) diff --git a/Tests/FlareTests/UnitTests/Providers/PaymentProviderTests.swift b/Tests/FlareTests/UnitTests/Providers/PaymentProviderTests.swift index bfef3e3ff..45c160fbd 100644 --- a/Tests/FlareTests/UnitTests/Providers/PaymentProviderTests.swift +++ b/Tests/FlareTests/UnitTests/Providers/PaymentProviderTests.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare @@ -92,54 +92,42 @@ class PaymentProviderTests: XCTestCase { XCTAssertEqual(paymentQueueMock.invokedRestoreCompletedTransactionsCount, 1) } - func test_thatPaymentProviderAddsPaymentToQueueWithTransactions() { + func test_thatPaymentProviderAddsPaymentToQueueWithTransactions() async throws { // given - var handledPaymentQueue: PaymentQueue? let product = SKProduct() let payment = SKPayment(product: product) let paymentQueue = SKPaymentQueue() - let paymentHandler: PaymentHandler = { payment, _ in handledPaymentQueue = payment } let paymentTransactions = transactionsStates.map { PurchaseManagerTestHelper.makePaymentTransaction(state: $0) } // when - paymentProvider.add(payment: payment, handler: paymentHandler) - paymentProvider.paymentQueue(paymentQueue, updatedTransactions: paymentTransactions) - - // then - XCTAssertTrue(paymentQueueMock.invokedAddPayment) - XCTAssertEqual(paymentQueueMock.invokedAddPaymentCount, 1) - XCTAssertTrue(handledPaymentQueue === paymentQueue) - } - - func test_thatPaymentProviderAddsPaymentToQueueWithoutTransactions() { - // given - var handledPaymentQueue: PaymentQueue? - let product = SKProduct() - let payment = SKPayment(product: product) - let paymentQueue = SKPaymentQueue() - let paymentHandler: PaymentHandler = { payment, _ in handledPaymentQueue = payment } + let handledPaymentQueue: PaymentQueue? = try await withCheckedThrowingContinuation { continuation in + paymentProvider.add(payment: payment) { payment, _ in + continuation.resume(returning: payment) + } - // when - paymentProvider.add(payment: payment, handler: paymentHandler) - paymentProvider.paymentQueue(paymentQueue, updatedTransactions: []) + paymentProvider.paymentQueue(paymentQueue, updatedTransactions: paymentTransactions) + } // then XCTAssertTrue(paymentQueueMock.invokedAddPayment) XCTAssertEqual(paymentQueueMock.invokedAddPaymentCount, 1) - XCTAssertNil(handledPaymentQueue) + XCTAssertTrue(handledPaymentQueue === paymentQueue) } - func test_thatPaymentProviderAddsPaymentHandler() { + func test_thatPaymentProviderAddsPaymentHandler() async throws { // given - var handledPaymentQueue: PaymentQueue? let paymentQueue = SKPaymentQueue() - let paymentHandler: PaymentHandler = { payment, _ in handledPaymentQueue = payment } let paymentTransactions = transactionsStates .map { PurchaseManagerTestHelper.makePaymentTransaction(identifier: .productId, state: $0) } // when - paymentProvider.addPaymentHandler(productID: .productId, handler: paymentHandler) - paymentProvider.paymentQueue(paymentQueue, updatedTransactions: paymentTransactions) + let handledPaymentQueue: PaymentQueue? = try await withCheckedThrowingContinuation { continuation in + paymentProvider.addPaymentHandler(productID: .productId) { payment, _ in + continuation.resume(returning: payment) + } + + paymentProvider.paymentQueue(paymentQueue, updatedTransactions: paymentTransactions) + } // then XCTAssertTrue(handledPaymentQueue === paymentQueue) @@ -157,15 +145,18 @@ class PaymentProviderTests: XCTestCase { XCTAssertEqual(paymentQueueMock.invokedFinishTransactionCount, 2) } - func test_thatPaymentProviderCallsFallbackHandler_whenPaymentHandlersDoNotExist() { + func test_thatPaymentProviderCallsFallbackHandler_whenPaymentHandlersDoNotExist() async throws { // given - var handledPaymentQueue: PaymentQueue? let transaction = PurchaseManagerTestHelper.makePaymentTransaction(state: .purchased) - let fallbackHandler: PaymentHandler = { paymentQueue, _ in handledPaymentQueue = paymentQueue } // when - paymentProvider.set(fallbackHandler: fallbackHandler) - paymentProvider.paymentQueue(paymentQueueMock, updatedTransactions: [transaction]) + let handledPaymentQueue: PaymentQueue? = try await withCheckedThrowingContinuation { continuation in + paymentProvider.set { payment, _ in + continuation.resume(returning: payment) + } + + paymentProvider.paymentQueue(paymentQueueMock, updatedTransactions: [transaction]) + } // then XCTAssertTrue(handledPaymentQueue === paymentQueueMock) @@ -219,41 +210,41 @@ class PaymentProviderTests: XCTestCase { XCTAssertTrue(paymentQueueMock.invokedRestoreCompletedTransactions) } - func test_thatPaymentQueueRestoresCompletedTransactions_whenTransactionFinished() { + func test_thatPaymentQueueRestoresCompletedTransactions_whenTransactionFinished() async throws { // when - var queueResult: SKPaymentQueue? - var errorResult: Error? - let restoreHandler: RestoreHandler = { queue, error in - queueResult = queue - errorResult = error - } + let result: Result? = try await withCheckedThrowingContinuation { continuation in + paymentProvider.restoreCompletedTransactions { queue, error in + if let error = error { + continuation.resume(returning: .failure(error)) + } else { + continuation.resume(returning: .success(queue)) + } + } - paymentProvider.restoreCompletedTransactions(handler: restoreHandler) - paymentProvider.paymentQueueRestoreCompletedTransactionsFinished(paymentQueueMock) + paymentProvider.paymentQueueRestoreCompletedTransactionsFinished(paymentQueueMock) + } // then - XCTAssertEqual(queueResult, paymentQueueMock) - XCTAssertNil(errorResult) + XCTAssertEqual(result?.success, paymentQueueMock) + XCTAssertNil(result?.error) } - func test_thatPaymentQueueDoesNotRestoreCompletedTransactions_whenRequestFailDueToTransactionError() { + func test_thatPaymentQueueDoesNotRestoreCompletedTransactions_whenRequestFailDueToTransactionError() async throws { // given let errorMock = IAPError.paymentNotAllowed // when - var queueResult: SKPaymentQueue? - var errorResult: Error? - let restoreHandler: RestoreHandler = { queue, error in - queueResult = queue - errorResult = error - } + let result: (SKPaymentQueue?, Error?) = try await withCheckedThrowingContinuation { continuation in + paymentProvider.restoreCompletedTransactions { queue, error in + continuation.resume(returning: (queue, error)) + } - paymentProvider.restoreCompletedTransactions(handler: restoreHandler) - paymentProvider.paymentQueue(paymentQueueMock, restoreCompletedTransactionsFailedWithError: errorMock) + paymentProvider.paymentQueue(paymentQueueMock, restoreCompletedTransactionsFailedWithError: errorMock) + } // then - XCTAssertEqual(queueResult, paymentQueueMock) - XCTAssertEqual(errorResult as? NSError, IAPError(error: errorMock) as NSError) + XCTAssertEqual(result.0, paymentQueueMock) + XCTAssertEqual(result.1 as? NSError, IAPError(error: errorMock) as NSError) } func test_thatPaymentQueueHandlesTransactions_whenPendingTransactionsExist() { diff --git a/Tests/FlareTests/UnitTests/Providers/ProductProviderTests.swift b/Tests/FlareTests/UnitTests/Providers/ProductProviderTests.swift index 9b3bba729..d5bd8137c 100644 --- a/Tests/FlareTests/UnitTests/Providers/ProductProviderTests.swift +++ b/Tests/FlareTests/UnitTests/Providers/ProductProviderTests.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import Concurrency @@ -37,18 +37,21 @@ final class ProductProviderTests: XCTestCase { // MARK: - Tests - func test_thatProductProviderReturnsInvalidProductIDs_whenRequestProductsAreFetchedWithInvalidIDs() { + func test_thatProductProviderReturnsInvalidProductIDs_whenRequestProductsAreFetchedWithInvalidIDs() async throws { // given - var fetchResult: Result<[StoreProduct], IAPError>? - let completionHandler: IProductProvider.ProductsHandler = { result in fetchResult = result } let request = PurchaseManagerTestHelper.makeRequest(with: .requestID) let response = ProductResponseMock() response.stubbedInvokedInvalidProductsIdentifiers = [.productID] // when - sut.fetch(productIDs: Set.productIDs, requestID: .requestID, completion: completionHandler) - sut.productsRequest(request, didReceive: response) + let fetchResult: Result<[StoreProduct], IAPError>? = try await withCheckedThrowingContinuation { continuation in + sut.fetch(productIDs: Set.productIDs, requestID: .requestID) { result in + continuation.resume(returning: result) + } + + sut.productsRequest(request, didReceive: response) + } // then if case let .failure(error) = fetchResult, case let .invalid(products) = error { @@ -58,34 +61,40 @@ final class ProductProviderTests: XCTestCase { } } - func test_thatProductProviderReturnsProducts_whenRequestProductsAreFetchedWithValidProductIDs() { + func test_thatProductProviderReturnsProducts_whenRequestProductsAreFetchedWithValidProductIDs() async throws { // given - var products: [StoreProduct]? = [] - let completionHandler: IProductProvider.ProductsHandler = { products = $0.success } let request = PurchaseManagerTestHelper.makeRequest(with: .requestID) let response = ProductResponseMock() // when - sut.fetch(productIDs: Set.productIDs, requestID: .requestID, completion: completionHandler) - sut.productsRequest(request, didReceive: response) + let fetchResult: Result<[StoreProduct], IAPError>? = try await withCheckedThrowingContinuation { continuation in + sut.fetch(productIDs: Set.productIDs, requestID: .requestID) { result in + continuation.resume(returning: result) + } + + sut.productsRequest(request, didReceive: response) + } // then - XCTAssertEqual(products?.compactMap { $0.product as? SK1StoreProduct }.map(\.product), response.products) + XCTAssertEqual(fetchResult?.success?.compactMap { $0.product as? SK1StoreProduct }.map(\.product), response.products) } - func test_thatProductProviderHandlesError_whenRequestDidFailWithError() { + func test_thatProductProviderHandlesError_whenRequestDidFailWithError() async throws { // given - var error: IAPError? - let completionHandler: IProductProvider.ProductsHandler = { error = $0.error } let request = PurchaseManagerTestHelper.makeRequest(with: .requestID) let errorStub = IAPError.unknown // when - sut.fetch(productIDs: Set.productIDs, requestID: .requestID, completion: completionHandler) - sut.request(request, didFailWithError: errorStub) + let fetchResult: Result<[StoreProduct], IAPError>? = try await withCheckedThrowingContinuation { continuation in + sut.fetch(productIDs: Set.productIDs, requestID: .requestID) { result in + continuation.resume(returning: result) + } + + sut.request(request, didFailWithError: errorStub) + } // then - XCTAssertEqual(error?.plainError as? NSError, errorStub.plainError as NSError) + XCTAssertEqual(fetchResult?.error?.plainError as? NSError, errorStub.plainError as NSError) } } diff --git a/Tests/FlareTests/UnitTests/Providers/ReceiptRefreshProviderTests.swift b/Tests/FlareTests/UnitTests/Providers/ReceiptRefreshProviderTests.swift index 64c84d419..d433f4459 100644 --- a/Tests/FlareTests/UnitTests/Providers/ReceiptRefreshProviderTests.swift +++ b/Tests/FlareTests/UnitTests/Providers/ReceiptRefreshProviderTests.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare @@ -50,18 +50,23 @@ class ReceiptRefreshProviderTests: XCTestCase { // MARK: - Tests - func test_thatReceiptRefreshProviderHandlesRequestError_whenErrorOccurred() { + func test_thatReceiptRefreshProviderHandlesRequestError_whenErrorOccurred() async throws { // given - receiptRefreshRequestFactoryMock.stubbedMakeResult = ReceiptRefreshRequestMock() + let stubbedRequest = ReceiptRefreshRequestMock() + stubbedRequest.stubbedId = .requestID + receiptRefreshRequestFactoryMock.stubbedMakeResult = stubbedRequest - var result: Result? let request = PurchaseManagerTestHelper.makeRequest(with: .requestID) - let handler: ReceiptRefreshHandler = { result = $0 } let error = IAPError.paymentCancelled // when - sut.refresh(requestID: .requestID, handler: handler) - sut.request(request, didFailWithError: error) + let result: Result = try await withCheckedThrowingContinuation { continuation in + sut.refresh(requestID: .requestID) { result in + continuation.resume(returning: result) + } + + sut.request(request, didFailWithError: error) + } // then if case let .failure(resultError) = result { @@ -69,20 +74,26 @@ class ReceiptRefreshProviderTests: XCTestCase { } } - func test_thatReceiptRefreshProviderFinishesRequest_whenRequestCompletedSuccessfully() { + func test_thatReceiptRefreshProviderFinishesRequest_whenRequestCompletedSuccessfully() async throws { // given - receiptRefreshRequestFactoryMock.stubbedMakeResult = ReceiptRefreshRequestMock() + let stubbedRequest = ReceiptRefreshRequestMock() + stubbedRequest.stubbedId = .requestID + receiptRefreshRequestFactoryMock.stubbedMakeResult = stubbedRequest - var result: Result? let request = PurchaseManagerTestHelper.makeRequest(with: .requestID) - let handler: ReceiptRefreshHandler = { result = $0 } // when - sut.refresh(requestID: .requestID, handler: handler) - sut.requestDidFinish(request) + let result: Result? = try await withCheckedThrowingContinuation { continuation in + sut.refresh(requestID: .requestID) { result in + continuation.resume(returning: result) + } + + sut.requestDidFinish(request) + } // then if case .failure = result { XCTFail("The result must be `success`") } + XCTAssertEqual(stubbedRequest.invokedIdGetterCount, 1) } func test_thatReceiptRefreshProviderLoadsAppStoreReceipt_whenReceiptExists() { diff --git a/Tests/FlareTests/UnitTests/Providers/RefundProviderTests.swift b/Tests/FlareTests/UnitTests/Providers/RefundProviderTests.swift index fb3be5f64..542537c29 100644 --- a/Tests/FlareTests/UnitTests/Providers/RefundProviderTests.swift +++ b/Tests/FlareTests/UnitTests/Providers/RefundProviderTests.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // #if os(iOS) || VISION_OS @@ -34,16 +34,23 @@ // MARK: - Tests + @MainActor func testThatRefundProviderThrowsAnError_whenVerificationDidFail() async throws { // given refundRequestProviderMock.stubbedVerifyTransaction = nil systemInfoProviderMock.stubbedCurrentScene = .failure(IAPError.unknown) // when - let error: Error? = await error(for: { try await sut.beginRefundRequest(productID: .productID) }) + var result: Error? = nil + + do { + _ = try await sut.beginRefundRequest(productID: .productID) + } catch { + result = error + } // then - XCTAssertEqual(error as? NSError, IAPError.unknown as NSError) + XCTAssertEqual(result as? NSError, IAPError.unknown as NSError) } // func testThatRefundProviderThrowsAnErrorWhenRefundRequestDidFail() async throws { diff --git a/Tests/FlareTests/UnitTests/Providers/SortingProductsProviderDecoratorTests.swift b/Tests/FlareTests/UnitTests/Providers/SortingProductsProviderDecoratorTests.swift index 75ea23512..5cb341f5e 100644 --- a/Tests/FlareTests/UnitTests/Providers/SortingProductsProviderDecoratorTests.swift +++ b/Tests/FlareTests/UnitTests/Providers/SortingProductsProviderDecoratorTests.swift @@ -32,35 +32,34 @@ final class SortingProductsProviderDecoratorTests: XCTestCase { // MARK: Tests - func test_ProductProviderSortsSetItems_whenFetchProducts() { - test_sort(collection: Set.productIDs) + func test_ProductProviderSortsSetItems_whenFetchProducts() async { + await test_sort(collection: Set.productIDs) } - func test_ProductProviderSortsArrayItems_whenFetchProducts() { - test_sort(collection: Array.productIDs) + func test_ProductProviderSortsArrayItems_whenFetchProducts() async { + await test_sort(collection: Array.productIDs) } // MARK: Private - private func test_sort(collection: some Collection) { + private func test_sort(collection: some Collection) async { // given let ids = collection + let arrayIDs = Array(ids) let products: [StoreProduct] = ids .map { .fake(productIdentifier: $0) } .shuffled() productProviderMock.stubbedFetchResult = .success(products) // when - var resultProducts: [StoreProduct] = [] sut.fetch(productIDs: ids, requestID: .requestID) { result in if case let .success(products) = result { - resultProducts = products + XCTAssertEqual(arrayIDs.count, products.count) + XCTAssertEqual(arrayIDs, products.map(\.productIdentifier)) + } else { + XCTFail("Fetch resulted in an unknown state.") } } - - // then - XCTAssertEqual(ids.count, resultProducts.count) - XCTAssertEqual(Array(ids), resultProducts.map(\.productIdentifier)) } } diff --git a/Tests/FlareTests/UnitTests/Providers/SystemInfoProviderTests.swift b/Tests/FlareTests/UnitTests/Providers/SystemInfoProviderTests.swift index a322e428d..e10c0334d 100644 --- a/Tests/FlareTests/UnitTests/Providers/SystemInfoProviderTests.swift +++ b/Tests/FlareTests/UnitTests/Providers/SystemInfoProviderTests.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // #if os(iOS) || VISION_OS @@ -16,7 +16,7 @@ // MARK: Initialization - override func setUp() { + @MainActor override func setUp() { super.setUp() scenesHolderMock = ScenesHolderMock() sut = SystemInfoProvider(scenesHolder: scenesHolderMock) diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Helpers/WindowSceneFactory.swift b/Tests/FlareTests/UnitTests/TestHelpers/Helpers/WindowSceneFactory.swift index 2e53b23d5..ed3529bcb 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Helpers/WindowSceneFactory.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Helpers/WindowSceneFactory.swift @@ -7,6 +7,7 @@ import UIKit enum WindowSceneFactory { + @MainActor static func makeWindowScene() -> UIWindowScene { UIApplication.shared.connectedScenes.first as! UIWindowScene } diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift index df42fa8a0..71a6924b1 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift @@ -52,13 +52,7 @@ final class PurchaseProviderMock: IPurchaseProvider, @unchecked Sendable { invokedPurchaseParameters = (product, promotionalOffer) invokedPurchaseParametersList.append((product, promotionalOffer)) if let result = stubbedPurchaseCompletionResult { - #if swift(>=6.0) - MainActor.assumeIsolated { - completion(result.0) - } - #else - completion(result.0) - #endif + completion(result.0) } } @@ -81,13 +75,7 @@ final class PurchaseProviderMock: IPurchaseProvider, @unchecked Sendable { invokedPurchaseWithOptionsParametersList.append((product, options, promotionalOffer)) if let result = stubbedinvokedPurchaseWithOptionsCompletionResult { - #if swift(>=6.0) - MainActor.assumeIsolated { - completion(result.0) - } - #else - completion(result.0) - #endif + completion(result.0) } } diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ScenesHolderMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ScenesHolderMock.swift index 6f1b8edc8..228ba0aee 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ScenesHolderMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/ScenesHolderMock.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare @@ -10,7 +10,7 @@ // MARK: - ScenesHolderMock -final class ScenesHolderMock: IScenesHolder { +final class ScenesHolderMock: IScenesHolder, @unchecked Sendable { #if os(iOS) || VISION_OS var invokedConnectedScenesGetter = false var invokedConnectedScenesGetterCount = 0 diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SystemInfoProviderMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SystemInfoProviderMock.swift index 50584eb93..fb168cd9e 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SystemInfoProviderMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/SystemInfoProviderMock.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare @@ -10,7 +10,7 @@ // MARK: - SystemInfoProviderMock -final class SystemInfoProviderMock: ISystemInfoProvider { +final class SystemInfoProviderMock: ISystemInfoProvider, @unchecked Sendable { #if os(iOS) || VISION_OS var invokedCurrentSceneGetter = false var invokedCurrentSceneGetterCount = 0 From 0a8bca38a0897654ab7fbfadbed24720ffe9e527 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 17:50:25 +0100 Subject: [PATCH 07/12] Update package --- Package.swift | 7 ++++--- Package@swift-5.10.swift | 3 +-- Package@swift-5.7.swift | 3 +-- Package@swift-5.8.swift | 3 +-- Package@swift-5.9.swift | 3 +-- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Package.swift b/Package.swift index 5023bf58b..4cad4ab29 100644 --- a/Package.swift +++ b/Package.swift @@ -44,7 +44,8 @@ let package = Package( .target( name: "FlareUI", dependencies: ["Flare"], - resources: [.process("Resources")] + resources: [.process("Resources")], + swiftSettings: [.swiftLanguageMode(.v5)] ), .target(name: "FlareMock", dependencies: ["Flare"]), .target(name: "FlareUIMock", dependencies: ["FlareMock", "FlareUI"]), @@ -72,6 +73,6 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ], - swiftLanguageModes: [.v5] + ] // , +// swiftLanguageModes: [.v5] ) diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift index b8d3cae20..9842c79a8 100644 --- a/Package@swift-5.10.swift +++ b/Package@swift-5.10.swift @@ -72,6 +72,5 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ], - swiftLanguageVersions: [.v5] + ] ) diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index 745a56bff..ef15de429 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -68,6 +68,5 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ], - swiftLanguageVersions: [.v5] + ] ) diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index c81ec1d1f..1946fdb3b 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -68,6 +68,5 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ], - swiftLanguageVersions: [.v5] + ] ) diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index 425feb053..0f0bc05d2 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -72,6 +72,5 @@ let package = Package( .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), ] ), - ], - swiftLanguageVersions: [.v5] + ] ) From 5d60a976ebf746f0b396d77f92727d0ccd9c391d Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 17:59:47 +0100 Subject: [PATCH 08/12] Update integration tests --- .../Tests/StoreProductTests.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Tests/IntegrationTests/Tests/StoreProductTests.swift b/Tests/IntegrationTests/Tests/StoreProductTests.swift index 91432b7e2..7a30662f4 100644 --- a/Tests/IntegrationTests/Tests/StoreProductTests.swift +++ b/Tests/IntegrationTests/Tests/StoreProductTests.swift @@ -34,15 +34,17 @@ final class StoreProductTests: StoreSessionTestCase { let expectation = XCTestExpectation(description: "Purchase a product") // when - var products: [StoreProduct] = [] - provider.fetch(productIDs: [String.productID], requestID: UUID().uuidString) { result in - switch result { - case let .success(skProducts): - products = skProducts.map { StoreProduct($0) } - case .failure: - break + let products: [StoreProduct] = try await withCheckedThrowingContinuation { continuation in { + provider.fetch(productIDs: [String.productID], requestID: UUID().uuidString) { result in + switch result { + case let .success(skProducts): + let products = skProducts.map { StoreProduct($0) } + completion.resume(returning: products) + case .failure: + completion.resume(returning: []) + } + expectation.fulfill() } - expectation.fulfill() } #if swift(>=5.9) From 4674eac22bd1e1404d4d5357b4cc9e621f933210 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 18:33:54 +0100 Subject: [PATCH 09/12] Update integration tests --- Tests/IntegrationTests/Tests/StoreProductTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/IntegrationTests/Tests/StoreProductTests.swift b/Tests/IntegrationTests/Tests/StoreProductTests.swift index 7a30662f4..0ef3c192e 100644 --- a/Tests/IntegrationTests/Tests/StoreProductTests.swift +++ b/Tests/IntegrationTests/Tests/StoreProductTests.swift @@ -34,7 +34,7 @@ final class StoreProductTests: StoreSessionTestCase { let expectation = XCTestExpectation(description: "Purchase a product") // when - let products: [StoreProduct] = try await withCheckedThrowingContinuation { continuation in { + let products: [StoreProduct] = try await withCheckedThrowingContinuation { _ in provider.fetch(productIDs: [String.productID], requestID: UUID().uuidString) { result in switch result { case let .success(skProducts): From d841af82a5ed079d3c56e38b66c43477d101181a Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 18:53:21 +0100 Subject: [PATCH 10/12] Update integration tests --- Tests/IntegrationTests/Tests/StoreProductTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/IntegrationTests/Tests/StoreProductTests.swift b/Tests/IntegrationTests/Tests/StoreProductTests.swift index 0ef3c192e..60512e6d7 100644 --- a/Tests/IntegrationTests/Tests/StoreProductTests.swift +++ b/Tests/IntegrationTests/Tests/StoreProductTests.swift @@ -34,14 +34,14 @@ final class StoreProductTests: StoreSessionTestCase { let expectation = XCTestExpectation(description: "Purchase a product") // when - let products: [StoreProduct] = try await withCheckedThrowingContinuation { _ in + let products: [StoreProduct] = try await withCheckedThrowingContinuation { continuation in provider.fetch(productIDs: [String.productID], requestID: UUID().uuidString) { result in switch result { case let .success(skProducts): let products = skProducts.map { StoreProduct($0) } - completion.resume(returning: products) + continuation.resume(returning: products) case .failure: - completion.resume(returning: []) + continuation.resume(returning: []) } expectation.fulfill() } From 9b86c7ac6cc15083cbf5327a2d5bca57a3525171 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Thu, 26 Dec 2024 20:05:48 +0100 Subject: [PATCH 11/12] Update `Package.swift` --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 4cad4ab29..702a2cbef 100644 --- a/Package.swift +++ b/Package.swift @@ -63,7 +63,8 @@ let package = Package( "FlareUI", "FlareMock", "FlareUIMock", - ] + ], + swiftSettings: [.swiftLanguageMode(.v5)] ), .testTarget( name: "SnapshotTests", From 4d4d09ea34a17bcd61ffefdfc10f42f2f8f51de6 Mon Sep 17 00:00:00 2001 From: Nikita Vasilev Date: Fri, 27 Dec 2024 09:41:40 +0100 Subject: [PATCH 12/12] Remove useless code and update `.swiftformat` --- .swiftformat | 4 ++-- .../AsyncSequence/AsyncSequence+Stream.swift | 15 --------------- .../ReceiptRefreshProvider.swift | 8 ++++---- 3 files changed, 6 insertions(+), 21 deletions(-) delete mode 100644 Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift diff --git a/.swiftformat b/.swiftformat index 8af055a0b..8b2df8e4b 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,6 +1,6 @@ # Stream rules ---swiftversion 5.3 +--swiftversion 5.7 # Use 'swiftformat --options' to list all of the possible options @@ -62,4 +62,4 @@ --wraparguments before-first --wrapcollections before-first ---maxwidth 140 \ No newline at end of file +--maxwidth 140 diff --git a/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift b/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift deleted file mode 100644 index 4806afeb0..000000000 --- a/Sources/Flare/Classes/Helpers/AsyncSequence/AsyncSequence+Stream.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// Flare -// Copyright © 2024 Space Code. All rights reserved. -// - -import Foundation - -// extension AsyncSequence where Element: Sendable { -// func toAsyncStream() -> AsyncStream { -// var asyncIterator = makeAsyncIterator() -// return AsyncStream { -// try? await asyncIterator.next() -// } -// } -// } diff --git a/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift b/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift index 2f800bc99..93b60b522 100644 --- a/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift +++ b/Sources/Flare/Classes/Providers/ReceiptRefreshProvider/ReceiptRefreshProvider.swift @@ -80,9 +80,9 @@ final class ReceiptRefreshProvider: NSObject, @unchecked Sendable { /// - request: The refresh request. /// - handler: The closure to be executed once the refresh is complete. private func fetch(request: IReceiptRefreshRequest, handler: @escaping ReceiptRefreshHandler) { - self.handlers[request.id] = handler - dispatchQueue.async { + self.handlers[request.id] = handler + self.dispatchQueueFactory.main().async { request.start() } @@ -116,8 +116,8 @@ extension ReceiptRefreshProvider: SKRequestDelegate { Logger.error(message: L10n.Receipt.refreshingReceiptFailed(request.id, error.localizedDescription)) dispatchQueue.async { + let handler = self.handlers.removeValue(forKey: request.id) self.dispatchQueueFactory.main().async { - let handler = self.handlers.removeValue(forKey: request.id) handler?(.failure(IAPError(error: error))) } } @@ -127,8 +127,8 @@ extension ReceiptRefreshProvider: SKRequestDelegate { Logger.info(message: L10n.Receipt.refreshedReceipt(request.id)) dispatchQueue.async { + let handler = self.handlers.removeValue(forKey: request.id) self.dispatchQueueFactory.main().async { - let handler = self.handlers.removeValue(forKey: request.id) handler?(.success(())) } }