diff --git a/.github/workflows/publish-pages.yml b/.github/workflows/publish-pages.yml new file mode 100644 index 000000000..c5e2c6c7f --- /dev/null +++ b/.github/workflows/publish-pages.yml @@ -0,0 +1,42 @@ +name: Deploy DocC + +on: + push: + branches: + - main + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: macos-12 + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v3 + - name: Build DocC + run: | + swift build; + swift package \ + --allow-writing-to-directory ./docs \ + generate-documentation \ + --target Flare \ + --output-path ./docs \ + --transform-for-static-hosting \ + --hosting-base-path flare; + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: 'docs' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index cec795e73..8deb14f03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ## Added +- Build a documentation using `Docc` + - Added in Pull Request [#11](https://github.com/space-code/flare/pull/11). + - Integrate the `StoreKit2` purchase method - Added in Pull Request [#10](https://github.com/space-code/flare/pull/10). diff --git a/Documentation/Resources/flare.png b/Documentation/Resources/flare.png deleted file mode 100644 index 5a1ac4071..000000000 Binary files a/Documentation/Resources/flare.png and /dev/null differ diff --git a/Documentation/Usage.md b/Documentation/Usage.md deleted file mode 100644 index fc6e788c1..000000000 --- a/Documentation/Usage.md +++ /dev/null @@ -1,186 +0,0 @@ -# Documentation - -### Overview - -* [Introduction](#introduction) -* [Flare Diagram](#flare-diagram) -* [In-App Purchases](#in-app-purchases) - - [Getting Products](#getting-products) - - [Purchasing Product](#purchasing-product) - - [Refreshing Receipt](#refreshing-receipt) - - [Finishing Transaction](#finishing-transaction) - - [Setup Observers](#setup-observers) -* [Errors](#handling-errors) - - [IAPError](#iaperror) - -## Introduction - -Flare provides an elegant interface for In-App Purchases, supporting non-consumable and consumable purchases as well as subscriptions. - -## Flare Diagram - -![Flare: Components](https://raw.githubusercontent.com/space-code/flare/dev/Documentation/Resources/flare.png) - -- `Flare` is a central component that serves as the client API for managing various aspects of in-app purchases and payments in your application. It is designed to simplify the integration of payment processing and in-app purchase functionality into your software. -- `IAPProvider` is a fundamental component of `Flare` that handles all in-app purchase operations. It offers an API to initiate, verify, and manage in-app purchases within your application. -- `IPaymentProvider` is a central component in `Flare` that orchestrates various payment-related operations within your application. It acts as the bridge between the payment gateway and your app's logic. -- `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 - -### Getting Products - -Before attempting to add a payment always check if the user can actually make payments. The `Flare` does it under the hood, if a user cannot make payments, you will get an `IAPError` with the value `paymentNotAllowed`. - -The `fetch` method sends a request to the App Store, which retrieves the products if they are available. The `productIDs` parameter takes the product ids, which should be given from the App Store. - -```swift -Flare.default.fetch(productIDs: Set(arrayLiteral: ["product_id"])) { result in - switch result { - case let .success(products): - debugPrint("Fetched products: \(products)") - case let .failure(error): - debugPrint("An error occurred while fetching products: \(error.localizedDescription)") - } -} -``` - -Additionally, there are versions of both `fetch` that provide an `async` method, allowing the use of `await`. - -```swift - do { - let products = try await Flare.default.fetch(productIDs: Set(arrayLiteral: ["product_id"])) - } catch { - debugPrint("An error occurred while fetching products: \(error.localizedDescription)") - } -``` - -### Purchasing Product - -The `purchase` method performs a purchase of the product. The method accepts an `productID` parameter which represents a product's id. - -```swift -Flare.default.purchase(productID: "product_id") { result in - switch result { - case let .success(transaction): - debugPrint("A transaction was received: \(transaction)") - case let .failure(error): - debugPrint("An error occurred while purchasing product: \(error.localizedDescription)") - } -} -``` - -You can also use the `async/await` implementation of the `purchase` method. - -```swift - do { - let products = try await Flare.default.purchase(productID: "product_id") - } catch { - debugPrint("An error occurred while purchasing product: \(error.localizedDescription)") - } -``` - -### Refreshing Receipt - -The `refresh` method refreshes the receipt, representing the user's transactions with your app. - -```swift -Flare.default.refresh { result in - switch result { - case let .success(receipt): - debugPrint("A receipt was received: \(receipt)") - case let .failure(error): - debugPrint("An error occurred while fetching receipt: \(error.localizedDescription)") - } -} -``` - -You can also use the `async/await` implementation of the `refresh` method. - -```swift - do { - let receipt = try await Flare.default.refresh() - } catch { - debugPrint("An error occurred while fetching receipt: \(error.localizedDescription)") - } -``` - -### Finishing Transaction - -The `finish` method removes a finished (i.e. failed or completed) transaction from the queue. - -```swift -Flare.default.finish(transaction: ) -``` - -### Setup Observers - -The transactions array will only be synchronized with the server while the queue has observers. These methods may require that the user authenticate. -It is important to set an observer on this queue as early as possible after your app launch. Observer is responsible for processing all events triggered by the queue. - -```swift -// Adds transaction observer to the payment queue and handles payment transactions. -Flare.default.addTransactionObserver { result in - switch result { - case let .success(transaction): - debugPrint("A transaction was received: \(transaction)") - case let .failure(error): - debugPrint("An error occurred while adding transaction observer: \(error.localizedDescription)") - } -} -``` - -```swift -// Removes transaction observer from the payment queue. -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 - -By default, all methods handlers in public interfaces produced the `IAPError` error type. The `IAPError` describes frequently used error types in the app. - -```swift -/// `IAPError` is the error type returned by Flare. -/// It encompasses a few different types of errors, each with their own associated reasons. -public enum IAPError: Swift.Error { - /// The empty array of products were fetched. - case emptyProducts - /// The attempt to fetch products with invalid identifiers. - case invalid(productIds: [String]) - /// The attempt to purchase a product when payments are not allowed. - case paymentNotAllowed - /// The payment was cancelled. - case paymentCancelled - /// The attempt to fetch a product that doesn't available. - case storeProductNotAvailable - /// The `SKPayment` returned unknown error. - case storeTrouble - /// The operation failed with an underlying 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 -} -``` - -If you need a `SKError`, you can simply look at the `plainError` property in the `IAPError`. diff --git a/Package.resolved b/Package.resolved index a03aac1cb..df6a8652a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -8,6 +8,24 @@ "revision" : "f9611694f77f64e43d9467a16b2f5212cd04099b", "version" : "0.0.1" } + }, + { + "identity" : "swift-docc-plugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-plugin", + "state" : { + "revision" : "26ac5758409154cc448d7ab82389c520fa8a8247", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-docc-symbolkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-docc-symbolkit", + "state" : { + "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", + "version" : "1.0.0" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index fea55f4e2..8953275aa 100644 --- a/Package.swift +++ b/Package.swift @@ -20,6 +20,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0"), ], targets: [ .target( diff --git a/Package@swift-5.7.swift b/Package@swift-5.7.swift index e645e1d10..4a19d4f04 100644 --- a/Package@swift-5.7.swift +++ b/Package@swift-5.7.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0"), ], targets: [ .target( diff --git a/Package@swift-5.8.swift b/Package@swift-5.8.swift index 256bf4f11..5c077121e 100644 --- a/Package@swift-5.8.swift +++ b/Package@swift-5.8.swift @@ -17,6 +17,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/space-code/concurrency.git", .upToNextMajor(from: "0.0.1")), + .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.1.0"), ], targets: [ .target( diff --git a/README.md b/README.md index 18bfb339a..73cd4116a 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Flare is a framework written in Swift that makes it easy for you to work with in - [x] Complete Unit Test Coverage ## Documentation -Check out [flare documentation](https://github.com/space-code/flare/blob/main/Documentation/Usage.md). +Check out [flare documentation](https://space-code.github.io/flare/documentation/flare/). ## Requirements - iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 7.0+ / visionOS 1.0+ diff --git a/Sources/Flare/Flare.swift b/Sources/Flare/Classes/Flare.swift similarity index 98% rename from Sources/Flare/Flare.swift rename to Sources/Flare/Classes/Flare.swift index 159ac18fc..f27396447 100644 --- a/Sources/Flare/Flare.swift +++ b/Sources/Flare/Classes/Flare.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2023 Space Code. All rights reserved. +// Copyright © 2024 Space Code. All rights reserved. // import StoreKit diff --git a/Sources/Flare/IFlare.swift b/Sources/Flare/Classes/IFlare.swift similarity index 99% rename from Sources/Flare/IFlare.swift rename to Sources/Flare/Classes/IFlare.swift index 69cc89aa3..6643cdd14 100644 --- a/Sources/Flare/IFlare.swift +++ b/Sources/Flare/Classes/IFlare.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2023 Space Code. All rights reserved. +// Copyright © 2024 Space Code. All rights reserved. // import Foundation diff --git a/Sources/Flare/Classes/Models/ProductCategory.swift b/Sources/Flare/Classes/Models/ProductCategory.swift index b9522c35b..cf7dc9e8f 100644 --- a/Sources/Flare/Classes/Models/ProductCategory.swift +++ b/Sources/Flare/Classes/Models/ProductCategory.swift @@ -1,10 +1,11 @@ // // Flare -// Copyright © 2023 Space Code. All rights reserved. +// Copyright © 2024 Space Code. All rights reserved. // import Foundation +/// Enumeration representing different categories of products in an app. public enum ProductCategory: Int { /// A non-renewable or auto-renewable subscription. case subscription diff --git a/Sources/Flare/Classes/Models/SubscriptionPeriod.swift b/Sources/Flare/Classes/Models/SubscriptionPeriod.swift index 9dedab4cb..9bf8a2f52 100644 --- a/Sources/Flare/Classes/Models/SubscriptionPeriod.swift +++ b/Sources/Flare/Classes/Models/SubscriptionPeriod.swift @@ -1,6 +1,6 @@ // // Flare -// Copyright © 2023 Space Code. All rights reserved. +// Copyright © 2024 Space Code. All rights reserved. // import Foundation @@ -8,7 +8,7 @@ import StoreKit // MARK: - SubscriptionPeriod -// A class representing a subscription period with a specific value and unit. +/// A class representing a subscription period with a specific value and unit. public final class SubscriptionPeriod: NSObject { // MARK: Types diff --git a/Sources/Flare/Classes/Models/VerificationError.swift b/Sources/Flare/Classes/Models/VerificationError.swift index 9ca93b178..8f098d05e 100644 --- a/Sources/Flare/Classes/Models/VerificationError.swift +++ b/Sources/Flare/Classes/Models/VerificationError.swift @@ -1,10 +1,12 @@ // // Flare -// Copyright © 2023 Space Code. All rights reserved. +// Copyright © 2024 Space Code. All rights reserved. // import Foundation -public enum VerificationError: Swift.Error { +/// Enumeration representing errors that can occur during verification. +public enum VerificationError: Error { + // Case for unverified product with associated productID and error details. case unverified(productID: String, error: Error) } diff --git a/Sources/Flare/Flare.docc/Articles/perform-purchase.md b/Sources/Flare/Flare.docc/Articles/perform-purchase.md new file mode 100644 index 000000000..55d439082 --- /dev/null +++ b/Sources/Flare/Flare.docc/Articles/perform-purchase.md @@ -0,0 +1,94 @@ +# Perform Purchase + +Learn how to perform a purchase. + +## Setup Observers + +> tip: This step isn't required if the app uses system higher than iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0. + +The transactions array will only be synchronized with the server while the queue has observers. These methods may require that the user authenticate. It is important to set an observer on this queue as early as possible after your app launch. Observer is responsible for processing all events triggered by the queue. + +```swift +// Adds transaction observer to the payment queue and handles payment transactions. +Flare.default.addTransactionObserver { result in + switch result { + case let .success(transaction): + debugPrint("A transaction was received: \(transaction)") + case let .failure(error): + debugPrint("An error occurred while adding transaction observer: \(error.localizedDescription)") + } +} +``` + +```swift +// Removes transaction observer from the payment queue. +Flare.default.removeTransactionObserver() +``` + +## Getting Products + +The fetch method sends a request to the App Store, which retrieves the products if they are available. The productIDs parameter takes the product ids, which should be given from the App Store. + +> important: Before attempting to add a payment always check if the user can actually make payments. The Flare does it under the hood, if a user cannot make payments, you will get an ``IAPError`` with the value ``IAPError/paymentNotAllowed``. + +```swift +Flare.default.fetch(productIDs: ["product_id"]) { result in + switch result { + case let .success(products): + debugPrint("Fetched products: \(products)") + case let .failure(error): + debugPrint("An error occurred while fetching products: \(error.localizedDescription)") + } +} +``` + +Additionally, there are versions of both fetch that provide an `async` method, allowing the use of await. + +```swift +do { + let products = try await Flare.default.fetch(productIDs: Set(arrayLiteral: ["product_id"])) +} catch { + debugPrint("An error occurred while fetching products: \(error.localizedDescription)") +} +``` + +## Purchasing Product + +Flare provides a few methods to perform a purchase: + +- ``IFlare/purchase(product:completion:)`` +- ``IFlare/purchase(product:)`` +- ``IFlare/purchase(product:options:)`` +- ``IFlare/purchase(product:options:completion:)`` + +The method accepts a product parameter which represents a product: + +```swift +Flare.default.purchase(product: product) { result in + switch result { + case let .success(transaction): + debugPrint("A transaction was received: \(transaction)") + case let .failure(error): + debugPrint("An error occurred while purchasing product: \(error.localizedDescription)") + } +} +``` + +If your app has a deployment target higher than iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, you can pass a set of [`options`](https://developer.apple.com/documentation/storekit/product/purchaseoption) along with a purchase request. + +```swift +let transaction = try await Flare.default.purchase(product: product, options: [.appAccountToken(UUID())]) +``` + +## Finishing Transaction + +Finishing a transaction tells StoreKit that your app completed its workflow to make a purchase complete. Unfinished transactions remain in the queue until they’re finished, so be sure to add the transaction queue observer every time your app launches, to enable your app to finish the transactions. Your app needs to finish each transaction, whether it succeeds or fails. + +To finish the transaction, call the ``IFlare/finish(transaction:completion:)`` method. + +```swift +Flare.default.finish(transaction: transaction, completion: nil) +``` + +> important: Don’t call the ``IFlare/finish(transaction:completion:)`` method before the transaction is actually complete and attempt to use some other mechanism in your app to track the transaction as unfinished. StoreKit doesn’t function that way, and doing that prevents your app from downloading Apple-hosted content and can lead to other issues. + diff --git a/Sources/Flare/Flare.docc/Articles/refund-purchase.md b/Sources/Flare/Flare.docc/Articles/refund-purchase.md new file mode 100644 index 000000000..f4f551738 --- /dev/null +++ b/Sources/Flare/Flare.docc/Articles/refund-purchase.md @@ -0,0 +1,17 @@ +# Refund Purchase + +Learn how to process a refund through an iOS app. + +## Refund a 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](https://developer.apple.com/documentation/storekit/transaction/3803220-beginrefundrequest/). + +Flare suggest to use ``IFlare/beginRefundRequest(productID:)`` for refunding purchase. + +```swift +let status = try await Flare.default.beginRefundRequest(productID: "product_id") +``` + +> important: If an issue occurs during the refund process, this method throws an ``IAPError/refund(error:)`` error. + +Call this function from account settings or a help menu to enable customers to request a refund for an in-app purchase within your app. When you call this function, the system displays a refund sheet with the customer’s purchase details and list of reason codes for the customer to choose from. diff --git a/Sources/Flare/Flare.docc/Articles/restore-purchase.md b/Sources/Flare/Flare.docc/Articles/restore-purchase.md new file mode 100644 index 000000000..18dae9f76 --- /dev/null +++ b/Sources/Flare/Flare.docc/Articles/restore-purchase.md @@ -0,0 +1,36 @@ +# Restore Purchase + +Learn how to restore a purchase. + +## Overview + +Users sometimes need to restore purchased content, such as when they upgrade to a new phone. Include some mechanism in your app, such as a Restore Purchases button, to let them restore their purchases. + +## Refresh the app receipt + +A request to the App Store to get the app receipt, which represents the user’s transactions with your app. + +> note: The receipt isn’t necessary if you use StoreKit2. Only use the receipt if your app supports deployment target is lower than iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0. + +Use this API to request a new app receipt from the App Store if the receipt is invalid or missing from its expected location. To request the receipt using the ``IFlare/receipt(completion:)``. + +> important: The receipt refresh request displays a system prompt that asks users to authenticate with their App Store credentials. For a better user experience, initiate the request after an explicit user action, like tapping or clicking a button. + +```swift +Flare.default.receipt { result in + switch result { + case let .success(receipt): + // Handle a receipt + case let .failure(error): + // Handle an error + } +} +``` + +> important: If a receipt isn't found, Flare throws an ``IAPError/receiptNotFound`` error. + +There is an ``IFlare/receipt()`` method for obtaining a receipt using async/await. + +```swift +let receipt = try await Flare.default.receipt() +``` diff --git a/Sources/Flare/Flare.docc/Flare.md b/Sources/Flare/Flare.docc/Flare.md new file mode 100644 index 000000000..0682d736d --- /dev/null +++ b/Sources/Flare/Flare.docc/Flare.md @@ -0,0 +1,66 @@ +# ``Flare`` + +Flare provides an elegant interface for In-App Purchases, supporting non-consumable and consumable purchases as well as subscriptions. + +## Overview + +Flare provides a clear and convenient API for making in-app purchases. + +```swift +import Flare + +/// Fetch a product with the given id +guard let product = try await Flare.default.products(productIDs: ["product_identifier"]) else { return } +/// Purchase a product +let transaction = try await Flare.default.purchase(product: product) +/// Finish a transaction +Flare.default.finish(transaction: transaction, completion: nil) +``` + +Flare supports both StoreKit and StoreKit2; it decides which one to use under the hood based on the operating system version. Flare provides two ways to work with in-app purchases (IAP): it supports the traditional closure-based syntax and the modern async/await approach. + +```swift +import Flare + +/// Fetch a product with the given id +Flare.default.products(productIDs: ["product_identifier"]) { result in + switch result { + case let .success(products): + // Purchase a product + case let .failure(error): + // Handler an error + } +} +``` + +## Minimum Requirements + +| Flare | Date | Swift | Xcode | Platforms | +|-------|------------|-------|---------|-------------------------------------------------------------| +| 3.0 | unreleased | 5.7 | 14.1 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0, visionOS 1.0 | +| 2.0 | 14/09/2023 | 5.7 | 14.1 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0, visionOS 1.0 | +| 1.0 | 21/01/2023 | 5.5 | 13.4.1 | iOS 13.0, watchOS 6.0, macOS 10.15, tvOS 13.0 | + +## License + +flare is available under the MIT license. See the LICENSE file for more info. + +## Topics + +### Essentials + +- ``IFlare`` +- ``IIAPProvider`` + +### Misc + +- ``IAPError`` +- ``ProductType`` +- ``StoreProduct`` +- ``StoreTransaction`` + +### Articles + +- +- +-