Lors des tests IAP avant mise en ligne, nous avons rencontré un problème classique mais très déroutant :

  • L’utilisateur appuie sur « Continuer l’achat »
  • Le bouton passe à « Traitement en cours »
  • Quelques secondes plus tard, il revient à « Continuer l’achat »
  • Aucune fenêtre de confirmation d’achat App Store ne s’affiche

Cet article retrace le chemin complet d’investigation, les logs clés, la cause racine et une stratégie de correction applicable avant le lancement.

1. Symptôme

La liste des produits se chargeait correctement sur l’écran d’achat, mais après le clic :

  • purchase() était bien appelé
  • Aucun dialogue système n’apparaissait
  • L’UI revenait finalement à l’état non acheté

À première vue, cela ressemblait à un « achat non déclenché ». Les logs ont montré autre chose.

2. Première étape : vérifier que le flux d’achat s’exécute vraiment

Nous avons ajouté des logs sur tous les points critiques du flux :

  • Paramètres au clic sur le bouton
  • Entrée dans purchaseSelectedProduct() et branches guard
  • Avant/après product.purchase()
  • Transaction.updates
  • refreshEntitlements()
  • Transaction.currentEntitlements
  • Transaction.latest(for:)

Résultat :

  • purchase() renvoyait .success
  • La transaction était vérifiée, et transaction.finish() était bien exécuté
  • Mais entitledIDs restait vide

Conclusion : le problème n’était pas « l’achat ne démarre pas », mais « les droits ne sont pas reconnus après l’achat ».

3. Constats décisifs après approfondissement

Avec un logging plus poussé, plusieurs signaux clés sont apparus :

  1. Le nombre d’itérations de Transaction.currentEntitlements était 0
  2. Transaction.latest(for: monthly) renvoyait un enregistrement valide, mais expiré
  3. Plus important : des transactions anciennes existaient dans Transaction.unfinished avant l’achat
  4. Le purchase() actuel renvoyait une transaction de l’ancien flux, pas d’un nouvel achat interactif

Cela explique pourquoi « aucune popup » :

  • StoreKit renvoie d’abord une ancienne transaction traitable (unfinished / chaîne historique)
  • Le code la termine via finish()
  • La requête semble « réussie », mais ne passe pas par la nouvelle confirmation d’achat attendue par l’utilisateur

4. Résumé de la cause racine

Ce n’était pas un problème isolé, mais la combinaison de deux facteurs :

  • Des transactions inachevées (unfinished transaction) qui perturbent le flux d’achat
  • Une chaîne d’abonnement sandbox en état expiré (même avec latest transaction, ce n’est pas forcément un entitlement actif)

Comportement final : pas de popup, succès apparent, droits toujours vides.

5. Stratégie de correction (validée en pratique)

5.1 Nettoyer proactivement les transactions unfinished avant achat

Au démarrage de l’app et avant l’achat, parcourir Transaction.unfinished et exécuter finish() sur les transactions vérifiées, pour éviter qu’une ancienne transaction intercepte le nouveau flux d’achat.

5.2 Conserver et renforcer les logs de diagnostic (recommandé avant mise en ligne)

Conserver ces logs jusqu’au lancement :

  • Snapshot de unfinished transaction
  • subscription status (state, renewal info)
  • Nombre d’itérations de currentEntitlements
  • Détails transaction/expiration de latest(for:)

5.3 Vérifier les droits via un double canal (renforcement optionnel)

Si currentEntitlements est temporairement vide dans certains environnements, utiliser latest(for:) en fallback (en validant toujours le statut actif).

6. Checklist IAP avant lancement

  1. Avant chaque test, vérifier l’état de l’abonnement du compte Sandbox (expiré ou non)
  2. Logger le nombre de unfinished sur les chemins critiques
  3. Tester trois scénarios : « premier achat / renouvellement après expiration / restauration d’achat »
  4. Vérifier la cohérence entre currentEntitlements et latest
  5. En cas de « pas de popup », vérifier d’abord si un ancien flux transactionnel a été déclenché

7. Conclusion

« Cliquer sur acheter sans popup » n’est pas forcément un problème d’UI ni de timing d’appel.
Avec StoreKit 2, l’état des transactions, la file unfinished et le cycle de vie de l’abonnement influencent directement le comportement final.

Ajouter ces logs et ce nettoyage avant lancement permet d’éviter de nombreux problèmes IAP difficiles à reproduire en production.