アプリ内課金の総まとめを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機能を実装
- ANEのHelloWorldサンプルで作成したファイルを用意する
- 「アプリ内課金をXcodeでやってみる」で作ったクラスを読み込む
- “Hello”を返している場所にアプリ内課金を呼び出す記述を追加
- リンケージオプションファイルを準備
- 一度、ここで動作確認してみる
購入の記録
- ANE側からAIRアプリに対してイベントを発行
- ネイティブ拡張ActionScriptライブラリ(.swc)にイベントを受け取る記述を追加
- StatusEvent.code、StatusEvent.levelで動作を振り分け
- 購入された場合に、購入記録を暗号化して保存
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):まとめスライドを公開しました。
東京造形大学卒業後、マクロメディア(現アドビ)に入社。QAやテクニカルサポートマネージャーとしてFlash、DreamweaverなどのWeb製品を担当。独立後、2007年に虫カゴデザインスタジオ株式会社を設立。2021年東京三鷹を拠点に。最近は、Unity, Unity Netcode for GameObjects, CakePHP, Laravel, ZBrush, Modo, Adobe Substance 3D, Adobe Firefly, Xcode, Apple Vision Pro, Firebaseにフォーカスしています。モバイルアプリ開発情報を主としたブログ「MUSHIKAGO APPS MEMO」の中の人。
コメント
大変参考になりました!
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
のページをみて解決しました。
😀
すみません、xmlをペーストしたのですがタグとして評価されてしまって、よくわからない文章になってますね… 🙁
「myplatformoptions.xml」の中のタグの一部を大文字に変える必要があった、という内容でした。
コメント欄を汚してしまいすみません 😥
コメントありがとうございます!後で追記に加えておきます!