+
+* Select your project in Xcode
+* Go to the section *Swift Package*
+* Click on *(+) Add Package Dependency*
+* Copy the Git URL: *https://github.com/iridescent-dev/iap-swift-lib.git*
+* Click on *Next* > *Next*
+* Make sure your project is selected in *Add to target*
+* Click on *Finish*
+
+*Note:* You have to `import InAppPurchaseLib` wherever you use the library.
diff --git a/Documentation/Getting Started/Micro Example.md b/Documentation/Getting Started/Micro Example.md
new file mode 100644
index 0000000..2491fe3
--- /dev/null
+++ b/Documentation/Getting Started/Micro Example.md
@@ -0,0 +1,90 @@
+# Micro Example
+
+
+```swift
+/** AppDelegate.swift */
+import UIKit
+import InAppPurchaseLib
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ // Initialize the library
+ InAppPurchase.initialize(
+ iapProducts: [
+ IAPProduct(productIdentifier: "my_product", productType: .nonConsumable)
+ ],
+ validatorUrlString: "https://validator.fovea.cc/v1/validate?appName=demo&apiKey=12345678"
+ )
+ return true
+ }
+
+ func applicationWillTerminate(_ application: UIApplication) {
+ // Clean
+ InAppPurchase.stop()
+ }
+}
+```
+
+```swift
+/** ViewController.swift */
+import UIKit
+import StoreKit
+import InAppPurchaseLib
+
+class ViewController: UIViewController {
+ private var loaderView = LoaderView()
+ @IBOutlet weak var statusLabel: UILabel!
+ @IBOutlet weak var productTitleLabel: UILabel!
+ @IBOutlet weak var productDescriptionLabel: UILabel!
+ @IBOutlet weak var purchaseButton: UIButton!
+ @IBOutlet weak var restorePurchasesButton: UIButton!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ // Add action for purchases and restore butons.
+ purchaseButton.addTarget(self, action: #selector(self.purchase), for: .touchUpInside)
+ restorePurchasesButton.addTarget(self, action: #selector(self.restorePurchases), for: .touchUpInside)
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ self.refreshView()
+ InAppPurchase.refresh(callback: { _ in
+ self.refreshView()
+ })
+ }
+
+ func refreshView() {
+ guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product") else {
+ self.productTitleLabel.text = "Product unavailable"
+ return
+ }
+ // Display product information.
+ productTitleLabel.text = product.localizedTitle
+ productDescriptionLabel.text = product.localizedDescription
+ purchaseButton.setTitle(product.localizedPrice, for: .normal)
+
+ // Disable the button if the product has already been purchased.
+ if InAppPurchase.hasActivePurchase(for: "my_product") {
+ statusLabel.text = "OWNED"
+ purchaseButton.isPointerInteractionEnabled = false
+ }
+ }
+
+ @IBAction func purchase(_ sender: Any) {
+ self.loaderView.show()
+ InAppPurchase.purchase(
+ productIdentifier: "my_product",
+ callback: { result in
+ self.loaderView.hide()
+ })
+ }
+
+ @IBAction func restorePurchases(_ sender: Any) {
+ self.loaderView.show()
+ InAppPurchase.restorePurchases(callback: { result in
+ self.loaderView.hide()
+ })
+ }
+}
+```
diff --git a/InAppPurchaseLib.png b/Documentation/Images/InAppPurchaseLib.png
similarity index 100%
rename from InAppPurchaseLib.png
rename to Documentation/Images/InAppPurchaseLib.png
diff --git a/ScreenshotInstallation.png b/Documentation/Images/ScreenshotInstallation.png
similarity index 100%
rename from ScreenshotInstallation.png
rename to Documentation/Images/ScreenshotInstallation.png
diff --git a/Documentation/License.md b/Documentation/License.md
new file mode 100644
index 0000000..ce44199
--- /dev/null
+++ b/Documentation/License.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Iridescent
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Documentation/Usage.md b/Documentation/Usage.md
new file mode 100644
index 0000000..e96c58e
--- /dev/null
+++ b/Documentation/Usage.md
@@ -0,0 +1,8 @@
+The process of implementing in-app purchases involves several steps:
+1. Displaying the list of purchasable products
+2. Initiating a purchase
+3. Delivering and finalizing a purchase
+4. Checking the current ownership of non-consumables and subscriptions
+5. Implementing the Restore Purchases button
+
+*Note:* You have to `import InAppPurchaseLib` wherever you use the library.
diff --git a/Documentation/Usage/Analytics.md b/Documentation/Usage/Analytics.md
new file mode 100644
index 0000000..946ebd1
--- /dev/null
+++ b/Documentation/Usage/Analytics.md
@@ -0,0 +1,42 @@
+# Analytics
+Tracking the purchase flow is a common things in apps. Especially as it's core to your revenue model.
+
+We can track 5 events, which step in the purchase pipeline a user reached.
+1. `purchase initiated`
+2. `purchase cancelled`
+3. `purchase failed`
+4. `purchase deferred`
+5. `purchase succeeded`
+
+Here's a quick example showing how to implement this correctly.
+
+``` swift
+func makePurchase() {
+ Analytics.trackEvent("purchase initiated")
+ InAppPurchase.purchase(
+ productIdentifier: "my_product_id",
+ callback: { result in
+ switch result.state {
+ case .purchased:
+ // Reminder: We are not processing the purchase here, only updating your UI.
+ // That's why we do not send an event to analytics.
+ case .failed:
+ Analytics.trackEvent("purchase failed")
+ case .deferred:
+ Analytics.trackEvent("purchase deferred")
+ case .cancelled:
+ Analytics.trackEvent("purchase cancelled")
+ }
+ })
+}
+
+// IAPPurchaseDelegate implementation
+func productPurchased(productIdentifier: String) {
+ Analytics.trackEvent("purchase succeeded")
+ InAppPurchase.finishTransactions(for: productIdentifier)
+}
+```
+
+The important part to remember is that a purchase can occur outside your app (or be approved when the app is not running), that's why tracking `purchase succeeded` has to be part of the `productPurchased` delegate function.
+
+Refer to the [Consumables](handling-purchases.html#consumables) section to learn more about the `productPurchased` function.
diff --git a/Documentation/Usage/Displaying products with purchases.md b/Documentation/Usage/Displaying products with purchases.md
new file mode 100644
index 0000000..0ec900c
--- /dev/null
+++ b/Documentation/Usage/Displaying products with purchases.md
@@ -0,0 +1,59 @@
+# Displaying products with purchases
+In your store screen, where you present your products titles and prices with a purchase button, there are some cases to handle that we skipped. Owned products and deferred purchases.
+
+### Owned products
+Non-consumables and active auto-renewing subscriptions cannot be purchased again. You should adjust your UI to reflect that state. Refer to `InAppPurchase.hasActivePurchase()` to and to the example later in this section.
+
+### Deferred purchases
+Apple's **Ask to Buy** feature lets parents approve any purchases initiated by children, including in-app purchases.
+
+With **Ask to Buy** enabled, when a child requests to make a purchase, the app is notified that the purchase is awaiting the parent’s approval in the purchase callback:
+
+``` swift
+InAppPurchase.purchase(
+ productIdentifier: productIdentifier,
+ callback: { result in
+ switch result.state {
+ case .deferred:
+ // Pending parent approval
+ }
+})
+```
+
+In the _deferred_ case, the child has been notified by StoreKit that the parents have to approve the purchase. He might then close the app and come back later. You don't have much to do, but to display in your UI that there is a purchase waiting for parental approval in your views.
+
+We will use the `hasDeferredTransaction` method:
+
+``` swift
+InAppPurchase.hasDeferredTransaction(for productIdentifier: String) -> Bool
+```
+
+### Example
+Here's an example that covers what has been discussed above. We will update our example `refreshView` function from before:
+
+``` swift
+@objc func refreshView() {
+ guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product_id") else {
+ self.titleLabel.text = "Product unavailable"
+ return
+ }
+ self.titleLabel.text = product.localizedTitle
+ // ...
+
+ // "Ask to Buy" deferred purchase waiting for parent's approval
+ if InAppPurchase.hasDeferredTransaction(for: "my_product_id") {
+ self.statusLabel.text = "Waiting for Approval..."
+ self.purchaseButton.isPointerInteractionEnabled = false
+ }
+ // "Owned" product
+ else if InAppPurchase.hasActivePurchase(for: "my_product_id") {
+ self.statusLabel.text = "OWNED"
+ self.purchaseButton.isPointerInteractionEnabled = false
+ }
+ else {
+ self.purchaseButton.isPointerInteractionEnabled = true
+ }
+}
+```
+
+When a product is owned or has a deferred purchase, we make sure the purchase button is grayed out. We also use a status label to display some details. Of course, you are free to design your UI as you see fit.
diff --git a/Documentation/Usage/Displaying products.md b/Documentation/Usage/Displaying products.md
new file mode 100644
index 0000000..83c71b0
--- /dev/null
+++ b/Documentation/Usage/Displaying products.md
@@ -0,0 +1,36 @@
+# Displaying products
+Let's start with the simplest case: you have a single product.
+
+You can retrieve all information about this product using the function `InAppPurchase.getProductBy(identifier: "my_product_id")`. This returns an [SKProduct](https://developer.apple.com/documentation/storekit/skproduct) extended with helpful methods.
+
+Those are the most important:
+ - `productIdentifier: String` - The string that identifies the product to the Apple AppStore.
+ - `localizedTitle: String` - The name of the product, in the language of the device, as retrieved from the AppStore.
+ - `localizedDescription: String` - A description of the product, in the language of the device, as retrieved from the AppStore.
+ - `localizedPrice: String` - The cost of the product in the local currency (_read-only property added by this library_).
+
+*Example*:
+
+You can add a function similar to this to your view.
+
+``` swift
+@objc func refreshView() {
+ guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product_id") else {
+ self.titleLabel.text = "Product unavailable"
+ return
+ }
+ self.titleLabel.text = product.localizedTitle
+ self.descriptionLabel.text = product.localizedDescription
+ self.priceLabel.text = product.localizedPrice
+}
+```
+
+This example assumes `self.titleLabel` is a UILabel, etc.
+
+Make sure to call this function when the view appears on screen, for instance by calling it from [`viewWillAppear`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621510-viewwillappear).
+
+``` swift
+override func viewWillAppear(_ animated: Bool) {
+ self.refreshView()
+}
+```
diff --git a/Documentation/Usage/Displaying subscriptions.md b/Documentation/Usage/Displaying subscriptions.md
new file mode 100644
index 0000000..0094c2e
--- /dev/null
+++ b/Documentation/Usage/Displaying subscriptions.md
@@ -0,0 +1,36 @@
+# Displaying subscriptions
+For subscription products, you also have some data about subscription periods and introductory offers.
+
+ - `func hasIntroductoryPriceEligible() -> Bool` - The product has an introductory price the user is eligible to.
+ - `localizedSubscriptionPeriod: String?` - The period of the subscription.
+ - `localizedIntroductoryPrice: String?` - The cost of the introductory offer if available in the local currency.
+ - `localizedIntroductoryPeriod: String?` - The subscription period of the introductory offer.
+ - `localizedIntroductoryDuration: String?` - The duration of the introductory offer.
+
+**Example**
+
+``` swift
+@objc func refreshView() {
+ guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product_id") else {
+ self.titleLabel.text = "Product unavailable"
+ return
+ }
+ self.titleLabel.text = product.localizedTitle
+ self.descriptionLabel.text = product.localizedDescription
+
+ // Format price text. Example: "0,99€ / month for 3 months (then 3,99 € / month)"
+ var priceText = "\(product.localizedPrice) / \(product.localizedSubscriptionPeriod!)"
+ if product.hasIntroductoryPriceEligible() {
+ if product.introductoryPrice!.numberOfPeriods == 1 {
+ priceText = "\(product.localizedIntroductoryPrice!) for \(product.localizedIntroductoryDuration!)" +
+ " (then \(priceText))"
+ } else {
+ priceText = "\(product.localizedIntroductoryPrice!) / \(product.localizedIntroductoryPeriod!)" +
+ " for \(product.localizedIntroductoryDuration!) (then \(priceText))"
+ }
+ }
+ self.priceLabel.text = priceText
+}
+```
+
+*Note:* You have to `import StoreKit` wherever you use `SKProduct`.
diff --git a/Documentation/Usage/Errors.md b/Documentation/Usage/Errors.md
new file mode 100644
index 0000000..a428e48
--- /dev/null
+++ b/Documentation/Usage/Errors.md
@@ -0,0 +1,20 @@
+# Errors
+
+When calling `refresh()`, `purchase()` or `restorePurchases()`, the callback can return an `IAPError` if the state is `failed`.
+Here is the list of `IAPErrorCode` you can receive:
+
+* Errors returned by `refresh()`, `purchase()` or `restorePurchases()`
+ - `libraryNotInitialized` - You must call the `initialize` fuction before using the library.
+ - `bundleIdentifierInvalid` - The Bundle Identifier is invalid.
+ - `validatorUrlInvalid` - The Validator URL String is invalid.
+ - `refreshReceiptFailed` - Failed to refresh the App Store receipt.
+ - `validateReceiptFailed` - Failed to validate the App Store receipt with Fovea.
+ - `readReceiptFailed` - Failed to read the receipt validation.
+
+* Errors returned by `refresh()`
+ - `refreshProductsFailed` - Failed to refresh products from the App Store.
+
+* Errors returned by `purchase()`
+ - `productNotFound` - The product was not found on the App Store and cannot be purchased.
+ - `cannotMakePurchase` - The user is not allowed to authorize payments.
+ - `alreadyPurchasing` - A purchase is already in progress.
diff --git a/Documentation/Usage/Handling purchases.md b/Documentation/Usage/Handling purchases.md
new file mode 100644
index 0000000..06febe8
--- /dev/null
+++ b/Documentation/Usage/Handling purchases.md
@@ -0,0 +1,93 @@
+# Handling purchases
+Finally, the magic happened: a user purchased one of your products! Let's see how we handle the different types of products.
+
+- [Non-Consumables](#non-consumables)
+- [Auto-Renewable Subscriptions](#auto-renewable-subscriptions)
+- [Consumables](#consumables)
+- [Non-Renewing Subscriptions](#non-renewing-subscriptions)
+
+
+## Non-Consumables
+Wherever your app needs to know if a non-consumable product has been purchased, use `InAppPurchase.hasActivePurchase(for:
+productIdentifier)`. This will return true if the user currently owns the product.
+
+**Note:** The last known state for the user's purchases is stored as [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults). As such, their status is always available to your app, even when offline.
+
+If you have a server that needs to know about the purchase. You should rely on Fovea's webhook instead of doing anything in here. We will see that later in the [Server integration](server-integration.html) section.
+
+
+## Auto-Renewable Subscriptions
+As with non-consumables, you will use `InAppPurchase.hasActivePurchase(for: productIdentifier)` to check if the user is an active subscriber to a given product.
+
+You might also like to call refresh regularly, for example when entering your main view. When appropriate, the library will refresh the receipt to detect subscription renewals or expiry.
+
+As we've seend in the [Refreshing](refreshing.html) section:
+
+``` swift
+override func viewWillAppear(_ animated: Bool) {
+ self.refreshView()
+ InAppPurchase.refresh(callback: { _ in
+ self.refreshView()
+ })
+}
+```
+
+**Note:** Don't be reluctant to call `refresh()` often. Internally, the library ensures heavy operation are only performed if necessary: for example when a subscription just expired. So in 99% of cases this call will result in no-operations.
+
+
+## Consumables
+If the purchased products in a **consumable**, your app is responsible for delivering the purchase then acknowlege that you've done so. Delivering generally consists in increasing a counter for some sort of virtual currency.
+
+Your app can be notified of a purchase at any time. So the library asks you to provide an **IAPPurchaseDelegate** from initialization.
+
+In `InAppPurchase.initialize()`, we can pass an **IAPPurchaseDelegate** instance. This object implements the **productPurchased(productIdentifier:)** function, which is called whenever a purchase is approved.
+
+Here's a example implementation:
+
+``` swift
+class AppDelegate: UIResponder, UIApplicationDelegate, IAPPurchaseDelegate {
+ ...
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ InAppPurchase.initialize(
+ iapProducts: [...],
+ iapPurchaseDelegate: self, // ADDED: iapPurchaseDelegate
+ validatorUrlString: "https://validator.fovea.cc/v1/validate?appName=demo&apiKey=12345678")
+ }
+
+ // IAPPurchaseDelegate implementation
+ func productPurchased(productIdentifier: String) {
+ // TODO
+ }
+}
+```
+
+It's also important to know that when a purchase is approved, money isn't yet to reach your bank account. You have to acknowledge delivery of the (virtual) item to finalize the transaction. That is why we have to call `InAppPurchase.finishTransactions(for: productIdentifier)` as soon as we delivered the product.
+
+**Example**
+
+Let's define a class that adopts the **IAPPurchaseDelegate** protocol, it can very well be your application delegate.
+
+``` swift
+func productPurchased(productIdentifier: String) {
+ switch productIdenfier {
+ case "10_silver":
+ addSilver(10)
+ case "100_silver":
+ addSilver(100)
+ }
+ InAppPurchase.finishTransactions(for: productIdentifier)
+ Analytics.trackEvent("purchase succeeded", productIdentifier)
+}
+```
+
+Here, we implement our own unlocking logic and call `InAppPurchase.finishTransactions()` afterward (assuming `addSilver` is synchronous).
+
+*Note:* `productPurchased` is called when a purchase has been confirmed by Fovea's receipt validator. If you have a server, he probably already has been notified of this purchase using the webhook.
+
+**Reminder**: Keep in mind that purchase notifications might occur even if you never called the `InAppPurchase.purchase()` function: purchases can be made from another device or the AppStore, they can be approved by parents when the app isn't running, purchase flows can be interupted, etc. The pattern above ensures your app is always ready to handle purchase events.
+
+
+## Non-Renewing Subscriptions
+For non-renewing subscriptions, delivering consists in increasing the amount of time a user can access a given feature. Apple doesn't manage the length and expiry of non-renewing subscriptions: you have to do this yourself, as for consumables.
+
+Basically, everything is identical to consumables.
diff --git a/Documentation/Usage/Initialization.md b/Documentation/Usage/Initialization.md
new file mode 100644
index 0000000..18f2e54
--- /dev/null
+++ b/Documentation/Usage/Initialization.md
@@ -0,0 +1,46 @@
+# Initialization
+Before everything else the library must be initialized. This has to happen as soon as possible. A good way is to call the `InAppPurchase.initialize()` method when the application did finish launching. In the background, this will load your products and refresh the status of purchases and subscriptions.
+
+`InAppPurchase.initialize()` requires the following arguments:
+* `iapProducts` - An array of `IAPProduct`
+* `validatorUrlString` - The validator url retrieved from [Fovea](https://billing.fovea.cc/?ref=iap-swift-lib)
+
+Each `IAPProduct` contains the following fields:
+* `productIdentifier` - The product unique identifier
+* `productType` - The `IAPProductType` (*consumable*, *nonConsumable*, *nonRenewingSubscription* or *autoRenewableSubscription*)
+
+*Example:*
+
+A good place is generally in your application delegate's `didFinishLaunchingWithOptions` function, like below:
+
+``` swift
+import InAppPurchaseLib
+
+class AppDelegate: UIResponder, UIApplicationDelegate, IAPPurchaseDelegate {
+ ...
+ func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ InAppPurchase.initialize(
+ iapProducts: [
+ IAPProduct(productIdentifier: "monthly_plan", productType: .autoRenewableSubscription),
+ IAPProduct(productIdentifier: "yearly_plan", productType: .autoRenewableSubscription),
+ IAPProduct(productIdentifier: "disable_ads", productType: .nonConsumable)
+ ],
+ validatorUrlString: "https://validator.fovea.cc/v1/validate?appName=demo&apiKey=12345678")
+ }
+
+ func productPurchased(productIdentifier: String) {
+ // ... process purchase (we'll see that later)
+ }
+}
+```
+
+You should also call the `stop` method when the application will terminate, for proper cleanup.
+``` swift
+ func applicationWillTerminate(_ application: UIApplication) {
+ InAppPurchase.stop()
+ }
+```
+
+For more advanced use cases, in particular when you have implemented user login, you'll have to make some adjustments. We'll learn more about this in the [Server integration](server-integration.html) section.
+
+*Tip:* If initialization was successful, you should see a new receipt validation event in [Fovea's Dashboard](https://billing-dashboard.fovea.cc/events).
diff --git a/Documentation/Usage/Purchasing.md b/Documentation/Usage/Purchasing.md
new file mode 100644
index 0000000..aede98d
--- /dev/null
+++ b/Documentation/Usage/Purchasing.md
@@ -0,0 +1,65 @@
+# Purchasing
+The purchase process is generally a little bit more involving than most people would expect. Why is it not just: purchase → on success unlock the feature?
+
+Several reasons:
+- In-app purchases can be initiated outside the app
+- In-app purchases can be deferred, pending parental approval
+- Apple wants to be sure you delivered the product before charging the user
+
+That is why the process looks like so:
+- being ready to handle purchase events from app startup
+- finalizing transactions when product delivery is complete
+- sending purchase request, for which successful doesn't always mean complete
+
+### Initiating a purchase
+To initiate a purchase, use the `InAppPurchase.purchase()` function. It takes the `productIdentifier` and a `callback` function, called when the purchase has been processed.
+
+**Important**: Do not process the purchase here, we'll handle that later!
+
+From this callback, you can for example unlock the UI by hiding your loading indicator and display a message to the user.
+
+*Example:*
+
+``` swift
+self.loaderView.show()
+InAppPurchase.purchase(
+ productIdentifier: "my_product_id",
+ callback: { _ in
+ self.loaderView.hide()
+})
+```
+
+This simple example locks the UI with a loader when the purchase is in progress. We'll see later how the purchase has to be processed by your applicaiton.
+
+The callback also gives more information about the outcome of the purchase, you might want to use it to update your UI as well. Note that some events are useful for analytics. So here's a more complete example.
+
+``` swift
+self.loaderView.show()
+InAppPurchase.purchase(
+ productIdentifier: "my_product_id",
+ callback: { result in
+ self.loaderView.hide()
+
+ switch result.state {
+ case .purchased:
+ // Product successfully purchased
+ // Reminder: Do not process the purchase here, only update your UI.
+ // that's why we do not send data to analytics.
+ openThankYouScreen()
+ case .failed:
+ // Purchase failed
+ // - Human formated reason can be found in result.localizedDescription
+ // - More details in either result.skError or result.iapError
+ showError(result.localizedDescription)
+ case .deferred:
+ // The purchase is deferred, waiting for the parent's approval
+ openWaitingParentApprovalScreen()
+ case .cancelled:
+ // The user canceled the request, generally only useful for analytics.
+ }
+})
+```
+
+If the purchase fails, result will contain either `.skError`, a [`SKError`](https://developer.apple.com/documentation/storekit/skerror/code) from StoreKit, or `.iapError`, an [`IAPError`](errors.html).
+
+*Tip:* After a successful purchase, you should see a new transaction in [Fovea's dashboard](https://billing-dashboard.fovea.cc/transactions).
diff --git a/Documentation/Usage/Refreshing.md b/Documentation/Usage/Refreshing.md
new file mode 100644
index 0000000..ff76a77
--- /dev/null
+++ b/Documentation/Usage/Refreshing.md
@@ -0,0 +1,13 @@
+# Refreshing
+Data might change or not be yet available when your "product" view is presented. In order to properly handle those cases, you should refresh your view after refreshing in-app products metadata. You want to be sure you're displaying up-to-date information.
+
+To achieve this, call `InAppPurchase.refresh()` when your view is presented.
+
+``` swift
+override func viewWillAppear(_ animated: Bool) {
+ self.refreshView()
+ InAppPurchase.refresh(callback: { _ in
+ self.refreshView()
+ })
+}
+```
diff --git a/Documentation/Usage/Restoring purchases.md b/Documentation/Usage/Restoring purchases.md
new file mode 100644
index 0000000..2eab331
--- /dev/null
+++ b/Documentation/Usage/Restoring purchases.md
@@ -0,0 +1,25 @@
+# Restoring purchases
+Except if you only sell consumable products, Apple requires that you provide a "Restore Purchases" button to your users. In general, it is found in your application settings.
+
+Call this method when this button is pressed.
+
+``` swift
+@IBAction func restorePurchases(_ sender: Any) {
+ self.loaderView.show()
+ InAppPurchase.restorePurchases(callback: { result in
+ self.loaderView.hide()
+ switch result.state {
+ case .succeeded:
+ if result.addedPurchases > 0 {
+ print("Restore purchases successful.")
+ } else {
+ print("No purchase to restore.")
+ }
+ case .failed:
+ print("Restore purchases failed.")
+ }
+ })
+}
+```
+
+The `callback` method is called once the operation is complete. You can use it to unlock the UI, by hiding your loader for example, and display the adapted message to the user.
diff --git a/Documentation/Usage/Server integration.md b/Documentation/Usage/Server integration.md
new file mode 100644
index 0000000..ed35b28
--- /dev/null
+++ b/Documentation/Usage/Server integration.md
@@ -0,0 +1,21 @@
+# Server integration
+In more advanced use cases, you have a server component. Users are logged in and you'll like to unlock the content for this user on your server. The safest approach is to setup a [Webhook on Fovea](https://billing.fovea.cc/documentation/webhook/?ref=iap-swift-lib). You'll receive notifications from Fovea that transaction have been processed and/or subscriptions updated.
+
+The information sent from Fovea has been verified from Apple's server, which makes it way more trustable than information sent from your app itself.
+
+To take advantage of this, you have to inform the library of your application username. This `applicationUsername` can be provided as a parameter of the `InAppPurchase.initialize` method and updated later by changing the associated property.
+
+*Example:*
+``` swift
+InAppPurchase.initialize(
+ iapProducts: [...],
+ validatorUrlString: "..."),
+ applicationUsername: UserSession.getUserId())
+
+// later ...
+InAppPurchase.applicationUsername = UserSession.getUserId()
+```
+
+If a user account is mandatory in your app, you will want to delay calls to `InAppPurchase.initialize()` to when your user's session is ready.
+
+Do not hesitate to [contact Fovea](mailto:support@fovea.cc) for help.
diff --git a/README.md b/README.md
index ee9697f..31b7d43 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,8 @@
-
+
-> An easy-to-use library for In-App Purchases, using Fovea.Billing for receipts validation.
-
-- [Features](#features)
-- [Getting Started](#getting-started)
- - [Requirements](#requirements)
- - [Installation](#installation)
-- [Usage](#usage)
- - [Initialization](#initialization)
- - [Displaying products](#displaying-products)
- - [Displaying subscriptions](#displaying-subscriptions)
- - [Refreshing](#refreshing)
- - [Purchasing](#purchasing)
- - [Making a purchase](#making-a-purchase)
- - [Processing purchases](#processing-purchases)
- - [Restoring purchases](#restoring-purchases)
- - [Purchased products](#purchased-products)
- - [Purchases information](#purchases-information)
- - [Errors](#errors)
-- [Server integration](#server-integration)
-- [Xcode Demo Project](#xcode-demo-project)
-- [References](#references)
-- [Troubleshooting](#troubleshooting)
-- [License](#license)
+> An easy-to-use Swift library for In-App Purchases, using Fovea.Billing for receipts validation.
# Features
@@ -39,21 +17,9 @@
* ✅ Server integration with a Webhook
# Getting Started
-If you haven't already, I highly recommend your read the *Overview* and *Preparing* section of Apple's [In-App Purchase official documentation](https://developer.apple.com/in-app-purchase)
-
-## Requirements
-* Configure your App and Xcode to support In-App Purchases.
- * [AppStore Connect Setup](https://help.apple.com/app-store-connect/#/devb57be10e7)
-* Create and configure your [Fovea.Billing](https://billing.fovea.cc/?ref=iap-swift-lib) project account:
- * Set your bundle ID
- * The iOS Shared Secret (or shared key) is to be retrieved from [AppStoreConnect](https://appstoreconnect.apple.com/)
- * The iOS Subscription Status URL (only if you want subscriptions)
+If you haven't already, I highly recommend your read the *Overview* and *Preparing* section of Apple's [In-App Purchase official documentation](https://developer.apple.com/in-app-purchase).
## Installation
-
-
-
-
* Select your project in Xcode
* Go to the section *Swift Package*
* Click on *(+) Add Package Dependency*
@@ -64,492 +30,117 @@ If you haven't already, I highly recommend your read the *Overview* and *Prepari
*Note:* You have to `import InAppPurchaseLib` wherever you use the library.
+## Micro Example
-# Usage
-
-The process of implementing in-app purchases involves several steps:
-1. Displaying the list of purchasable products
-2. Initiating a purchase
-3. Delivering and finalizing a purchase
-4. Checking the current ownership of non-consumables and subscriptions
-5. Implementing the Restore Purchases button
-
-## Initialization
-Before everything else the library must be initialized. This has to happen as soon as possible. A good way is to call the `InAppPurchase.initialize()` method when the application did finish launching. In the background, this will load your products and refresh the status of purchases and subscriptions.
-
-`InAppPurchase.initialize()` accepts the following arguments:
-* `iapProducts` - An array of **IAPProduct** (REQUIRED)
-* `validatorUrlString` - The validator url retrieved from [Fovea](https://billing.fovea.cc/?ref=iap-swift-lib) (REQUIRED)
-* `applicationUsername` - The user name, if your app implements user login (optional)
-
-Each **IAPProduct** contains the following fields:
-* `productIdentifier` - The product unique identifier
-* `productType` - The **IAPProductType** (`consumable`, `nonConsumable`, `nonRenewingSubscription` or `autoRenewableSubscription`)
-
-*Example:*
-
-A good place is generally in your application delegate's `didFinishLaunchingWithOptions` function, like below:
-
-``` swift
+```swift
+/** AppDelegate.swift */
+import UIKit
import InAppPurchaseLib
-class AppDelegate: UIResponder, UIApplicationDelegate, IAPPurchaseDelegate {
- ...
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ // Initialize the library
InAppPurchase.initialize(
iapProducts: [
- IAPProduct(productIdentifier: "monthly_plan", productType: .autoRenewableSubscription),
- IAPProduct(productIdentifier: "yearly_plan", productType: .autoRenewableSubscription),
- IAPProduct(productIdentifier: "disable_ads", productType: .nonConsumable)
+ IAPProduct(productIdentifier: "my_product", productType: .nonConsumable)
],
- validatorUrlString: "https://validator.fovea.cc/v1/validate?appName=demo&apiKey=12345678")
+ validatorUrlString: "https://validator.fovea.cc/v1/validate?appName=demo&apiKey=12345678"
+ )
+ return true
}
- func productPurchased(productIdentifier: String) {
- // ... process purchase (we'll see that later)
- }
-}
-```
-
-You should also call the `stop` method when the application will terminate, for proper cleanup.
-``` swift
func applicationWillTerminate(_ application: UIApplication) {
+ // clean
InAppPurchase.stop()
}
-```
-
-For more advanced use cases, in particular when you have implemented user login, you'll have to make some adjustments. We'll learn more about this in the [Server integration](#server-integration) section.
-
-*Tip:* If initialization was successful, you should see a new receipt validation event in [Fovea's Dashboard](https://billing-dashboard.fovea.cc/events).
-
-## Displaying products
-Let's start with the simplest case: you have a single product.
-
-You can retrieve all information about this product using the function `InAppPurchase.getProductBy(identifier: "my_product_id")`. This returns an [SKProduct](https://developer.apple.com/documentation/storekit/skproduct) extended with helpful methods.
-
-Those are the most important:
- - `productIdentifier: String` - The string that identifies the product to the Apple AppStore.
- - `localizedTitle: String` - The name of the product, in the language of the device, as retrieved from the AppStore.
- - `localizedDescription: String` - A description of the product, in the language of the device, as retrieved from the AppStore.
- - `localizedPrice: String` - The cost of the product in the local currency (_read-only property added by this library_).
-
-*Example*:
-
-You can add a function similar to this to your view.
-
-``` swift
-@objc func refreshView() {
- guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product_id") else {
- self.titleLabel.text = "Product unavailable"
- return
- }
- self.titleLabel.text = product.localizedTitle
- self.descriptionLabel.text = product.localizedDescription
- self.priceLabel.text = product.localizedPrice
-}
-```
-
-This example assumes `self.titleLabel` is a UILabel, etc.
-
-Make sure to call this function when the view appears on screen, for instance by calling it from [`viewWillAppear`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621510-viewwillappear).
-
-``` swift
-override func viewWillAppear(_ animated: Bool) {
- self.refreshView()
}
```
-## Displaying subscriptions
-For subscription products, you also have some data about subscription periods and introductory offers.
-
- - `func hasIntroductoryPriceEligible() -> Bool` - The product has an introductory price the user is eligible to.
- - `localizedSubscriptionPeriod: String?` - The period of the subscription.
- - `localizedIntroductoryPrice: String?` - The cost of the introductory offer if available in the local currency.
- - `localizedIntroductoryPeriod: String?` - The subscription period of the introductory offer.
- - `localizedIntroductoryDuration: String?` - The duration of the introductory offer.
-
-**Example**
-
-``` swift
-@objc func refreshView() {
- guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product_id") else {
- self.titleLabel.text = "Product unavailable"
- return
- }
- self.titleLabel.text = product.localizedTitle
- self.descriptionLabel.text = product.localizedDescription
-
- // Format price text. Example: "0,99€ / month for 3 months (then 3,99 € / month)"
- var priceText = "\(product.localizedPrice) / \(product.localizedSubscriptionPeriod!)"
- if product.hasIntroductoryPriceEligible() {
- if product.introductoryPrice!.numberOfPeriods == 1 {
- priceText = "\(product.localizedIntroductoryPrice!) for \(product.localizedIntroductoryDuration!)" +
- " (then \(priceText))"
- } else {
- priceText = "\(product.localizedIntroductoryPrice!) / \(product.localizedIntroductoryPeriod!)" +
- " for \(product.localizedIntroductoryDuration!) (then \(priceText))"
- }
- }
- self.priceLabel.text = priceText
-}
-```
-
-*Note:* You have to `import StoreKit` wherever you use `SKProduct`.
-
-## Refreshing
-Data might change or not be yet available when your "product" view is presented. In order to properly handle those cases, you should refresh your view after refreshing in-app products metadata. You want to be sure you're displaying up-to-date information.
-
-To achieve this, call `InAppPurchase.refresh()` when your view is presented.
-
-``` swift
-override func viewWillAppear(_ animated: Bool) {
- self.refreshView()
- InAppPurchase.refresh(callback: { _ in
- self.refreshView()
- })
-}
-```
-
-## Purchasing
-The purchase process is generally a little bit more involving than most people would expect. Why is it not just: purchase → on success unlock the feature?
-
-Several reasons:
-- In-app purchases can be initiated outside the app
-- In-app purchases can be deferred, pending parental approval
-- Apple wants to be sure you delivered the product before charging the user
-
-That is why the process looks like so:
-- being ready to handle purchase events from app startup
-- finalizing transactions when product delivery is complete
-- sending purchase request, for which successful doesn't always mean complete
-
-### Initiating a purchase
-To initiate a purchase, use the `InAppPurchase.purchase()` function. It takes the `productIdentifier` and a `callback` function, called when the purchase has been processed.
-
-**Important**: Do not process the purchase here, we'll handle that later!
-
-From this callback, you can for example unlock the UI by hiding your loading indicator and display a message to the user.
-
-*Example:*
-
-``` swift
-self.loaderView.show()
-InAppPurchase.purchase(
- productIdentifier: "my_product_id",
- callback: { _ in
- self.loaderView.hide()
-})
-```
-
-This simple example locks the UI with a loader when the purchase is in progress. We'll see later how the purchase has to be processed by your applicaiton.
-
-The callback also gives more information about the outcome of the purchase, you might want to use it to update your UI as well. Note that some events are useful for analytics. So here's a more complete example.
-
-``` swift
-self.loaderView.show()
-InAppPurchase.purchase(
- productIdentifier: "my_product_id",
- callback: { result in
- self.loaderView.hide()
+```swift
+/** ViewController.swift */
+import UIKit
+import StoreKit
+import InAppPurchaseLib
- switch result.state {
- case .purchased:
- // Product successfully purchased
- // Reminder: Do not process the purchase here, only update your UI.
- // that's why we do not send data to analytics.
- openThankYouScreen()
- case .failed:
- // Purchase failed
- // - Human formated reason can be found in result.localizedDescription
- // - More details in either result.skError or result.iapError
- showError(result.localizedDescription)
- case .deferred:
- // The purchase is deferred, waiting for the parent's approval
- openWaitingParentApprovalScreen()
- case .cancelled:
- // The user canceled the request, generally only useful for analytics.
+class ViewController: UIViewController {
+ private var loaderView = LoaderView()
+ @IBOutlet weak var statusLabel: UILabel!
+ @IBOutlet weak var productTitleLabel: UILabel!
+ @IBOutlet weak var productDescriptionLabel: UILabel!
+ @IBOutlet weak var purchaseButton: UIButton!
+ @IBOutlet weak var restorePurchasesButton: UIButton!
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ // Add action for purchases and restore butons.
+ purchaseButton.addTarget(self, action: #selector(self.purchase), for: .touchUpInside)
+ restorePurchasesButton.addTarget(self, action: #selector(self.restorePurchases), for: .touchUpInside)
}
-})
-```
-
-If the purchase fails, result will contain either `.skError`, a [`SKError`](https://developer.apple.com/documentation/storekit/skerror/code) from StoreKit, or `.iapError`, an [`IAPError`](#errors).
-
-*Tip:* After a successful purchase, you should see a new transaction in [Fovea's dashboard](https://billing-dashboard.fovea.cc/transactions).
-
-## Handling purchases
-Finally, the magic happened: a user purchased one of your products! Let's see how we handle the different types of products.
-### Non-Consumables
-Wherever your app needs to know if a non-consumable product has been purchased, use `InAppPurchase.hasActivePurchase(for:
-productIdentifier)`. This will return true if the user currently owns the product.
-
-**Note:** The last known state for the user's purchases is stored as [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults). As such, their status is always available to your app, even when offline.
-
-If you have a server that needs to know about the purchase. You should rely on Fovea's webhook instead of doing anything in here. We will see that later in the [Server integration](#server-integration) section.
-
-### Auto-Renewable Subscriptions
-As with non-consumables, you will use `InAppPurchase.hasActivePurchase(for: productIdentifier)` to check if the user is an active subscriber to a given product.
-
-You might also like to call refresh regularly, for example when entering your main view. When appropriate, the library will refresh the receipt to detect subscription renewals or expiry.
-
-As we've seend in the [Refreshing](#refreshing) section:
-
-``` swift
-override func viewWillAppear(_ animated: Bool) {
- self.refreshView()
- InAppPurchase.refresh(callback: { _ in
+ override func viewWillAppear(_ animated: Bool) {
+ self.refreshView()
+ InAppPurchase.refresh(callback: { _ in
self.refreshView()
- })
-}
-```
-
-**Note:** Don't be reluctant to call `refresh()` often. Internally, the library ensures heavy operation are only performed if necessary: for example when a subscription just expired. So in 99% of cases this call will result in no-operations.
-
-### Consumables
-If the purchased products in a **consumable**, your app is responsible for delivering the purchase then acknowlege that you've done so. Delivering generally consists in increasing a counter for some sort of virtual currency.
-
-Your app can be notified of a purchase at any time. So the library asks you to provide an **IAPPurchaseDelegate** from initialization.
-
-In `InAppPurchase.initialize()`, we can pass an **IAPPurchaseDelegate** instance. This object implements the **productPurchased(productIdentifier:)** function, which is called whenever a purchase is approved.
-
-Here's a example implementation:
-
-``` swift
-class AppDelegate: UIResponder, UIApplicationDelegate, IAPPurchaseDelegate {
- ...
- func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- InAppPurchase.initialize(
- iapProducts: [...],
- iapPurchaseDelegate: self, // ADDED: iapPurchaseDelegate
- validatorUrlString: "https://validator.fovea.cc/v1/validate?appName=demo&apiKey=12345678")
+ })
}
- // IAPPurchaseDelegate implementation
- func productPurchased(productIdentifier: String) {
- // TODO
+ func refreshView() {
+ guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product") else {
+ self.productTitleLabel.text = "Product unavailable"
+ return
+ }
+ // Display product information.
+ productTitleLabel.text = product.localizedTitle
+ productDescriptionLabel.text = product.localizedDescription
+ purchaseButton.setTitle(product.localizedPrice, for: .normal)
+
+ // Disable the button if the product has already been purchased.
+ if InAppPurchase.hasActivePurchase(for: "my_product") {
+ statusLabel.text = "OWNED"
+ purchaseButton.isPointerInteractionEnabled = false
+ }
}
-}
-```
-
-It's also important to know that when a purchase is approved, money isn't yet to reach your bank account. You have to acknowledge delivery of the (virtual) item to finalize the transaction. That is why we have to call `InAppPurchase.finishTransactions(for: productIdentifier)` as soon as we delivered the product.
-
-**Example**
-Let's define a class that adopts the **IAPPurchaseDelegate** protocol, it can very well be your application delegate.
-
-``` swift
-func productPurchased(productIdentifier: String) {
- switch productIdenfier {
- case "10_silver":
- addSilver(10)
- case "100_silver":
- addSilver(100)
+ @IBAction func purchase(_ sender: Any) {
+ self.loaderView.show()
+ InAppPurchase.purchase(
+ productIdentifier: "my_product",
+ callback: { result in
+ self.loaderView.hide()
+ })
}
- InAppPurchase.finishTransactions(for: productIdentifier)
- Analytics.trackEvent("purchase succeeded", productIdentifier)
-}
-```
-
-Here, we implement our own unlocking logic and call `InAppPurchase.finishTransactions()` afterward (assuming `addSilver` is synchronous).
-*Note:* `productPurchased` is called when a purchase has been confirmed by Fovea's receipt validator. If you have a server, he probably already has been notified of this purchase using the webhook.
-
-**Reminder**: Keep in mind that purchase notifications might occur even if you never called the `InAppPurchase.purchase()` function: purchases can be made from another device or the AppStore, they can be approved by parents when the app isn't running, purchase flows can be interupted, etc. The pattern above ensures your app is always ready to handle purchase events.
-
-### Non-Renewing Subscriptions
-For non-renewing subscriptions, delivering consists in increasing the amount of time a user can access a given feature. Apple doesn't manage the length and expiry of non-renewing subscriptions: you have to do this yourself, as for consumables.
-
-Basically, everything is identical to consumables.
-
-## Restoring purchases
-Except if you only sell consumable products, Apple requires that you provide a "Restore Purchases" button to your users. In general, it is found in your application settings.
-
-Call this method when this button is pressed.
-
-``` swift
-@IBAction func restorePurchases(_ sender: Any) {
- self.loaderView.show()
- InAppPurchase.restorePurchases(callback: { result in
+ @IBAction func restorePurchases(_ sender: Any) {
+ self.loaderView.show()
+ InAppPurchase.restorePurchases(callback: { result in
self.loaderView.hide()
- switch result.state {
- case .succeeded:
- if result.addedPurchases > 0 {
- print("Restore purchases successful.")
- } else {
- print("No purchase to restore.")
- }
- case .failed:
- print("Restore purchases failed.")
- }
- })
-}
-```
-
-The `callback` method is called once the operation is complete. You can use it to unlock the UI, by hiding your loader for example, and display the adapted message to the user.
-
-
-## Displaying products with purchases
-In your store screen, where you present your products titles and prices with a purchase button, there are some cases to handle that we skipped. Owned products and deferred purchases.
-
-### Owned products
-Non-consumables and active auto-renewing subscriptions cannot be purchased again. You should adjust your UI to reflect that state. Refer to `InAppPurchase.hasActivePurchase()` to and to the example later in this section.
-
-### Deferred purchases
-Apple's **Ask to Buy** feature lets parents approve any purchases initiated by children, including in-app purchases.
-
-With **Ask to Buy** enabled, when a child requests to make a purchase, the app is notified that the purchase is awaiting the parent’s approval in the purchase callback:
-
-``` swift
-InAppPurchase.purchase(
- productIdentifier: productIdentifier,
- callback: { result in
- switch result.state {
- case .deferred:
- // Pending parent approval
- }
-})
-```
-
-In the _deferred_ case, the child has been notified by StoreKit that the parents have to approve the purchase. He might then close the app and come back later. You don't have much to do, but to display in your UI that there is a purchase waiting for parental approval in your views.
-
-We will use the `hasDeferredTransaction` method:
-
-``` swift
-InAppPurchase.hasDeferredTransaction(for productIdentifier: String) -> Bool
-```
-
-### Example
-Here's an example that covers what has been discussed above. We will update our example `refreshView` function from before:
-
-``` swift
-@objc func refreshView() {
- guard let product: SKProduct = InAppPurchase.getProductBy(identifier: "my_product_id") else {
- self.titleLabel.text = "Product unavailable"
- return
- }
- self.titleLabel.text = product.localizedTitle
- // ...
-
- // "Ask to Buy" deferred purchase waiting for parent's approval
- if InAppPurchase.hasDeferredTransaction(for: "my_product_id") {
- self.statusLabel.text = "Waiting for Approval..."
- self.purchaseButton.isPointerInteractionEnabled = false
- }
- // "Owned" product
- else if InAppPurchase.hasActivePurchase(for: "my_product_id") {
- self.statusLabel.text = "OWNED"
- self.purchaseButton.isPointerInteractionEnabled = false
- }
- else {
- self.purchaseButton.isPointerInteractionEnabled = true
+ })
}
}
```
-When a product is owned or has a deferred purchase, we make sure the purchase button is grayed out. We also use a status label to display some details. Of course, you are free to design your UI as you see fit.
+# Documentation
+- [Getting Started](https://iridescent-dev.github.io/iap-swift-lib/Getting%20Started.html)
+- [Usage](https://iridescent-dev.github.io/iap-swift-lib/Usage.html)
+- [API documentation](https://iridescent-dev.github.io/iap-swift-lib/API%20documentation.html)
-## Errors
-
-When calling `refresh()`, `purchase()` or `restorePurchases()`, the callback can return an `IAPError` if the state is `failed`.
-Here is the list of `IAPErrorCode` you can receive:
-
-* Errors returned by `refresh()`, `purchase()` or `restorePurchases()`
- - `libraryNotInitialized` - You must call the `initialize` fuction before using the library.
- - `bundleIdentifierInvalid` - The Bundle Identifier is invalid.
- - `validatorUrlInvalid` - The Validator URL String is invalid.
- - `refreshReceiptFailed` - Failed to refresh the App Store receipt.
- - `validateReceiptFailed` - Failed to validate the App Store receipt with Fovea.
- - `readReceiptFailed` - Failed to read the receipt validation.
-
-* Errors returned by `refresh()`
- - `refreshProductsFailed` - Failed to refresh products from the App Store.
-
-* Errors returned by `purchase()`
- - `productNotFound` - The product was not found on the App Store and cannot be purchased.
- - `cannotMakePurchase` - The user is not allowed to authorize payments.
- - `alreadyPurchasing` - A purchase is already in progress.
-
-## Analytics
-Tracking the purchase flow is a common things in apps. Especially as it's core to your revenue model.
-
-We can track 5 events, which step in the purchase pipeline a user reached.
-1. `purchase initiated`
-2. `purchase cancelled`
-3. `purchase failed`
-4. `purchase deferred`
-5. `purchase succeeded`
-
-Here's a quick example showing how to implement this correctly.
-
-``` swift
-func makePurchase() {
- Analytics.trackEvent("purchase initiated")
- InAppPurchase.purchase(
- productIdentifier: "my_product_id",
- callback: { result in
- switch result.state {
- case .purchased:
- // Reminder: We are not processing the purchase here, only updating your UI.
- // That's why we do not send an event to analytics.
- case .failed:
- Analytics.trackEvent("purchase failed")
- case .deferred:
- Analytics.trackEvent("purchase deferred")
- case .cancelled:
- Analytics.trackEvent("purchase cancelled")
- }
- })
-}
-
-// IAPPurchaseDelegate implementation
-func productPurchased(productIdentifier: String) {
- Analytics.trackEvent("purchase succeeded")
- InAppPurchase.finishTransactions(for: productIdentifier)
-}
-```
-
-The important part to remember is that a purchase can occur outside your app (or be approved when the app is not running), that's why tracking `purchase succeeded` has to be part of the `productPurchased` delegate function.
-
-Refer to the [Consumables](#consumables) section to learn more about the `productPurchased` function.
-
-## Server integration
-In more advanced use cases, you have a server component. Users are logged in and you'll like to unlock the content for this user on your server. The safest approach is to setup a [Webhook on Fovea](https://billing.fovea.cc/documentation/webhook/?ref=iap-swift-lib). You'll receive notifications from Fovea that transaction have been processed and/or subscriptions updated.
-
-The information sent from Fovea has been verified from Apple's server, which makes it way more trustable than information sent from your app itself.
-
-To take advantage of this, you have to inform the library of your application username. This `applicationUsername` can be provided as a parameter of the `InAppPurchase.initialize` method and updated later by changing the associated property.
-
-*Example:*
-``` swift
-InAppPurchase.initialize(
- iapProducts: [...],
- validatorUrlString: "..."),
- applicationUsername: UserSession.getUserId())
-
-// later ...
-InAppPurchase.applicationUsername = UserSession.getUserId()
-```
-
-If a user account is mandatory in your app, you will want to delay calls to `InAppPurchase.initialize()` to when your user's session is ready.
-
-Do not hesitate to [contact Fovea](mailto:support@fovea.cc) for help.
+See also:
+- [In-App Purchase official documentation](https://developer.apple.com/in-app-purchase)
+- [StoreKit Documentation](https://developer.apple.com/documentation/storekit/in-app_purchase)
# Xcode Demo Project
Do not hesitate to check the demo project available on here: [iap-swift-lib-demo](https://github.com/iridescent-dev/iap-swift-lib-demo).
-# References
-- [API documentation](https://billing.fovea.cc/iap-swift-lib/api)
-- [StoreKit Documentation](https://developer.apple.com/documentation/storekit/in-app_purchase)
-
# Coding
-Generate the documentation, using [this fork](https://github.com/johankool/swift-doc/tree/access-level-option) of swift-doc (on `--minimum-access-level` is part of the main distrib).
-
+Generate the documentation, using [Jazzy](https://github.com/realm/jazzy) by running the following command:
```
-swift-doc generate sources --module-name InAppPurchase --format html --output docs --minimum-access-level public --base-url /iap-swift-lib/
+jazzy
```
# Troubleshooting
Common issues are covered here: https://github.com/iridescent-dev/iap-swift-lib/wiki/Troubleshooting
+
# License
InAppPurchaseLib is open-sourced library licensed under the MIT License. See [LICENSE](LICENSE) for details.
diff --git a/Sources/InAppPurchaseLib/Common/IAPCallback.swift b/Sources/InAppPurchaseLib/Common/IAPCallback.swift
index ac65678..0e3309b 100644
--- a/Sources/InAppPurchaseLib/Common/IAPCallback.swift
+++ b/Sources/InAppPurchaseLib/Common/IAPCallback.swift
@@ -10,6 +10,8 @@ import StoreKit
public typealias IAPPurchaseCallback = (IAPPurchaseResult) -> Void
+
+/// The result returned in the `purchase()` callback.
public struct IAPPurchaseResult {
public internal(set) var state: IAPPurchaseResultState
public internal(set) var iapError: IAPError? = nil
@@ -22,15 +24,22 @@ public struct IAPPurchaseResult {
}
}
+/// The list of the different states of the `IAPPurchaseResult`.
public enum IAPPurchaseResultState {
+ /// The purchase was successful.
case purchased
+ /// Puchase failed.
case failed
+ /// The purchase was cancelled by the user.
case cancelled
+ /// The purchase is deferred.
case deferred
}
public typealias IAPRefreshCallback = (IAPRefreshResult) -> Void
+
+/// The result returned in the `refresh()` or `restorePurchases()` callback.
public struct IAPRefreshResult {
public internal(set) var state: IAPRefreshResultState
public internal(set) var iapError: IAPError? = nil
@@ -38,8 +47,12 @@ public struct IAPRefreshResult {
public internal(set) var updatedPurchases: Int = 0
}
+/// The list of the different states of the `IAPRefreshResult`.
public enum IAPRefreshResultState {
+ /// Refresh was successful.
case succeeded
+ /// Refresh failed.
case failed
+ /// Refresh has been skipped because it is not necessary.
case skipped
}
diff --git a/Sources/InAppPurchaseLib/Common/IAPError.swift b/Sources/InAppPurchaseLib/Common/IAPError.swift
index 748f9ec..b659701 100644
--- a/Sources/InAppPurchaseLib/Common/IAPError.swift
+++ b/Sources/InAppPurchaseLib/Common/IAPError.swift
@@ -12,23 +12,49 @@ public protocol IAPErrorProtocol: LocalizedError {
var code: IAPErrorCode { get }
}
+/// The list of error codes that can be returned by the library.
public enum IAPErrorCode {
+ /* MARK: - Errors returned by `refresh()`, `purchase()` or `restorePurchases()` */
+ /// You must call the `initialize` fuction before using the library.
case libraryNotInitialized
- case productNotFound
- case cannotMakePurchase
- case alreadyPurchasing
+ /// The Bundle Identifier is invalid.
case bundleIdentifierInvalid
+
+ /// The Validator URL String is invalid.
case validatorUrlInvalid
+
+ /// Failed to refresh the App Store receipt.
case refreshReceiptFailed
+
+ /// Failed to validate the App Store receipt with Fovea.
case validateReceiptFailed
+
+ /// Failed to read the receipt validation.
case readReceiptFailed
+ /* MARK: - Errors returned by `refresh()` */
+ /// Failed to refresh products from the App Store.
case refreshProductsFailed
+
+ /* MARK: - Errors returned by `purchase()` */
+ /// The product was not found on the App Store and cannot be purchased.
+ case productNotFound
+
+ /// The user is not allowed to authorize payments.
+ case cannotMakePurchase
+
+ /// A purchase is already in progress.
+ case alreadyPurchasing
}
+/// When calling `refresh()`, `purchase()` or `restorePurchases()`, the callback can return an `IAPError` if the state is `failed`.
public struct IAPError: IAPErrorProtocol {
+ /// The error code.
+ /// - See also: `IAPErrorCode`.
public var code: IAPErrorCode
+
+ /// The error description.
public var localizedDescription: String {
switch code {
case .libraryNotInitialized:
diff --git a/Sources/InAppPurchaseLib/InAppPurchase.swift b/Sources/InAppPurchaseLib/InAppPurchase.swift
index 5eb946f..caa0efe 100644
--- a/Sources/InAppPurchaseLib/InAppPurchase.swift
+++ b/Sources/InAppPurchaseLib/InAppPurchase.swift
@@ -8,7 +8,7 @@
import Foundation
import StoreKit
-
+///
public class InAppPurchase: NSObject, InAppPurchaseLib {
// InAppPurchaseLib version number.
internal static let versionNumber = "1.0.2"
@@ -58,7 +58,7 @@ public class InAppPurchase: NSObject, InAppPurchaseLib {
/// Refresh Product list and user Receipt.
/// - Parameter callback: The function that will be called after processing.
- /// - See also:`IAPRefreshCallback` and `IAPRefreshResult`.
+ /// - See also: `IAPRefreshResult`
public static func refresh(callback: @escaping IAPRefreshCallback) {
if !initialized {
callback(IAPRefreshResult(state: .failed, iapError: IAPError(code: .libraryNotInitialized)))
@@ -124,7 +124,7 @@ public class InAppPurchase: NSObject, InAppPurchaseLib {
/* MARK: - Products information */
/// Gets all products retrieved from the App Store
/// - Returns: An array of products.
- /// - See also: `SKProduct`.
+ /// - See also: `SKProduct`
public static func getProducts() -> Array {
return IAPProductService.shared.getProducts()
}
@@ -132,7 +132,7 @@ public class InAppPurchase: NSObject, InAppPurchaseLib {
/// Gets the product by its identifier from the list of products retrieved from the App Store.
/// - Parameter identifier: The identifier of the product.
/// - Returns: The product if it was retrieved from the App Store.
- /// - See also: `SKProduct`.
+ /// - See also: `SKProduct`
public static func getProductBy(identifier: String) -> SKProduct? {
return IAPProductService.shared.getProductBy(identifier: identifier)
}
@@ -150,7 +150,7 @@ public class InAppPurchase: NSObject, InAppPurchaseLib {
/// - productIdentifier: The identifier of the product to purchase.
/// - quantity: The quantity to purchase (default value = 1).
/// - callback: The function that will be called after processing.
- /// - See also:`IAPPurchaseCallback` and `IAPPurchaseResult`.
+ /// - See also: `IAPPurchaseResult`
public static func purchase(productIdentifier: String, quantity: Int, callback: @escaping IAPPurchaseCallback) {
if !initialized {
callback(IAPPurchaseResult(state: .failed, iapError: IAPError(code: .libraryNotInitialized)))
@@ -166,7 +166,7 @@ public class InAppPurchase: NSObject, InAppPurchaseLib {
/// Restore purchased products.
/// - Parameter callback: The function that will be called after processing.
- /// - See also:`IAPRefreshCallback` and `IAPRefreshResult`.
+ /// - See also: `IAPRefreshResult`
public static func restorePurchases(callback: @escaping IAPRefreshCallback) {
if !initialized {
callback(IAPRefreshResult(state: .failed, iapError: IAPError(code: .libraryNotInitialized)))
diff --git a/Sources/InAppPurchaseLib/InAppPurchaseLib.swift b/Sources/InAppPurchaseLib/InAppPurchaseLib.swift
index 8e746ab..f1bdbf2 100644
--- a/Sources/InAppPurchaseLib/InAppPurchaseLib.swift
+++ b/Sources/InAppPurchaseLib/InAppPurchaseLib.swift
@@ -9,7 +9,7 @@ import Foundation
import StoreKit
-/// The protocol that `InAppPurchase`` adopts.
+/// The protocol that `InAppPurchase` adopts.
public protocol InAppPurchaseLib {
/// The array of `IAPProduct`.
static var iapProducts: Array { get }
@@ -33,20 +33,20 @@ public protocol InAppPurchaseLib {
/// Refresh Product list and user Receipt.
/// - Parameter callback: The function that will be called after processing.
- /// - See also:`IAPRefreshCallback` and `IAPRefreshResult`.
+ /// - See also:`IAPRefreshResult`
static func refresh(callback: @escaping IAPRefreshCallback) -> Void
/* MARK: - Products information */
/// Gets all products retrieved from the App Store
/// - Returns: An array of products.
- /// - See also: `SKProduct`.
+ /// - See also: `SKProduct`
static func getProducts() -> Array
/// Gets the product by its identifier from the list of products retrieved from the App Store.
/// - Parameter identifier: The identifier of the product.
/// - Returns: The product if it was retrieved from the App Store.
- /// - See also: `SKProduct`.
+ /// - See also: `SKProduct`
static func getProductBy(identifier: String) -> SKProduct?
@@ -60,12 +60,12 @@ public protocol InAppPurchaseLib {
/// - productIdentifier: The identifier of the product to purchase.
/// - quantity: The quantity to purchase (default value = 1).
/// - callback: The function that will be called after processing.
- /// - See also:`IAPPurchaseCallback` and `IAPPurchaseResult`.
+ /// - See also:`IAPPurchaseResult`
static func purchase(productIdentifier: String, quantity: Int, callback: @escaping IAPPurchaseCallback) -> Void
/// Restore purchased products.
/// - Parameter callback: The function that will be called after processing.
- /// - See also:`IAPRefreshCallback` and `IAPRefreshResult`.
+ /// - See also:`IAPRefreshResult`
static func restorePurchases(callback: @escaping IAPRefreshCallback) -> Void
/// Finish all transactions for the product.
@@ -116,8 +116,7 @@ public extension InAppPurchaseLib {
}
-/* MARK: - The protocol that you must adopt. */
-/// The protocol that you must adopt if you have `consumable` and/or `nonRenewingSubscription` products.
+/// The protocol that you must adopt if you have *consumable* and/or *non-renewing subscription* products.
public protocol IAPPurchaseDelegate {
/// Called when a product is newly purchased, updated or restored.
/// - Parameter productIdentifier: The identifier of the product.
@@ -127,7 +126,7 @@ public protocol IAPPurchaseDelegate {
}
-/// The default implementation of `IAPPurchaseDelegate` if no other is provided.
+/// The default implementation of `IAPPurchaseDelegate` if no other is provided. It is enough if you only have *non-consumable* and/or *auto-renewable subscription* products.
public class DefaultPurchaseDelegate: IAPPurchaseDelegate {
public init(){}
diff --git a/Sources/InAppPurchaseLib/Product/IAPProduct.swift b/Sources/InAppPurchaseLib/Product/IAPProduct.swift
index 74e919e..115a87e 100644
--- a/Sources/InAppPurchaseLib/Product/IAPProduct.swift
+++ b/Sources/InAppPurchaseLib/Product/IAPProduct.swift
@@ -7,7 +7,6 @@
import Foundation
-
public struct IAPProduct {
/// The identifier of the product.
diff --git a/docs/API documentation.html b/docs/API documentation.html
new file mode 100644
index 0000000..70cd86c
--- /dev/null
+++ b/docs/API documentation.html
@@ -0,0 +1,680 @@
+
+
+
+ API documentation Reference
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The most important class is InAppPurchase. All the functions you need are defined in this class.
+
+
If you have consumable and/or non-renewing subscription products in your application, you must have a class that adopts the IAPPurchaseDelegate protocol.
+
Products
+
+
+
Input: the library requires an array of IAPProduct when it is initialized.
refresh(), purchase() and restorePurchases() are asynchronous functions. You must provide a callback that will allow you to perform actions depending on the result.
+
+
+
For refresh() and restorePurchases() functions, the result will be IAPRefreshResult.
When calling refresh(), purchase() or restorePurchases(), the callback can return an IAPError if the state is failed. Look at IAPErrorCode to see the list of error codes you can receive.
The default implementation of IAPPurchaseDelegate if no other is provided. It is enough if you only have non-consumable and/or auto-renewable subscription products.