月額課金モデル(Auto-Renewable Subscriptions)

 本記事の最新版はこちらです。
本サイトは今後更新されませんのでご注意ください。

 プラットフォーム iOS
iOS(iPhone、iPad、iPod touch)で課金機能を実装したアプリを開発する場合は、Appleが提供している下記の課金モデルの中から選んで利用します。

  • Consumable
    • 一度利用すると無くなるような消費型の課金モデルです。 同じアイテムを何度でも購入できるため、ゲームアプリのアイテム課金等で利用します。
  • Non-Consumable
    • 一度購入すればずっと利用できるような非消費型の課金モデルです。 無料アプリの広告を解除する場合やゲームアプリなどで機能制限を解除する場合等に利用します。
  • Auto-Renewable Subscriptions本ページで扱う課金モデルです
    • 自動継続型の課金モデルです。雑誌や新聞等、一定の期間で継続して自動課金する場合に利用します。
  • Free Subscription
    • 無料購読型の課金モデルです。Newsstandで無料購読のコンテンツを配布する際に利用します。
  • Non-Renewing Subscription
    • 購読型の課金モデルです。期間を限定したサービス用として使われますが、AppleがAuto-Renewable Subscriptions(自動更新)購読を推奨しているため利用機会は少ないです。
本ページでは、今後利用が広がると予想されるAuto-Renewable Subscriptionsについての記事です。

 特徴

Auto-Renewable Subscriptionsの特徴は以下のとおりです。
  • 自動継続課金するタイミングは下記のいずれかから選択できます。
    • 7日
    • 1ヶ月
    • 2ヶ月
    • 3ヶ月
    • 6ヶ月
    • 1年
  • 上記の期間が経過する前に自動継続する旨のメールがユーザに送付されます。ユーザはiTunesのアカウント画面で自動継続をオフにしない限り、自動で新しい課金が発生します。
  • 課金は開始した日から◯日(又は◯ヶ月)経過後に自動課金するため、日本の携帯キャリアのサービスのように「同月内での課金」という概念はありません。したがって、課金を終了したユーザが同じ月に再度購入した場合であっても二重で課金が発生します。


 活用例

  • 電子書籍アプリや雑誌アプリの定期購読処理
  • プレミアムサービス(ただし、リジェクトされる可能性が高い)

 注意点

  • 利用可能なアプリについて Auto-Renewable Subscriptionsは雑誌アプリや新聞アプリの定期購読で利用されることを想定しているため、フリーミアムモデルのプレミアム会員等で利用した場合、アップルの審査でリジェクト(差し戻し)される可能性が高いようです。 当社のヘアカタログアプリ(STA-LOG)のプレミアム会員機能についてもAuto-Renewable Subscriptionsで実装してAppStoreへ公開しようとしたのですが、上記理由によりリジェクトされたため、最終的にはConsumable(消費型)で実装し、サーバ側で課金状態を保有することにしました。 しかしながら、クックパッドのプレミアム会員やナビタイムのプレミアムコースで利用されていることが確認できるため、審査の基準はまちまちのようです。
  • テスト環境(サンドボックス)について アプリを開発してAuto-Renewable Subscriptionsのテストを実施する場合、実際の課金期間(1年など)では長すぎるため、短い期間でテストできるようにアップルが課金用のテスト環境(サンドボックス)を用意してくれています。
    (参照:http://d.hatena.ne.jp/iRSS/20111028/1319763704しかしながらこのテスト環境の仕様が非常に不明瞭で、予期しない動作をすることが多く、テストが非常に大変です。詳細は下記の「開発者による苦労話」を御覧ください。
  • 本番環境で課金が失敗した場合 本番環境で課金が失敗して多重課金等が発生したとしても、アップルが返金に応じてくれることはほとんどありません。本番環境で処理を失敗しないよう、十分注意しましょう。

 実装コード例

#import <StoreKit/StoreKit.h>

#import "Subscription.h"

#import "Base64EncDec.h"

#import "JSON.h"


@implementation Subscription


// レシートの復元処理

- (NSDictionary *)verifyReceipt:(NSString *)base64Receipt {    

    NSURL *url = [NSURL URLWithString:SUBSCRIPTION_URL];

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    NSString *json = [NSString stringWithFormat:@"{\"receipt-data\":\"%@\", \"password\":\"%@\"}", base64Receipt, SHARED_SECRET];

    [request setHTTPBody:[json dataUsingEncoding:NSUTF8StringEncoding]];

    [request setHTTPMethod:@"POST"];

    NSError *error;

    NSURLResponse *response;

    NSData *decodeData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];    

    NSString *receipt = [[NSString alloc] initWithData:decodeData encoding:NSUTF8StringEncoding];

    NSDictionary *dict = [receipt JSONValue];

    [receipt release];

    return dict;

}


// レシートデータの必要な項目をアプリ内に保存

- (NSInteger)savePurchaseInfo:(NSDictionary *)dict {

    NSNumber *status = [dict objectForKey:@"status"];

    if ([status isEqual:[NSNumber numberWithInt:0]] == NO) {

        return [status integerValue];

    }

    // レシートデータをアプリ内に保存

    NSDictionary *receiptDict = [dict objectForKey:@"latest_receipt_info"];

    NSNumber *expired = [receiptDict objectForKey:@"expires_date"];

    NSString *latestReceipt = [dict objectForKey:@"latest_receipt"];

    //

    completed = YES;

    return 0;

}


// レシートのデコードと保存

- (BOOL)decodeReceiptWithTransaction:(NSString *)_receipt isRestore:(BOOL)_isRestore {

    NSDictionary *dict = [self verifyReceipt:_receipt];

    NSInteger code = [self savePurchaseInfo:dict];

    if (code != 0) {

        return NO;

    } else {

        if (_isRestore) {

            restored = YES;

        }

        return YES;

    }

}


// プロダクトの取得で呼び出されるデリゲート

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {

    if (response == nil) {

        return;

    }

    for (SKProduct *product in response.products ) {

        SKPayment *payment = [SKPayment paymentWithProduct:product];

        [[SKPaymentQueue defaultQueue] addPayment:payment];

    }

}


- (void)failedTransaction:(SKPaymentTransaction *)transaction {  

    if (!showedAlert) {

        showedAlert = YES;

        switch ([transaction.error code]) {

            case SKErrorUnknown:

                // 購入処理を中止

                break;

            case SKErrorClientInvalid:

                // 不正なクライアント

                break;

            case SKErrorPaymentCancelled:

                // 購入処理をキャンセル

                break;

            case SKErrorPaymentInvalid:

                // 不正な購入

                break;

            case SKErrorPaymentNotAllowed:

                // 購入が許可されていない

                break;

            default:

                break;

        }

    }

}


// 購入(リストア)トランザクション

- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions {

    for (SKPaymentTransaction *transaction in transactions) {

        switch (transaction.transactionState) {

            case SKPaymentTransactionStatePurchasing: {

                // 購入中の処理

                break;

            }

            case SKPaymentTransactionStatePurchased: {

                // 購入成功時の処理

                [self decodeReceiptWithTransaction:[transaction.transactionReceipt stringEncodedWithBase64] isRestore:NO];

                [queue finishTransaction:transaction];

                break;

            }

            case SKPaymentTransactionStateFailed: {

                // 購入失敗時の処理

                [self failedTransaction:transaction];

                [queue finishTransaction:transaction];

                break;

            }

            case SKPaymentTransactionStateRestored: {

                // 購入履歴復元時の処理

                [self decodeReceiptWithTransaction:[transaction.transactionReceipt stringEncodedWithBase64] isRestore:YES];

                [queue finishTransaction:transaction];

                break;

            }

        }

    }

    

    // 購入(リストア)成功時の終了処理

    if (restored || completed) {

        //

    }

}


// リストア成功時の処理

- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue {

    restored = YES;

    //

}


// リストア失敗時の処理

- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error {

    //

}


// 課金処理(クライアントから呼び出されるメソッド)

- (void) subscribe {

    completed = NO;

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];

    productIds = [NSSet setWithObject:"プロダクトID"];

    skRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIds];

    skRequest.delegate = self;

    [skRequest start];

}


// リストア処理(クライアントから呼び出されるメソッド)

- (void)restore {

    restored = NO;

    [[SKPaymentQueue defaultQueue] addTransactionObserver:self];

    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];

}


- (void) dealloc {

    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];

[super dealloc];

}


@end


 開発者による苦労話

iOSアプリでの課金処理はiOS SDKに含まれているStoreKitと呼ばれるコンポーネントを利用するため一件簡単に実装できそうなのですが、Auto-Renewable Subscriptionsについては時間の経過により自動で課金されるという特性があるため、テストが非常にややこしいくなっています。 一応、課金テストがやりやすいようにアップルがテスト環境(サンドボックス)を用意してくれているのですが、このテスト環境の仕様が全く明らかにされておらず、手探り状態でテストをする必要があります。 例えば、Auto-Renewable Subscriptionsでは課金タイミングを1週間〜1年の間で選択できるのですが、サンドボックスでは短い期間でテストできるよう、3分〜1時間で自動更新が実行されるようになっています。アプリの仕様により、購入の日時や更新の日時などを自社のサーバに保管し、アプリ内で利用する等した場合、この本番とサンドボックス環境とのタイミングの違いで、非常にややこしいテストケースを用意しないといけなかったり、シビアなタイミングが要求されたりします。 また、課金テストする際にはあらかじめテスト用ユーザ(AppleID)を用意するのですが、同じテストユーザで何度もAuto-Renewable Subscriptionsのテストを実行していると、自動継続がうまくいかなかったり、リストアが正常に終了しなかったりすることがあります。 さらにさらに、課金後にAppleから送られてくるレシート情報がサンドボックスと本番で微妙に違うことがあります。サンドボックスではうまくいっていたものが、いざ本番にアップすると失敗する・・・なんてこともまれにあります。

さらにさらにさらに、iOSのバージョンによるかもしれませんが、iPhoneとiPadでリストア(既に自動課金しているユーザが新しいデバイスやアプリの再インストールにより購読状態を復元させる処理)の動きが微妙に違ったりします。 とにかく、自動課金処理が全てブラックボックスになっており、仕様が不明瞭なため、Auto-Renewable Subscriptionsを利用する場合は念入りなテストケースと失敗した場合のリカバリの手段を用意しておく必要があります。


 活用事例

Auto-Renewable Subscriptionsを利用しているアプリの一部を紹介します。




※上記は当社が開発したアプリではありません。

 まとめ

課金の部分は最もバグが怖い部分です。
弊社の実績のある課金処理部品化を使用することで、通常ならたくさんの時間をかけなければならない
テストや処理を効率良く安心して使用することができます。