5. November 2017

In-App-Käufe

Über In-App-Käufe können in einer App digitale Güter oder Services gegen Entgelt angeboten werden. Die Abwicklung der Zahlung erfolgt dabei analog zu dem Kauf von Apps über den iTunes-Account des Benutzers.

Für In-App-Käufe gelten eine Reihe von Beschränkungen, so dürfen keine „realen“ Güter oder Services angeboten werden und dem Käufer muss klar sein, was genau erworben wird. Genauere Informationen finden Sie in den App Store Review Guidelines.

Dabei werden verschiedene Arten von In App Purchases unterschieden. Einmal-Käufe können entweder verbraucht werden, beispielsweise ein zusätzliches Item in einem Spiel welches man nur einmal bekommt (Consumable), oder unendlich gültig bleiben, beispielsweise für zusätzliche Features in der App, die man auch nach einer Neuinstallation der App erneut freischalten kann (Non-Consumable). Zudem können Abonnements angeboten werden, die sich automatisch verlängern (Auto-Renewable Subscription) oder für eine bestimmte Zeit gültig sind (Non-Renewing subscription).

Im Store werden Apps mit In App Purchases hervorgehoben und eine Liste der zehn am häufigsten gekauften In-App-Käufe angezeigt:

In-App-Käufe einrichten

Jedes Angebot muss in App Store Connect eingerichtet und von Apple gereviewt werden:

Einrichtung von In-App-Purchases in App Store Connect

Für das Testen von In-App-Käufen stellt Apple eine Sandbox bereit, so dass Artikel gekauft werden können, ohne Zahlungen auszulösen. Dafür können unter Benutzer und Rollen » Sandbox-Tester Testnutzer eingerichtet werden:

Sandbox-Tester

StoreKit-Framework

Mit dem StoreKit-Framework können In-App-Käufe mit einem SKProductsRequest abgefragt werden und der Kauf über ein SKPayment bei der SKPaymentQueue eingestellt werden. Bestätigt der Anwender den Kauf, wird ein bei der SKPaymentQueue registrierter SKPaymentTransactionObserver darüber benachrichtigt und kann dann die entsprechenden Features freischalten (Beispielcode siehe Tutorial).

Tutorial 1: In-App-Käufe einrichten und in der App anzeigen

  1. Laden Sie den Start-Stand von dem Flashcards-Beispielprojekt: Flashcards.zip.

  2. Geben Sie im Projekt-Target einen eigenen Bundle-Identifier an: Dieser wird im nächsten Schritt als App ID in Ihrem Apple Developer Account registriert und muss dazu weltweit eindeutig sein:

    Bundle Identifier

    Hinweis: Aktuell können zu Testzwecken in App Store Connect eingestellte Apps nicht gelöscht werden. Tipp: Verwenden Sie eine allgemeingültige App-ID wie de.meinefirma.DemoApp, die Sie auch über die Tutorials hinaus zu Testzwecken verwenden können.

  3. Aktivieren Sie In-App-Purchases im Projekt-Target unter Capabilities:

    Capabilities: In-App-Purchases
  4. Loggen Sie sich im Apple Developer Account ein und prüfen Sie unter Certificates, Identifiers & Profiles » Identifiers » App-IDs, dass In-App Purchases für die App-ID aktiviert wurden:

    Apple Developer Account: App ID
  5. Loggen Sie sich in App Store Connect ein und erstellen Sie unter My Apps » + » New App eine neue iOS-App:

    App in App Store Connect hinzufügen

  6. Fügen Sie unter Features » In-App-Käufe einen neuen In-App-Kauf hinzu:

    Einrichtung von In-App-Purchases in App Store Connect
  7. Wählen Sie Nicht-Verbrauchsartikel als Typ: Einmal gekauft, bleibt der In-App-Kauf für immer gültig (wenn diese Option nicht angeboten wird: Prüfen Sie in App Store Connect unter Vereinbarungen, Steuern und Bankverbindungen, dass der Account für iOS Paid Applications freigeschaltet ist)

  8. Konfigurieren Sie den In-App-Kauf:

    • Reference Name: „Flashcards: Capitals“
    • Product ID: „FLASHCARDS_CAPITALS“
    • Cleared for Sale: Yes
    • Price Tier: Tier 1
    • Language German, Display Name „Flashcards: Hauptstädte“, Description „Lernkarten: Hauptstädte aller Länder“
    • Language English, Display Name „Flashcards: Capitals“, Description „Flashcards: Capitals of all countries“
  9. Entfernen Sie den bestehenden Übergang zwischen dem bestehenden Download-Controller und dem +-Button in der Hauptsicht und fügen Sie einen statischen TableViewController dazwischen ein, so dass der Anwender zwischen kostenfreien Kartenstapeln und den In-App-Käufen auswählen kann (siehe Tabellen darstellen mit UITableViewController). Duplizieren Sie den bestehenden Downloads-TableViewController, so dass es eine eigene Sicht für kostenfreie Downloads und Store gibt:

    Storyboard für In-App-Käufe
  10. Implementieren Sie eine Klasse FlashcardStoreTableViewController, die per StoreKit-Framework die Produkte lädt und in der Tabelle zur Anzeige bringt. Binden Sie diese Klasse im Storyboard ein. Beispielcode:

    import UIKit
    import StoreKit
    
    class FlashcardStoreTableViewController: UITableViewController, SKProductsRequestDelegate {
    
        private var request : SKProductsRequest!
        private var products : [SKProduct] = []
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.request = SKProductsRequest(productIdentifiers: Set(["FLASHCARDS_CAPITALS"]))
    self.request.delegate = self
    self.request.start()
        }
    
        // MARK: - UITableViewDataSource
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            return products.count
        }
    
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell = tableView.dequeueReusableCell(withIdentifier: "DownloadCell", for: indexPath)
            let product = products[indexPath.row]
    
    let numberFormatter = NumberFormatter()
    numberFormatter.numberStyle = .currency
    numberFormatter.locale = product.priceLocale
    
    cell.textLabel?.text = product.localizedTitle
    cell.detailTextLabel?.text = numberFormatter.string(from: product.price)
            return cell
        }
    
        // MARK: - SKProductsRequestDelegate
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        self.products = response.products
        self.tableView.reloadData()
        self.request = nil
    }
    
    func request(_ request: SKRequest, didFailWithError error: Error) {
        NSLog("%@", error as NSError)
        self.request = nil
    }
    }
  11. Testen Sie Anzeige der In-App-Käufe:

  12. Lösen Sie den Kauf über die SKPaymentQueue aus, wenn eine Tabellenzelle betätigt wird:

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let product = products[indexPath.row]
        let payment = SKPayment(product: product)
        SKPaymentQueue.default().add(payment)
    }
  13. Fügen Sie dem FlashcardStore-Screen im Storyboard ein Bar Button Item mit dem Titel "Restore Purchases" hinzu:

  14. Verknüpfen Sie dieses mit einer IBAction-Methode im Controller und lösen Sie hier die Wiederherstellung bereits getätigter Käufe aus:

    @IBAction func restorePurchases() {
        SKPaymentQueue.default().restoreCompletedTransactions()
    }
  15. Erstellen Sie eine neue Klasse FlashcardsPurchaseHandler die das SKPaymentTransactionObserver-Protokoll implementiert und den Kauf behandelt:

    import Foundation
    import StoreKit
    
    class FlashcardsPurchaseHandler : NSObject, SKPaymentTransactionObserver {
    
        func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
            for tx in transactions {
                switch (tx.transactionState) {
    
                case .purchased:
                    unlock(productId: tx.payment.productIdentifier)
                    queue.finishTransaction(tx)
    
                case .restored:
                    unlock(productId: tx.original!.payment.productIdentifier)
                    queue.finishTransaction(tx)
    
                case .failed:
                    NSLog("%@", "Payment Queue Error: \(String(describing: tx.error))")
                    queue.finishTransaction(tx)
    
                case .purchasing: break   // do nothing
                case .deferred:   break   // do nothing
                }
            }
        }
    
        func unlock(productId : String) {
            NSLog("Unlock %@", productId)
        }
    
        func register() {
            SKPaymentQueue.default().add(self)
        }
    }
  16. Implementieren Sie unlock-Methode so, dass ein entsprechendes Deck angelegt wird:

    func unlock(productId : String) {
        NSLog("Unlock %@", productId)
    
        switch (productId) {
        case "FLASHCARDS_CAPITALS":
            let model = FlashcardsModel.shared
            let deck = model.createDeck(name: "Capitals")
            deck.createCard(frontText: "Deutschland", backText: "Berlin")
            deck.createCard(frontText: "Frankreich", backText: "Paris")
            deck.createCard(frontText: "Italien", backText: "Rom")
            model.save()
        default:
            NSLog("Unknown product ID: %@", productId)
        }
    }
  17. Instantiieren und registrieren Sie einen FlashcardsPurchaseHandler im AppDelegate:

    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        // ...
    
        let purchaseHandler = FlashcardsPurchaseHandler()
    
        func application(_ application: UIApplication,
            didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
            // ...
            purchaseHandler.register()
            // ...
        }
    
    }
  18. Legen Sie in App Store Connect unter Benutzer & Rollen » Sandbox-Tester einen Test-Nutzer an:

    Sandbox-Tester
  19. Hinweis: Im Simulator schlagen In-App-Käufe nach der Bestätigung mit der Fehlermeldung „Cannot connect to iTunes Store“ fehl. Um den Kauf selbst zu testen, wird ein Testgerät benötigt.

  20. Es empfiehlt sich beim Testen mit dedizierten Testgeräten zu arbeiten. Melden Sie sich auf dem Testgerät unter Einstellungen » iTunes & App Store » Apple-ID » Abmelden ab, um den Kauf im Folgenden mit den Testnutzer durchzuführen.

  21. Testen Sie den Kauf in der App mit dem Test-Nutzer:

  22. Committen Sie Ihre Änderungen: „24.1. In-App-Käufe“.