Je l'ai fait tourner dans le bac à sable, presque en direct...
on doit utiliser un serveur pour vérifier les reçus.
sur le serveur, vous pouvez enregistrer l'udid du périphérique avec les données du reçu, puisque les reçus sont toujours fraîchement générés. et cela fonctionnera sur plusieurs périphériques, puisque les reçus sont toujours fraîchement générés.
sur l'appareil, il n'est pas nécessaire de stocker des données sensibles. et il ne faut pas le faire :)
il faut vérifier le dernier reçu auprès du magasin chaque fois que l'application est lancée. l'application appelle le serveur et le serveur valide avec le magasin. tant que le magasin renvoie un reçu valide, l'application sert la fonction.
J'ai développé une application Rails3.x pour gérer le côté serveur, le code actuel pour la vérification ressemble à ceci :
APPLE_SHARED_PASS = "enter_yours"
APPLE_RECEIPT_VERIFY_URL = "https://sandbox.itunes.apple.com/verifyReceipt" #test
# APPLE_RECEIPT_VERIFY_URL = "https://buy.itunes.apple.com/verifyReceipt" #real
def self.verify_receipt(b64_receipt)
json_resp = nil
url = URI.parse(APPLE_RECEIPT_VERIFY_URL)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
json_request = {'receipt-data' => b64_receipt, 'password' => APPLE_SHARED_PASS}.to_json
resp, resp_body = http.post(url.path, json_request.to_s, {'Content-Type' => 'application/x-www-form-urlencoded'})
if resp.code == '200'
json_resp = JSON.parse(resp_body)
logger.info "verify_receipt response: #{json_resp}"
end
json_resp
end
#App Store error responses
#21000 The App Store could not read the JSON object you provided.
#21002 The data in the receipt-data property was malformed.
#21003 The receipt could not be authenticated.
#21004 The shared secret you provided does not match the shared secret on file for your account.
#21005 The receipt server is not currently available.
#21006 This receipt is valid but the subscription has expired.
UPDATE
mon application a été rejetée, parce que le méta-données n'indiquait pas clairement certaines informations sur les abonnements auto-renouvelables.
Dans vos méta-données sur iTunes Connect (dans la description de votre application) : Vous devez divulguer clairement et de manière visible aux utilisateurs les informations concernant votre abonnement à renouvellement automatique :
- Titre de la publication ou du service
- Durée de l'abonnement (période et/ou nombre de livraisons pendant chaque période d'abonnement)
- Prix de l'abonnement, et prix par numéro le cas échéant
- Le paiement sera débité du compte iTunes lors de la confirmation de l'achat.
- L'abonnement est automatiquement renouvelé, sauf si le renouvellement automatique est désactivé au moins 24 heures avant la fin de la période en cours.
- Le compte sera facturé pour le renouvellement dans les 24 heures précédant la fin de la période en cours, et identifiera le coût du renouvellement.
- Les abonnements peuvent être gérés par l'utilisateur et le renouvellement automatique peut être désactivé en allant dans les paramètres du compte de l'utilisateur après l'achat.
- Aucune annulation de l'abonnement en cours n'est autorisée pendant la période d'abonnement active.
- Liens vers votre politique de confidentialité et vos conditions d'utilisation
- Toute partie non utilisée d'une période d'essai gratuite, si elle est offerte, sera perdue lorsque l'utilisateur achètera un abonnement à cette publication."
UPDATE II
L'application a encore été rejetée. Le reçu de l'abonnement n'est pas vérifié par l'url de vérification de l'AppStore de production. Je ne peux pas reproduire ce problème dans le bac à sable, mon application fonctionne parfaitement. La seule façon de déboguer ce problème est de soumettre à nouveau l'application pour examen et de regarder le journal du serveur.
UPDATE III
Un autre rejet. Pendant ce temps, Apple a documenté deux autres statuts :
#21007 This receipt is a sandbox receipt, but it was sent to the production service for verification.
#21008 This receipt is a production receipt, but it was sent to the sandbox service for verification.
Avant de soumettre l'application pour révision, il ne faut pas basculer le serveur en production. Si on le fait, on obtient le statut 21007 renvoyé lors de la vérification.
Cette fois, le refus se lit comme suit :
L'application lance le processus d'achat In App d'une manière non standard. Nous avons inclus les détails suivants pour aider à expliquer le problème et nous espérons que vous envisagerez de réviser et de soumettre à nouveau votre application.
Le nom d'utilisateur et le mot de passe iTunes sont demandés immédiatement au lancement de l'application. Veuillez vous référer à la capture d'écran ci-jointe pour plus informations.
Je n'ai aucune idée de la raison pour laquelle cela se produit. La boîte de dialogue du mot de passe s'affiche-t-elle parce qu'une transaction précédente est en cours de restauration ? ou s'affiche-t-elle au moment de la demande d'informations sur les produits auprès de l'App Store ?
UPDATE IV
je l'ai eu après 5 rejets. mon code faisait l'erreur la plus évidente. on devrait vraiment s'assurer de toujours terminer les transactions lorsqu'elles sont livrées à l'application.
si les transactions ne sont pas terminées, elles sont renvoyées à l'application et les choses se passent étrangement mal.
il faut d'abord initier un paiement, comme ceci :
//make the payment
SKPayment *payment = [SKPayment paymentWithProductIdentifier:productIdentifier];
[[SKPaymentQueue defaultQueue] addPayment:payment];
alors l'application abandonnera rapidement son état actif et cette méthode du délégué de l'application sera appelée :
- (void)applicationWillResignActive:(UIApplication *)application
lorsque l'application est inactive, l'AppStore fait apparaître ses boîtes de dialogue. lorsque l'application redevient active :
- (void)applicationDidBecomeActive:(UIApplication *)application
le système d'exploitation effectue la transaction :
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
{
for (SKPaymentTransaction *transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchased: {
[self completeTransaction:transaction];
break;
}
case SKPaymentTransactionStateFailed: {
[self failedTransaction:transaction];
break;
}
case SKPaymentTransactionStateRestored: {
[self restoreTransaction:transaction];
break;
}
default:
break;
}
}
}
et ensuite on complète la transaction :
//a fresh purchase
- (void) completeTransaction: (SKPaymentTransaction *)transaction
{
[self recordTransaction: transaction];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
voir, comment on appelle la méthode finishTransaction
juste après avoir transmis la transaction reçue à recordTransaction
qui appelle ensuite le serveur d'applications et effectue la vérification du reçu de l'abonnement auprès de l'appstore, comme ceci :
- (void)recordTransaction: (SKPaymentTransaction *)transaction
{
[self subscribeWithTransaction:transaction];
}
- (void)subscribeWithTransaction:(SKPaymentTransaction*)transaction {
NSData *receiptData = [transaction transactionReceipt];
NSString *receiptEncoded = [Kriya base64encode:(uint8_t*)receiptData.bytes length:receiptData.length];//encode to base64 before sending
NSString *urlString = [NSString stringWithFormat:@"%@/api/%@/%@/subscribe", [Kriya server_url], APP_ID, [Kriya deviceId]];
NSURL *url = [NSURL URLWithString:urlString];
ASIFormDataRequest *request = [[[ASIFormDataRequest alloc] initWithURL:url] autorelease];
[request setPostValue:[[transaction payment] productIdentifier] forKey:@"product"];
[request setPostValue:receiptEncoded forKey:@"receipt"];
[request setPostValue:[Kriya deviceModelString] forKey:@"model"];
[request setPostValue:[Kriya deviceiOSString] forKey:@"ios"];
[request setPostValue:[appDelegate version] forKey:@"v"];
[request setDidFinishSelector:@selector(subscribeWithTransactionFinished:)];
[request setDidFailSelector:@selector(subscribeWithTransactionFailed:)];
[request setDelegate:self];
[request startAsynchronous];
}
Auparavant, mon code essayait d'appeler finishTransaction
seulement après que mon serveur ait vérifié le reçu, mais à ce moment-là, la transaction avait déjà été perdue d'une manière ou d'une autre. finishTransaction
dès que possible.
un autre problème que l'on peut rencontrer est le fait que lorsque l'application est dans le bac à sable, elle appelle l'url de vérification de l'appstore du bac à sable, mais lorsqu'elle est en révision, elle est en quelque sorte entre les deux mondes. j'ai donc dû changer le code de mon serveur comme ceci :
APPLE_SHARED_PASS = "83f1ec5e7d864e89beef4d2402091cd0" #you can get this in iTunes Connect
APPLE_RECEIPT_VERIFY_URL_SANDBOX = "https://sandbox.itunes.apple.com/verifyReceipt"
APPLE_RECEIPT_VERIFY_URL_PRODUCTION = "https://buy.itunes.apple.com/verifyReceipt"
def self.verify_receipt_for(b64_receipt, receipt_verify_url)
json_resp = nil
url = URI.parse(receipt_verify_url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
json_request = {'receipt-data' => b64_receipt, 'password' => APPLE_SHARED_PASS}.to_json
resp, resp_body = http.post(url.path, json_request.to_s, {'Content-Type' => 'application/x-www-form-urlencoded'})
if resp.code == '200'
json_resp = JSON.parse(resp_body)
end
json_resp
end
def self.verify_receipt(b64_receipt)
json_resp = Subscription.verify_receipt_for(b64_receipt, APPLE_RECEIPT_VERIFY_URL_PRODUCTION)
if json_resp!=nil
if json_resp.kind_of? Hash
if json_resp['status']==21007
#try the sandbox then
json_resp = Subscription.verify_receipt_for(b64_receipt, APPLE_RECEIPT_VERIFY_URL_SANDBOX)
end
end
end
json_resp
end
En gros, on vérifie toujours avec l'URL de production, mais s'il renvoie le code 21007, cela signifie qu'un reçu de sandbox a été envoyé à l'URL de production et il suffit alors de réessayer avec l'URL de sandbox. De cette façon, votre application fonctionne de la même manière en mode sandbox et en mode production.
et enfin, Apple m'a demandé d'ajouter un bouton RESTORE à côté des boutons d'abonnement, pour gérer le cas de plusieurs appareils appartenant à un même utilisateur. ce bouton appelle alors [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
et l'application sera livrée avec les transactions restaurées (s'il y en a).
De plus, il arrive parfois que les comptes d'utilisateur de test soient contaminés et que les choses cessent de fonctionner. Vous pouvez alors obtenir un message "Can not connect to iTunes store" lors de l'inscription. il est utile de créer un nouvel utilisateur de test.
voici le reste du code correspondant :
- (void) restoreTransaction: (SKPaymentTransaction *)transaction
{
[self recordTransaction: transaction];
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
- (void) failedTransaction: (SKPaymentTransaction *)transaction
{
if (transaction.error.code == SKErrorPaymentCancelled)
{
//present error to user here
}
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
je vous souhaite une bonne expérience de programmation InAppPurchase :-)