Tous les articles
Deep dive8 min de lecture

La détection des remboursements dans StoreKit 2 tient à une seule propriété de la transaction, et c'est revocationDate

Quand Apple rembourse un de vos clients, le remboursement est déjà présent dans votre app, dans la revocationDate de la transaction, avant que le job de votre serveur ne s'exécute. Voici où la détection des remboursements de StoreKit 2 apparaît sur l'appareil, ce que revocationDate et revocationReason vous disent, et pourquoi le client sert à la rapidité et le serveur à la vérité.

Un smartphone qui brille sur un bureau sombre de développeur à côté d'une horloge mécanique, illustrant la détection des remboursements de StoreKit 2 qui apparaît dans une app

Points clés

  • Dans StoreKit 2, un achat remboursé porte une revocationDate non nil sur sa Transaction, donc votre app peut détecter le remboursement toute seule, sans appeler votre serveur.
  • revocationDate est définie quand l'App Store rembourse une transaction ou quand un client la perd via Family Sharing, donc une date non nil n'est pas toujours un remboursement.
  • revocationReason vous dit pourquoi : developerIssue signifie que le client a signalé un problème dans votre app, et other couvre tous les autres motifs de remboursement.
  • Transaction.currentEntitlements exclut déjà les achats remboursés et révoqués, donc le filtre d'accès le plus propre côté client est simplement de savoir si un produit y figure encore.
  • Transaction.updates ne livre un remboursement survenu pendant que votre app était fermée que si vous commencez à écouter au lancement, donc un Task manquant signifie un remboursement manqué.
  • La détection côté client ne se déclenche que lorsque l'app est ouverte, c'est pourquoi le REFUND des App Store Server Notifications V2 reste le signal de référence qui vous évite de payer pour servir un utilisateur remboursé.
  • Un remboursement peut être annulé, et dans ce cas, les champs de révocation sont retirés de la transaction et vous êtes censé rétablir l'accès que vous aviez coupé.

La plupart des apps apprennent un remboursement Apple par leur serveur, via une App Store Server Notification, et ne remarquent jamais que le même remboursement se trouve déjà dans l'app. Il est sur la transaction, dans une propriété appelée revocationDate, et la lire permet à votre app de couper l'accès d'un client remboursé à sa prochaine ouverture, au lieu d'attendre un job du backend. La détection des remboursements de StoreKit 2 est un signal côté client que la plupart des équipes ignorent. Voici exactement où un remboursement apparaît sur l'appareil, ce qu'il vous dit, ce qu'il ne vous dit pas, et pourquoi il doit accompagner vos notifications serveur plutôt que les remplacer.

Où apparaît un remboursement dans StoreKit 2

StoreKit 2 vous remet les transactions sous forme de valeurs signées, et un remboursement ne supprime pas la transaction. Il la marque. Deux propriétés de la Transaction portent cette marque, et toutes deux restent à nil pendant toute la vie d'un achat sain. Quand l'une d'elles devient non nil, l'App Store a repris l'achat.

revocationDate est le champ qui bascule

revocationDate est un Date optionnel. La description d'Apple elle-même est précise : c'est la date à laquelle l'App Store a remboursé la transaction ou l'a révoquée de Family Sharing. Pour un achat encore valable, elle vaut nil. Dès qu'un remboursement est traité, elle contient l'horodatage de ce remboursement. Cette seule vérification, revocationDate est-elle non nil, constitue toute la détection des remboursements côté client. Tout le reste n'est que nuance empilée par-dessus.

revocationReason vous dit pourquoi Apple l'a retiré

revocationReason se trouve à côté de la date et en explique la cause. StoreKit lui donne deux valeurs qui comptent pour les remboursements. developerIssue signifie que le client a dit à Apple que le remboursement était dû à un problème réel ou perçu dans votre app. other couvre tous les autres motifs. Une troisième valeur, upgradedToBundle, n'est pas du tout un remboursement ; elle signale une transaction que l'App Store a révoquée parce que le client est passé à un pack d'abonnement. Lisez le motif avant d'agir, car developerIssue est celui qui mérite d'être compté : un groupe de ces cas, c'est votre propre produit qui vous dit où il a échoué.

PropriétéTypeCe que signifie une valeur non nil
revocationDateDate?L'App Store a remboursé cette transaction, ou l'a révoquée via Family Sharing, à cette date
revocationReason vaut developerIssuemotifLe client a signalé un problème réel ou perçu dans votre app
revocationReason vaut othermotifLe remboursement a eu lieu pour un autre motif qu'Apple ne détaille pas
revocationReason vaut upgradedToBundlemotifPas un remboursement ; la transaction a été révoquée parce que le client est passé à un pack d'abonnement

currentEntitlements écarte déjà un achat remboursé

Vous n'avez pas toujours à lire vous-même les champs de révocation. Transaction.currentEntitlements est la séquence des achats auxquels un client a encore droit à l'instant, et Apple la construit pour écarter ceux que vous ne devriez pas honorer. Un produit que l'App Store a remboursé ou révoqué n'y figure pas. Pas plus que les abonnements expirés, ni les consommables, qui disparaissent dès qu'ils sont utilisés.

Cela fait de currentEntitlements le filtre le plus propre pour l'accès. Demandez-lui ce que le client possède, accordez exactement cela, et un remboursement retire le droit à votre place sans une seule vérification de revocationDate. Les champs de révocation servent quand vous voulez le détail, la date et le motif, pour journaliser l'événement ou y réagir. La liste des droits sert à la simple question de savoir s'il faut garder la lumière allumée.

La détection des remboursements de StoreKit 2 en pratique, dès le lancement et pendant que l'app tourne

Il y a deux moments où votre app peut attraper un remboursement sur l'appareil, et ils demandent un code différent. L'un est pendant que l'app est ouverte et qu'un remboursement survient en direct ou sur un autre appareil. L'autre est au lancement, en rattrapant tout ce qui a changé pendant que vous étiez fermé. Ratez le second et votre détection des remboursements de StoreKit 2 a un trou exactement là où tombe la plupart des remboursements, car les clients ont rarement votre app ouverte quand ils en demandent un.

Commencez à écouter au lancement ou vous manquez les remboursements survenus pendant la fermeture

Transaction.updates est la séquence asynchrone qui émet une transaction chaque fois que le système en crée ou en met à jour une en dehors de votre app ou sur un autre appareil, un remboursement compris. La consigne d'Apple est nette : lancez un Task qui la parcourt dès que votre app démarre, sinon vous risquez de manquer les transactions qu'elle ne livre qu'une seule fois au démarrage. Un remboursement arrivé dans la nuit parvient via updates à la prochaine ouverture de l'app, mais seulement si un écouteur tourne déjà pour le recevoir. Pas d'écouteur, pas d'événement, et le remboursement reste invisible jusqu'à ce qu'autre chose le réconcilie.

Un achat sur le même appareil ne passe pas par updates

Un piège attrape ceux qui testent les remboursements à la main. Un achat normal fait sur le même appareil n'arrive pas par updates ; StoreKit le renvoie directement depuis le résultat de l'appel d'achat. updates est pour les changements hors bande : remboursements, approbations Ask to Buy, utilisations de codes promotionnels et achats faits ailleurs. Alors construisez votre gestion des remboursements autour de updates et de currentEntitlements, pas autour du flux d'achat, car le remboursement ne repassera jamais par le chemin qu'a pris la vente.

Un reçu en papier froissé sur une surface sombre avec un léger tampon rouge apposé dessus, représentant une transaction remboursée que StoreKit 2 marque d'une date de révocation

Ce que la détection côté client ne peut pas faire pour vous

Lire les remboursements sur l'appareil est rapide et gratuit, mais cela a une limite, et prétendre le contraire, c'est ainsi que le revenu fuit. L'appareil ne sait que ce que StoreKit lui a dit, et StoreKit ne parle que pendant que votre app tourne. Un client qui obtient un remboursement et ne rouvre jamais votre app est un client que votre vérification côté client ne voit jamais.

Une revocationDate n'est pas toujours un remboursement

Le même champ bascule pour Family Sharing. Quand un client perd l'accès à un achat partagé, parce que l'organisateur l'a retiré ou que le partage a pris fin, cette transaction reçoit elle aussi une revocationDate. Donc une date non nil signifie que le client n'a plus cet achat, ce qui est exactement ce dont vous avez besoin pour le contrôle d'accès, mais cela ne veut pas toujours dire que de l'argent est reparti. Si vous comptez les remboursements pour le chiffre d'affaires, séparez les révocations Family Sharing des vraies avant de vous fier au nombre.

Un remboursement peut être annulé

Un remboursement n'est pas toujours définitif. Apple peut en annuler un, et dans ce cas, les champs de révocation sont retirés de la transaction et l'achat redevient valable. Si vous avez coupé l'accès au remboursement, vous êtes censé le rétablir à l'annulation. Sur l'appareil, cela apparaît comme un autre événement updates avec une transaction propre ; sur votre serveur, c'est une notification distincte, REFUND_REVERSED. Ne traitez que le remboursement et vous laisserez un client payant en rade, sans accès et avec un reçu valide.

Ce que coûte réellement une révocation tardive

Un remboursement, c'est rarement juste le prix de vente qui quitte votre compte. Le temps qu'il soit réglé, vous avez généralement déjà dépensé de l'argent réel à servir cet achat, et cette dépense ne revient pas. Les images générées coûtent des minutes de GPU. Les réponses du chat coûtent des appels à l'API du modèle facturés au token. Les envois coûtent du stockage que vous payez encore pour le conserver. Si l'achat a financé un versement à un créateur, cet argent est déjà parti. Rien de tout cela ne s'inverse avec le remboursement.

La détection côté client réduit la fenêtre sur la seule partie que vous pouvez encore maîtriser, la dépense future. Plus tôt vous savez qu'un achat est remboursé, plus tôt vous cessez de le servir. Mais l'appareil ne vous prévient que lorsque l'app est ouverte, donc un utilisateur remboursé qui ne revient jamais conserve l'accès côté serveur que vous lui avez accordé, vous coûtant en silence chaque fois qu'un job en arrière-plan ou un appareil synchronisé agit en son nom. Le client rend la révocation rapide. Il ne la rend pas garantie.

Utilisez le client pour la rapidité et le serveur pour la vérité

La conception propre utilise les deux signaux pour ce que chacun sait faire. Sur l'appareil, Transaction.updates et currentEntitlements vous donnent une réaction locale et instantanée dès qu'un client remboursé ouvre l'app, utile pour l'interface et pour finaliser les changements de droits sans aller-retour. Sur le serveur, les App Store Server Notifications V2 envoient un message REFUND qui arrive que l'app soit rouverte ou non, ce qui est le seul signal qui arrête de façon fiable la dépense de votre backend sur un compte remboursé.

SignalOù il vitSe déclenche quandFaites-y confiance pour
revocationDate sur une transactionAppareil, StoreKit 2Votre app lit la transactionVous dire qu'un achat précis a été remboursé ou révoqué
Transaction.updatesAppareil, StoreKit 2Un remboursement arrive pendant que l'app tourne, ou au lancement si vous écoutezRéagir sur-le-champ pour un client présent
currentEntitlementsAppareil, StoreKit 2Vous vérifiez ce que le client possède maintenantFiltrer l'accès sans suivre vous-même les remboursements
Notification REFUNDVotre serveur, App Store Server Notifications V2Apple traite le remboursement, app ouverte ou nonArrêter la dépense côté serveur sur un client qui ne revient jamais

Câblez les signaux de l'appareil pour le client qui tient le téléphone, et la notification serveur pour celui qui ne le tient pas. Le remboursement apparaît aux deux endroits à dessein. N'en lire qu'un seul, c'est ainsi qu'un compte remboursé continue de vous coûter après que la vente est déjà partie.

Questions fréquentes

Comment détecter un remboursement dans StoreKit 2 ?
Vérifiez la revocationDate de la transaction. Elle vaut nil pour un achat valable et contient une date dès que l'App Store rembourse la transaction, donc une revocationDate non nil est le signal qu'un achat a été remboursé ou révoqué.
Quelle est la différence entre revocationDate et revocationReason ?
revocationDate est le moment où l'App Store a repris l'achat, et revocationReason en est le pourquoi. Le motif est developerIssue quand le client a signalé un problème dans votre app et other pour tout le reste.
Un achat remboursé apparaît-il encore dans currentEntitlements ?
Non. Transaction.currentEntitlements exclut les achats que l'App Store a remboursés ou révoqués, donc un produit remboursé sort de lui-même des droits du client, ce qui en fait un filtre sûr pour l'accès.
StoreKit préviendra-t-il mon app d'un remboursement survenu pendant que l'app était fermée ?
Seulement si vous écoutez dès le lancement. Transaction.updates livre ces changements une seule fois au démarrage, donc vous devez lancer un Task qui la parcourt au démarrage de votre app, sinon le remboursement est manqué jusqu'à ce qu'autre chose le réconcilie.
La détection des remboursements côté client suffit-elle à elle seule ?
Non. L'appareil n'apprend un remboursement que pendant que votre app tourne, donc un client qui ne rouvre jamais l'app lui est invisible. Le REFUND des App Store Server Notifications V2 est le signal qui vous parvient de toute façon.
Une revocationDate signifie-t-elle toujours que le client a été remboursé ?
Non. revocationDate est aussi définie quand un client perd un achat via Family Sharing, donc une date non nil signifie qu'il n'a plus l'achat mais pas toujours que de l'argent a été rendu.

Sources et lectures complémentaires

RefundHalt

Le pilote automatique des remboursements pour l'App Store et Google Play

Poursuivre la lecture

La prochaine demande de remboursement est déjà en route.

Configurez RefundHalt dans le temps qu'il vous faudrait pour lire un nouvel e-mail d'assistance au sujet d'un remboursement que vous n'avez pas pu contester.