AIR for iOS : アプリ内課金(In-App Purchase)のネイティブ拡張(ANE)を自力で作ってみる

スポンサーリンク

アプリ内課金の総まとめをMEMOしておきます。
いろいろ試してきたことを踏まえて、今度は自力でAIR for iOSアプリにアプリ内課金を実装してみます。

自力でやる!


iOS : Flash Proでネイティブ拡張(ANE)に挑戦」でHelloWorldなANEを作ってそれを実機で確認するところまで試し、「iOS : アプリ内課金(In-App Purchase)をXcodeでやってみる」でXcodeで作ったアプリにアプリ内課金を実装するところまで試してみました。「Flash ProでMilkman GamesのIn-App Purchase iOS Extensionを試す」では既存のANEを購入し、実際にアプリ内課金がAIRアプリ上で利用できることも確認できました。
そしたらいよいよ、これらをまとめて自力でアプリ内課金を行うANEを作ってみましょう。

アプリ内課金のテストには、iOS Dev Centerでアプリ内課金用のプロダクトIDを登録しておく必要があるので「日本のApple StoreでiOS Developer Programを購入しActivateするまでの全スクリーンショット」を参考にプログラムを購入し、iTunes Connectでアプリとそのアプリ内課金用のプロダクトを登録しましょう。課金をテストするためにテストユーザを作っておく必要もあります。

このMEMOのステップとしてはこんな感じ。

In-App Purchase機能を実装

  1. ANEのHelloWorldサンプルで作成したファイルを用意する
  2. 「アプリ内課金をXcodeでやってみる」で作ったクラスを読み込む
  3. “Hello”を返している場所にアプリ内課金を呼び出す記述を追加
  4. リンケージオプションファイルを準備
  5. 一度、ここで動作確認してみる

購入の記録

  1. ANE側からAIRアプリに対してイベントを発行
  2. ネイティブ拡張ActionScriptライブラリ(.swc)にイベントを受け取る記述を追加
  3. StatusEvent.code、StatusEvent.levelで動作を振り分け
  4. 購入された場合に、購入記録を暗号化して保存
  5. 1.ANEのHelloWorldサンプルで作成したファイルを用意する

    iOS : Flash Proでネイティブ拡張(ANE)に挑戦」で作成したHelloWorldのANEを少しカスタマイズして、アプリ内課金機能をつけるので、このMEMOをひと通りやってファイルを準備してください。

    2.「アプリ内課金をXcodeでやってみる」で作ったクラスを読み込む

    iOS : アプリ内課金(In-App Purchase)をXcodeでやってみる」で作成したクラスを読み込みます。
    プロジェクトのコンテキストメニューから「Add Files to “HelloWorldANE”…」を選択し、上記MEMOで作った「InAppPurchaseManager.h」と「InAppPurchaseManager.m」を追加します。

    3.”Hello”を返している場所にアプリ内課金を呼び出す記述を追加

    「HelloWorldANE.h」ファイルに「#import “InAppPurchaseManager.h”」の一行を追加し、
    「HelloWorldANE.m」のGetHelloWorld内に ///ここから〜///ここまで 部分を追加。

    HelloWorldANE.m
    FREObject GetHelloWorld(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]) {
        
        ///ここからアプリ内課金用に追加///
        const char *str = "Hello, app in purchase!";
        
        InAppPurchaseManager *appMng = [[InAppPurchaseManager alloc]init];
        if (appMng.canMakePurchases) {
            [appMng requestProductData:@"com.mushikago.InAppPurchaseXcode.unLock"];
        }else {
            str = "課金が許可されていません。";
        }
        
        ///ここまで
        
        FREObject retStr;
        FRENewObjectFromUTF8(strlen(str)+1, (const uint8_t *)str, &retStr);
        
        return retStr;
    }
    

    4.リンケージオプションファイルを準備

    iOSフレームワークに対してデフォルト以外のリンケージオプションを指定する場合、ADTコマンドを打つ際に「-platformoptions ◯◯.xml」を付け加える必要があります。そのXMLの内容は以下のような感じ。StoreKitフレームワークを追加しています。
    ※iOSネイティブライブラリとリンケージオプションの詳細な説明はこちらに。

    
        5.1
        
            
        
    
    

    追記:タグの”sdkVersion”と”linkerOptions”部分では、大文字小文字の違いがあるようです。VとOは大文字で。

    リンケージオプションを追加したADTコマンドはこんな感じです。

    adt -package
     -storetype pkcs12
 -keystore mykey.p12
     -target ane HelloWorldANE.ane extension.xml
    
 -swc HelloWorldANE_Event_ASLIB.swc
     -platform iPhone-ARM library.swf libHelloWorldANE.a
     -platformoptions myplatformoptions.xml
    

    ※↑実際は一行です。

    5.一度、ここで動作確認してみる

    ここまでに作ったANEを実機で確認してみると、起動時に(しばらく待っていると)「アドオンを入手」というアイテム購入のダイアログが出る事が確認できると思います。
    ここではキャンセルをして次に進みましょう。

    ここまでのファイルを一式まとめておきました。


    ※ADT時に問われるパスワードは、”mykey“にしてあります。
    ※FlashBuilder用の「HelloWorldANE_Event_ASLIB」というファイルは、次のステップで設定するイベントを受け取る記述まですでに書いてあるファイルになっています。
    ※FlashPro.flaではANEをもう一度読み込み直す必要があるかもしれません。
    ※パブリッシュ中にクラッシュするようなら、こちらのステップ5にあるiOS SDKの指定が正しいかご確認を。
    ※このサンプルファイルは、”com.mushikago.InAppPurchaseXcode.unLock”を購入するサンプルになっていますので、このプロダクト部分やパブリッシュするApp IDは、自分のアプリのものに変更してみてください。

    6.ANE側からAIRアプリに対してイベントを発行

    購入ダイアログを出した後、ユーザがどのような選択をしたのかをAIRアプリに対してカスタムイベントを送ります。
    イベントの流れは、ネイティブ拡張ActionScriptライブラリが、受け取ったイベントをそのままAIRアプリにスルーパスしてる感じです。(上記のサンプルファイル内のswcでは、この記述まで含めてあります)

    アプリ内課金の処理結果判定部分にイベント発行の記述をします。
    Xcodeで以下のハイライト部分を修正します。

    InAppPurchaseManager.h
    #import <UIKit/UIKit.h>
    #import <StoreKit/StoreKit.h>
    //↓これ追加するの忘れずに
    #import "FlashRuntimeExtensions.h"
    
    @interface InAppPurchaseManager : NSObject <SKProductsRequestDelegate,SKPaymentTransactionObserver>
    {
        //SKProduct *myProduct;
        SKProductsRequest *myProductRequest;
        FREContext *myContext; //step5
    }
    
    @property FREContext *myContext;
    
    //public method
    - (BOOL)canMakePurchases;
    - (void)requestProductData:(NSString *) pID;
    @end
    
    
    InAppPurchaseManager.m
    @implementation InAppPurchaseManager 
    
    @synthesize myContext; //step5
    

    〜中略〜

    // 「7. MyStoreObserverにpaymentQueue:updatedTransactions:メソッドを実装します。」部分
    - (void)paymentQueue:(SKPaymentQueue *)queue
      updatedTransactions:(NSArray *)transactions
    {
        
        for (SKPaymentTransaction *transaction in transactions)
        {
            const uint8_t* msg1; //step5
            const uint8_t* msg2 = (uint8_t *)[transaction.payment.productIdentifier UTF8String]; //step5
            
            switch (transaction.transactionState)
            {
                case SKPaymentTransactionStatePurchasing:
                    // 購入処理中。基本何もしなくてよい。処理中であることがわかるようにインジケータをだすなど。
                    break;
                case SKPaymentTransactionStatePurchased:
                    // 購入処理成功
                    msg1 = (const uint8_t*)"Purchased"; //step5
                    FREDispatchStatusEventAsync(myContext,msg1,msg2); //step5
                    
                    [self showAlert:@"購入処理成功!"];
                    NSLog(@"購入処理成功!");
                    NSLog(@"transaction : %@",transaction);
                    NSLog(@"transaction.payment.productIdentifier : %@",transaction.payment.productIdentifier);
                    [queue finishTransaction: transaction];
                    break;
                case SKPaymentTransactionStateFailed:
                    msg1 = (const uint8_t*)"Failed"; //step5
                    FREDispatchStatusEventAsync(myContext,msg1,msg2); //step5
                    
                    
                    // 購入処理失敗。ユーザが購入処理をキャンセルした場合もここ
                    if (transaction.error.code != SKErrorPaymentCancelled)
                    {
                        // 必要に応じてここでエラーを表示する
                         [self showAlert:[transaction.error localizedDescription]];
                        NSLog(@"購入処理失敗。");
                        NSLog(@"error : %@",[transaction.error localizedDescription]);
                    }
                   [queue finishTransaction:transaction];
                    break;
                case SKPaymentTransactionStateRestored:
                    //リストア処理開始
                    msg1 = (const uint8_t*)"Restored"; //step5
                    FREDispatchStatusEventAsync(myContext,msg1,msg2); //step5
                    
                    [self showAlert:@"リストア処理開始"];
                    NSLog(@"リストア処理");
                    NSLog(@"transaction : %@",transaction);
                    NSLog(@"transaction.originalTransaction.payment.productIdentifier : %@",transaction.originalTransaction.payment.productIdentifier);
                    [queue finishTransaction:transaction];
                default:
                    break;
            } }
    }
    
    HelloWorldANE.m
    FREObject GetHelloWorld(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]) {
        
        ///ここからアプリ内課金用に追加///
        const char *str = "Hello, app in purchase!";
        
        InAppPurchaseManager *appMng = [[InAppPurchaseManager alloc]init];
        
        appMng.myContext = ctx; //step5
        
        if (appMng.canMakePurchases) {
            [appMng requestProductData:@"com.mushikago.InAppPurchaseXcode.unLock"];
        }else {
            str = "課金が許可されていません。";
        }
        
        ///ここまで
        
        FREObject retStr;
        FRENewObjectFromUTF8(strlen(str)+1, (const uint8_t *)str, &retStr);
        
        return retStr;
    }
    

    Buildして、新たに「libHelloWorldANE.a」を作成します。

    7.ネイティブ拡張ActionScriptライブラリ(.swc)にイベントを受け取る記述を追加

    次に、FlashBuilderでswcの元となっているHelloWorldExtensionクラスを以下のように修正します。(ハイライト部分)
    まず、importを忘れずに追加し、次に、addEventListenerでStatusEvent.STATUSを受信したら、onStatusEventを呼び出す感じ。(上記のサンプルファイル内のswcでは、この記述まで含めてあるのでコンパイルし直さなくても使えると思います。)

    package com.mushikago
    {
    	import flash.events.Event;
    	import flash.events.EventDispatcher;
    	import flash.events.IEventDispatcher;
    	import flash.events.StatusEvent;
    	import flash.external.ExtensionContext;
    	
    	public class HelloWorldExtension extends EventDispatcher
    	{
    		private var context:ExtensionContext;
    		public function HelloWorldExtension(target:IEventDispatcher=null)
    		{
    			super(target);
    			context= ExtensionContext.createExtensionContext("mushikago.ane.HelloWorld", "type"); 
    			context.addEventListener(StatusEvent.STATUS, onStatusEvent);
    		}
    		
    		protected function onStatusEvent(event:StatusEvent):void
    		{
    			dispatchEvent(event);
    		}
    
    		public function getHelloWorld():String
    		{
    			return context.call("GetHelloWorld") as String;
    		}
    		
    		public function dispose():void
    		{
    			return context.dispose();
    		}
    		
    	}
    }
    

    プロジェクトのビルドをして、新たにswcとlibrary.swfを作成します。(上記のサンプルの「HelloWorldANE_Event_ASLIB.swc」はすでにこの記述をしてあります。)

    証明書ファイル(.p12)や記述ファイル(.xml)、リンケージオプションファイル(.xml)は修正せずそのまま使えるので、「libHelloWorldANE.a」、「HelloWorldANE_Event_ASLIB.swc」、「library.swf」を新しくしたら、ADTコマンドでANEを作り直します。

    8.StatusEvent.code、StatusEvent.levelで動作を振り分け

    ここまでで購入状況によってカスタムイベントが発生するようになっているはずですが、それをFlash Proで受け取り、その結果に応じて処理を変えるようにしてみます。
    HelloWorldを表示させたAIR for iOSのFLAファイルをFlash Proで開き、ステージ上に「test2_txt」というテキストフィールドを追加して、第一フレームのActionScriptを次の様に修正します。

    //ANE HelloWorld
    import com.mushikago.HelloWorldExtension;
    test_txt.text = "test";
    var _ane:HelloWorldExtension = new HelloWorldExtension();
    test_txt.text = _ane.getHelloWorld();
    
    import flash.events.StatusEvent
    _ane.addEventListener(StatusEvent.STATUS, onStatusEvent);
    
    function onStatusEvent(event:StatusEvent):void
    {
    	test2_txt.text = event.code + " : " + event.level;
    }
    

    この例を見てわかると思いますが、ネイティブ拡張ライブラリで設定したカスタムイベント発生部分の

    FREDispatchStatusEventAsync(myContext,msg1,msg2); //step5
    

    の第2パラメータ、第3パラメータが、ActionScriptの

    test2_txt.text = event.code + " : " + event.level;
    

    のevent.code、event.level(「event」はStatusEvent)で受け取るようになっています。
    ANEとAIRアプリ間では、このcodeとlevelの2つのパラメータを駆使してやり取りすることになります。

    ここまでで一度パブリッシュして実機で動作確認し、購入ダイアログが出たら「キャンセル」をしてみましょう。(もちろん、テストアカウントで購入してみてもいいのですが、一度購入したユーザはその購入履歴をリセットできませんので、次にテストをする際は、新たにテストユーザを作る必要があります。)

    キャンセルを押すと…

    “Failed : ” + プロダクトIDが返ってきてテキストフィールドに表示されたと思います。
    これは、

    InAppPurchaseManager.m
                case SKPaymentTransactionStateFailed:
                    msg1 = (const uint8_t*)"Failed"; //step5
                    FREDispatchStatusEventAsync(myContext,msg1,msg2); //step5
    

    の部分で発生した”キャンセルした場合のカスタムイベント“を受けっとって、テキストフィールドに表示した状態となります。

    9.購入された場合に、購入記録を暗号化して保存

    そしたら最後に、「ユーザがアプリ内でプロダクトを購入した場合は、それを結果をローカルに保存し、アプリ起動時にその購入状態をチェックして、未購入の場合のみダイアログを出す」ようにしてみます。これは、つまり「ロック解除」というアプリ内のプロダクトを購入済みのユーザは、ロック解除してフル機能をアプリに与えるような場合に使います。

    SharedObjectを使いたいと思われるところですが、ここは「EncryptedLocalStore」というものをつかって暗号化して保存するようにします。

    カスタムイベントを発生させているところまでは同じなので、Flashファイル(.fla)のみ修正します。
    第一フレームに設定されているAcrionScriptを次の様に修正します。

    //ANE HelloWorld
    import com.mushikago.HelloWorldExtension;
    import flash.data.EncryptedLocalStore;
    import flash.utils.ByteArray;
    
    test_txt.text = "test";
    var _ane:HelloWorldExtension = new HelloWorldExtension();
    
    if(EncryptedLocalStore.isSupported){
    	
    	var storedValue:ByteArray = EncryptedLocalStore.getItem("PurchasedProdID");
    	if(storedValue == null){
    		purchase()
    	}else if(storedValue.readUTFBytes(storedValue.length) == "com.mushikago.InAppPurchaseXcode.unLock"){
    		test_txt.text = "UnLock!!"
    	}else{
    		purchase()
    	}
    	
    }else{
    	test_txt.text = "EncryptedLocalStore is not Supported.";
    }
    
    function purchase():void
    {
    	test_txt.text = _ane.getHelloWorld();
    }
    
    //テストのためにEncryptedLocalStoreを削除したい場合
    /*
    button.addEventListener(MouseEvent.CLICK, function(e:MouseEvent){
    	EncryptedLocalStore.removeItem("PurchasedProdID");
    	});
    */
    
    import flash.events.StatusEvent
    _ane.addEventListener(StatusEvent.STATUS, onStatusEvent);
    
    function onStatusEvent(event:StatusEvent):void
    {
    	test2_txt.text = event.code + " : " + event.level;
    	
    	if(event.code == "Purchased"){
    		var str:String = event.level;
    		var bytes:ByteArray = new ByteArray();
    		bytes.writeUTFBytes(str);
    		EncryptedLocalStore.setItem("PurchasedProdID", bytes, false);
    	}
    }
    

    14,15行目でローカルに保存されているプロダクトIDが一致した場合は、購入プロセスを開始せず、”UnLock!!”と表示するようになっています。(購入プロセスを開始するのも通常は「購入ボタン」を押したときにすべきですのでご注意を)

    また、実際に「購入」をテストするには、iTunes Connectでテストユーザを作り、実機で確認する必要があります。(実際のAppleIDを使わないように!テストユーザでログインし直すには、iPhone実機の設定>Storeの最下部のApple IDをクリックしてサインアウトします。)また、このタイプのプロダクトは一度購入すると、そのユーザではその後「購入済み」となってしまいます。再度、購入のテストをするためには、新たにテストユーザを作ってテストするようになりますので、「購入した場合」をテストする必要がない場合は購入ダイアログを出すまでにしてキャンセルするのがいいと思います。

    ※テストユーザは、実在するメールアドレスである必要はありませんが、他の開発者と被らないように独自ドメインのものにしておいた方がいいです。

    ちなみに、このサンプルアプリはホームボタンを押しても、マルチタスクで後ろで起動したままになっています。強制終了するには、ホームボタンを2度連打して、最近起動したアプリリストでアイコンを長押ししてXボタンが出たらそれを押して終了させましょう。

    実機にてテストアカウントで購入してみると...

    一度購入するとその結果がローカル保存され、次回起動時以降は購入プロセスを開始しない。

    最後に

    本当にお疲れ様でした。かなり長くなってしまいましたが、ANEでなんとかアプリ内課金を試す事ができたと思います。
    最後の状態のファイルも用意しておきましたので、ご参考までに。(プロダクトIDやANEまでのパスなどは作成するアプリや環境によって修正しましょう。)

    最終形態のファイル:_smaMatome_part2.zip


    追記:
    FlexUGのいくつかのスレ、有川さんのADC記事AKABANAブログを大変参考にさせていただきました。ありがとうございます!

    追記(2012.10.05):まとめスライドを公開しました。

コメント

  1. kuni より:

    大変参考になりました!

    1つだけ、adt -packageが通らなかった場所があり、
    それは「myplatformoptions.xml」の一部を大文字にする事で通りましたので、報告させていただきます。
    (※Airのバージョンを上げているので、もしかしたらそのせいかもしれません。)

    5.1

    -framework StoreKit

    のうち、

    sdkversion→sdkVersion
    linkeroptions→linkerOptions

    に修正したら通りました。

    ※http://help.adobe.com/ja_JP/air/extensions/WSf268776665d7970d-2e74ffb4130044f3619-7fff.html
    のページをみて解決しました。
    😀

  2. kuni より:

    すみません、xmlをペーストしたのですがタグとして評価されてしまって、よくわからない文章になってますね… :sad:

    「myplatformoptions.xml」の中のタグの一部を大文字に変える必要があった、という内容でした。

    コメント欄を汚してしまいすみません 😥

  3. tshiraishi より:

    コメントありがとうございます!後で追記に加えておきます!

//gist-embed