Hatena::Groupiphone-dev

iOS プログラミングメモ

2012-10-02 (Tue)

アプリを起動するたびに AppleID を求められる状況を再現する方法 15:01  アプリを起動するたびに AppleID を求められる状況を再現する方法 - iOS プログラミングメモ を含むブックマーク

今日ちょっと話題になっていたので思い出したのですが、自分のアプリがタイトルのような状況になってしまったので、その時の対策とどういった場合にこの現象が発生するかについてまとめてみたいと思います。

簡単にいうと StoreKit の restoreCompletedTransactions をした時に、最後に finishTransaction しないと発生します。いわゆる「購入履歴から再購入する」機能を呼び出した際ですね。通常の購入処理でも同じだと思うのですが、確認してないのでよくわかんないです。

リストアがあることからお分かりいただける通り、Non-Consumable での話です。

以下はとある Moca という超便利なカレンダーアプリがあるんですが、そのアプリ内課金で使ってるコードの一部です。


- (void) dealloc {
   [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
   [super dealloc];
}

- (void) awakeFromNib {
   [super awakeFromNib];

   [[SKPaymentQueue defaultQueue] addTransactionObserver:self]; // (B)
}

- (void)viewDidLoad {
   [super viewDidLoad];

   self.request = [[[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithObject:AD_ID]] autorelease];
   self.request.delegate = self;
   [self.request start];
}

#pragma mark - Table view delegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
   if([cell.reuseIdentifier hasPrefix:@"Purchase"]) {
      if(self.product) {
         SKPayment *payment = [SKPayment paymentWithProduct:self.product];
         [[SKPaymentQueue defaultQueue] addPayment:payment];
      }
   } else if([cell.reuseIdentifier hasPrefix:@"Restore"]) {
      [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; // (1)
   }
   [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

#pragma mark - SKProductsRequest delegate
- (void) productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
   for (SKProduct *p in response.products) {
      NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
      [formatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
      [formatter setNumberStyle:NSNumberFormatterCurrencyStyle];
      [formatter setLocale:p.priceLocale];

      NSString *price = [formatter stringFromNumber:p.price];
      [formatter release];

      self.product = p;
   }
}

#pragma mark - SKPaymentTransactionObserver
- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { // (2)
   for (SKPaymentTransaction *transaction in transactions) {
      switch (transaction.transactionState) {
         case SKPaymentTransactionStatePurchasing:
            break;
         case SKPaymentTransactionStatePurchased:
            // 購入処理
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
         case SKPaymentTransactionStateFailed:
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction];
            break;
         case SKPaymentTransactionStateRestored:
            [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; // (A)
            break;
         default:
            NSLog(@"unknown transactionState: %d", transaction.transactionState);
            break;
      }
   }
}

- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue { // (3)
   for(SKPaymentTransaction *transaction in queue.transactions) {
      if([transaction.payment.productIdentifier isEqualToString:AD_ID]) {
         // リストア
         break;
      }
   }
}

リストア処理の流れはといいますと、まず(1)で restoreCompletedTransactions を呼び出してリストア処理を開始します。アカウントとパスワードの入力をすると (2) の (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions が呼ばれて transactionState は SKPaymentTransactionStateRestored になります。最後に (3) の (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue が呼ばれてフィニッシュです。

(A) で finishTransaction していますが、ここを省略する表題のような状況になります。iOS5とiOS6で状況が異なります。

  • iOS5
    • (B) の処理を実行すると、AppleIDのパスワード入力を求められます
  • iOS6
    • アプリを起動する毎にAppleIDのパスワード入力を求められます
      • アプリをマルチタスクから削除すると、パスワードの入力は求められなくなる
      • ただし再度 (B) の処理が呼ばれると、またパスワードの入力を求められるようになる

いずれの場合も一度AppleIDのパスワードを入力すると、しばらくは鳴りを潜めます。ようするに、普通に restoreCompletedTransactions を実行するとその時パスワードを入力するので、AppleIDのダイアログはしばらく出てこなくなるからデバッグ時に判明しにくいんですよね。

ちなみにこの状態で再度通常の購入処理を実行しようとすると、以下のようなエラーがでます。

すでにこのアイテムを購入されていますが、ダウンロードされていません。今すぐダウンロードするには「OK」をタップします。

f:id:ktakayama:20121002150256p:image:w160

ちなみにちなみに「OK」をタップしてもエラーコード SKErrorPaymentCancelled として (void) failedToTransactionError:(NSError *)error がエラーコード呼ばれるだけで、あんまり役に立ちません。

なにが起こっているのか?

AppleIDを入力するダイアログが出る時に何が起きてるのかというと、transactionState = SKPaymentTransactionStateRestored として (2) の updatedTransactions が呼び出されています。多分トランザクションが終わってないから、早くリストア処理を実行しろよってことなんだろうと思います。明示的にリストア処理を終わらせるまで自動的にリトライしてくれるわけだから、ちゃんと実装すれば便利なんだろうと思います。なお、この時 paymentQueueRestoreCompletedTransactionsFinished は呼ばれません。

解決方法

(A) の処理を入れると解決します。

なお、いろいろ試したのですがユーザ側でできることはありません。

たとえばサインアウトすればよさそうに思えますが、restoreCompletedTransactions を実行したアカウントをどこかに保持しているらしくて、それがどうしてもクリアできませんでした。再起動して iTunes と同期してうまくいったという情報も見かけたんですが、うまくいかず。

トラックバック - http://iphone-dev.g.hatena.ne.jp/ktakayama/20121002