Проверка квитанции тестового приложения iOS

Существует множество примеров того, как протестировать проверку квитанции о покупке в приложении с помощью учетной записи тестера песочницы.

Но как устроена сама квитанция за платное приложение? Как получить квитанцию ​​о приложении в среде разработки?

Я хочу сделать две вещи:

  • Чтобы предотвратить незаконное копирование нашего приложения, запущенное пользователем, который не купил приложение. Как я видел, приложение, которое обнаружило, что учетная запись iTune была подключена, не владеет приложением (оно показывает предупреждение пользователю, которому не принадлежит приложение, но они не могут остановить пользователя, чтобы он продолжил использовать приложение)

  • Отправьте квитанцию ​​о покупке приложения на наш сервер. Мы хотим знать, когда они покупают наше приложение, какую версию приложения они принесли.


person King Chan    schedule 01.06.2016    source источник


Ответы (4)


Большинство ответов можно найти на здесь в документации Apple. Но есть пробелы, и в коде objective-c используются устаревшие методы.

Этот код Swift 3 показывает, как получить квитанцию ​​о приложении и отправить ее в магазин приложений для проверки. Вы обязательно должны подтвердить квитанцию ​​о приложении в магазине приложений, прежде чем сохранять нужные данные. Преимущество запроса магазина приложений на проверку заключается в том, что он отвечает данными, которые вы можете легко сериализовать в JSON, и оттуда извлекать значения для нужных вам ключей. Криптография не требуется.

Как Apple описывает в этой документации, предпочтительный поток выглядит следующим образом ...

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

Когда магазин приложений вернется на ваш сервер, в случае успеха, вы сериализуете, извлечете требуемые данные и сохраните их по своему усмотрению. См. JSON ниже. И вы можете отправить результат и все, что хотите, обратно в приложение.

В validateAppReceipt() ниже, чтобы сделать его рабочим примером, он просто использует этот поток ...

device -> app store -> device

Чтобы это работало с вашим сервером, просто измените validationURLString, чтобы он указывал на ваш сервер, и добавьте все, что вам нужно, в requestDictionary.

Чтобы протестировать это в разработке, вам необходимо:

  • убедитесь, что у вас настроен пользователь песочницы в itunesconnect
  • на тестовом устройстве выйдите из iTunes и App Store
  • во время тестирования при появлении запроса используйте пользователя песочницы

Вот код. Счастливый путь протекает прекрасно. Ошибки и точки сбоев просто печатаются или комментируются. Работайте с ними по своему усмотрению.

Эта часть берет квитанцию ​​приложения. Если его там нет (что произойдет при тестировании), он попросит обновить магазин приложений.

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/
}

Эта часть проверяет квитанцию ​​приложения. Это не местная проверка. См. Примечания 1 и 2 в комментариях.

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)")
    }
}

У вас должно получиться что-то вроде этого. В вашем случае это то, с чем вы будете работать на своем сервере.

{
    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;
}
person Murray Sagal    schedule 22.09.2016
comment
Спасибо за этот пример! Я искал довольно долго, и это работает безупречно. - person EricH206; 15.12.2016
comment
Спасибо за это. Очень и очень полезно. Однако неопытным (и я включаю себя) следует отметить, что если вы хотите передать данные квитанции в собственный php-скрипт на доверенном сервере, данные в кодировке base64 должны быть очищены, прежде чем передавать их на проверку Apple. Я все еще пытаюсь понять, как это сделать лучше всего. Это не проблема для приведенного выше примера, потому что он передал словарь запроса напрямую https://sandbox.itunes.apple.com/verifyReceipt - person Peter Wiley; 13.08.2017
comment
@Murray Sagal Извините, если я не прав, но как это может быть продуктом, если мы тестируем его из учетной записи песочницы в среде песочницы? Или эта среда изменится, если мы попробуем ее с реальной? - person wm.p1us; 31.10.2017
comment
@ wm.p1us В производстве используйте этот URL: https://buy.itunes.apple.com/verifyReceipt - person Murray Sagal; 03.11.2017
comment
Как изменить validateAppReceipt для отправки на собственный сервер? - person Zhanserik; 10.10.2018
comment
@Zhanserik См. Примечание 2 в комментариях. - person Murray Sagal; 10.10.2018
comment
У меня нет серверной части для моего приложения. Если я использую это только для получения original_application_version для платного приложения, а не для IAP или подписки, нужно ли мне проверять на сервере Apple? - person Augie; 07.12.2018
comment
@ Оги Да. Вы получаете квитанцию ​​такого вида: let receipt = try Data(contentsOf: receiptURL). Итак, теперь у вас есть буфер данных. Из этого нельзя вытащить original_application_version. Возможно, есть взлом, чтобы преобразовать его в словарь, но легко попросить Apple сделать это. Кроме того, просто к сведению, original_application_version не возвращает номер версии. Возвращает номер сборки. Итак, я начал добавлять номер версии к своему номеру сборки. Например, для версии 2.0.1 мой номер сборки будет 2.0.1.1. - person Murray Sagal; 08.12.2018
comment
@Augie Plus нет гарантии, что квитанция о приложении будет присутствовать в комплекте. Так что вы все равно должны быть готовы попросить об этом у Apple. - person Murray Sagal; 09.12.2018
comment
@MurraySagal. Спасибо. Хорошее примечание о фактическом номере версии. Мои версии App Store только основные / микро, например 1.12, а номер сборки - что-то вроде 34. Так вы говорите, что я вернусь 34? - person Augie; 10.12.2018
comment
@ Оги Да. Вернешься 34. - person Murray Sagal; 06.01.2019
comment
@MurraySagal. Спасибо. - person Augie; 07.01.2019
comment
Я получаю код ошибки 21004. Нужно ли мне добавлять общий секрет в полезную нагрузку? Пожалуйста помогите. - person Vijay Kharage; 09.01.2019
comment
Я получаю ответ ниже: Пароль с общим секретом является обязательным? {environment = Sandbox; status = 21004; } - person bhavik; 04.05.2020
comment
Да, вам нужно добавить пароль, сгенерированный в iTunes Connect для приложения или всей учетной записи, например: let requestDictionary = [Receipt-квитанции: base64encodedReceipt, пароль: YOUR_PASSWORD_HERE] - person nickeyzzz; 13.07.2020

Я предполагаю, что вы знаете, как совершать покупки в приложении.

Мы должны подтвердить квитанцию ​​после завершения транзакции.

- (void)completeTransaction:(SKPaymentTransaction *)transaction 
{
    NSLog(@"completeTransaction...");
    
    [appDelegate setLoadingText:VALIDATING_RECEIPT_MSG];
    [self validateReceiptForTransaction];
}

После того, как продукт был успешно приобретен, его необходимо проверить. Сервер делает это за нас, нам просто нужно передать данные квитанции, возвращаемые сервером 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 ... */
                               }
                           }];
}

Полезные данные ответа - это объект JSON, который содержит следующие ключи и значения:

статус:

Либо 0, если квитанция действительна, либо один из кодов ошибки, указанных ниже:

введите описание изображения здесь

Для квитанций транзакций в стиле iOS 6 код статуса отражает статус квитанции конкретной транзакции.

Для квитанций приложений в стиле iOS 7 код статуса отражает статус квитанции приложения в целом. Например, если вы отправляете действительную квитанцию ​​приложения, содержащую истекшую подписку, ответ будет 0, потому что квитанция в целом действительна.

квитанция:

Представление квитанции в формате JSON, отправленной на проверку.

Помните:

  • Мы получим код статуса 21007 для успешной проверки квитанции в среде Sandbox.

  • В тестовой среде используйте https://sandbox.itunes.apple.com/verifyReceipt как URL-адрес. В рабочей среде используйте https://buy.itunes.apple.com/verifyReceipt в качестве URL-адреса. .

  • Вам нужно будет настроить тестовую учетную запись пользователя в iTunes Connect для тестовой покупки в среде песочницы.


ИЗМЕНИТЬ 1

transactionReceipt устарело: впервые не рекомендуется в 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. */
    }

}
person NSPratik    schedule 01.06.2016
comment
Так что даже в разрабатываемой сборке (которая еще не опубликована) я все равно могу получить квитанцию ​​о приложении? (в настоящее время мы все еще находимся в процессе расследования, поэтому у меня нет среды для тестирования) - person King Chan; 02.06.2016
comment
Да, вы получите квитанцию ​​о проверке еще в тестовой среде. Обычно вам нужно создать тестовую учетную запись пользователя в iTunes Connect. Затем вам нужно просто совершить покупку с этими учетными данными, и вы получите квитанцию ​​без финансовых транзакций. - person NSPratik; 02.06.2016
comment
См. Эту ссылку - person NSPratik; 02.06.2016
comment
Ссылка говорит о тестировании покупки в приложении, но я говорю о получении самого приложения (платного приложения)? - person King Chan; 02.06.2016
comment
Квитанция о скачивании платного приложения? - person NSPratik; 02.06.2016
comment
Да, квитанция о скачивании платного приложения - person King Chan; 02.06.2016
comment
Вы имеете в виду для разработки build? Поскольку есть поле App Receipt developer.apple.com/library/ios/releasenotes/General/ - person King Chan; 02.06.2016
comment
Нет, прокрутите вверх, там указано «Поля квитанции приложения», содержащие поля: идентификатор пакета, версия приложения, непрозрачное значение, хэш SHA-1, квитанция о покупке в приложении и т. Д. И вы можете увидеть раздел квитанции приложения и внутри приложения Раздел квитанции слева. - person King Chan; 03.06.2016
comment
Это идентификатор пакета, версия приложения - приложения, для которого была сделана покупка. - person NSPratik; 03.06.2016
comment
Извините, если я не совсем понимаю, я не уверен, что вы все еще имеете в виду покупку в приложении. Что мне нужно, так это квитанция о самом приложении, чтобы предотвратить запуск нелегальных копий приложения. Как описано здесь: developer.apple.com/library/ios/ Releasenotes / Общие / - person King Chan; 04.06.2016

если вы хотите протестировать приложение, перейдите в среду песочницы для проверки квитанции и примите во внимание, что интервалы обновления в песочнице

1 неделя 3 минуты 1 месяц 5 минут 2 месяца 10 минут 3 месяца 15 минут 6 месяцев 30 минут 1 год 1 час

Лучший способ подтвердить получение - это связать ваш сервер с сервером Apple для проверки.

person Shubham kapoor    schedule 28.07.2017

Работает на iOS 13

Вот шаги, чтобы проверить получение на устройстве без кода сервера:

Перед проверкой квитанции вам понадобится пароль. Это будет общий секретный ключ.

Как его создать:

Перейдите в -> Подключение к iTunes, перейдите в «Контракты, налоги и банковское дело» и нажмите «Запрос» в контракте на платные приложения iOs, затем примите контракт.

Посетите эту ссылку

https://appstoreconnect.apple.com

1: - Нажмите "Возможности"

2: - Нажмите «Покупки в приложении» и создайте пакет подписки.

3: - После успешного создания подписки нажмите Общий секрет для конкретного приложения.

4: - Создание общего секрета для конкретного приложения


Обновленный код для подтверждения квитанции о подписке в приложении:

-(void) verifyReceipt
{
/* 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. */
}

/* Create the JSON object that describes the request */
NSError *error;

/* reciept data and password to be sent, password would be the Shared Secret Key from Apple Developer account for given app. */
NSDictionary *requestContents = @{
                                  @"receipt-data": [receipt base64EncodedStringWithOptions:0]
                                 ,@"password": @"2008687bb49145445457ff2b25e9bff3"};

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://sandbox.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];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:storeRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    // handle request error
    if (error) {
        //completion(nil, error);
        return;
    } else {
        NSError *error;
        NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];

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

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

надеюсь, это поможет

Спасибо

person Harjot Singh    schedule 17.12.2019