Skip to content

Commit

Permalink
[StoreKit 2]: Refunding Purchases Integration (#6)
Browse files Browse the repository at this point in the history
* Bump mint dependencies

- Bumps `swiftformat` from `0.47.12` to `0.52.7`
- Bumps `SwiftLint` from `0.47.1` to `0.53.0`

* Integrate refund purchase feature

Implement a refund purchase feature as a step towards supporting `StoreKit 2`

* Update `Usage.md`

* Update `UML` diagram

* Update `.swiftformat` rules

* Increase test coverage

- Implement unit tests for `IAPError`
- Implement unit tests for `ProcessInfo`
- Implement unit tests for `SystemInfoProvider`

* Implement unit tests

- Implement unit tests for `Flare`
- Implement unit tests for `IAPProvider`

* Implement `RefundRequestProvider` tests

* Update `codecov.yml`

* Update `CHANGELOG.md`

* Update `Package.swift`

* Bump min `swift` version from `5.5` to `5.7`

* Update `README.md`

* Update `CHANGELOG.md`
  • Loading branch information
ns-vasilev authored Oct 15, 2023
1 parent 1d2975d commit ba39791
Show file tree
Hide file tree
Showing 39 changed files with 1,052 additions and 30 deletions.
4 changes: 2 additions & 2 deletions .swiftformat
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
--enable redundantSelf
--enable redundantVoidReturnType
--enable semicolons
--enable sortedImports
--enable sortedSwitchCases
--enable sortImports
--enable sortSwitchCases
--enable spaceAroundBraces
--enable spaceAroundBrackets
--enable spaceAroundComments
Expand Down
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
excluded:
- Tests
- Package.swift
- [email protected]
- .build

# Rules
Expand Down
11 changes: 7 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# Change Log
All notable changes to this project will be documented in this file.

#### 2.x Releases
- `2.0.x` Releases - [2.0.0](#200)
## [Unreleased]

## [Unreleased]()
## Added
- Implement a refund for purchases
- Added in Pull Request [#6](https://github.com/space-code/flare/pull/6).

#### Added
- Added `visionOS` to list of supported platforms
- Added in Pull Request [#5](https://github.com/space-code/flare/pull/5).

#### 2.x Releases
- `2.0.x` Releases - [2.0.0](#200)

## [2.0.0](https://github.com/space-code/flare/releases/tag/2.0.0)
Released on 2023-09-13.

Expand Down
Binary file modified Documentation/Resources/flare.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions Documentation/Usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Flare provides an elegant interface for In-App Purchases, supporting non-consuma
- `IProductProvider` is a component of `Flare` that helps managing the products or services available for purchase within your app.
- `IReceiptRefreshProvider` is responsible for refreshing and managing receipt associated with in-app purchases.
- `IAppStoreReceiptProvider` manages and provides access to the app's receipt, which contains a record of all in-app purchases made by the user.
- `IRefundProvider` is responsible for refunding purchases. This API is available starting from iOS 15.

## In-App Purchases

Expand Down Expand Up @@ -137,6 +138,18 @@ Flare.default.addTransactionObserver { result in
Flare.default.removeTransactionObserver()
```

### Refunding Purchase

Starting with iOS 15, `Flare` now includes support for refunding purchases as part of `StoreKit 2`. Under the hood, 'Flare' obtains the active window scene and displays the sheets on it. You can read more about the refunding process in the official Apple documentation [here](https://developer.apple.com/documentation/storekit/transaction/3803220-beginrefundrequest/).

```swift
do {
let status = try await Flare.default.beginRefundRequest(productID: "product_id")
} catch {
debugPrint("An error occurred while refunding purchase: \(error.localizedDescription)")
}
```

## Handling Errors

### IAPError
Expand All @@ -163,6 +176,8 @@ public enum IAPError: Swift.Error {
case with(error: Swift.Error)
/// The App Store receipt wasn't found.
case receiptNotFound
/// The refund error.
case refund(error: RefundError)
/// The unknown error occurred.
case unknown
}
Expand Down
4 changes: 2 additions & 2 deletions Mintfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
nicklockwood/SwiftFormat@0.47.12
realm/SwiftLint@0.47.1
nicklockwood/SwiftFormat@0.52.7
realm/SwiftLint@0.53.0
33 changes: 20 additions & 13 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
{
"object": {
"pins": [
{
"package": "Concurrency",
"repositoryURL": "https://github.com/space-code/concurrency",
"state": {
"branch": null,
"revision": "f9611694f77f64e43d9467a16b2f5212cd04099b",
"version": "0.0.1"
}
"pins" : [
{
"identity" : "concurrency",
"kind" : "remoteSourceControl",
"location" : "https://github.com/space-code/concurrency",
"state" : {
"revision" : "f9611694f77f64e43d9467a16b2f5212cd04099b",
"version" : "0.0.1"
}
]
},
"version": 1
},
{
"identity" : "objects-factory",
"kind" : "remoteSourceControl",
"location" : "https://github.com/space-code/objects-factory.git",
"state" : {
"revision" : "be016801934d18d91e33845e5e5b9a12617698b0",
"version" : "1.0.0"
}
}
],
"version" : 2
}
10 changes: 8 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
// swift-tools-version: 5.5
// 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",
platforms: [
.macOS(.v10_15),
.iOS(.v13),
.watchOS(.v7),
.tvOS(.v13),
.visionOS(.v1),
],
products: [
.library(name: "Flare", targets: ["Flare"]),
],
dependencies: [
.package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")),
.package(url: "https://github.com/space-code/objects-factory.git", .upToNextMajor(from: "1.0.0")),
],
targets: [
.target(
name: "Flare",
dependencies: [
.product(name: "Concurrency", package: "concurrency"),
]
],
swiftSettings: [visionOSSetting]
),
.testTarget(
name: "FlareTests",
dependencies: [
"Flare",
.product(name: "ObjectsFactory", package: "objects-factory"),
.product(name: "TestConcurrency", package: "concurrency"),
]
),
Expand Down
5 changes: 3 additions & 2 deletions [email protected][email protected]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// swift-tools-version: 5.9
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.
// swiftlint:disable all

Expand All @@ -11,13 +11,13 @@ let package = Package(
.iOS(.v13),
.watchOS(.v7),
.tvOS(.v13),
.visionOS(.v1),
],
products: [
.library(name: "Flare", targets: ["Flare"]),
],
dependencies: [
.package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")),
.package(url: "https://github.com/space-code/objects-factory.git", .upToNextMajor(from: "1.0.0")),
],
targets: [
.target(
Expand All @@ -30,6 +30,7 @@ let package = Package(
name: "FlareTests",
dependencies: [
"Flare",
.product(name: "ObjectsFactory", package: "objects-factory"),
.product(name: "TestConcurrency", package: "concurrency"),
]
),
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<p align="center">
<a href="https://github.com/space-code/flare/blob/main/LICENSE"><img alt="Liscence" src="https://img.shields.io/cocoapods/l/service-core.svg?style=flat"></a>
<a href="https://developer.apple.com/"><img alt="Platform" src="https://img.shields.io/badge/platform-ios%20%7C%20osx%20%7C%20watchos%20%7C%20tvos-%23989898"/></a>
<a href="https://developer.apple.com/swift"><img alt="Swift5.5" src="https://img.shields.io/badge/language-Swift5.5-orange.svg"/></a>
<a href="https://developer.apple.com/swift"><img alt="Swift5.7" src="https://img.shields.io/badge/language-Swift5.7-orange.svg"/></a>
<a href="https://github.com/space-code/flare"><img alt="CI" src="https://github.com/space-code/flare/actions/workflows/ci.yml/badge.svg?branch=main"></a>
<a href="https://codecov.io/gh/space-code/flare"><img alt="CodeCov" src="https://codecov.io/gh/space-code/flare/graph/badge.svg?token=WUWUSKQZWY"></a>
<a href="https://github.com/apple/swift-package-manager" alt="Flare on Swift Package Manager" title="Flare on Swift Package Manager"><img src="https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg" /></a>
Expand Down Expand Up @@ -36,7 +36,7 @@ Check out [flare documentation](https://github.com/space-code/flare/blob/main/Do
## Requirements
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ / visionOS 1.0+
- Xcode 14.0
- Swift 5.5
- Swift 5.7

## Installation
### Swift Package Manager
Expand Down
29 changes: 29 additions & 0 deletions Sources/Flare/Classes/Helpers/ProcessInfo/ProcessInfo+.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Flare
// Copyright © 2023 Space Code. All rights reserved.
//

import Foundation

#if DEBUG
extension ProcessInfo {
static var isRunningUnitTests: Bool {
self[.XCTestConfigurationFile] != nil
}
}

// MARK: - Extensions

extension ProcessInfo {
static subscript(key: String) -> String? {
processInfo.environment[key]
}
}

// MARK: - Constants

private extension String {
static let XCTestConfigurationFile = "XCTestConfigurationFilePath"
}

#endif
22 changes: 22 additions & 0 deletions Sources/Flare/Classes/Helpers/ScenesHolder/IScenesHolder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Flare
// Copyright © 2023 Space Code. All rights reserved.
//

#if canImport(UIKit)
import UIKit
#endif

// MARK: - IScenesHolder

/// A type that holds all connected scenes.
protocol IScenesHolder {
#if os(iOS) || VISION_OS
/// The scenes that are connected to the app.
var connectedScenes: Set<UIScene> { get }
#endif
}

#if os(iOS) || VISION_OS
extension UIApplication: IScenesHolder {}
#endif
9 changes: 9 additions & 0 deletions Sources/Flare/Classes/Models/IAPError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ public enum IAPError: Swift.Error {
case with(error: Swift.Error)
/// The App Store receipt wasn't found.
case receiptNotFound
/// The transaction wasn't found.
case transactionNotFound(productID: String)
/// The refund error.
case refund(error: RefundError)
/// The unknown error occurred.
case unknown
}
Expand Down Expand Up @@ -63,6 +67,7 @@ extension IAPError {

// MARK: Equatable

// swiftlint:disable cyclomatic_complexity
extension IAPError: Equatable {
public static func == (lhs: IAPError, rhs: IAPError) -> Bool {
switch (lhs, rhs) {
Expand All @@ -82,10 +87,14 @@ extension IAPError: Equatable {
return (lhs as NSError) == (rhs as NSError)
case (.receiptNotFound, .receiptNotFound):
return true
case let (.refund(lhs), .refund(rhs)):
return lhs == rhs
case (.unknown, .unknown):
return true
default:
return false
}
}
}

// swiftlint:enable cyclomatic_complexity
14 changes: 14 additions & 0 deletions Sources/Flare/Classes/Models/RefundError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
// Flare
// Copyright © 2023 Space Code. All rights reserved.
//

import Foundation

/// It encompasses all types of refund errors.
public enum RefundError: Error, Equatable {
/// The duplicate refund request.
case duplicateRequest
/// The refund request failed.
case failed
}
18 changes: 18 additions & 0 deletions Sources/Flare/Classes/Models/RefundRequestStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Flare
// Copyright © 2023 Space Code. All rights reserved.
//

import Foundation

/// It encompasses all refund request states.
public enum RefundRequestStatus: Sendable {
/// A user cancelled the refund request.
case userCancelled
/// The request completed successfully.
case success
/// The refund request failed with an error.
case failed(error: Error)
/// The unknown error occurred.
case unknown
}
17 changes: 16 additions & 1 deletion Sources/Flare/Classes/Providers/IAPProvider/IAPProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,24 @@ final class IAPProvider: IIAPProvider {
private let productProvider: IProductProvider
private let paymentProvider: IPaymentProvider
private let receiptRefreshProvider: IReceiptRefreshProvider
private let refundProvider: IRefundProvider

// MARK: Initialization

init(
paymentQueue: PaymentQueue = SKPaymentQueue.default(),
productProvider: IProductProvider = ProductProvider(),
paymentProvider: IPaymentProvider = PaymentProvider(),
receiptRefreshProvider: IReceiptRefreshProvider = ReceiptRefreshProvider()
receiptRefreshProvider: IReceiptRefreshProvider = ReceiptRefreshProvider(),
refundProvider: IRefundProvider = RefundProvider(
systemInfoProvider: SystemInfoProvider()
)
) {
self.paymentQueue = paymentQueue
self.productProvider = productProvider
self.paymentProvider = paymentProvider
self.receiptRefreshProvider = receiptRefreshProvider
self.refundProvider = refundProvider
}

// MARK: Internal
Expand Down Expand Up @@ -124,4 +129,14 @@ final class IAPProvider: IIAPProvider {
func removeTransactionObserver() {
paymentProvider.removeTransactionObserver()
}

#if os(iOS) || VISION_OS
@available(iOS 15.0, *)
@available(macOS, unavailable)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
func beginRefundRequest(productID: String) async throws -> RefundRequestStatus {
try await refundProvider.beginRefundRequest(productID: productID)
}
#endif
}
13 changes: 13 additions & 0 deletions Sources/Flare/Classes/Providers/IAPProvider/IIAPProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,17 @@ public protocol IIAPProvider {
///
/// - Note: This may require that the user authenticate.
func removeTransactionObserver()

#if os(iOS) || VISION_OS
/// Present the refund request sheet for the specified transaction in a window scene.
///
/// - Parameter productID: The identifier of the transaction the user is requesting a refund for.
///
/// - Returns: The result of the refund request.
@available(iOS 15.0, *)
@available(macOS, unavailable)
@available(watchOS, unavailable)
@available(tvOS, unavailable)
func beginRefundRequest(productID: String) async throws -> RefundRequestStatus
#endif
}
Loading

0 comments on commit ba39791

Please sign in to comment.