web-dev-qa-db-fra.com

test iOS Validation du reçu de l'application

Il existe de nombreux exemples sur la manière de tester la validation des reçus d'achat dans l'application à l'aide d'un compte de testeur de sandbox.

Mais comment est le reçu pour l'application payante elle-même? Comment pouvons-nous obtenir le reçu d'application dans l'environnement de développement?

Il y a deux choses que je veux faire:

  • Pour empêcher la copie illégale de notre application en cours d'exécution par l'utilisateur qui n'a pas acheté l'application. Comme j'ai vu une application qui a détecté que le compte iTune était connecté, elle ne la possédait pas (elle avertit l'utilisateur qui ne la possédait pas, mais ne réussit pas à empêcher l'utilisateur de continuer à utiliser l'application)

  • Envoyez le reçu d'achat de l'application à notre serveur. Nous voulons savoir quand ils achètent notre application, quelle version de l'application ils ont apportée.

14
King Chan

La plupart des réponses peuvent être trouvées ici dans la documentation Apple. Mais il y a des lacunes et le code objective-c utilise des méthodes obsolètes. 

Ce code Swift 3 montre comment obtenir le reçu d’application et l’envoyer à l’app store pour validation. Avant de sauvegarder les données souhaitées, vous devez absolument valider le reçu de l’application avec App Store. L'avantage de demander à l'app store de valider est qu'il répond avec des données que vous pouvez facilement sérialiser en JSON et extraire ensuite les valeurs des clés souhaitées. Aucune cryptographie requise. 

Comme Apple le décrit dans cette documentation, le flux préféré est le suivant ...

device -> your trusted server -> app store -> your trusted server -> device

Lorsque l'App Store retourne sur votre serveur, en cas de succès, vous pourrez sérialiser et extraire les données dont vous avez besoin et les enregistrer à votre guise. Voir le JSON ci-dessous. Et vous pouvez envoyer le résultat et tout ce que vous voulez à l'application. 

Dans validateAppReceipt() ci-dessous, pour en faire un exemple de travail, il utilise simplement ce flux ...

device -> app store -> device

Pour que cela fonctionne avec votre serveur, il suffit de changer validationURLString pour qu'il pointe vers votre serveur et ajoutez ce que vous souhaitez à requestDictionary

Pour tester cela en développement, vous devez: 

  • assurez-vous d'avoir un utilisateur sandbox configuré dans itunesconnect
  • sur votre appareil de test, déconnectez-vous de l'iTunes et de l'App Store
  • pendant les tests, lorsque vous y êtes invité, utilisez votre utilisateur de bac à sable

Voici le code. Le chemin heureux coule très bien. Les erreurs et les points d'échec sont simplement imprimés ou commentés. Traitez avec ceux que vous avez besoin. 

Cette partie récupère le reçu de l'application. Si ce n'est pas là (ce qui se passera lorsque vous testez), il demande à l'App Store pour se rafraîchir. 

let receiptURL = Bundle.main.appStoreReceiptURL

func getAppReceipt() {
    guard let receiptURL = receiptURL else {  /* receiptURL is nil, it would be very weird to end up here */  return }
    do {
        let receipt = try Data(contentsOf: receiptURL)
        validateAppReceipt(receipt)
    } catch {
        // there is no app receipt, don't panic, ask Apple to refresh it
        let appReceiptRefreshRequest = SKReceiptRefreshRequest(receiptProperties: nil)
        appReceiptRefreshRequest.delegate = self
        appReceiptRefreshRequest.start()
        // If all goes well control will land in the requestDidFinish() delegate method.
        // If something bad happens control will land in didFailWithError.
    }
}

func requestDidFinish(_ request: SKRequest) {
    // a fresh receipt should now be present at the url
    do {
        let receipt = try Data(contentsOf: receiptURL!) //force unwrap is safe here, control can't land here if receiptURL is nil
        validateAppReceipt(receipt)
    } catch {
        // still no receipt, possible but unlikely to occur since this is the "success" delegate method
    }
}

func request(_ request: SKRequest, didFailWithError error: Error) {
    print("app receipt refresh request did fail with error: \(error)")
    // for some clues see here: https://samritchie.net/2015/01/29/the-operation-couldnt-be-completed-sserrordomain-error-100/
}

Cette partie valide le reçu de l'application. Ce n'est pas une validation locale. Voir les notes 1 et 2 dans les commentaires. 

func validateAppReceipt(_ receipt: Data) {

    /*  Note 1: This is not local validation, the app receipt is sent to the app store for validation as explained here:
            https://developer.Apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html#//Apple_ref/doc/uid/TP40010573-CH104-SW1
        Note 2: Refer to the url above. For good reasons Apple recommends receipt validation follow this flow:
            device -> your trusted server -> app store -> your trusted server -> device
        In order to be a working example the validation url in this code simply points to the app store's sandbox servers.
        Depending on how you set up the request on your server you may be able to simply change the 
        structure of requestDictionary and the contents of validationURLString.
    */
    let base64encodedReceipt = receipt.base64EncodedString()
    let requestDictionary = ["receipt-data":base64encodedReceipt]
    guard JSONSerialization.isValidJSONObject(requestDictionary) else {  print("requestDictionary is not valid JSON");  return }
    do {
        let requestData = try JSONSerialization.data(withJSONObject: requestDictionary)
        let validationURLString = "https://sandbox.iTunes.Apple.com/verifyReceipt"  // this works but as noted above it's best to use your own trusted server
        guard let validationURL = URL(string: validationURLString) else { print("the validation url could not be created, unlikely error"); return }
        let session = URLSession(configuration: URLSessionConfiguration.default)
        var request = URLRequest(url: validationURL)
        request.httpMethod = "POST"
        request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringCacheData
        let task = session.uploadTask(with: request, from: requestData) { (data, response, error) in
            if let data = data , error == nil {
                do {
                    let appReceiptJSON = try JSONSerialization.jsonObject(with: data)
                    print("success. here is the json representation of the app receipt: \(appReceiptJSON)")
                    // if you are using your server this will be a json representation of whatever your server provided
                } catch let error as NSError {
                    print("json serialization failed with error: \(error)")
                }
            } else {
                print("the upload task returned an error: \(error)")
            }
        }
        task.resume()
    } catch let error as NSError {
        print("json serialization failed with error: \(error)")
    }
}

Vous devriez vous retrouver avec quelque chose comme ça. Dans votre cas, c’est ce que vous utiliseriez sur votre serveur. 

{
    environment = Sandbox;
    receipt =     {
        "adam_id" = 0;
        "app_item_id" = 0;
        "application_version" = "0";  // for me this was showing the build number rather than the app version, at least in testing
        "bundle_id" = "com.yourdomain.yourappname";  // your app's actual bundle id
        "download_id" = 0;
        "in_app" =         (
        );
        "original_application_version" = "1.0"; // this will always return 1.0 when testing, the real thing in production.
        "original_purchase_date" = "2013-08-01 07:00:00 Etc/GMT";
        "original_purchase_date_ms" = 1375340400000;
        "original_purchase_date_pst" = "2013-08-01 00:00:00 America/Los_Angeles";
        "receipt_creation_date" = "2016-09-21 18:46:39 Etc/GMT";
        "receipt_creation_date_ms" = 1474483599000;
        "receipt_creation_date_pst" = "2016-09-21 11:46:39 America/Los_Angeles";
        "receipt_type" = ProductionSandbox;
        "request_date" = "2016-09-22 18:37:41 Etc/GMT";
        "request_date_ms" = 1474569461861;
        "request_date_pst" = "2016-09-22 11:37:41 America/Los_Angeles";
        "version_external_identifier" = 0;
    };
    status = 0;
}
27
Murray Sagal

Je suppose que vous savez comment effectuer l'achat InApp.

Nous devons valider un reçu une fois la transaction terminée. 

- (void)completeTransaction:(SKPaymentTransaction *)transaction 
{
    NSLog(@"completeTransaction...");

    [appDelegate setLoadingText:VALIDATING_RECEIPT_MSG];
    [self validateReceiptForTransaction];
}

Une fois que le produit a été acheté avec succès, il doit être validé. Le serveur le fait pour nous, nous juste besoin de transmettre les données de réception renvoyées par le serveur Apple.

-(void)validateReceiptForTransaction
{
    /* Load the receipt from the app bundle. */

    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
    NSData *receipt = [NSData dataWithContentsOfURL:receiptURL];

    if (!receipt) { 
        /* No local receipt -- handle the error. */
    }

    /* ... Send the receipt data to your server ... */

    NSData *receipt; // Sent to the server by the device

    /* Create the JSON object that describes the request */

    NSError *error;

    NSDictionary *requestContents = @{
                                      @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                     };

    NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents
                                                          options:0
                                                            error:&error];

    if (!requestData) { 
        /* ... Handle error ... */ 
    }

    // Create a POST request with the receipt data.

    NSURL *storeURL = [NSURL URLWithString:@"https://buy.iTunes.Apple.com/verifyReceipt"];

    NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
    [storeRequest setHTTPMethod:@"POST"];
    [storeRequest setHTTPBody:requestData];

    /* Make a connection to the iTunes Store on a background queue. */

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    [NSURLConnection sendAsynchronousRequest:storeRequest queue:queue
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

                               if (connectionError) {

                                   /* ... Handle error ... */

                               } else {

                                   NSError *error;
                                   NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

                                   if (!jsonResponse) { 
                                       /* ... Handle error ...*/ 
                                   }

                                   /* ... Send a response back to the device ... */
                               }
                           }];
}

La charge utile de la réponse est un objet JSON contenant les clés et les valeurs suivantes:

statut:

Soit 0 si le reçu est valide, soit l’un des codes d’erreur mentionnés ci-dessous:

 enter image description here

Pour les reçus de transaction de style iOS 6, le code de statut reflète le statut du reçu de la transaction spécifique.

Pour les reçus d'applications de style iOS 7, le code d'état reflète l'état du reçu d'application dans son ensemble. Par exemple, si vous envoyez un accusé de réception d'application valide contenant un abonnement arrivé à expiration, la réponse est 0 car le reçu dans son ensemble est valide.

le reçu:

Une représentation JSON du reçu envoyé pour vérification. 

Rappelles toi:


EDIT 1

transactionReceipt est obsolète: d'abord déconseillé dans iOS 7.0 

if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1) {
    // iOS 6.1 or earlier.
    // Use SKPaymentTransaction's transactionReceipt.

} else {
    // iOS 7 or later.

    NSURL *receiptFileURL = nil;
    NSBundle *bundle = [NSBundle mainBundle];
    if ([bundle respondsToSelector:@selector(appStoreReceiptURL)]) {

        // Get the transaction receipt file path location in the app bundle.
        receiptFileURL = [bundle appStoreReceiptURL];

        // Read in the contents of the transaction file.

    } else {
        /* Fall back to deprecated transaction receipt,
           which is still available in iOS 7.
           Use SKPaymentTransaction's transactionReceipt. */
    }

}
5
NSPratik

si vous souhaitez tester l'application in-app, validez les réceptions dans l'environnement en sandbox et tenez compte du fait que les intervalles de renouvellement dans le sandbox sont 

1 semaine 3 minutes 1 mois 5 minutes 2 mois 10 minutes 3 mois 15 minutes 6 mois 30 minutes 1 an 1 heure

Le meilleur moyen de valider la réception est de communiquer votre serveur avec le serveur Apple pour validation. 

0
Shubham kapoor