Guide de mise en Ɠuvre des abonnements auto-renouvelables dans les applications iOS

image


Bonjour à tous! Je m'appelle Denis, je développe Apphud , un service d'analyse des abonnements auto-renouvelables dans les applications iOS.


Dans cet article, je vais vous expliquer comment configurer, implémenter et valider des abonnements auto-renouvelables dans iOS 12 et iOS 13. En bonus, je vais vous parler de points subtils et d'embûches que tous les développeurs ne prennent pas en compte.


Configurer des abonnements sur l'App Store Connect


Si vous avez déjà un ID de bundle et une application créée, vous pouvez ignorer ces étapes. Si vous créez une application pour la premiÚre fois, procédez comme suit:


Vous devez créer un ID de bundle explicite (ID d'application) sur le portail des développeurs Apple . Une page intitulée Certificats, identifiants et profils étant ouverte, accédez à l'onglet Identifiants . En juin 2019, Apple a finalement mis à jour la disposition du portail conformément à ASC (abréviation de App Store Connect).


Nouveau design pour Apple Developer Portal en 2019
Nouveau design pour Apple Developer Portal en 2019



L'ID de com.apphud.subscriptionstest explicite est généralement spécifié dans le style de domaine ( com.apphud.subscriptionstest ). Dans la section Capacités , vous remarquerez que la coche en regard des achats intégrés est déjà cochée. AprÚs avoir créé le Bundle ID ( App ID ), accédez à l'App Store Connect.


Utilisateurs de test (utilisateurs Sandbox)


Pour tester les futurs achats, vous devrez créer un utilisateur test. Pour ce faire, accédez à ASC dans l'onglet Utilisateurs et accÚs , puis à Sandbox Testers.


Formulaire de création de sandbox utilisateur
Formulaire de création de sandbox utilisateur


Lors de la création d'un testeur, vous pouvez spécifier toutes les données inexistantes, surtout, n'oubliez pas l'e-mail et le mot de passe!

Je vais vous expliquer comment tester les achats avec des informations d'identification de test vers la fin de l'article.


Une autre étape importante est la mise en place de contrats et de données bancaires dans la section « Accords, taxes et opérations bancaires ». Si vous n'avez pas d'accord pour les applications payantes, vous ne pourrez pas tester les abonnements auto-renouvelables!


AprÚs cela, vous pouvez créer une nouvelle application dans l'App Store Connect. Spécifiez un nom unique et sélectionnez votre ID de bundle comme ID de package.


L'ID de package est votre ID de bundle
L'ID de package est votre ID de bundle


Immédiatement aprÚs la création de l'application, accédez à l'onglet Fonctionnalités.


Si vous avez déjà créé l'application, vous pouvez continuer à lire à partir d'ici.

Le processus de création d'un abonnement auto-renouvelable comprend plusieurs étapes:


1. CrĂ©ez un identifiant d'abonnement et crĂ©ez un groupe d'abonnements . Un groupe d'abonnements est un ensemble d'abonnements avec des pĂ©riodes et des prix diffĂ©rents, mais qui ouvrent la mĂȘme fonctionnalitĂ© dans l'application. De plus, dans le groupe d'abonnements, vous ne pouvez activer la pĂ©riode d'essai gratuite qu'une seule fois et un seul des abonnements peut ĂȘtre actif. Si vous souhaitez que votre application ait deux abonnements diffĂ©rents en mĂȘme temps, vous devrez crĂ©er deux groupes d'abonnements.


2. Remplir les données d'abonnement: durée, nom d'affichage dans l'App Store (à ne pas confondre avec le seul nom) et description. Si vous ajoutez le premier abonnement au groupe, vous devrez indiquer le nom d'affichage du groupe d'abonnement. N'oubliez pas d'enregistrer vos modifications plus souvent, ASC peut se bloquer à tout moment et cesser de répondre.


Page d'abonnement
Écran d'abonnement


3. Remplir le prix de l'abonnement. Il y a deux étapes: créer des prix et des offres spéciales. Indiquez le prix réel dans n'importe quelle devise, il est automatiquement recalculé pour tous les autres pays. Offres de lancement: vous pouvez ici offrir aux utilisateurs une période d'essai gratuite ou des remises prépayées. Des promotions sont apparues récemment sur l'App Store en 2019: elles vous permettent d'offrir des remises spéciales aux utilisateurs qui se sont désabonnés et que vous souhaitez retourner.


Génération de clé secrÚte partagée


Sur la page avec une liste de tous vos abonnements créés, vous verrez la clé partagée pour le bouton de l'application . Il s'agit d'une ligne spéciale qui est nécessaire pour valider une vérification dans une application iOS. Nous devrons valider le chÚque pour déterminer le statut de l'abonnement.


La clĂ© partagĂ©e peut ĂȘtre de deux types: une clĂ© unique pour votre application ou une seule clĂ© pour votre compte. Important: en aucun cas recrĂ©er la clĂ© si vous avez dĂ©jĂ  l'application dans l'App Store, sinon les utilisateurs ne pourront pas valider le chĂšque et votre application cessera de fonctionner comme prĂ©vu.


Dans cet exemple, trois groupes d'abonnements et 3 abonnements annuels sont créés.
Dans cet exemple, trois groupes d'abonnements et 3 abonnements annuels sont créés.


Copiez l'ID de tous vos abonnements et la clé partagée, cela vous sera utile plus tard dans le code.


Partie logiciel


Passons Ă  la partie pratique. Que faut-il pour faire un directeur commercial complet? Au minimum, les Ă©lĂ©ments suivants devraient ĂȘtre mis en Ɠuvre:


  1. Commander


  2. Vérifier l'état de l'abonnement


  3. Vérifier la mise à jour


  4. Récupération de transaction (à ne pas confondre avec la mise à jour d'un chÚque!)



Commander


L'ensemble du processus d'achat peut ĂȘtre divisĂ© en 2 Ă©tapes: rĂ©ception des produits (classe SKProduct ) et initialisation du processus d'achat (classe SKPayment ). Tout d'abord, nous devons spĂ©cifier le dĂ©lĂ©guĂ© du protocole SKPaymentTransactionObserver .


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

La notification IAP_PRODUCTS_DID_LOAD_NOTIFICATION utilisée pour mettre à jour l'interface utilisateur dans une application.


Ensuite, nous écrivons une méthode pour initialiser l'achat:


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

Le délégué SKPaymentTransactionObserver ressemble à ceci:


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

En cas d'abonnement réussi, la méthode déléguée est appelée dans laquelle la transaction a le statut purchased .


Mais comment déterminer la date d'expiration d'un abonnement? Pour ce faire, faites une demande distincte à Apple.


Vérifier l'état de l'abonnement


Le chĂšque est validĂ© Ă  l'aide de la demande verifyReceipt POST Ă  ​​Apple, nous envoyons le chĂšque cryptĂ© sous la forme d'une chaĂźne codĂ©e en base64 comme paramĂštre, et dans la rĂ©ponse, nous recevons le mĂȘme chĂšque au format JSON. Dans le tableau, la clĂ© latest_receipt_info rĂ©pertorie toutes les transactions de chaque pĂ©riode de chaque abonnement, y compris les pĂ©riodes d'essai. Nous pouvons uniquement analyser la rĂ©ponse et obtenir la date d'expiration actuelle de chaque produit.


Lors de la WWDC 2017, ils ont ajouté la possibilité de recevoir uniquement les chÚques en cours pour chaque abonnement à l'aide de la clé exclude-old-transactions dans la demande verifyReceipt .

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

Au début de la méthode, vous pouvez voir qu'il existe un contrÎle de l'existence d'une copie locale du contrÎle. Une vérification locale peut ne pas exister, par exemple, si l'application a été installée via iTunes. S'il n'y a pas de vérification, nous ne pouvons pas exécuter la demande verifyReceipt . Nous devons d'abord obtenir le chÚque local actuel, puis essayer de le valider à nouveau. La mise à jour de la vérification s'effectue à l'aide de la classe 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)") } 

La mise à jour de la vérification est implémentée dans la fonction refreshReceipt() . Si la vérification a été mise à jour avec succÚs, la méthode déléguée requestDidFinish(_ request : SKRequest) est appelée, qui appelle à refreshSubscriptionsStatus méthode refreshSubscriptionsStatus .


Comment l'analyse des informations d'achat est-elle implémentée? Nous sommes retournés un objet JSON dans lequel il y a un tableau imbriqué de transactions (par la derniÚre latest_receipt_info ). Nous expires_date le tableau, obtenons la date d'expiration à l'aide de la clé expires_date et l'enregistrons si cette date n'est pas encore arrivée.


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

J'ai donné un exemple simple de la façon d'extraire la date d'expiration actuelle d'un abonnement. Il n'y a pas de gestion des erreurs et, par exemple, il n'y a pas de contrÎle pour le retour d'un achat (la date d'annulation est ajoutée).


Pour déterminer si un abonnement est actif ou non, il suffit de comparer la date actuelle avec la date des valeurs par défaut de l' utilisateur par clé de produit. S'il est absent ou inférieur à la date actuelle, l'abonnement est alors considéré comme inactif.


 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() 

La récupération de transaction est effectuée sur une seule ligne SKPaymentQueue.default().restoreCompletedTransactions() . Cette fonction restaure toutes les transactions terminées, appelant à nouveau la méthode déléguée func paymentQueue(**_** queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) .


Quelle est la différence entre la mise à jour d'un chÚque à partir de la récupération de transaction?


Les deux méthodes permettent de restaurer vos données d'achat. Mais quelles sont leurs différences? Il y a une merveilleuse table avec une vidéo wwdc :


Tableau des différences de deux façons de restaurer les achats de la WWDC
Tableau des différences de deux façons de restaurer les achats de la WWDC


Dans la plupart des cas, il vous suffit d'utiliser SKReceiptRefreshRequest() , car nous souhaitons uniquement recevoir un chÚque pour le calcul ultérieur de la date d'expiration.


Dans le cas des abonnements auto-renouvelables, les transactions elles-mĂȘmes ne nous intĂ©ressent pas, il suffit donc de n'utiliser que la mise Ă  jour des chĂšques. Cependant, il existe des cas oĂč vous devez utiliser la mĂ©thode de rĂ©cupĂ©ration de transaction: si votre application tĂ©lĂ©charge du contenu lors de l'achat (contenu hĂ©bergĂ© par Apple) ou si vous prenez toujours en charge les versions infĂ©rieures Ă  iOS 7.


Test de magasinage (test de bac Ă  sable)


Auparavant, pour tester les achats, vous deviez vous connecter depuis l'App Store dans les paramĂštres de votre iPhone. C'Ă©tait trĂšs gĂȘnant (par exemple, toute la bibliothĂšque musicale Apple a Ă©tĂ© effacĂ©e). Cependant, cela n'a pas besoin d'ĂȘtre fait maintenant: le compte sandbox existe dĂ©sormais sĂ©parĂ©ment du compte principal.



Le processus d'achat est similaire par rapport aux achats réels dans l'App Store, mais il y a quelques points:


  • Vous devrez toujours saisir le mot de passe de connexion via la fenĂȘtre systĂšme. Les achats utilisant Touch ID / Face ID ne sont toujours pas pris en charge.


  • Si, lors de la saisie correcte du login et du mot de passe, le systĂšme demande encore et encore le mot de passe de connexion, cliquez sur «Annuler» , minimisez l'application, puis rĂ©essayez. Cela ressemble Ă  un non-sens, mais cela fonctionne pour beaucoup. Mais parfois, aprĂšs la deuxiĂšme entrĂ©e du mot de passe, le processus continue.


  • Vous ne pourrez pas tester le processus de dĂ©sinscription.


  • La durĂ©e des pĂ©riodes d'abonnement est bien infĂ©rieure Ă  la rĂ©alitĂ©. Et ils ne sont pas mis Ă  jour plus de 6 fois par jour.



Durée réelleDurée du test
1 semaine3 minutes
1 mois5 minutes
2 mois10 minutes
3 mois15 minutes
6 mois30 minutes
1 an1 heure

Quoi de neuf dans StoreKit dans iOS 13?


De la nouvelle - uniquement la classe SKStorefront , qui donne des informations sur le pays dans lequel l'utilisateur est enregistrĂ© dans l'App Store. Cela peut ĂȘtre utile aux dĂ©veloppeurs qui utilisent diffĂ©rents abonnements pour diffĂ©rents pays. Auparavant, tout le monde vĂ©rifiait par gĂ©olocalisation ou par rĂ©gion de l'appareil, mais cela ne donnait pas de rĂ©sultat prĂ©cis. Il est dĂ©sormais trĂšs facile de dĂ©couvrir le pays dans l'App Store: SKPaymentQueue.default().storefront?.countryCode . Un dĂ©lĂ©guĂ© de mĂ©thode a Ă©galement Ă©tĂ© ajoutĂ© si le pays de l'App Store a changĂ© au cours du processus d'achat. Dans ce cas, vous pouvez continuer ou annuler vous-mĂȘme le processus d'achat.


PiĂšges lors de l'utilisation d'abonnements


  • VĂ©rifier un chĂšque directement Ă  partir d'un appareil n'est pas recommandĂ© par Apple. Ils en ont parlĂ© plusieurs fois Ă  la WWDC (Ă  partir de 5h50) et cela est indiquĂ© dans la documentation . Ceci n'est pas sĂ»r car un attaquant peut intercepter des donnĂ©es Ă  l'aide d'une attaque par l'homme du milieu. La bonne façon de vĂ©rifier les chĂšques est la validation locale Ă  l'aide de votre serveur.
  • Il y a un problĂšme avec la vĂ©rification de la date d'expiration. Si vous n'utilisez pas votre serveur, l'heure du systĂšme sur l'appareil peut ĂȘtre remplacĂ©e par une ancienne, puis notre code donnera le mauvais rĂ©sultat - l'abonnement sera considĂ©rĂ© comme actif. Si cela ne vous convient pas, vous pouvez utiliser n'importe quel service qui Ă©met l'heure exacte du monde.
  • Tous les utilisateurs ne peuvent pas bĂ©nĂ©ficier d'un essai gratuit. L'utilisateur peut rĂ©installer l'application aprĂšs un certain temps et l'application montrera que la version d'essai est disponible comme d'habitude. Il sera correct de mettre Ă  jour la vĂ©rification, de la valider et de vĂ©rifier dans JSON la disponibilitĂ© de la version d'essai pour cet utilisateur. Beaucoup ne le font pas.
  • Si l'utilisateur a demandĂ© un remboursement, la date d' cancellation_date sera ajoutĂ©e Ă  l'abonnement JSON, mais la date d' expires_date restera inchangĂ©e. Par consĂ©quent, il est important de toujours vĂ©rifier la prĂ©sence du champ d' cancellation_date , qui est prĂ©fĂ©rable Ă  expires_date .
  • Cela ne vaut pas la peine de mettre Ă  jour la vĂ©rification chaque fois que l'application est lancĂ©e, car, premiĂšrement, cela est inutile, et deuxiĂšmement, trĂšs probablement, l'utilisateur verra la fenĂȘtre de saisie du mot de passe Apple ID. Cela vaut la peine de mettre Ă  jour le chĂšque, par exemple, lorsque l'utilisateur a lui-mĂȘme cliquĂ© sur le bouton de restauration des achats.
  • Comment dĂ©terminer Ă  quel moment il vaut la peine de valider un chĂšque pour obtenir la date d'expiration actuelle d'un abonnement? Vous pouvez valider le chĂšque Ă  chaque dĂ©marrage, ou uniquement Ă  la fin de l'abonnement. Cependant, si vous ne cochez le chĂšque qu'Ă  la fin de l'abonnement, l'utilisateur qui a Ă©mis le remboursement pourra utiliser gratuitement votre application jusqu'Ă  la fin de la pĂ©riode.

Conclusion


J'espĂšre que cet article vous sera utile. J'ai essayĂ© d'ajouter non seulement le code, mais aussi d'expliquer les points subtils du dĂ©veloppement. Le code complet de la classe peut ĂȘtre tĂ©lĂ©chargĂ© ici . Ce cours sera trĂšs utile pour faire la connaissance des dĂ©veloppeurs novices et de ceux qui veulent en savoir plus sur le fonctionnement de tout. Pour les applications en direct, il est recommandĂ© d'utiliser des solutions plus sĂ©rieuses, par exemple SwiftyStoreKit .


Vous souhaitez mettre en Ɠuvre des abonnements dans votre application iOS en 10 minutes? IntĂ©grez Apphud et:
  • Faites des achats en utilisant une seule mĂ©thode;
  • suivre automatiquement l'Ă©tat de l'abonnement de chaque utilisateur;
  • IntĂ©grez facilement les offres d'abonnement
  • envoyer des Ă©vĂ©nements d'abonnement Ă  Amplitude, Mixpanel, Slack et Telegram en tenant compte de la devise locale de l'utilisateur;
  • diminuer le taux de dĂ©sabonnement dans les applications et renvoyer les utilisateurs non abonnĂ©s.

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


All Articles