Skip to content

Commit

Permalink
Implement Restoring Transactions (#57)
Browse files Browse the repository at this point in the history
* Implement the `restore` method for StoreKit

* Update the logic to refresh the receipt

The method includes a new parameter, `updateTransactions`, which forces the update of transactions first. By default, this parameter is set to `false`.

* Update `CHANGELOG.md`

* Update `restore-purchase.md`
  • Loading branch information
nik3212 authored Aug 8, 2024
1 parent 7792783 commit d2b764a
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 32 deletions.
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 @@ 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, any Error>) -> 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<String, IAPError>) -> Void) {
iapProvider.refreshReceipt(updateTransactions: updateTransactions, completion: completion)
}

#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 @@ 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<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 @@ public protocol IFlare {
@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 @@ 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<Result<String, IAPError>>) {
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)
}
}
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 @@ final class IAPProvider: IIAPProvider {
}
}

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

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)))
}
}
} 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 @@ 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, any Error>) -> Void) {
purchaseProvider.restore(completion)
}

#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 @@ 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, Error>) -> Void) {
paymentProvider.restoreCompletedTransactions { _, error in
if let error = error {
completion(.failure(error))
} else {
completion(.success(()))
}
}
}
}

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

0 comments on commit d2b764a

Please sign in to comment.