Skip to content

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")
])
]
)

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 month
Marketplace.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.

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 = true
case .googlePlayStore: canUseNativeBillling = true
case .other(let id): canUseNativeBillling = false // handle other markerplaces here
default: canUseNativeBillling = false
}

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 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)")
}
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)")
}

“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)")
}
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)")
}

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.

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.