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

[StoreKit 2]: Refunding Purchases Integration #6

Merged
merged 14 commits into from
Oct 15, 2023
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 @@
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 @@

// 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 @@
return (lhs as NSError) == (rhs as NSError)
case (.receiptNotFound, .receiptNotFound):
return true
case let (.refund(lhs), .refund(rhs)):
return lhs == rhs

Check warning on line 91 in Sources/Flare/Classes/Models/IAPError.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Flare/Classes/Models/IAPError.swift#L91

Added line #L91 was not covered by tests
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