Marketplace
This module provide support for interfacing with an app’s marketplace, such as the Google Play Store for Android and the Apple App Store for iOS.
Currently, the framework provides the ability to request a store rating for the app from the user. In the future, this framework will provide the ability to perform in-app purchases and subscription management.
To include this framework in your project, add the following
dependency to your Package.swift file:
let package = Package( name: "my-package", products: [ .library(name: "MyProduct", targets: ["MyTarget"]), ], dependencies: [ .package(url: "https://source.skip.tools/skip-marketplace.git", "0.0.0"..<"2.0.0"), ], targets: [ .target(name: "MyTarget", dependencies: [ .product(name: "SkipMarketplace", package: "skip-marketplace") ]) ])App Review Requests
Section titled “App Review Requests”You can use this library to request that the app marketplace show a prompt to the user requesting a rating for the app for the given marketplace.
import SkipMarketplace
// request that the system show an app review request at most once every monthMarketplace.current.requestReview(period: .days(31))For guidance on how and when to make these sorts of requests, see the relevant documentation for the Apple App Store ↗ and Google PlayStore ↗.
Querying App Installation Source
Section titled “Querying App Installation Source”Determining which source was used to install the app (Apple App store, Google Play Store, AltStore, F-Droid, etc.) can be useful for determining what billing mechanism to use. This can be done by querying the Marketplace.current.installationSource property like:
switch await Marketplace.current.installationSource {case .appleAppStore: canUseNativeBillling = truecase .googlePlayStore: canUseNativeBillling = truecase .other(let id): canUseNativeBillling = false // handle other markerplaces heredefault: canUseNativeBillling = false}Listing and purchasing in-app purchases
Section titled “Listing and purchasing in-app purchases”Android Configuration
Section titled “Android Configuration”You must set the com.android.vending.BILLING permission in your AndroidManifest.xml file like so:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="com.android.vending.BILLING"/></manifest>Start by defining your products in App Store Connect and/or the Google Play Store
Section titled “Start by defining your products in App Store Connect and/or the Google Play Store”- One-time products
- App Store Connect: Create consumable or non-consumable In-App Purchases ↗
- Google Play Console: Overview of one-time products ↗
- Subscriptions
- App Store Connect: Offer auto-renewable subscriptions ↗
- Google Play Console: Create and manage subscriptions ↗
One-Time Purchases: Fetch ProductInfo and Prices
Section titled “One-Time Purchases: Fetch ProductInfo and Prices”do { let productIdentifiers = ["product1", "product2", "product3"] let products: [ProductInfo] = try await Marketplace.current.fetchProducts( for: productIdentifiers, subscription: false )
for product in products { print("product \(product.id) \(product.displayName)") let oneTimePurchaseOfferInfo: [OneTimePurchaseOfferInfo] = product.oneTimePurchaseOfferInfo! for offer in oneTimePurchaseOfferInfo { // On iOS, there will be only one offer, and its ID will be nil // On GPS, there may be multiple offers, if you configured additional offers in the console print("product \(product.id) offer \(offer.id ?? "nil") \(offer.displayPrice) \(offer.price)") } }}Subscriptions: Fetch ProductInfo and Prices
Section titled “Subscriptions: Fetch ProductInfo and Prices”do { let productIdentifiers = ["product1", "product2", "product3"] let products: [ProductInfo] = try await Marketplace.current.fetchProducts(for: productIdentifiers, subscription: true)
for product in products { print("product \(product.id) \(product.displayName)") let subscriptionOffers: [SubscriptionOfferInfo] = product.subscriptionOffers! for offer in subscriptionOffers { #if !SKIP print("product \(product.id) offer \(offer.id ?? "nil") type \(offer.type)") #endif let pricingPhases: [SubscriptionPricingPhase] = offer.pricingPhases for pricingPhase in pricingPhases { print("product \(product.id) offer \(offer.id ?? "nil") \(pricingPhase.displayPrice) \(pricingPhase.price)") } } }} catch { print("Error fetching products: \(error)")}Purchasing (displaying a purchase sheet)
Section titled “Purchasing (displaying a purchase sheet)”do { let product: ProductInfo = try await Marketplace.current.fetchProducts(for: ["productIdentifier"], subscription: false).first! if let purchaseTransaction: PurchaseTransaction = try await Marketplace.current.purchase(item: product) { print("Purchased \(purchaseTransaction.products)") // after you've stored the transaction somewhere, you should finish every PurchaseTransaction to acknowledge receipt try await Marketplace.current.finish(purchaseTransaction: purchaseTransaction) }} catch { print("Error purchasing product: \(error)")}You can also pass in a purchase offer (with a discounted price).
do { let product: ProductInfo = try await Marketplace.current.fetchProducts(for: ["productIdentifier"], subscription: false).first! let offer = product.oneTimePurchaseOfferInfo.first! if let purchaseTransaction: PurchaseTransaction = try await Marketplace.current.purchase(item: product, offer: offer) { print("Purchased \(purchaseTransaction.products)") // after you've stored the transaction somewhere, you should finish every PurchaseTransaction to acknowledge receipt try await Marketplace.current.finish(purchaseTransaction: purchaseTransaction) }} catch { print("Error purchasing product: \(error)")}Querying for entitlements
Section titled “Querying for entitlements”“Entitlements” ane non-consumable one-time products and subscriptions, something that the user is entitled to because they’ve currently purchased it.
do { let entitlements: [PurchaseTransaction] = try await Marketplace.current.fetchEntitlements() for purchaseTransaction in entitlements { let products: [String] = purchaseTransaction.products print("You own \(products)") // after you've stored the transaction somewhere, you should finish every PurchaseTransaction to acknowledge receipt // it's OK to "finish" the same transaction more than once try await Marketplace.current.finish(purchaseTransaction: purchaseTransaction) }} catch { print("Error fetching entitlements: \(error)")}Handling updates to purchase transactions
Section titled “Handling updates to purchase transactions”do { for try await purchaseTransaction in Marketplace.current.getPurchaseTransactionUpdates() { print("Transaction update: \(purchaseTransaction)") // after you've stored the transaction somewhere, you should finish every PurchaseTransaction to acknowledge receipt // it's OK to "finish" the same transaction more than once try await Marketplace.current.finish(purchaseTransaction: purchaseTransaction) }} catch { print("Error loading transaction updates: \(error)")}Testing purchases during development
Section titled “Testing purchases during development”- iOS: Setting up StoreKit testing in Xcode ↗
- Google Play: Test your Google Play Billing Library integration ↗
Building
Section titled “Building”This project is a free Swift Package Manager module that uses the Skip plugin to transpile Swift into Kotlin.
Building the module requires that Skip be installed using
Homebrew ↗ with brew install skiptools/skip/skip.
This will also install the necessary build prerequisites:
Kotlin, Gradle, and the Android build tools.
Testing
Section titled “Testing”The module can be tested using the standard swift test command
or by running the test target for the macOS destination in Xcode,
which will run the Swift tests as well as the transpiled
Kotlin JUnit tests in the Robolectric Android simulation environment.
Parity testing can be performed with skip test,
which will output a table of the test results for both platforms.