-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
5dbb33f
commit 32b4f86
Showing
168 changed files
with
18,941 additions
and
11,676 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# `jazzy --help config` | ||
|
||
output: docs | ||
clean: true | ||
author: Iridescent | ||
author_url: https://iridescent.dev | ||
module: InAppPurchaseLib | ||
title: InAppPurchaseLib documentation | ||
copyright: "Copyright © 2020 [Iridescent](https://iridescent.dev)." | ||
documentation: Documentation/*/*.md | ||
abstract: Documentation/*.md | ||
github_url: https://github.com/iridescent-dev/iap-swift-lib | ||
hide_documentation_coverage: false | ||
custom_categories: | ||
- name: "Getting Started" | ||
children: | ||
- "Installation" | ||
- "Micro Example" | ||
- "License" | ||
- name: "Usage" | ||
children: | ||
- "Initialization" | ||
- "Displaying products" | ||
- "Displaying subscriptions" | ||
- "Refreshing" | ||
- "Purchasing" | ||
- "Handling purchases" | ||
- "Restoring purchases" | ||
- "Displaying products with purchases" | ||
- "Errors" | ||
- "Analytics" | ||
- "Server integration" | ||
- name: "API documentation" | ||
children: | ||
- InAppPurchase | ||
- InAppPurchaseLib | ||
- DefaultPurchaseDelegate | ||
- IAPPurchaseDelegate | ||
- IAPProduct | ||
- IAPProductType | ||
- SKProduct | ||
- IAPPeriodFormat | ||
- IAPPurchaseCallback | ||
- IAPRefreshCallback | ||
- IAPPurchaseResult | ||
- IAPRefreshResult | ||
- IAPPurchaseResultState | ||
- IAPRefreshResultState | ||
- IAPError | ||
- IAPErrorCode | ||
- IAPErrorProtocol | ||
theme: fullwidth |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Classes and Protocols | ||
|
||
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. | ||
|
||
* Output: the library will returns [SKProduct](https://developer.apple.com/documentation/storekit/skproduct) extended with helpful methods. See the [`SKProduct` extension](Extensions/SKProduct.html). | ||
|
||
# Callbacks | ||
`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`. | ||
|
||
* For `purchase()` function, the result will be `IAPPurchaseResult`. | ||
|
||
# Errors | ||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
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) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Installation | ||
<p align="center"> | ||
<img src="https://github.com/iridescent-dev/iap-swift-lib/blob/master/Documentation/Images/ScreenshotInstallation.png" title="Installation"> | ||
</p> | ||
|
||
* 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
}) | ||
} | ||
} | ||
``` |
File renamed without changes
File renamed without changes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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`. |
Oops, something went wrong.