Report: State of in-app subscriptions in the US 2023 Get a report

Quelles sont les nouveautés de l’API StoreKit 2 et comment Apple a simplifié l’intégration des achats intégrés aux applications.

Kirill Potekhin

Updated: mars 20, 2023

Content

62fdf0da8b35ff3ba074dbce jp android tutorial 1 configuration 5

Apple a présenté une nouvelle version de StoreKit 2 lors de la WWDC 2021 qui s’est déroulée récemment. Il s’agit d’un cadre responsable de la réalisation des achats dans iOS. Une part des applications (app) avec des fonctions d’achat intégré (in-app purchase) et d’abonnement (subscription)  ne cesse de croître, et Apple a considérablement simplifié l’intégration des achats intégrés à l’application en publiant StoreKit 2. Aujourd’hui, nous allons envisager de travailler avec StoreKit 2 sur la partie du serveur, autrement dit, à l’aide de App Store Server API.

Requête d’authentification

Dans la version actuelle de l’API, vous avez besoin du Secret Partagé (shared secret) pour envoyer une requête. Il s’agit d’une chaîne secrète fixe que vous pouvez obtenir dans App Store Connect. Une nouvelle version de l’API utilise la norme JSON Web Token (JWT) pour l’authentification des requêtes.   

Génération de clés 

Tout d’abord, créez une clé privée qui sera utilisée pour autoriser les demandes. Ouvrez App Store Connect et allez dans la section Users and Access, puis dans l’onglet Keys. Sélectionnez le type de clé pour les achats intégrés à l’application. Téléchargez une nouvelle clé. Vous aurez également besoin de son ID – vous pouvez le copier sur la même page que l’Issue ID qui se trouve dans l’onglet App Store Connect API.

Creating a private key to work with StoreKit 2
Création d’une clé privée pour travailler avec App Store Server API

Création d’un jeton

L’étape suivante consiste à créer un jeton qui sera utilisé pour autoriser les requêtes. Ce processus est décrit en détail dans documentation, il n’y a donc aucune raison d’y prêter trop d’attention. Voici un exemple d’une implémentation prête à l’emploi pour Python. Il convient de noter qu’il est inutile de générer un nouveau jeton pour chaque nouvelle requête. Lorsque vous créez un jeton, vous fixez sa durée de vie à 60 minutes maximum et vous utilisez le même jeton pendant cette période. 

import time, uuid
from authlib.jose import jwt

BUNDLE_ID = 'com.adapty.sample_app'
ISSUER_ID = '4336a124-f214-4d40-883b-6db275b5e4aa'
KEY_ID = 'J65UYBDA74'
PRIVATE_KEY = '''
-----BEGIN PRIVATE KEY-----
MIGTAgMGByqGSMBHkAQQgR/fR+3Lkg4...
-----END PRIVATE KEY-----
'''

issue_time = round(time.time())
expiration_time = issue_time + 60 * 60 # 1 hour expiration


header = {
 'alg': 'ES256',
 'kid': KEY_ID,
 'typ': 'JWT'
}

payload = {
 'iss': ISSUER_ID,
 'iat': issue_time,
 'exp': expiration_time,
 'aud': 'appstoreconnect-v1',
 'nonce': str(uuid.uuid4()),
 'bid': BUNDLE_ID
}

token_encoded = jwt.encode(header, payload, PRIVATE_KEY)
token_decoded = token_encoded.decode()

authorization_header = {
 'Authorization': f'Bearer {token_decoded}'
}

Transactions signées

Dans une nouvelle version de l’API, toutes les transactions sont renvoyées dans la norme JSON Web Signature (JWS). Il s’agit d’une chaîne composée de trois parties divisées par des points.  

  1. En-tête en base64.
  2. Données utiles de la transaction en base64.
  3. Signature de la transaction.
Base64(header) + "." + Base64(payload) + "." + sign(Base64(header) + "." + Base64(payload))

En-tête de la transaction

Un en-tête est nécessaire pour s’assurer que la transaction est authentique. La clé Alg contient un algorithme de cryptage, la clé x5c contient une chaîne de certificats.

{
  "kid": "AMP/DEV",
  "alg": "ES256",
  "x5c": [
    "MIIEO...",
    "MIIDK..."
  ]
}

Données utiles de la transaction

{
  "transactionId": "1000000831360853",
  "originalTransactionId": "1000000806937552",
  "webOrderLineItemId": "1000000063561721",
  "bundleId": "com.adapty.sample_app",
  "productId": "basic_subscription_1_month",
  "subscriptionGroupIdentifier": "27636320",
  "purchaseDate": 1624446341000,
  "originalPurchaseDate": 1619686337000,
  "expiresDate": 1624446641000,
  "quantity": 1,
  "type": "Auto-Renewable Subscription",
  "appAccountToken": "fd12746f-2d3a-46c8-bff8-55b75ed06aca",
  "inAppOwnershipType": "PURCHASED",
  "signedDate": 1624446484882,
  "offerType": 2,
  "offerIdentifier": "basic_subscription_1_month.pay_as_you_go.3_months"
}

Apple a modifié et étendu le format de la transaction. De mon point de vue, maintenant, c’est plus pratique de travailler avec eux. Vous pouvez obtenir des détails sur un nouveau format dans documentation. Je vais décrire ci-dessous les changements les plus importants.    

  • Apple a ajouté le champ appAccountToken, qui contient l’ID utilisateur de votre système. Cet ID doit être au format UUID, il est défini dans l’application mobile lors de l’initialisation d’un achat. S’il est défini, il sera renvoyé dans toutes les transactions de cette chaîne (renouvellement, problèmes de facturation, etc.), et vous pourrez facilement comprendre quel utilisateur a effectué un achat.    
  • Apple a également ajouté les champs offerType et offerIdentifier qui contiennent les informations relatives à une offre utilisée (le cas échéant). Voici les valeurs du champ offerType :
  • 1  — offre de lancement (intro offer)  (disponible uniquement pour les utilisateurs sans abonnement actif ou expiré)
  • 2  — offre promotionnelle (promo offer) (disponible uniquement pour les abonnements actuels et expirés)
  •  3  — code d’offre

Si une offre promotionnelle ou un code d’offre a été utilisé, la clé offerIdentifier contiendra l’ID de l’offre utilisée. Dans le passé, il était impossible de suivre l’utilisation de l’offre du côté du serveur (server side), ce qui aggravait les analyses. Désormais, vous pouvez utiliser les codes d’offre pour l’analyse.   

  • Apple a ajouté le champ inAppOwnershipType, qui permet de comprendre si un utilisateur a acheté un produit ou y a accédé grâce à un abonnement familial. Valeurs possibles :
  • PURCHASED
  • FAMILY_SHARED
  • Un autre nouveau champ – le type – comprend le type de transaction. Valeurs possibles :
  • Abonnement auto-renouvelable
  • Non consommable
  • Consommable
  • Abonnement non renouvelé
  • Les champs cancellation_date et cancellation_reason ont désormais de nouveaux noms : revocationDate et revocationReason. Pour rappel, ils contiennent une date et un motif de révocation d’abonnement (subscription revocation) suite à un remboursement, donc le nouveau nom semble plus logique.
  • Toutes les clés sont renvoyées au format camelCase (comme dans toutes les requêtes de App Store Server API ).
  • Toutes les dates sont affichées au format timestamp Unix en millisecondes.   

Subscribe to Adapty newsletter

Get fresh paywall ideas, subscription insights, and mobile app news every month!

Statut d’abonnement de l’utilisateur

Pour vérifier le statut d’abonnement de l’utilisateur actuel, envoyez une requête GET à https://api.storekit.itunes.apple.com/inApps/v1/subscriptions/{originalTransactionId}, où {originalTransactionId} est l’ID de toute chaîne de transactions de l’utilisateur. En retour, vous obtiendrez des transactions avec des statuts pour chaque groupe d’abonnements.   

{
  "environment": "Sandbox",
  "bundleId": "com.adapty.sample_app",
  "data": [
    {
      "subscriptionGroupIdentifier": "39636320",
      "lastTransactions": [
        {
          "originalTransactionId": "1000000819078552",
          "status": 2,
          "signedTransactionInfo": "eyJraWQiOi...",
          "signedRenewalInfo": "eyJraWQiOi..."
        }
      ]
    }
  ]
}

La touche d’état affiche le statut actuel d’abonnement. Sur cette base, vous pouvez décider si vous devez donner à un utilisateur l’accès aux fonctionnalités payantes de l’application. Valeurs possibles :

  • 1 —  l’abonnement est actif, l’utilisateur doit pouvoir accéder aux fonctionnalités payantes.
  • 2 — l’abonnement a expiré, l’utilisateur ne doit pas pouvoir accéder aux fonctions payantes.  
  • 3 —  le statut d’abonnement est Billing Retry, ce qui signifie que l’utilisateur ne l’a pas annulé, mais qu’il a rencontré des problèmes de paiement. Apple va essayer de débiter la carte pendant 60 jours. Un utilisateur ne doit pas pouvoir accéder à des fonctions payantes.  
  • 4 — le statut d’abonnement est Période de grâce (grace period), ce qui signifie que l’utilisateur ne l’a pas annulé, mais qu’il rencontre des problèmes de paiement. Le délai de grâce est activé dans App Store Connect, ce qui signifie que l’utilisateur doit pouvoir accéder aux fonctionnalités payantes.    
  • 5 —  l’abonnement a été annulé à la suite d’un remboursement, l’utilisateur ne doit pas pouvoir accéder aux fonctions payantes. 

La clé SignedTransactionInfo contient les informations relatives à la dernière transaction de la chaîne. Vous pouvez trouver les détails sur son format ci-dessus.   

Informations sur le renouvellement de l’abonnement (subscription renewal)

La clé SignedRenewalInfo contient les informations relatives au renouvellement de l’abonnement.

{
  "expirationIntent": 1,
  "originalTransactionId": "1000000819078552",
  "autoRenewProductId": "basic_subscription_1_month",
  "productId": "basic_subscription_1_month",
  "autoRenewStatus": 0,
  "isInBillingRetryPeriod": false,
  "signedDate": 1624520884048
}

Ces informations nous permettent de comprendre ce qu’il adviendra de l’abonnement lors de la prochaine période de paiement. Par exemple, si vous constatez qu’un utilisateur a annulé le renouvellement automatique, vous pouvez lui proposer de passer à une autre formule d’abonnement ou lui proposer une offre promotionnelle. Il est pratique de suivre ce type d’événements (events) à l’aide des notifications du serveur (server notifications), dont je vous parlerai bientôt.  

Historique des transactions de l’utilisateur

Pour obtenir l’historique des transactions de l’utilisateur, envoyez une requête GET à https://api.storekit.itunes.apple.com/inApps/v1/history/{originalTransactionId}, où {originalTransactionId} est l’ID de toute chaîne de transactions de l’utilisateur. En retour, vous obtiendrez un tableau des transactions classées par heure.     

{
	"revision": "1625872984000_1000000212854038",
	"bundleId": "com.adapty.sample_app",
	"environment": "Sandbox",
	"hasMore": true,
	"signedTransactions": [
		"eyJraWQiOiJ...",
		"joiRVMyNeyX...",
		"5MnkvOTlOZl...",
		...
	]
}

Une demande ne peut contenir plus de 20 transactions. Si un utilisateur en a plus, la valeur de l’indicateur hasMore sera vraie. Si vous avez besoin de la page de transaction suivante, envoyez à nouveau la demande avec le paramètre GET de révision contenue. Il contiendra la valeur de la même clé.  

Notifications des transactions du serveur

Les notifications du serveur permettent d’obtenir des informations sur les nouveaux achats, les renouvellements, les problèmes de facturation, etc. Cela permet d’établir des analyses plus précises et de simplifier la gestion du statut de l’abonné.   

Les notifications du serveur existantes (V1) peuvent résoudre la plupart des problèmes, mais elles sont parfois peu pratiques. Il s’agit surtout de la situation où vous recevez plusieurs notifications pour une seule action d’un utilisateur. Par exemple, maintenant, lorsqu’un utilisateur met à niveau un abonnement, Apple envoie deux notifications : DID_CHANGE_RENEWAL_STATUS et INTERACTIVE_RENEWAL. Pour traiter ce cas actuellement, vous devez enregistrer le statut d’une manière ou d’une autre et vérifier si la deuxième notification a été envoyée. Dans une nouvelle version des notifications du serveur (V2), il n’y a qu’une seule notification pour une action d’un utilisateur. C’est beaucoup plus pratique.   

La deuxième version des notifications de serveur présente de nouveaux événements : OFFER_REDEEMED, EXPIRED et GRACE_PERIOD_EXPIRED. Ils facilitent grandement la gestion du statut des abonnés. Les événements SUBSCRIBED et PRICE_INCREASE sont des événements améliorés par rapport à la première version.

Transaction notifications in StoreKit 2 feature new events

Types de notification

Les notifications ont maintenant des types, ainsi, une notification pour toute action d’un utilisateur est suffisante pour comprendre ce qui s’est passé. 

Notifications in StoreKit 2 have types

Types de notification

{
  "notificationType": "SUBSCRIBED",
  "subtype": "INITIAL_BUY",
  "version": 2,
  "data": {
    "environment": "Sandbox",
    "bundleId": "com.adapty.sample_app",
    "appAppleId": 739104078,
    "bundleVersion": 1,
    "signedTransactionInfo": "eyJraWQiOi...",
    "signedRenewalInfo": "eyJraWQiOi..."
  }
}

Les notifications du serveur contiennent des informations sur une transaction et un renouvellement au format JWS que j’ai décrit précédemment.  

Travailler avec un environnement Sandbox

Vous devez utiliser l’URL de l’environnement Sandbox pour tester les achats : https://api.storekit-sandbox.itunes.apple.com.

Une nouvelle version des notifications de serveur n’est pas encore disponible pour être testée. Dès qu’elle sera disponible, il sera possible de spécifier des URL différentes pour les notifications de Production et de Sandbox. Vous pouvez choisir V2 pour Sandbox, et V1 pour Production pour les tests. 

En outre, App Store Connect permet désormais de :

  • Effacement de l’historique des achats pour l’utilisateur de la Sandbox, ce qui signifie que vous n’avez plus besoin de créer un nouveau compte pour le faire.   
  • Changez le pays du magasin pour l’utilisateur Sandbox.
  • Modifiez la période de renouvellement de l’abonnement Sandbox. Par exemple, vous pouvez effectuer un achat mensuel qui dure 1 heure au lieu de 5 minutes.     

Conclusion

Apple a considérablement amélioré le fonctionnement des achats et des abonnements intégrés à l’application du côté serveur. De mon point de vue, voici les nouvelles fonctionnalités les plus utiles :

  • Prise en charge complète des offres promotionnelles et des codes d’offre ;  
  • Des notifications du serveur plus simples et plus informatives ;   
  • Une occasion de connaître le statut d’abonnement en cours par un simple appel à l’API ;
  • Effacement de l’historique des achats dans le Sandbox de l’utilisateur.   

Le passage à une nouvelle API ne sera pas difficile, il suffit d’obtenir l’originalTransactionId pour chaque reçu. Il est probable qu’il soit déjà contenu dans votre base.    

Quoi qu’il en soit, la partie la plus difficile de l’intégration des abonnements dans une application mobile consiste à mettre en place un système d’analyse et à optimiser l’économie. Adapty peut bien résoudre ces problèmes :

  • L’analyse intégrée nous permet de comprendre rapidement les paramètres de l’application principale.
  • L’analyse des cohortes (cohort analysis) permet de comprendre s’il existe des problèmes d’économie.
  • Les tests A/B augmentent la rentabilité de l’application.   
  • Les intégrations avec des systèmes externes permettent d’envoyer des transactions à des services d’attribution et d’analyse de produits.    
  • Les campagnes promotionnelles réduisent la perte d’audience.   
  • Le SDK open-source permet d’intégrer les abonnements dans une application en quelques heures.  

Apprenez-en davantage sur ces fonctionnalités, afin de mettre en œuvre plus rapidement les abonnements dans votre application et de monétiser votre application plus rapidement et mieux.