React-native-iap: IOS vérifie si l'abonnement au renouvellement automatique est toujours actif

Créé le 27 sept. 2018  ·  61Commentaires  ·  Source: dooboolab/react-native-iap

Version de react-native-iap

2.2.2

Les plates-formes sur lesquelles vous avez rencontré l'erreur (IOS ou Android ou les deux ?)

IOS

Comportement attendu

En utilisant l'API RNIAP, nous pourrions vérifier si un abonnement à renouvellement automatique est toujours actif

Comportement réel

Sur Android, je fais ceci pour vérifier que :

export const isUserSubscriptionActive = async (subscriptionId) =>{
    // Get all the items that the user has
    const availablePurchases = await getAvailablePurchases();
    if(availablePurchases !== null && availablePurchases.length > 0){
        const subscription = availablePurchases.find((element)=>{
            return subscriptionId === element.productId;
        });
        if(subscription){
             // check for the autoRenewingAndroid flag. If it is false the sub period is over
              return subscription["autoRenewingAndroid"] == true;
            }
        }else{
            return false;
        }
    }
}

Sur ios, il n'y a pas d'indicateur pour vérifier cela, et la méthode getAvailablePurchases renvoie tous les achats effectués, même les abonnements qui ne sont pas actifs pour le moment.

Y a-t-il un moyen de vérifier cela?

Salutations,
Marcos

📱 iOS 🚶🏻 stale

Commentaire le plus utile

Voici la fonction que j'utilise qui fonctionne à la fois sur Android et iOS, en triant correctement sur iOS pour nous assurer que nous obtenons les dernières données de réception :

import * as RNIap from 'react-native-iap';
import {ITUNES_CONNECT_SHARED_SECRET} from 'react-native-dotenv';

const SUBSCRIPTIONS = {
  // This is an example, we actually have this forked by iOS / Android environments
  ALL: ['monthlySubscriptionId', 'yearlySubscriptionId'],
}

async function isSubscriptionActive() {
  if (Platform.OS === 'ios') {
    const availablePurchases = await RNIap.getAvailablePurchases();
    const sortedAvailablePurchases = availablePurchases.sort(
      (a, b) => b.transactionDate - a.transactionDate
    );
    const latestAvailableReceipt = sortedAvailablePurchases[0].transactionReceipt;

    const isTestEnvironment = __DEV__;
    const decodedReceipt = await RNIap.validateReceiptIos(
      {
        'receipt-data': latestAvailableReceipt,
        password: ITUNES_CONNECT_SHARED_SECRET,
      },
      isTestEnvironment
    );
    const {latest_receipt_info: latestReceiptInfo} = decodedReceipt;
    const isSubValid = !!latestReceiptInfo.find(receipt => {
      const expirationInMilliseconds = Number(receipt.expires_date_ms);
      const nowInMilliseconds = Date.now();
      return expirationInMilliseconds > nowInMilliseconds;
    });
    return isSubValid;
  }

  if (Platform.OS === 'android') {
    // When an active subscription expires, it does not show up in
    // available purchases anymore, therefore we can use the length
    // of the availablePurchases array to determine whether or not
    // they have an active subscription.
    const availablePurchases = await RNIap.getAvailablePurchases();

    for (let i = 0; i < availablePurchases.length; i++) {
      if (SUBSCRIPTIONS.ALL.includes(availablePurchases[i].productId)) {
        return true;
      }
    }
    return false;
  }
} 

Tous les 61 commentaires

Je travaille également à comprendre cela. Je ne l'ai pas encore testé, mais d'après ce que je lis, je comprends que c'est possible en créant un "secret partagé" dans iTunes Connect et en le transmettant à validerReceiptIos avec la clé "mot de passe". Je pense que cela renverra alors un objet JSON qui contiendra un code indiquant l'état de validation de l'abonnement et les clés last_receipt, last_receipt_info et last_expired_receipt info, entre autres, que vous pouvez utiliser pour déterminer l'état de l'abonnement. Je suis littéralement en train de comprendre cela, donc je ne l'ai pas encore testé. C'est ce que je rassemble à partir des problèmes et des documents d'Apple. Si cela fonctionne, cela devrait être très clair dans la documentation au lieu d'être enterré dans les problèmes.
Je pense que les liens suivants sont pertinents :
https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html

203 #237

EDIT : Je peux confirmer que le processus que j'ai mentionné ci-dessus fonctionne. Je pense que nous devrions travailler pour obtenir une explication complète de cela dans les documents. J'implémenterais quelque chose comme ceci au lancement de l'application pour déterminer si un abonnement a expiré :

RNIap.getPurchaseHistory()
        .then(purchases => {
                RNIap.validateReceiptIos({
                   //Get receipt for the latest purchase
                    'receipt-data': purchases[purchases.length - 1].transactionReceipt,
                    'password': 'whateveryourpasswordis'
                }, __DEV__)
                    .then(receipt => {
                       //latest_receipt_info returns an array of objects each representing a renewal of the most 
                       //recently purchased item. Kinda confusing terminology
                        const renewalHistory = receipt.latest_receipt_info
                       //This returns the expiration date of the latest renewal of the latest purchase
                        const expiration = renewalHistory[renewalHistory.length - 1].expires_date_ms
                       //Boolean for whether it has expired. Can use in your app to enable/disable subscription
                        console.log(expiration > Date.now())
                    })
                    .catch(error => console.log(`Error`))
        })

@kevinEsherick

Brillant!

Juste quelques choses qui ne sont pas si triviales à propos de votre code ;

  1. Ça devrait être

const expiration = renouvellementHistory [renewalHistory.length - 1].expires_date_ms

au lieu de (probablement juste une faute de frappe)

const expiration = lastRenewalReceipt [latestRenewal.length - 1].expires_date_ms

2) Pour ceux qui ne savent pas ce qu'est 'password' : 'whateveryourpasswordis' :

Vous pouvez créer votre clé secrète partagée pour vos paments In App et l'utiliser sur votre application

Les étapes sont ici : https://www.appypie.com/faqs/how-can-i-get-shared-secret-key-for-in-app-purchase

+1 pour mettre à jour la doc, aussi, peut-être que @dooboolab vous pouvez séparer les cas d'utilisation dans IOS et Android, il semble qu'il y ait quelques choses différentes. Je peux t'aider si tu veux.

Salutations

@kevinEsherick

Que se passe-t-il avec les abonnements à renouvellement automatique ?

Il semble que expires_date_ms soit la première date d'expiration, elle n'est pas mise à jour

J'ai testé ceci :

  1. S'abonner à un élément d'abonnement
  2. Vérifiez la date d'expiration (comme 5 minutes après)
  3. Attend 10 minutes
  4. L'abonnement toujours actif semble être un abonnement à renouvellement automatique
  5. Si je vérifie que l'expiration du dernier reçu de renouvellement est toujours la même qu'au point 2

Des idées?

Mon mauvais, la date d'expiration semble être mise à jour, cela prend juste un certain temps.

Je laisserai ce commentaire ici au cas où quelqu'un serait dans le même scénario.

Salutations

  1. Ça devrait être

const expiration = renouvellementHistory [renewalHistory.length - 1].expires_date_ms

au lieu de (probablement juste une faute de frappe)

const expiration = lastRenewalReceipt [latestRenewal.length - 1].expires_date_ms

Oups, vous avez raison, c'était une faute de frappe. J'avais changé le nom pour rendre sa signification plus évidente mais j'avais oublié de changer toutes les occurrences. Je l'ai édité, merci.

Et je n'ai pas eu ce scénario exact avec la date d'expiration, mais il y avait des trucs sans renouvellement automatique du tout, bien qu'il se soit résolu tout seul (ce qui m'inquiète, mais je ne peux pas reproduire, alors je ne sais pas quoi faire d'autre). Le problème que vous avez rencontré est assez courant. C'est un comportement attendu de la part d'Apple, vous devez donc laisser une période tampon pour que l'abonnement se renouvelle avant de désabonner l'utilisateur. Ce problème sur SO explique plus en détail : https://stackoverflow.com/questions/42158460/autorenewable-subscription-iap-renewing-after-expiry-date-in-sandbox

Merci! Excellente information, pourrait être la raison

À propos de ce problème, je pense que le code @kevinEsherick devrait être sur la doc C'est un excellent moyen de vérifier les abonnés !

Oui. J'apprécierais un doc soigné en readme si l'un d'entre vous demande un PR . Merci.

Salut les gars. J'ai ajouté une section Q & A dans le fichier readme. J'espère que l'un d'entre vous pourra nous donner un PR pour ce problème.

Je vais chercher à faire un PR dans les prochains jours :)

Je cherche également à utiliser cette bibliothèque spécifiquement pour les abonnements. À la fois iOS et Android. Documenter la bonne façon de déterminer si un utilisateur a un abonnement valide serait grandement apprécié. ??

@curiousdustin L'implémentation d'Apple à ce sujet est vraiment terrible si vous vous référez au support . Étant donné que vous devez gérer ce genre de chose dans votre backend, nous ne pouvions pas le prendre entièrement en charge dans notre module. Cependant, vous pouvez obtenir availablePurchases dans Android en utilisant notre méthode getAvailablePurchases qui ne fonctionnera pas sur le produit d'abonnement ios.

Alors, êtes-vous en train de dire que la solution sur laquelle travaillent @ marcosmartinez7 et @kevinEsherick dans ce fil n'est PAS une solution et qu'un serveur principal autre que celui d'Apple est nécessaire pour confirmer le statut d'un abonnement iOS ?

À la lecture de l'article Medium que vous avez publié avec les documents Apple, il semble que l'utilisation d'un serveur soit préférable, mais il n'est pas impossible de déterminer le statut de l'abonnement avec l'appareil uniquement.

Désolé d'avoir raté des informations dans mon écriture. Je vais gérer ça maintenant.
Apple suggère de sauvegarder le receipt sur votre propre serveur pour éviter une attaque intermédiaire. Par conséquent, vous pouvez vérifier ultérieurement ce reçu pour savoir si le reçu est valide ou si l'abonnement est toujours disponible.

Cependant, il n'est pas impossible de continuer à essayer avec react-native-iap puisque nous fournissons la récupération directe de la validation du reçu sur le propre serveur Apple (bac à sable et production) qui, heureusement, a fonctionné avec @marcosmartinez7 et @kevinEsherick .

Je pense qu'actuellement, c'est la solution à adopter pour vérifier l'abonnement ios .

Hé les gars, désolé ça fait plus de quelques jours, j'ai été pris dans le travail mais je vais quand même soumettre ce PR pour mettre à jour le README. @curiousdustin, la méthode dont j'ai parlé ci-dessus fonctionne définitivement et je l'ajouterai à la documentation. Je soumettrai le PR ce soir ou demain :)

Merci la mise à jour.

Une autre chose que j'ai remarquée dans votre exemple.

« données de réception » : achats[purchases.length - 1].transactionReceipt,
...
const expiration = renouvellementHistory[renewalHistory.length - 1].expires_date_ms

Dans mes tests, les achats et l'historique de renouvellement ne sont pas nécessairement dans un ordre particulier. N'aurions-nous pas besoin de les trier ou de quelque chose pour vérifier que nous utilisons celui que nous avons l'intention de faire ?

Pareil ici. Les achats et l'historique de renouvellement ne sont définitivement pas commandés, nous devons donc d'abord trier. En développement, cela finit par être beaucoup d'itérations.

Voici la fonction que j'utilise qui fonctionne à la fois sur Android et iOS, en triant correctement sur iOS pour nous assurer que nous obtenons les dernières données de réception :

import * as RNIap from 'react-native-iap';
import {ITUNES_CONNECT_SHARED_SECRET} from 'react-native-dotenv';

const SUBSCRIPTIONS = {
  // This is an example, we actually have this forked by iOS / Android environments
  ALL: ['monthlySubscriptionId', 'yearlySubscriptionId'],
}

async function isSubscriptionActive() {
  if (Platform.OS === 'ios') {
    const availablePurchases = await RNIap.getAvailablePurchases();
    const sortedAvailablePurchases = availablePurchases.sort(
      (a, b) => b.transactionDate - a.transactionDate
    );
    const latestAvailableReceipt = sortedAvailablePurchases[0].transactionReceipt;

    const isTestEnvironment = __DEV__;
    const decodedReceipt = await RNIap.validateReceiptIos(
      {
        'receipt-data': latestAvailableReceipt,
        password: ITUNES_CONNECT_SHARED_SECRET,
      },
      isTestEnvironment
    );
    const {latest_receipt_info: latestReceiptInfo} = decodedReceipt;
    const isSubValid = !!latestReceiptInfo.find(receipt => {
      const expirationInMilliseconds = Number(receipt.expires_date_ms);
      const nowInMilliseconds = Date.now();
      return expirationInMilliseconds > nowInMilliseconds;
    });
    return isSubValid;
  }

  if (Platform.OS === 'android') {
    // When an active subscription expires, it does not show up in
    // available purchases anymore, therefore we can use the length
    // of the availablePurchases array to determine whether or not
    // they have an active subscription.
    const availablePurchases = await RNIap.getAvailablePurchases();

    for (let i = 0; i < availablePurchases.length; i++) {
      if (SUBSCRIPTIONS.ALL.includes(availablePurchases[i].productId)) {
        return true;
      }
    }
    return false;
  }
} 

@andrewze

Sur le scénario Android, si l'abonnement est auto-renouvelable, il sera retourné sur les achats disponibles

@curiousdustin @andrewzey Vous avez raison, les achats ne sont pas commandés. renouvellementHistory/latest_receipt_info, cependant, est toujours commandé pour moi. Je me souviens avoir réalisé que les achats n'avaient pas été commandés, mais je pensais avoir trouvé une raison pour laquelle ce n'était pas vraiment un problème. Je vais voir ça dans quelques heures une fois rentré chez moi. Je ne veux pas soumettre de RP tant que nous n'avons pas compris.
EDIT: Donc, la raison pour laquelle je ne pense pas que vous ayez besoin de trier les achats est que expires_date_ms renvoie le même quel que soit l'achat que vous avez interrogé. Ce n'est pas pareil pour vous les gars ? Ou avez-vous besoin d'informations à trier ? Laisse moi savoir ce que tu penses. Je suis d'accord pour dire que les documents doivent toujours préciser que les achats ne sont pas classés par ordre chronologique, mais pour autant que je sache, le tri n'est pas nécessaire pour obtenir la date d'expiration.

@kevinEsherick Merci ! Dans mon cas, ni les achats ni l'historique de renouvellement n'ont été commandés.

@kevinEsherick @andrewzey Pouvez-vous donner PR sur cette solution ? Nous aimerions partager avec d'autres qui souffrent.

Bien sûr, je vais le préparer juste après que nous ayons terminé le lancement de notre application publique =).

J'attendais plus de conversation concernant mon dernier commentaire. J'ai soulevé quelques problèmes qui restent en suspens. S'il vous plaît voir ci-dessus pour référence. Une fois que nous aurons réglé cela, l'un de nous pourra faire un PR.

@kevinEsherick Ah désolé, j'ai raté ça.

expires_date_ms est définitivement unique pour moi sur chaque reçu de renouvellement. J'ai peut-être mal compris ? J'ai remarqué que le tableau de reçus de renouvellement attaché à chaque reçu décodé est le même quel que soit le reçu sélectionné.

const sortedAvailablePurchases = availablePurchases.sort(
      (a, b) => b.transactionDate - a.transactionDate
    );

Mais je l'ai quand même fait pour essayer d'être précis et pour compenser d'éventuelles divergences dans la réponse réelle de l'API par rapport aux documents d'Apple. Je ne pense pas qu'une opération de tri y soit très coûteuse, étant donné le nombre total de reçus que vous êtes susceptible de rencontrer pour les abonnements à renouvellement automatique.

Un ETA sur le PR? J'envisage de quitter ce package car il ne gère pas du tout ce cas.

@schumannd Étant donné que le PR ne servirait qu'à mettre à jour les documents, vous pouvez maintenant passer à react-native-iap .

J'ai confirmé que mon extrait de code fonctionne en fait sans problème en production avec notre application récemment lancée : https://itunes.apple.com/us/app/rp-diet/id1330041267?ls=1&mt=8

EDIT Je vais certainement faire le PR (je l'ai dans ma liste "Give back to OSS" dans notre Trello), mais après la folie du week-end du vendredi noir

@schumannd Je @andrewzey a dit. Tout ce qu'un PR dirait est contenu ici dans ce post. J'avais l'intention de m'y atteler, mais il nous a fallu un certain temps pour déterminer exactement ce qui devait être fait, et cela, mélangé aux voyages et aux heures de démarrage mouvementées, signifie que je n'ai pas encore eu le temps de le faire. Je prévois toujours de le faire bientôt, mais n'importe qui d'autre peut intervenir en attendant s'il le souhaite. Et @andrewzey bravo mec ! Ça a l'air super, je vais le télécharger!

Je ne suis pas convaincu que ce fil documente avec précision la bonne façon de gérer les abonnements à renouvellement automatique dans iOS, mais décrit plutôt un flux de travail "de contournement" selon l'API exposée par cette bibliothèque. Cependant, il ne s'agit que de spéculations de ma part et je suis bien conscient que je peux me tromper, j'aimerais donc avoir des commentaires sur les observations que j'ai décrites ci-dessous.

J'ai recherché la bonne façon de gérer les abonnements à renouvellement automatique dans React Native pendant des semaines en vain, j'ai donc changé de vitesse et commencé à rechercher comment les gérer de manière native dans Objective-C / Swift. Cela m'a donné un point de référence pour comparer les API StoreKit natives et l'utilisation documentée à ce qui a été implémenté dans cette bibliothèque.

Cet article est la meilleure explication que j'ai trouvée jusqu'à présent sur la façon dont l'achat, la validation et la restauration d'abonnement doivent être gérés par une application iOS native : https://www.raywenderlich.com/659-in-app-purchases-auto -renewable-subscriptions-tutorial et bien que toutes les API soient utilisées dans react-native-iap , elles implémentent un workflow différent.

Une différence clé que je vois est l'utilisation de appStoreReceiptUrl .

StoreKit fournit le appStoreReceiptUrl qui, à ma connaissance, est le bon moyen de charger les informations d'achat/de réception actuelles pour une application iOS. Bien que je voie cela utilisé et référencé dans le code react-native-iap , il semble que ce soit dans un contexte différent de celui du flux de travail documenté dans l'article référencé.

Je ne sais pas si/comment cette différence d'implémentation est problématique, mais l'API react-native-iap semble créer une dépendance inutile sur le restoreCompletedTransactions StoreKit qui est finalement appelé par getPurchaseHistory .

Il me semble que l'API react-native-iap devrait prendre en charge le workflow suivant pour iOS :

  • Essayez de charger le reçu de l'application actuelle (via appStoreReceiptUrl ) au démarrage de l'application
  • Offrir la possibilité de valider le reçu
  • Si appStoreReceiptUrl est nul, lancez une "restauration" sur le périphérique actuel qui définira alors la valeur de appStoreReceiptUrl et la logique de validation peut être réessayée

Des pensées ?

Encore une fois, je pourrais mal comprendre l'implémentation de cette bibliothèque ou l'implémentation native référencée.

@sellmeadog

Je pense qu'un point clé qui est également mentionné dans l'article que vous avez lié est le suivant :

Remarque : l'application du projet exécute SelfieService, qui accepte les reçus et les télécharge directement sur le service de validation des reçus d'Apple. Il s'agit d'une « triche » pour que le didacticiel reste concentré sur la configuration des abonnements. Dans une vraie application, vous devez utiliser un serveur distant pour cela, pas l'application.

Il m'a fallu un certain temps pour comprendre tout le problème de l'abonnement auto-renouvelable, mais d'après ce que je comprends, la meilleure façon de le gérer est d'utiliser votre propre backend comme source unique de vérité :

  • envoyer toutes les données de transaction immédiatement à votre backend
  • fournir des moyens de renvoi en cas de problèmes de transmission (par exemple, restauration d'achats)
  • périodiquement (par exemple une fois par jour) vérifier tous les reçus stockés sur votre backend
  • l'application interroge le backend pour l'état de l'abonnement, par exemple, le retour de l'arrière-plan.

Cela me semblait la seule façon saine de gérer les abonnements. J'ai également vérifié ce processus avec d'autres développeurs utilisant des abonnements auto-renouvelables dans le commerce depuis un certain temps.

Ma réponse à la question OP serait donc : vérifiez périodiquement les abonnements dans le backend, pas dans le frontend

@schumannd

Je ne suis pas sûr que nous comprenions ou parlions du même problème.

La validation du reçu doit être effectuée sur le serveur. Selon la propre documentation d'Apple :

N'appelez pas le point de terminaison du serveur App Store /verifyReceipt depuis votre application.

J'ai interprété la "triche" dans l'article référencé comme spécifiquement les appels directs de l'exemple d'application à /verifyReceipt directement à partir de l'application par souci de concision au lieu de déléguer à un serveur principal.

Il me semble qu'Apple / l'App Store est et doit être l'unique source de vérité. Apple traite en fin de compte les achats, les renouvellements, les annulations, etc. et génère des reçus en conséquence et rend ces données disponibles via les API StoreKit .

react-native-iap gère l'achat et la restauration des achats sans problème, mais je pense toujours qu'il y a un écart dans l'accès aux données de reçu de l'App Store qui sont disponibles localement à appStoreReceiptUrl qui est encore une fois le moyen documenté d'Apple d'accéder au reçu données :

Pour récupérer les données du reçu, utilisez la méthode appStoreReceiptURL de NSBundle pour localiser le reçu de l'application...

Le flux de travail que je comprends en lisant les documents Apple et en référençant les implémentations natives d'Object-C / Swift est le suivant :

  • Charger les produits à acheter
  • Acheter un produit (consommable, non consommable, abonnement, peu importe)
  • Un reçu pour cet achat est stocké localement à appStoreReceiptUrl par StoreKit dans les coulisses
  • L'application doit envoyer les données de réception à un serveur principal (pas directement à l'App Store /verifyReceipt ) pour validation si nécessaire
  • appStoreReceiptUrl est mis à jour par StoreKit chaque fois que des annulations et/ou des renouvellements se produisent (en supposant que les délégués StoreKit appropriés soient enregistrés correctement)
  • Lorsque appStoreReceiptUrl est nul, parce que l'utilisateur est sur un nouvel appareil, etc., utilisez le workflow d'achat de restauration qui récupérera les données de reçu actuelles d'Apple / AppStore et StoreKit le conservera localement à appStoreReceiptUrl dans les coulisses

Encore une fois, react-native-iap gère tout cela, à l'exception du chargement des données de reçu à partir de appStoreReceiptUrl . Je pense qu'une méthode getReceipt qui demandait le reçu local allégerait le fardeau de la gestion des abonnements et/ou de tout autre achat.

Apple a rendu cela beaucoup plus difficile à raisonner qu'il ne devrait l'être.

appStoreReceiptUrl est mis à jour par StoreKit chaque fois que des annulations et/ou des renouvellements se produisent (en supposant que les délégués StoreKit appropriés sont enregistrés correctement)

Cela décrit le problème que j'ai avec cette approche : vous n'êtes pas informé de tout renouvellement / annulation / expiration, etc. si l'utilisateur n'ouvre pas votre application par la suite avec une connexion Internet fonctionnelle.

En dehors de cela, cela semble être une bonne alternative à l'approche que j'ai décrite. Je me demande lequel est le plus utilisé pour les applications de production.

@sellmeadog Je pense que c'est la même idée que j'ai postée ici : #356
La validation des reçus localement est une méthode, recommande Apple. C'est complètement indépendant de toute requête serveur/réseau.

Ouais. Il y a des inconvénients concernant "pas de connexion réseau - pas de validation".
Mais il existe des cas d'utilisation pour cela.
Je veux une validation rapide du reçu à chaque lancement d'application et je ne veux pas suivre moi-même les achats intégrés via UserDefaults ou Keychain . StoreKit fournit déjà un mécanisme de stockage.

Comment êtes-vous censé vérifier si l'utilisateur IOS a renouvelé ou annulé son abonnement sans l'inviter à se connecter au compte iTunes via quelque chose comme getAvailablePurchases ?

Par example:
Lors du premier achat d'abonnement, nous validons le reçu et enregistrons la date d'expiration dans leur profil Firebase. Nous utilisons ce nœud Firebase pour vérifier au démarrage s'ils ont un abonnement actif. Le problème survient lorsque cette date d'expiration est dépassée et que la nouvelle période de facturation commence. Comment pouvons-nous vérifier s'ils se sont renouvelés ou annulés automatiquement ? Comme mentionné, nous utilisions getAvailablePurchases, mais cela incite la connexion iTunes comme le ferait un bouton de restauration - ce qui est une interface utilisateur terrible

Je me demande aussi la même chose.

Dans notre application, nous n'en avons pas besoin, car nous avons une API côté serveur qui communique directement avec Apple pour vérifier l'état. Après l'achat initial, les données du reçu sont stockées sur nos serveurs, puis utilisées ultérieurement pour demander le statut mis à jour.

Cependant, je pense que cela devrait toujours être possible du côté de l'appareil. Je ne suis pas un expert en la matière, mais d'après ce que je comprends, l'application recevra une mise à jour des transactions/reçus du système d'exploitation chaque fois qu'un renouvellement aura lieu. Je soupçonne que ce plugin gère ces reçus au lancement de l'application. (Je vois un tas de journaux lors de l'exécution à partir de Xcode, qui semblent traiter plusieurs transactions) MAIS, d'après ce que je peux dire, il n'y a aucun moyen d'exploiter ces événements dans le code JS.

Pourrions-nous avoir plus d'informations à ce sujet ?

@csumrell @curiousdustin avez-vous ce comportement confirmé dans un environnement de production ? Je me demande si cela pourrait simplement se produire à cause du sandbox, le compte sandbox devant se connecter car il y a probablement un autre compte non sandbox sur votre appareil, alors qu'en production, l'utilisateur sera vraisemblablement connecté à son compte normal et cela module peut être en mesure d'obtenir des achats sans demander de connexion.

Désolé, je n'ai PAS confirmé que getAvailablePurchases() provoque des invites de mot de passe en production.

J'ai juste supposé que ce serait le cas parce que la documentation parle de cette méthode utilisée pour la fonctionnalité communément appelée restauration des achats, qui, d'après mon expérience, implique une authentification, au moins sur iOS.

@hyochan @JJMoon Pourrions-nous avoir plus d'informations à ce sujet également ? Plus précisément, existe-t-il un moyen d'obtenir de manière fiable des achats disponibles ou historiques sans que le système d'exploitation ne tente une authentification ? Veuillez également consulter mes commentaires sur la question 2 ci-dessus, concernant la capacité de réagir aux achats détectés et traités au lancement de l'application.

@curiousdustin oui, donc lorsque je me déconnecte de mon compte iTunes normal et que je me connecte au compte sandbox, il ne m'invite plus à me connecter lorsque cette méthode est appelée. Donc, je suppose / j'espère qu'en production, où les utilisateurs n'ont pas de compte sandbox en cours d'utilisation, le problème ne devrait pas apparaître. Les tests bêta devraient fonctionner de la même manière que la production car les utilisateurs n'ont pas de comptes sandbox configurés sur l'appareil, c'est juste un problème des appareils de développement. Je vais donc tester ça demain avec les utilisateurs de la bêta et vous dire ce que je trouve.
EDIT : je peux confirmer que cela se produit sur les appareils non sandbox. C'est une UX terrible et c'est assez problématique. Quelqu'un, des idées ?

Ainsi, lorsque vous validez sur votre propre serveur en appelant le point de terminaison du serveur App Store/verifyReceipt à partir de votre application, il renverra toujours les dernières informations de mise à jour à jour pour cet utilisateur avec uniquement le premier reçu d'achat d'origine.

Est-ce que quelqu'un sait si l'utilisation de la méthode de validation locale renverra également toujours les informations les plus récentes ? Ou est-ce strictement pour ce récépissé avec lequel vous le validez localement ?

Aussi, est-ce que quelqu'un a des conseils pour configurer son propre serveur à valider avec Apple ? je n'ai pas d'expérience avec ça

@csumrell, vous pouvez obtenir le reçu du dernier achat en appelant getPurchaseHistory et en le triant. Les commentaires ci-dessus expliquent comment procéder. Cela devrait avoir les informations les plus à jour.

Et je suis sur le point d'explorer la possibilité de valider sur le serveur afin que nous puissions nous entraider. J'adorerais entendre comment quelqu'un d'autre s'y est pris.

@kevinEsherick oui mais getPurchaseHistory déclenchera la fenêtre contextuelle de connexion Itunes. Je parle d'enregistrer le reçu d'origine dans le stockage local lorsque l'utilisateur achète pour la première fois. Ensuite, au lieu d'appeler getPurchaseHistory pour vérifier la prochaine facturation, appelez validateReceiptIos(originalReceipt) qui est validé localement. Cependant, je ne sais pas si validateReceiptIos renvoie les transactions à jour comme le ferait le serveur Apple / verifyReceipt

@csumrell Gotcha. Eh bien, c'est rapidement et facilement testable. Vous ne l'avez pas essayé ? Sinon, je le vérifierai plus tard dans la journée.
EDIT: @csumrell validateReceiptIos suit en fait les derniers renouvellements automatiques de cet achat, il s'agit donc d'une méthode valide. Je dirais que c'est mieux que les méthodes proposées ci-dessus pour vérifier les abonnements, car cela évite l'invite de connexion iTunes (à moins que quelqu'un puisse trouver un correctif pour cela). L'inconvénient est que vous devez stocker les reçus localement. Cela ne devrait pas être trop compliqué. De plus, vous ne voudrez peut-être pas que ces reçus soient envoyés à votre backend pour des raisons de sécurité ou du fait qu'ils sont volumineux (environ 60 000 caractères), cela signifierait donc que si un utilisateur abonné se déconnecte ou commence à utiliser un nouvel appareil, vous devrez invitez-les à se connecter pour obtenir leur dernier reçu d'achat. Vous pouvez stocker leur statut d'abonnement en interne afin de n'inviter que les utilisateurs qui se sont abonnés la dernière fois que vous avez vérifié.

@hyochan a financé 20,00 $ pour ce problème. Voir sur IssueHunt

@hyochan quelle solution spécifique

@kevinEsherick

Financez-vous quelqu'un pour rassembler ces solutions en un seul PR concis au fichier README ?

Oui. C'est tout à fait exact. De plus, je voulais juste tester comment issue hunt pouvait fonctionner hors de la boîte. Mon prochain plan est de financer la création de cas de test.

@kevinEsherick Vous avez raison, la validation locale avec validateReceiptIos renvoie en fait les dernières informations sur l'abonnement en utilisant uniquement le premier ID de transaction d'origine (stocké dans l'état redux)

J'utiliserai cette méthode pour éviter d'avoir à configurer mon propre serveur et pour éviter la fenêtre contextuelle de connexion iTunes lors de la vérification si l'abonnement est actif.

Une note pour ceux qui envisagent d'utiliser ce flux de validation que @csumrell et moi-même avons présenté : j'ai constaté qu'en cours de développement, l'invite de connexion iTunes apparaît toujours lors du premier lancement. Cela ne se produit pas en production. Tant que vous suivez les étapes que nous avons décrites, tout devrait bien se passer. Quant à savoir pourquoi il fait cela, peut-être que lorsque iOS voit qu'IAP est utilisé/fait partie de la carte de module en développement, il invite automatiquement à se connecter. Je ne suis pas tout à fait sûr, mais il semble que ce ne soit qu'une fonctionnalité des tests sandbox.

Salut, il semble qu'il n'y ait eu aucune activité sur ce problème récemment. Le problème a-t-il été résolu ou nécessite-t-il toujours l'attention de la communauté ? Ce problème peut être fermé si aucune autre activité ne se produit. Vous pouvez également étiqueter ce problème comme « Pour discussion » ou « Bon premier numéro » et je le laisserai ouvert. Merci pour vos contributions.

Fermeture de ce problème après une période d'inactivité prolongée. Si ce problème est toujours présent dans la dernière version, n'hésitez pas à créer un nouveau problème avec des informations à jour.

Peut-être que tout ce flux a besoin d'être clarifié dans la documentation ?

Surtout quand il s'agit de l'abonnement à renouvellement automatique ? Il est très difficile pour un nouveau venu de savoir si vous pouvez vérifier le expires_date_ms pour savoir si l'abonnement est toujours actif ou non ?

Je suggère que le meilleur endroit pour cela serait dans un exemple de travail plus complet ?

Les gens seraient-ils intéressés par ça ? Si j'ai un peu de temps, je pourrais travailler dessus ?

@alexpchin oui, cela permettrait certainement d'économiser une tonne de temps de développement et votre karma deviendra limpide :D

Accepter! Est-ce que quelqu'un serait intéressé par le #856 ?

Salut,

La méthode de @andrewzey a très bien fonctionné pour nous jusqu'à hier, lorsque nous ne devions soudain plus valider aucun reçu pour nos abonnements - nous recevons une erreur d'analyse JSON d'iOS lors de l'appel de validateReceiptIos(). Je suppose que personne d'autre n'a rencontré ça...? Le seul changement qui a été apporté depuis hier était l'ajout d'un code promotionnel sur ITC que nous avons depuis supprimé pour aider à éliminer les variables. Y a-t-il une raison pour laquelle un reçu échoue à l'analyse JSON ? Il échoue pour chaque reçu renvoyé - pas seulement pour le reçu à l'index 0 de triésAvailablePurchases. Le code est fondamentalement identique à l'exemple d'Andrewzey - j'ai tout omis après validateReceiptIos puisque l'erreur y est renvoyée.

Nous utilisons RN 0.61.4, RNIap: 4.4.2 et iOS 13.3.1

Nous avons essayé :

  • Réinstallation de l'application
  • Nouveau compte utilisateur
  • Utiliser un nouveau secret d'application
  • Test de tous les reçus sur chaque utilisateur - pas un seul reçu ne peut être analysé

Nous nous demandons si nous n'utilisions pas correctement finishTransactionIOS lorsque nous avons effectué les achats de sandbox ?

Ce qui est vraiment étrange, c'est que lorsque nous validons le reçu à l'aide de cet outil en ligne , tout semble normal et nous pouvons voir toutes les métadonnées du reçu.

  isSubscriptionActive = async () => {
      const availablePurchases = await RNIap.getAvailablePurchases();
      const sortedAvailablePurchases = availablePurchases.sort(
        (a, b) => b.transactionDate - a.transactionDate
      );
      const latestAvailableReceipt = sortedAvailablePurchases[0].transactionReceipt;
      const isTestEnvironment = __DEV__;

      try {
        const decodedReceipt = await RNIap.validateReceiptIos(
          {
            'receipt-data': latestAvailableReceipt,
            password: Config.IN_APP_PURCHASE_SECRET,
          },
          isTestEnvironment
        );
        console.log('response!', decodedReceipt)
      } catch (error) {
        console.warn('Error validating receipt', error) // JSON PARSE ERROR HERE
      }
...

Question concernant le code que @andrewzey a placé. Date.now() serait-il pas l'heure actuelle de l'appareil et un utilisateur pourrait éventuellement changer cela et avoir un abonnement sans fin ? ??

De plus, la vérification du reçu de l'application elle-même n'est-elle pas découragée ?

@doteric Pourriez-vous m'indiquer où ils mentionnent que c'est découragé ? Je sais que vous pouvez configurer des notifications côté serveur, mais il semble beaucoup plus facile de mon point de vue de gérer cette validation sur le client plutôt que sur le serveur.
J'ai du mal à trouver la bonne documentation à partir d'Apple ou d'autres sources natives de réaction solide.

@doteric yes Date.now() est l'heure de l'appareil afin qu'elle puisse être contournée. Pourtant, les chances qu'un utilisateur fasse cela sont minuscules, et même d'autres méthodes pourraient être contournées pour un abonnement sans fin. Par exemple, s'ils utilisent une simple validation côté serveur, ils peuvent passer en mode avion avant d'ouvrir l'application pour empêcher l'application de se rendre compte que l'abonnement a expiré. Bien sûr, vous pouvez mettre en place d'autres protections, mais ce que je veux dire, c'est que le côté client utilisant Date.now() est certainement fonctionnel. @captaincole Je n'ai pas les documents sous la main mais je peux confirmer que j'ai moi aussi lu que la validation côté client est découragée. Je l'ai lu dans la doc Apple je crois. Cela étant dit, je pense que le côté client fait le travail.

Salut,

J'essayais l'une des approches discutées dans ce fil, en utilisant validateReceiptIos et les last_receipt_data (en comparant expires_date_ms avec la date réelle). Cela a très bien fonctionné lors du développement dans Xcode. Cependant, lorsque je l'ai testé dans Testflight, cela ne fonctionnait plus. C'est difficile à déboguer, donc je ne suis pas en mesure d'identifier le problème exact, il semble qu'il n'obtienne pas les données de réception. Quelqu'un a-t-il eu un problème similaire avec testflight?

Merci d'avance!

Salut,

J'essayais l'une des approches discutées dans ce fil, en utilisant validateReceiptIos et les last_receipt_data (en comparant expires_date_ms avec la date réelle). Cela a très bien fonctionné lors du développement dans Xcode. Cependant, lorsque je l'ai testé dans Testflight, cela ne fonctionnait plus. C'est difficile à déboguer, donc je ne suis pas en mesure d'identifier le problème exact, il semble qu'il n'obtienne pas les données de réception. Quelqu'un a-t-il eu un problème similaire avec testflight?

Merci d'avance!

+1

@asobralr @Somnus007 Je me

@asobralr @Somnus007 Je me

Correct. Vous ne pouvez même pas simuler des scénarios d'échec dans le bac à sable, ce qui est dommage. ??

Salut @kevinEsherick , merci pour ta réponse. Vous vous trompez peut-être. L'utilisateur peut effectuer des achats via teslflight. Sur testflight, il semble que l'utilisateur doive se connecter à un compte sandbox qui a la même adresse e-mail que son vrai compte Apple. Ensuite, tout fonctionne bien. BTW, getPurchaseHistory déclenchera-t-il la fenêtre contextuelle de connexion Itunes dans l'

On dirait que ios 14 casse cette solution en raison du fait qu'il y a des problèmes lorsque vous appelez getAvailablePurchases() avant d'appeler requestSubscription().

Cette page vous a été utile?
0 / 5 - 0 notes