Handbuch zum Implementieren von automatisch erneuerbaren Abonnements in iOS-Anwendungen

Bild


Hallo allerseits! Mein Name ist Denis, ich entwickle Apphud , einen Dienst zur Analyse von automatisch erneuerbaren Abonnements in iOS-Anwendungen.


In diesem Artikel werde ich Ihnen erklären, wie Sie automatisch erneuerbare Abonnements in iOS 12 und iOS 13 konfigurieren, implementieren und validieren. Als Bonus werde ich Sie über subtile Punkte und Fallstricke informieren, die nicht alle Entwickler berücksichtigen.


Richten Sie Abonnements im App Store Connect ein


Wenn Sie bereits eine Bundle-ID und eine Anwendung erstellt haben, können Sie diese Schritte überspringen. Wenn Sie zum ersten Mal eine Anwendung erstellen, gehen Sie wie folgt vor:


Sie müssen im Apple Developer Portal eine explizite Bundle-ID (App-ID) erstellen. Wechseln Sie bei geöffneter Seite Zertifikate, Kennungen und Profile zur Registerkarte Kennungen. Im Juni 2019 aktualisierte Apple schließlich das Layout des Portals gemäß ASC (kurz für App Store Connect).


Neues Design für das Apple Entwicklerportal im Jahr 2019
Neues Design für das Apple Developer Portal im Jahr 2019



Die explizite Bundle-ID wird normalerweise im Domänenstil ( com.apphud.subscriptionstest ) angegeben. Im Abschnitt Funktionen werden Sie feststellen, dass das Häkchen neben In-App-Käufe bereits aktiviert ist. Wechseln Sie nach dem Erstellen der Bundle-ID ( App-ID ) zum App Store Connect.


Benutzer testen (Sandbox-Benutzer)


Um zukünftige Einkäufe zu testen, müssen Sie einen Testbenutzer erstellen. Wechseln Sie dazu auf der Registerkarte Benutzer und Zugriff zu ASC und dann zu Sandbox-Testern.


Benutzer-Sandbox-Erstellungsformular
Benutzer-Sandbox-Erstellungsformular


Wenn Sie einen Tester erstellen, können Sie nicht vorhandene Daten angeben. Vergessen Sie vor allem nicht die E-Mail-Adresse und das Passwort!

Ich werde am Ende des Artikels darüber sprechen, wie Sie Einkäufe mit Testanmeldeinformationen testen können.


Ein weiterer wichtiger Schritt ist die Einrichtung von Verträgen und Bankdaten im Abschnitt „ Vereinbarungen, Steuern und Bankgeschäfte “. Wenn Sie keine Vereinbarung für kostenpflichtige Anträge haben, können Sie automatisch erneuerbare Abonnements nicht testen!


Danach können Sie im App Store Connect eine neue Anwendung erstellen. Geben Sie einen eindeutigen Namen an und wählen Sie Ihre Bundle-ID als Paket- ID aus .


Die Paket-ID ist Ihre Bundle-ID
Die Paket-ID ist Ihre Bundle-ID


Wechseln Sie unmittelbar nach dem Erstellen der Anwendung zur Registerkarte Funktionen.


Wenn Sie die Anwendung bereits erstellt haben, können Sie von hier aus weiterlesen.

Das Erstellen eines Abonnements mit automatischer Verlängerung besteht aus mehreren Schritten:


1. Erstellen Sie eine Abonnement-ID und eine Abonnementgruppe . Eine Abonnementgruppe ist eine Sammlung von Abonnements mit unterschiedlichen Zeiträumen und Preisen, die jedoch dieselbe Funktionalität in der Anwendung öffnen. Außerdem können Sie in der Abonnementgruppe den kostenlosen Testzeitraum nur einmal aktivieren, und nur eines der Abonnements kann aktiv sein. Wenn Ihre Anwendung zwei verschiedene Abonnements gleichzeitig haben soll, müssen Sie zwei Gruppen von Abonnements erstellen.


2. Geben Sie die Abonnementdaten ein: Dauer, Anzeigename im App Store (nicht zu verwechseln mit dem Namen) und Beschreibung. Wenn Sie der Gruppe das erste Abonnement hinzufügen, müssen Sie den Anzeigenamen der Abonnementgruppe angeben. Denken Sie daran, Ihre Änderungen häufiger zu speichern. ASC kann jederzeit einfrieren und nicht mehr reagieren.


Abonnementseite
Abonnement-Bildschirm


3. Den Abonnementpreis ausfüllen. Es gibt zwei Phasen: Preiserstellung und Sonderangebote. Geben Sie den realen Preis in einer beliebigen Währung an. Er wird automatisch für alle anderen Länder neu berechnet. Einführungsangebote: Hier können Sie Benutzern eine kostenlose Testphase oder Prepaid-Rabatte anbieten. Im Jahr 2019 wurden im App Store Werbeaktionen veröffentlicht, mit denen Sie Benutzern, die sich abgemeldet haben und die Sie zurückgeben möchten, Sonderrabatte gewähren können.


Gemeinsame Generierung geheimer Schlüssel


Auf der Seite mit einer Liste aller von Ihnen erstellten Abonnements sehen Sie den freigegebenen Schlüssel für die Anwendungsschaltfläche . Dies ist eine spezielle Zeile, die zum Überprüfen einer Prüfung in einer iOS-Anwendung benötigt wird. Wir müssen die Prüfung validieren, um den Status des Abonnements zu ermitteln.


Es gibt zwei Arten von gemeinsam genutzten Schlüsseln: einen eindeutigen Schlüssel für Ihre Anwendung oder einen einzelnen Schlüssel für Ihr Konto. Wichtig: Erstellen Sie den Schlüssel in keinem Fall neu, wenn Sie die Anwendung bereits im App Store haben. Andernfalls können Benutzer die Prüfung nicht validieren und Ihre Anwendung funktioniert nicht mehr wie erwartet.


In diesem Beispiel werden drei Abonnementgruppen und drei Jahresabonnements behandelt.
In diesem Beispiel werden drei Abonnementgruppen und drei Jahresabonnements erstellt.


Kopieren Sie die ID aller Ihrer Abonnements und den freigegebenen Schlüssel. Dies ist später im Code hilfreich.


Software-Teil


Kommen wir zum praktischen Teil. Was braucht es, um einen kompletten Einkaufsmanager zu machen? Zumindest sollte Folgendes implementiert werden:


  1. Kasse


  2. Überprüfen Sie den Abonnementstatus


  3. Überprüfen Sie das Update


  4. Transaktionswiederherstellung (nicht zu verwechseln mit der Aktualisierung eines Schecks!)



Kasse


Der gesamte Kaufprozess kann in zwei Phasen unterteilt werden: Empfang von Produkten ( SKProduct Klasse) und Initialisierung des Kaufprozesses ( SKPayment Klasse). Zunächst müssen wir den Delegaten des SKPaymentTransactionObserver Protokolls angeben.


 // Starts products loading and sets transaction observer delegate @objc func startWith(arrayOfIds : Set<String>!, sharedSecret : String){ SKPaymentQueue.default().add(self) self.sharedSecret = sharedSecret self.productIds = arrayOfIds loadProducts() } private func loadProducts(){ let request = SKProductsRequest.init(productIdentifiers: productIds) request.delegate = self request.start() } public func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) { products = response.products DispatchQueue.main.async { NotificationCenter.default.post(name: IAP_PRODUCTS_DID_LOAD_NOTIFICATION, object: nil) } } func request(_ request: SKRequest, didFailWithError error: Error){ print("error: \(error.localizedDescription)") } 

Die Benachrichtigung IAP_PRODUCTS_DID_LOAD_NOTIFICATION zum Aktualisieren der Benutzeroberfläche in einer Anwendung verwendet.


Als nächstes schreiben wir eine Methode, um den Kauf zu initialisieren:


 func purchaseProduct(product : SKProduct, success: @escaping SuccessBlock, failure: @escaping FailureBlock){ guard SKPaymentQueue.canMakePayments() else { return } guard SKPaymentQueue.default().transactions.last?.transactionState != .purchasing else { return } self.successBlock = success self.failureBlock = failure let payment = SKPayment(product: product) SKPaymentQueue.default().add(payment) } 

Der SKPaymentTransactionObserver Delegat sieht folgendermaßen aus:


 extension IAPManager: SKPaymentTransactionObserver { public func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { for transaction in transactions { switch (transaction.transactionState) { case .purchased: SKPaymentQueue.default().finishTransaction(transaction) notifyIsPurchased(transaction: transaction) break case .failed: SKPaymentQueue.default().finishTransaction(transaction) print("purchase error : \(transaction.error?.localizedDescription ?? "")") self.failureBlock?(transaction.error) cleanUp() break case .restored: SKPaymentQueue.default().finishTransaction(transaction) notifyIsPurchased(transaction: transaction) break case .deferred, .purchasing: break default: break } } } private func notifyIsPurchased(transaction: SKPaymentTransaction) { refreshSubscriptionsStatus(callback: { self.successBlock?() self.cleanUp() }) { (error) in // couldn't verify receipt self.failureBlock?(error) self.cleanUp() } } func cleanUp(){ self.successBlock = nil self.failureBlock = nil } } 

Nach erfolgreichem Abonnement wird die Delegatmethode aufgerufen, bei der die Transaktion den Status " purchased .


Aber wie kann man das Ablaufdatum eines Abonnements bestimmen? Stellen Sie dazu eine separate Anfrage an Apple.


Überprüfen Sie den Abonnementstatus


Die Prüfung wird mit der POST-Anforderung verifyReceipt an Apple validiert. Wir senden die verschlüsselte Prüfung als base64-codierte Zeichenfolge als Parameter und erhalten in der Antwort dieselbe Prüfung im JSON-Format. In dem Array latest_receipt_info der Schlüssel latest_receipt_info alle Transaktionen aus jedem Zeitraum jedes Abonnements auf, einschließlich der Testzeiträume. Wir können nur die Antwort analysieren und das aktuelle Verfallsdatum für jedes Produkt ermitteln.


Auf der WWDC 2017 wurde die Möglichkeit hinzugefügt, nur aktuelle Schecks für jedes Abonnement mit dem Schlüssel zum exclude-old-transactions in der verifyReceipt Anforderung zu erhalten.

 func refreshSubscriptionsStatus(callback : @escaping SuccessBlock, failure : @escaping FailureBlock){ // save blocks for further use self.refreshSubscriptionSuccessBlock = callback self.refreshSubscriptionFailureBlock = failure guard let receiptUrl = Bundle.main.appStoreReceiptURL else { refreshReceipt() // do not call block yet return } #if DEBUG let urlString = "https://sandbox.itunes.apple.com/verifyReceipt" #else let urlString = "https://buy.itunes.apple.com/verifyReceipt" #endif let receiptData = try? Data(contentsOf: receiptUrl).base64EncodedString() let requestData = ["receipt-data" : receiptData ?? "", "password" : self.sharedSecret, "exclude-old-transactions" : true] as [String : Any] var request = URLRequest(url: URL(string: urlString)!) request.httpMethod = "POST" request.setValue("Application/json", forHTTPHeaderField: "Content-Type") let httpBody = try? JSONSerialization.data(withJSONObject: requestData, options: []) request.httpBody = httpBody URLSession.shared.dataTask(with: request) { (data, response, error) in DispatchQueue.main.async { if data != nil { if let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments){ self.parseReceipt(json as! Dictionary<String, Any>) return } } else { print("error validating receipt: \(error?.localizedDescription ?? "")") } self.refreshSubscriptionFailureBlock?(error) self.cleanUpRefeshReceiptBlocks() } }.resume() } 

Zu Beginn der Methode sehen Sie, dass eine Prüfung auf das Vorhandensein einer lokalen Kopie der Prüfung vorliegt. Eine lokale Prüfung ist möglicherweise nicht vorhanden, wenn die Anwendung beispielsweise über iTunes installiert wurde. Wenn keine Prüfung erfolgt, können wir die Anforderung verifyReceipt nicht ausführen. Wir müssen zuerst die aktuelle lokale Prüfung abrufen und dann erneut versuchen, sie zu validieren. Die Aktualisierung der Prüfung erfolgt mit der Klasse SKReceiptRefreshRequest :


 private func refreshReceipt(){ let request = SKReceiptRefreshRequest(receiptProperties: nil) request.delegate = self request.start() } func requestDidFinish(_ request: SKRequest) { // call refresh subscriptions method again with same blocks if request is SKReceiptRefreshRequest { refreshSubscriptionsStatus(callback: self.successBlock ?? {}, failure: self.failureBlock ?? {_ in}) } } func request(_ request: SKRequest, didFailWithError error: Error){ if request is SKReceiptRefreshRequest { self.refreshSubscriptionFailureBlock?(error) self.cleanUpRefeshReceiptBlocks() } print("error: \(error.localizedDescription)") } 

Check Update ist in der Funktion refreshReceipt() implementiert. Wenn die Prüfung erfolgreich aktualisiert wurde, wird die Delegatenmethode requestDidFinish(_ request : SKRequest) aufgerufen, die die refreshSubscriptionsStatus Methode refreshSubscriptionsStatus .


Wie wird das Parsen von Kaufinformationen implementiert? Wir erhalten ein JSON-Objekt zurück, in dem sich ein verschachteltes Array von Transaktionen befindet (mit dem Schlüssel " latest_receipt_info ). Wir gehen das Array durch, ermitteln das Ablaufdatum mit dem Schlüssel expires_date und speichern es, wenn dieses Datum noch nicht eingetroffen ist.


 private func parseReceipt(_ json : Dictionary<String, Any>) { // It's the most simple way to get latest expiration date. Consider this code as for learning purposes. Do not use current code in production apps. guard let receipts_array = json["latest_receipt_info"] as? [Dictionary<String, Any>] else { self.refreshSubscriptionFailureBlock?(nil) self.cleanUpRefeshReceiptBlocks() return } for receipt in receipts_array { let productID = receipt["product_id"] as! String let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd HH:mm:ss VV" if let date = formatter.date(from: receipt["expires_date"] as! String) { if date > Date() { // do not save expired date to user defaults to avoid overwriting with expired date UserDefaults.standard.set(date, forKey: productID) } } } self.refreshSubscriptionSuccessBlock?() self.cleanUpRefeshReceiptBlocks() } 

Ich habe ein einfaches Beispiel gegeben, wie das aktuelle Ablaufdatum eines Abonnements extrahiert wird. Es gibt keine Fehlerbehandlung und zum Beispiel keine Überprüfung der Rückgabe eines Kaufs ( Stornierungsdatum wird hinzugefügt).


Um festzustellen, ob ein Abonnement aktiv ist oder nicht, vergleichen Sie einfach das aktuelle Datum mit dem Datum aus den Benutzerstandards nach Produktschlüssel. Wenn es nicht vorhanden ist oder unter dem aktuellen Datum liegt, gilt das Abonnement als inaktiv.


 func expirationDateFor(_ identifier : String) -> Date?{ return UserDefaults.standard.object(forKey: identifier) as? Date } let subscriptionDate = IAPManager.shared.expirationDateFor("YOUR_PRODUCT_ID") ?? Date() let isActive = subscriptionDate > Date() 

Die Transaktionswiederherstellung erfolgt in einer einzelnen Zeile SKPaymentQueue.default().restoreCompletedTransactions() . Diese Funktion stellt alle abgeschlossenen Transaktionen wieder her, indem die Delegatenmethode func paymentQueue(**_** queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) .


Was ist der Unterschied zwischen der Aktualisierung eines Schecks aus der Transaktionswiederherstellung?


Beide Methoden helfen bei der Wiederherstellung Ihrer Kaufdaten. Aber was sind ihre Unterschiede? Es gibt einen wunderbaren Tisch mit wwdc Video :


Differenztabelle mit zwei möglichen zum gleichen Stellen von Einklösen bei WWDC
Differenztabelle mit zwei Möglichkeiten zum Wiederherstellen von Einkäufen bei WWDC


In den meisten Fällen müssen Sie nur SKReceiptRefreshRequest() , da wir nur an einem Scheck für die spätere Berechnung des Ablaufdatums interessiert sind.


Bei automatisch erneuerbaren Abonnements sind die Transaktionen selbst für uns nicht von Interesse. Daher reicht es aus, nur ein Scheck-Update zu verwenden. Es gibt jedoch Fälle, in denen Sie die Transaktionswiederherstellungsmethode verwenden müssen: Wenn Ihre Anwendung beim Kauf Inhalte herunterlädt (von Apple gehostete Inhalte) oder wenn Sie weiterhin Versionen unter iOS 7 unterstützen.


Einkaufstests (Sandbox-Tests)


Zuvor mussten Sie sich zum Testen von Einkäufen im App Store in den Einstellungen Ihres iPhones anmelden. Dies war sehr unpraktisch (zum Beispiel wurde die gesamte Apple Music Library gelöscht). Dies muss jetzt jedoch nicht mehr durchgeführt werden: Das Sandbox-Konto ist jetzt getrennt vom Hauptkonto vorhanden.



Der Kaufprozess ist im Vergleich zu echten Einkäufen im App Store ähnlich, aber es gibt einige Punkte:


  • Sie müssen das Anmeldekennwort immer über das Systemfenster eingeben. Käufe mit Touch ID / Face ID werden weiterhin nicht unterstützt.


  • Wenn das System bei korrekter Eingabe von Login und Passwort immer wieder nach dem Login-Passwort fragt, klicken Sie auf "Abbrechen" , minimieren Sie die Anwendung und versuchen Sie es erneut. Es sieht nach Unsinn aus, aber es funktioniert für viele. Aber manchmal geht der Vorgang nach der zweiten Passworteingabe weiter.


  • Sie können den Abmeldevorgang in keiner Weise testen.


  • Die Dauer der Abonnementlaufzeiten ist viel kürzer als real. Und sie werden nicht mehr als 6 Mal am Tag aktualisiert.



Tatsächliche DauerTestdauer
1 Woche3 Minuten
1 Monat5 Minuten
2 Monate10 Minuten
3 Monate15 Minuten
6 Monate30 Minuten
1 Jahr1 Stunde

Was ist neu in StoreKit in iOS 13?


Von der neuen - nur die SKStorefront Klasse, die Informationen darüber enthält, in welchem ​​Land der Benutzer im App Store registriert ist. Dies kann für Entwickler nützlich sein, die unterschiedliche Abonnements für unterschiedliche Länder verwenden. Bisher wurden alle nach Geolokalisierung oder Region des Geräts überprüft. Dies ergab jedoch kein genaues Ergebnis. Jetzt ist es sehr einfach, das Land im App Store SKPaymentQueue.default().storefront?.countryCode : SKPaymentQueue.default().storefront?.countryCode . Ein Methodendelegierter wurde ebenfalls hinzugefügt, wenn sich das Land im App Store während des Kaufvorgangs geändert hat. In diesem Fall können Sie den Kaufvorgang selbst fortsetzen oder abbrechen.


Fallstricke bei der Arbeit mit Abonnements


  • Das Überprüfen eines Schecks direkt von einem Gerät aus wird von Apple nicht empfohlen. Sie haben mehrmals auf der WWDC darüber gesprochen (ab 5:50 Uhr), und dies ist in der Dokumentation angegeben . Dies ist unsicher, da ein Angreifer Daten mithilfe eines Man-in-the-Middle-Angriffs abfangen kann. Die korrekte Methode zum Überprüfen von Überprüfungen ist die lokale Überprüfung entweder über Ihren Server.
  • Beim Überprüfen des Ablaufdatums ist ein Problem aufgetreten. Wenn Sie Ihren Server nicht verwenden, kann die Systemzeit auf dem Gerät auf eine ältere geändert werden. Unser Code führt dann zu einem falschen Ergebnis. Das Abonnement wird als aktiv betrachtet. Wenn dies nicht zu Ihnen passt, können Sie jeden Dienst verwenden, der die genaue Weltzeit ausgibt.
  • Möglicherweise haben nicht alle Benutzer eine kostenlose Testversion. Der Benutzer kann die Anwendung nach einiger Zeit neu installieren, und die Anwendung zeigt an, dass die Testversion wie gewohnt verfügbar ist. Es ist korrekt, die Prüfung zu aktualisieren, zu validieren und in JSON die Verfügbarkeit der Testversion für diesen Benutzer zu überprüfen. Viele nicht.
  • Wenn der Benutzer eine Rückerstattung beantragt hat, wird das cancellation_date zum Abonnement-JSON hinzugefügt, das expires_date bleibt jedoch unverändert. Daher ist es wichtig, immer zu prüfen, ob das Feld " cancellation_date ist, das dem expires_date vorgezogen expires_date .
  • Es lohnt sich nicht, die Prüfung bei jedem Start der Anwendung zu aktualisieren, da dies zum einen sinnlos ist und zum anderen dem Benutzer höchstwahrscheinlich ein Kennworteingabefenster von Apple ID angezeigt wird. Es lohnt sich, den Scheck zu aktualisieren, wenn der Benutzer selbst auf die Schaltfläche "Einkauf wiederherstellen" geklickt hat.
  • Wie kann festgestellt werden, an welchen Punkten es sich lohnt, einen Scheck zu validieren, um das aktuelle Ablaufdatum eines Abonnements zu erhalten? Sie können den Scheck bei jedem Start oder nur am Ende des Abonnements validieren. Wenn Sie den Scheck jedoch erst am Ende des Abonnements überprüfen, kann der Benutzer, der die Rückerstattung ausgestellt hat, Ihren Antrag bis zum Ende des Zeitraums kostenlos verwenden.

Fazit


Ich hoffe, dieser Artikel wird Ihnen nützlich sein. Ich habe versucht, nicht nur den Code hinzuzufügen, sondern auch die subtilen Punkte in der Entwicklung zu erklären. Der vollständige Klassencode kann hier heruntergeladen werden . Diese Klasse ist sehr nützlich, um unerfahrene Entwickler und diejenigen kennenzulernen, die mehr darüber erfahren möchten, wie alles funktioniert. Für Live-Anwendungen wird empfohlen, seriösere Lösungen zu verwenden, z. B. SwiftyStoreKit .


Möchten Sie in 10 Minuten Abonnements in Ihrer iOS-App implementieren? Integrieren Sie Apphud und:
  • Kaufen Sie nur mit einer Methode ein.
  • Verfolgen Sie automatisch den Status des Abonnements jedes Benutzers.
  • Abonnementangebote einfach integrieren
  • Senden Sie Abonnementereignisse an Amplitude, Mixpanel, Slack und Telegram unter Berücksichtigung der lokalen Währung des Benutzers.
  • Verringern Sie die Abwanderungsrate in Anwendungen und geben Sie nicht abonnierte Benutzer zurück.

Source: https://habr.com/ru/post/de456602/


All Articles