diff --git a/.github/actions/upload_test_coverage_report/action.yml b/.github/actions/upload_test_coverage_report/action.yml index 3599ff664..4cdd4df4a 100644 --- a/.github/actions/upload_test_coverage_report/action.yml +++ b/.github/actions/upload_test_coverage_report/action.yml @@ -15,20 +15,14 @@ runs: using: "composite" steps: - - name: Dir - shell: bash - run: pwd && ls -al - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3.1.0 + uses: codecov/codecov-action@v4.5.0 with: token: ${{ inputs.token }} xcode: true flags: ${{ inputs.scheme_name }} xcode_archive_path: test_output/${{ inputs.filename }}.xcresult - - name: Dir - shell: bash - run: pwd && ls -al - uses: actions/upload-artifact@v4 with: name: ${{ inputs.filename }} - path: test_output \ No newline at end of file + path: test_output diff --git a/CHANGELOG.md b/CHANGELOG.md index 098f603e0..d72fbc1dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ All notable changes to this project will be documented in this file. #### 3.x Releases -- `3.0.x` Releases - [`3.0.0`](#300) +- `3.0.x` Releases - [`3.0.0`](#300) | [`3.0.1`](#301) - `3.0.0` Release Candidates - [`3.0.0-rc.1`](#300-rc1) | [`3.0.0-rc.2`](#300-rc2) #### 2.x Releases @@ -11,6 +11,19 @@ All notable changes to this project will be documented in this file. #### 1.x Releases - `1.0.x` Releases - [1.0.0](#100) +## [3.0.1](https://github.com/space-code/flare/releases/tag/3.0.1) +Released on 2024-08-09. + +## Added +- Implement restoring transactions for StoreKit 1 + - Added in Pull Request [#57](https://github.com/space-code/flare/pull/57). + +## Updated +- Update `codecov` version + - Updated in Pull Request [#59](https://github.com/space-code/flare/pull/59) +- Update mocks + - Updated in Pull Request [#60](https://github.com/space-code/flare/pull/60) + ## [3.0.0](https://github.com/space-code/flare/releases/tag/3.0.0) Released on 2024-06-15. diff --git a/Gemfile.lock b/Gemfile.lock index 02e9385ea..e25445e09 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,8 +47,8 @@ GEM open4 (1.3.4) public_suffix (5.0.3) rchardet (1.8.0) - rexml (3.2.8) - strscan (>= 3.0.9) + rexml (3.3.3) + strscan ruby2_keywords (0.0.5) sawyer (0.9.2) addressable (>= 2.3.5) diff --git a/Package.resolved b/Package.resolved index 18547265b..9478aa8ff 100644 --- a/Package.resolved +++ b/Package.resolved @@ -50,14 +50,14 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "8ddd519780452729c6634ad6bd0d2595938e9ea3", - "version" : "1.16.1" + "revision" : "6d932a79e7173b275b96c600c86c603cf84f153c", + "version" : "1.17.4" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax", "state" : { "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", "version" : "509.1.1" diff --git a/Sources/Flare/Classes/Flare.swift b/Sources/Flare/Classes/Flare.swift index d2e5f2842..4d508e60b 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 @@ -153,11 +153,22 @@ extension Flare: IFlare { try await iapProvider.checkEligibility(productIDs: productIDs) } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) public func restore() async throws { try await iapProvider.restore() } + public func restore(_ completion: @escaping (Result) -> Void) { + iapProvider.restore(completion) + } + + public func receipt(updateTransactions: Bool) async throws -> String { + try await iapProvider.refreshReceipt(updateTransactions: updateTransactions) + } + + public func receipt(updateTransactions: Bool, completion: @escaping (Result) -> Void) { + iapProvider.refreshReceipt(updateTransactions: updateTransactions, completion: completion) + } + #if os(iOS) || VISION_OS @available(iOS 15.0, *) @available(macOS, unavailable) diff --git a/Sources/Flare/Classes/IFlare.swift b/Sources/Flare/Classes/IFlare.swift index c108dd85c..0cc638c80 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 @@ -105,17 +105,31 @@ public protocol IFlare { promotionalOffer: PromotionalOffer? ) async throws -> StoreTransaction - /// Refreshes the receipt, representing the user's transactions with your app. + /// Refreshes the receipt and optionally updates transactions. /// - /// - Parameter completion: The closure to be executed when the refresh operation ends. - func receipt(completion: @escaping Closure>) + /// - Parameters: + /// - updateTransactions: A boolean indicating whether to update transactions. + /// - If `true`, the method will refresh completed transactions. + /// - If `false`, only the receipt will be refreshed. + /// - completion: A closure that gets called with the result of the refresh operation. + /// - On success, it returns a `Result` containing the updated receipt information as a `String`. + /// - 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) - /// Refreshes the receipt, representing the user's transactions with your app. + /// Refreshes the receipt and optionally updates transactions. /// - /// `IAPError(error:)` if the request did fail with error. + /// - Parameter updateTransactions: A boolean indicating whether to update transactions. + /// - If `true`, the method will refresh completed transactions. + /// - If `false`, only the receipt will be refreshed. /// - /// - Returns: A receipt. - func receipt() async throws -> String + /// - Returns: A `String` containing the updated receipt information. + /// + /// - Throws: An `IAPError` if the refresh process encounters an issue. + /// + /// - Note: Use this method for an asynchronous refresh operation with error handling and receipt data retrieval. + func receipt(updateTransactions: Bool) async throws -> String /// Removes a finished (i.e. failed or completed) transaction from the queue. /// Attempting to finish a purchasing transaction will throw an exception. @@ -151,9 +165,30 @@ public protocol IFlare { @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func checkEligibility(productIDs: Set) async throws -> [String: SubscriptionEligibility] - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + /// Restores completed transactions. + /// + /// This method initiates the process of restoring any previously completed transactions. + /// It is an asynchronous function that might throw an error if the restoration fails. + /// + /// - Throws: An error if the restoration process encounters an issue. + /// + /// - Note: This method should be called when you need to restore purchases made by the user on a different device or after + /// reinstallation. func restore() async throws + /// Restores completed transactions. + /// + /// This method initiates the process of restoring any previously completed transactions. + /// It uses a completion handler to provide the result of the restoration process. + /// + /// - Parameter completion: A closure that gets called with a `Result` indicating success or failure of the restoration. + /// - On success, it returns `Result.success(())`. + /// - On failure, it returns `Result.failure(Error)` with an error describing the issue. + /// + /// - 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) + #if os(iOS) || VISION_OS /// Present the refund request sheet for the specified transaction in a window scene. /// @@ -259,4 +294,20 @@ public extension IFlare { ) async throws -> StoreTransaction { try await purchase(product: product, options: options, promotionalOffer: nil) } + + /// 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>) { + receipt(updateTransactions: false, completion: completion) + } + + /// Refreshes the receipt, representing the user's transactions with your app. + /// + /// `IAPError(error:)` if the request did fail with error. + /// + /// - Returns: A receipt. + func receipt() async throws -> String { + try await receipt(updateTransactions: false) + } } diff --git a/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift b/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift index fd787f988..15a76942b 100644 --- a/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift +++ b/Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // import StoreKit @@ -143,19 +143,46 @@ final class IAPProvider: IIAPProvider { } } - func refreshReceipt(completion: @escaping Closure>) { - receiptRefreshProvider.refresh(requestID: UUID().uuidString) { [weak self] result in - switch result { - case .success: - if let receipt = self?.receiptRefreshProvider.receipt { - completion(.success(receipt)) - } else { - completion(.failure(.receiptNotFound)) + func refreshReceipt(updateTransactions: Bool) async throws -> String { + try await withCheckedThrowingContinuation { continuation in + refreshReceipt(updateTransactions: updateTransactions) { result in + continuation.resume(with: result) + } + } + } + + func refreshReceipt(updateTransactions: Bool, completion: @escaping (Result) -> Void) { + let refresh = { [weak self] in + self?.receiptRefreshProvider.refresh(requestID: UUID().uuidString) { [weak self] result in + switch result { + case .success: + if let receipt = self?.receiptRefreshProvider.receipt { + completion(.success(receipt)) + } else { + completion(.failure(.receiptNotFound)) + } + case let .failure(error): + completion(.failure(error)) } - case let .failure(error): - completion(.failure(error)) } } + + if updateTransactions { + restore { result in + switch result { + case .success: + refresh() + case let .failure(error): + completion(.failure(IAPError.with(error: error))) + } + } + } else { + refresh() + } + } + + func refreshReceipt(completion: @escaping Closure>) { + refreshReceipt(updateTransactions: false, completion: completion) } func refreshReceipt() async throws -> String { @@ -192,11 +219,14 @@ final class IAPProvider: IIAPProvider { return try await eligibilityProvider.checkEligibility(products: products) } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func restore() async throws { try await purchaseProvider.restore() } + func restore(_ completion: @escaping (Result) -> Void) { + purchaseProvider.restore(completion) + } + #if os(iOS) || VISION_OS @available(iOS 15.0, *) @available(macOS, unavailable) diff --git a/Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift b/Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift index 318a1435f..c2b18e33f 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 @@ -103,6 +103,32 @@ public protocol IIAPProvider { promotionalOffer: PromotionalOffer? ) async throws -> StoreTransaction + /// Refreshes the receipt and optionally updates transactions. + /// + /// - Parameters: + /// - updateTransactions: A boolean indicating whether to update transactions. + /// - If `true`, the method will refresh completed transactions. + /// - If `false`, only the receipt will be refreshed. + /// - completion: A closure that gets called with the result of the refresh operation. + /// - On success, it returns a `Result` containing the updated receipt information as a `String`. + /// - 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) + + /// Refreshes the receipt and optionally updates transactions. + /// + /// - Parameter updateTransactions: A boolean indicating whether to update transactions. + /// - If `true`, the method will refresh completed transactions. + /// - If `false`, only the receipt will be refreshed. + /// + /// - Returns: A `String` containing the updated receipt information. + /// + /// - Throws: An `IAPError` if the refresh process encounters an issue. + /// + /// - Note: Use this method for an asynchronous refresh operation with error handling and receipt data retrieval. + func refreshReceipt(updateTransactions: Bool) async throws -> String + /// Refreshes the receipt, representing the user's transactions with your app. /// /// - Parameter completion: The closure to be executed when the refresh operation ends. @@ -150,9 +176,30 @@ public protocol IIAPProvider { @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func checkEligibility(productIDs: Set) async throws -> [String: SubscriptionEligibility] - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + /// Restores completed transactions. + /// + /// This method initiates the process of restoring any previously completed transactions. + /// It is an asynchronous function that might throw an error if the restoration fails. + /// + /// - Throws: An error if the restoration process encounters an issue. + /// + /// - Note: This method should be called when you need to restore purchases made by the user on a different device or after + /// reinstallation. func restore() async throws + /// Restores completed transactions. + /// + /// This method initiates the process of restoring any previously completed transactions. + /// It uses a completion handler to provide the result of the restoration process. + /// + /// - Parameter completion: A closure that gets called with a `Result` indicating success or failure of the restoration. + /// - On success, it returns `Result.success(())`. + /// - On failure, it returns `Result.failure(Error)` with an error describing the issue. + /// + /// - 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) + #if os(iOS) || VISION_OS /// Present the refund request sheet for the specified transaction in a window scene. /// diff --git a/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift b/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift index cd290ad09..f5951a3a6 100644 --- a/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift +++ b/Sources/Flare/Classes/Providers/PurchaseProvider/IPurchaseProvider.swift @@ -58,8 +58,29 @@ protocol IPurchaseProvider { completion: @escaping PurchaseCompletionHandler ) - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + /// Restores completed transactions. + /// + /// This method initiates the process of restoring any previously completed transactions. + /// It is an asynchronous function that might throw an error if the restoration fails. + /// + /// - Throws: An error if the restoration process encounters an issue. + /// + /// - Note: This method should be called when you need to restore purchases made by the user on a different device or after + /// reinstallation. func restore() async throws + + /// Restores completed transactions. + /// + /// This method initiates the process of restoring any previously completed transactions. + /// It uses a completion handler to provide the result of the restoration process. + /// + /// - Parameter completion: A closure that gets called with a `Result` indicating success or failure of the restoration. + /// - On success, it returns `Result.success(())`. + /// - On failure, it returns `Result.failure(Error)` with an error describing the issue. + /// + /// - 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) } extension IPurchaseProvider { diff --git a/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift b/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift index 479ffcd84..2290c6871 100644 --- a/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift +++ b/Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift @@ -230,9 +230,26 @@ extension PurchaseProvider: IPurchaseProvider { paymentProvider.removeTransactionObserver() } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func restore() async throws { - try await AppStore.sync() + if #available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) { + try await AppStore.sync() + } else { + try await withCheckedThrowingContinuation { continuation in + restore { result in + continuation.resume(with: result) + } + } + } + } + + func restore(_ completion: @escaping (Result) -> Void) { + paymentProvider.restoreCompletedTransactions { _, error in + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } + } } } diff --git a/Sources/Flare/Flare.docc/Articles/restore-purchase.md b/Sources/Flare/Flare.docc/Articles/restore-purchase.md index 1164954b4..b4644da4d 100644 --- a/Sources/Flare/Flare.docc/Articles/restore-purchase.md +++ b/Sources/Flare/Flare.docc/Articles/restore-purchase.md @@ -34,3 +34,5 @@ There is an ``IFlare/receipt()`` method for obtaining a receipt using async/awai ```swift let receipt = try await Flare.shared.receipt() ``` + +The ``IFlare/receipt(updateTransactions:completion:)`` method has a parameter, `updateTransactions`, which controls whether transactions are updated first. diff --git a/Sources/FlareUIMock/Mocks/FlareMock.swift b/Sources/FlareUIMock/Mocks/FlareMock.swift index 364f3cfa6..6e21c3003 100644 --- a/Sources/FlareUIMock/Mocks/FlareMock.swift +++ b/Sources/FlareUIMock/Mocks/FlareMock.swift @@ -250,12 +250,24 @@ public final class FlareMock: IFlare { public var invokedRestore = false public var invokedRestoreCount = 0 - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) public func restore() async throws { invokedRestore = true invokedRestoreCount += 1 } + public func restore(_: @escaping (Result) -> Void) {} + + public var invokedReceiptUpdateTransactions = false + public var invokedReceiptUpdateTransactionsCount = 0 + public var stubbedReceiptUpdateTransactions = "" + public func receipt(updateTransactions _: Bool) async throws -> String { + invokedReceiptUpdateTransactions = true + invokedReceiptUpdateTransactionsCount += 1 + return stubbedReceiptUpdateTransactions + } + + public func receipt(updateTransactions _: Bool, completion _: @escaping (Result) -> Void) {} + #if os(iOS) || VISION_OS public var invokedBeginRefundRequest = false public var invokedBeginRefundRequestCount = 0 diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/IAPProviderMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/IAPProviderMock.swift index 296b91b41..610bd92a8 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/IAPProviderMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/IAPProviderMock.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2024 Space Code. All rights reserved. +// Copyright © 2023 Space Code. All rights reserved. // @testable import Flare @@ -297,6 +297,18 @@ final class IAPProviderMock: IIAPProvider { invokedPresentOfferCodeRedeemSheetCount += 1 } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func restore() async throws {} + + func restore(_: @escaping (Result) -> Void) {} + + var invokedRefreshReceiptUpdateTransaction = false + var invokedRefreshReceiptUpdateTransactionCount = 0 + var stubbedInvokedRefreshReceiptUpdateTransaction: String = "" + func refreshReceipt(updateTransactions _: Bool) async throws -> String { + invokedRefreshReceiptUpdateTransaction = true + invokedRefreshReceiptUpdateTransactionCount += 1 + return stubbedInvokedRefreshReceiptUpdateTransaction + } + + func refreshReceipt(updateTransactions _: Bool, completion _: @escaping (Result) -> Void) {} } diff --git a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift index f58fd3121..ec176852a 100644 --- a/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift +++ b/Tests/FlareTests/UnitTests/TestHelpers/Mocks/PurchaseProviderMock.swift @@ -81,6 +81,7 @@ final class PurchaseProviderMock: IPurchaseProvider { } } - @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func restore() async throws {} + + func restore(_: @escaping (Result) -> Void) {} }