Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Restoring Transactions #57

Merged
merged 4 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

## 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)
Expand Down
15 changes: 13 additions & 2 deletions Sources/Flare/Classes/Flare.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
// Copyright © 2023 Space Code. All rights reserved.
//

import struct Log.LogLevel
Expand Down Expand Up @@ -153,11 +153,22 @@
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, any Error>) -> Void) {
iapProvider.restore(completion)

Check warning on line 161 in Sources/Flare/Classes/Flare.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Flare.swift#L160-L161

Added lines #L160 - L161 were not covered by tests
}

public func receipt(updateTransactions: Bool) async throws -> String {
try await iapProvider.refreshReceipt(updateTransactions: updateTransactions)

Check warning on line 165 in Sources/Flare/Classes/Flare.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Flare.swift#L164-L165

Added lines #L164 - L165 were not covered by tests
}

public func receipt(updateTransactions: Bool, completion: @escaping (Result<String, IAPError>) -> Void) {
iapProvider.refreshReceipt(updateTransactions: updateTransactions, completion: completion)

Check warning on line 169 in Sources/Flare/Classes/Flare.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Flare.swift#L168-L169

Added lines #L168 - L169 were not covered by tests
}

#if os(iOS) || VISION_OS
@available(iOS 15.0, *)
@available(macOS, unavailable)
Expand Down
69 changes: 60 additions & 9 deletions Sources/Flare/Classes/IFlare.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
// Copyright © 2023 Space Code. All rights reserved.
//

import Foundation
Expand Down Expand Up @@ -105,17 +105,31 @@
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<Result<String, IAPError>>)
/// - 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<String, IAPError>` containing the updated receipt information as a `String`.
/// - On failure, it returns a `Result<String, IAPError>` 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<String, IAPError>) -> 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.
Expand Down Expand Up @@ -151,9 +165,30 @@
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func checkEligibility(productIDs: Set<String>) 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<Void, Error>.success(())`.
/// - On failure, it returns `Result<Void, Error>.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, Error>) -> Void)

#if os(iOS) || VISION_OS
/// Present the refund request sheet for the specified transaction in a window scene.
///
Expand Down Expand Up @@ -259,4 +294,20 @@
) 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<Result<String, IAPError>>) {
receipt(updateTransactions: false, completion: completion)

Check warning on line 302 in Sources/Flare/Classes/IFlare.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/IFlare.swift#L301-L302

Added lines #L301 - L302 were not covered by tests
}

/// 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)

Check warning on line 311 in Sources/Flare/Classes/IFlare.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/IFlare.swift#L310-L311

Added lines #L310 - L311 were not covered by tests
}
}
54 changes: 42 additions & 12 deletions Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
// Copyright © 2023 Space Code. All rights reserved.
//

import StoreKit
Expand Down Expand Up @@ -143,19 +143,46 @@
}
}

func refreshReceipt(completion: @escaping Closure<Result<String, IAPError>>) {
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)

Check warning on line 149 in Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift#L146-L149

Added lines #L146 - L149 were not covered by tests
}
}
}

func refreshReceipt(updateTransactions: Bool, completion: @escaping (Result<String, IAPError>) -> 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)))

Check warning on line 176 in Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift#L171-L176

Added lines #L171 - L176 were not covered by tests
}
}
} else {
refresh()
}
}

func refreshReceipt(completion: @escaping Closure<Result<String, IAPError>>) {
refreshReceipt(updateTransactions: false, completion: completion)
}

func refreshReceipt() async throws -> String {
Expand Down Expand Up @@ -192,11 +219,14 @@
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, any Error>) -> Void) {
purchaseProvider.restore(completion)

Check warning on line 227 in Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift#L226-L227

Added lines #L226 - L227 were not covered by tests
}

#if os(iOS) || VISION_OS
@available(iOS 15.0, *)
@available(macOS, unavailable)
Expand Down
51 changes: 49 additions & 2 deletions Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Flare
// Copyright © 2024 Space Code. All rights reserved.
// Copyright © 2023 Space Code. All rights reserved.
//

import StoreKit
Expand Down Expand Up @@ -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<String, IAPError>` containing the updated receipt information as a `String`.
/// - On failure, it returns a `Result<String, IAPError>` 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<String, IAPError>) -> 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.
Expand Down Expand Up @@ -150,9 +176,30 @@ public protocol IIAPProvider {
@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func checkEligibility(productIDs: Set<String>) 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<Void, Error>.success(())`.
/// - On failure, it returns `Result<Void, Error>.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, Error>) -> Void)

#if os(iOS) || VISION_OS
/// Present the refund request sheet for the specified transaction in a window scene.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void, Error>.success(())`.
/// - On failure, it returns `Result<Void, Error>.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, Error>) -> Void)
}

extension IPurchaseProvider {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,26 @@
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)

Check warning on line 239 in Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift#L234-L239

Added lines #L234 - L239 were not covered by tests
}
}
}
}

func restore(_ completion: @escaping (Result<Void, Error>) -> Void) {
paymentProvider.restoreCompletedTransactions { _, error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(()))

Check warning on line 250 in Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Providers/PurchaseProvider/PurchaseProvider.swift#L245-L250

Added lines #L245 - L250 were not covered by tests
}
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions Sources/Flare/Flare.docc/Articles/restore-purchase.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 2 additions & 1 deletion Sources/FlareUIMock/Mocks/FlareMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,12 +250,13 @@ 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, any Error>) -> Void) {}

#if os(iOS) || VISION_OS
public var invokedBeginRefundRequest = false
public var invokedBeginRefundRequestCount = 0
Expand Down
Loading
Loading