Selaa lähdekoodia

Added iap module.

Mark Sibly 7 vuotta sitten
vanhempi
commit
690769d0c3

+ 103 - 0
modules/iap/README.TXT

@@ -0,0 +1,103 @@
+
+***** The IAP (In App Purchases) module *****
+
+***** THE IAP API *****
+
+Enum ProductType
+	Consumable=1
+	NonConsumable=2
+End
+
+Class Product
+
+	Method New( identifier:String,type:ProductType )	'Creates a new product
+
+	Property Identifier:String 		'Product identifier, used to identify product to store.
+	Property Type:ProductType 		'Product type: ProductType.Consumable or ProductType.NonConsumable
+	Property Valid:Bool				'True if product was found in store by OpenStore. 
+    Property Title:String			'Product title, only valid once store is open and if Valid property is also true.
+	Property Description:String		'Product description, only valid once store is open and if Valid property is also true.
+	Property Price:String 			'Product price, only valid once store is open and if Valid property is also true.
+End
+
+Class IAPStore
+
+	Field OpenStoreComplete:Void( result:Int,interrupted:Product[] )		'Called when OpenStore completes.
+	
+	'If result is 0, the store was opened successfully. Otherwise, an error occured. You can try and open the store again a bit later if you want.
+	'If the store opened successfully, interrupted contains an array of interrupted product purchases. That is, purchases made by the user that the app was never informed of. This can happen if an app crashes halfway through a purchase.
+	
+	Field BuyProductComplete:Void( result:Int,product:Product )				'Called when BuyProduct completes.
+	
+	'If result is 0, the product was successfully purchased.
+	'If result is 1, the purchase process was cancelled by the user.
+	'If result is >1, a non fatal error occured and the product was not purchased.
+	'If result is <0, a fatal error occured and the store will need to be reopened before it can be used again.
+	
+	Field GetOwnedProductsComplete:Void( result:Int,products:Product[] )	'Called when GetOwnedProducts completes.
+
+	'If result is 0, owned is an array of all non-consumable products owned by the user.
+	'If result is >0, a non fatal error occured.
+	'If result is <0, a fatal error occured and the store will need to be reopened before it can be used again.
+
+	Method New()
+	
+	Property Open:Bool()	'True if store is open, ie: OpenStore has successfully completed.
+	Property Busy:Bool()	'True if store is busy, ie: OpenStore, BuyProduct or GetOwnedProducts is currently in progress.
+	
+	Method OpenStore( products:Product[] )	'Attempt to open the store.
+	Method BuyProduct( product:Product )	'Attempt to buy a product.
+	Method GetOwnedProducts()				'Attempty to get owned products.
+End
+
+
+***** Android *****
+
+* Building IAP apps.
+
+To be able to build android apps that can use iap, You will need to modify your android studio project as per steps 1 and 2 here:
+
+https://developer.android.com/google/play/billing/billing_integrate.html
+
+For step 1, I've included an 'aidl' directory in the iap module's 'native' folder, just copy this into your android studio project's app/src/main folder.
+
+
+* Testing IAP apps.
+
+1) Create a new app via the GooglePlay console. This involves filling in a bunch of forms, uploading some artwork etc.
+
+2) Upload a *signed* APK of your app via the GooglePlay console. You can sign an app using the Build->Generate Signed APK... menu in android studio. More information on app signing can be found here: https://developer.android.com/studio/publish/app-signing.html#sign-apk
+
+3) Publish your app as an alpha. Note that this can take several hours, during which time your app's status will be 'pending publication'. You can still add in app purchases etc at this point, but you wont be able to test IAP from your device until the app is successfully published. Note that the app is still only alpha, so only you and your testers will be able to see it.
+
+4) Add in app purchase products to the app via the GooglePlay console. Make sure they are enabled!
+
+5) Add yourself as a 'tester' for your GooglePlay account. This is done via Settings->License Testing on the GooglePlay comsole. You may need a gmail account for this.
+
+5) Upload the *signed* APK of your app to your device. This can be done via the ADB command line tool in the android SDK.
+
+6) You can also get andriod studio to automatically sign your app each time it builds it -  see: https://developer.android.com/studio/publish/app-signing.html#sign-auto
+
+6) Note that it is important that the key you use to sign the apk you upload to google play is the same as the one you use to sign the app for testing on your device - do not lose this key!
+
+
+***** iOS *****
+
+* Building IAP apps
+
+You will need to add the StoreKit framework to your app project in xcode.
+
+
+* Testing IAP apps *
+
+Publishing on iOS is hard! All I can really offer is some tips:
+
+1) If you're having problems with 'provisioning profiles', it may help to delete them from your keychain and to let XCode automatically restore them when necessary.
+
+2) Along the same lines, delete any expired or invalid certificates and provisioning profiles from your developer account online.
+
+3) It's not actually necessary to upload an app to itunes connect to test out iap. Just create the app, create the iap products, add the items to the app, and add a test user. See: https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/TestingInAppPurchases.html
+
+4) You do need to make sure all the itunes paperwork is up to date though. This held me up for several hours!
+
+5) If you're having trouble uploading a screenshot for iap products (ie: 'invalid screenshot' errors), try leaving the page and creating the item from scratch again using the same screenshot. Worked for me several times!

+ 259 - 0
modules/iap/bananas/iaptest.monkey2

@@ -0,0 +1,259 @@
+
+#rem
+
+IMPORTANT!
+
+This wont work 'as is'! You'll need to set up a bunch of stuff on GooglePlay/iTunes Connect developer portal such as app/products etc.
+
+#end
+Namespace myapp
+
+#Import "<iap>"
+#Import "<std>"
+#Import "<mojo>"
+#Import "<mojox>"
+
+Using iap..
+Using std..
+Using mojo..
+Using mojox..
+
+Class MyWindow Extends Window
+	
+	Field _purchases:JsonObject
+	
+	Field _products:Product[]
+	
+	Field _info:String
+
+	Field _listView:ListView
+
+	Field _iap:IAPStore
+	
+	Method New( title:String="Simple mojo app",width:Int=640,height:Int=480,flags:WindowFlags=Null )
+
+		Super.New( title,width,height,flags )
+
+		Layout="stretch"
+		
+		'Create our IAP products.
+		'
+		_products=New Product[](
+			New Product( "speed_boost",ProductType.Consumable ),
+			New Product( "bullet_boost",ProductType.Consumable ),
+			New Product( "ship_upgrade",ProductType.NonConsumable ) )
+		
+		'Load purchases we've alreday bought.
+		'
+		LoadPurchases()
+
+		'Create a ListView to fill in with products later...
+		'	
+		_listView=New ListView
+		_listView.Layout="float"
+		_listView.Gravity=New Vec2f( .5,.5 )
+		
+		_listView.ItemClicked=Lambda( item:ListView.Item )
+		
+			If _iap.Busy Return
+			
+			Local index:= _listView.IndexOfItem( item )
+			
+			If index=_products.Length
+				
+				_iap.GetOwnedProducts()
+				
+				Return
+			
+			Endif
+			
+			Local product:=_products[ index ]
+			
+			'it's actually harmless to let someone purchase a non consumable again, they'll
+			'only be charged for it once, but still...
+			If product.Type=ProductType.NonConsumable And _purchases.GetNumber( product.Identifier )>0 Return
+		
+			_iap.BuyProduct( product )
+
+			_info="Buying product "+product.Identifier
+		End
+		
+		ContentView=_listView
+		
+		'Create IAPStore and various handlers.
+		'
+		_iap=New IAPStore
+		
+		'This isc alled when OpenStore completes.
+		'
+		_iap.OpenStoreComplete=Lambda( result:Int,interrupted:Product[],owned:Product[] )
+		
+			Select result
+			Case 0
+
+				'Handle interrupted purchases - these are purchases that the user was charged for but your app never
+				'got notified about coz it or the OS crashed or something...
+				'
+				'This array is always empty on ios which apparently handles this edge case better?
+				'
+				For Local product:=Eachin interrupted
+					MakePurchase( product )
+				Next
+				
+				'Handle owned products - these are non-consumables that have been bought by the user. Android tracks these for
+				'you (as long as you're logged in anyway) so you don't strictly need to save them, but you do on ios so for the sake of 
+				'uniformity we will on android too.
+				'
+				'On Android, you never really need to use GetOwnProducts() because you are given this array of owned products when the store
+				'opens. So in a sense, android automatically 'restores purchases' whenever you open the store.
+				'
+				'This array is always empty on ios. To restore non-consumables on ios you must call GetOwnedProducts in response to the push
+				'of a 'Restore Purchases' button or similar. Apps with IAP but no 'restore purchases' mechanism will be rejected from the 
+				'app store. GetOwnedProducts may cause dialogs to appear etc as the user may need to log into itunes, which is why it's not
+				'just done by default when the store opens the way it is on android.
+				'
+				For Local product:=Eachin owned
+					MakePurchase( product )
+				Next
+				
+				SavePurchases()
+				UpdateListView()
+				
+				_info="OpenStore successful"
+			Default
+				_info="OpenStore error:"+result
+			End
+		
+		End
+		
+		'This is called when BuyProduct completes.
+		'
+		_iap.BuyProductComplete=Lambda( result:Int,product:Product )
+		
+			Select result
+			Case 0	'success!
+				MakePurchase( product )
+				SavePurchases()
+				UpdateListView()
+				_info="BuyProduct successful:"+product.Identifier
+			Case 1	'cancelled!
+				_info="BuyProduct cancelled:"+product.Identifier
+			Default
+				_info="BuyProduct error:"+result
+			End
+				
+		End
+		
+		'This is called when GetOwnProducts completes.
+		'
+		_iap.GetOwnedProductsComplete=Lambda( result:Int,owned:Product[] )
+
+			Select result
+			Case 0	'success!
+				
+				'Make sure we really own all owned products.
+				'
+				For Local product:=Eachin owned
+					MakePurchase( product )
+				Next
+				
+				SavePurchases()
+				UpdateListView()
+				_info="GetOwnProducts successful"
+			Default
+				_info="GetOwnedProducts error:"+result
+			End
+			
+		End
+		
+
+		_iap.OpenStore( _products )
+		
+		_info="Opening store..."
+	End
+	
+	Method MakePurchase( product:Product )
+		
+		Select product.Type
+		Case ProductType.Consumable
+			_purchases.SetNumber( product.Identifier,_purchases.GetNumber( product.Identifier )+1 )
+		Case ProductType.NonConsumable
+			_purchases.SetNumber( product.Identifier,1 )
+		End
+	End
+	
+	Method LoadPurchases()
+		
+		'load purchases file from internal app storage.
+		'
+		_purchases=JsonObject.Load( "internal::purchases.json" )
+		
+		'if not found, create new purchases file.
+		'
+		If Not _purchases
+			
+			_purchases=New JsonObject
+			
+			For Local product:=Eachin _products
+				_purchases.SetNumber( product.Identifier,0 )
+			Next
+			
+			SavePurchases()
+			
+		Endif
+		
+	End
+	
+	Method SavePurchases()
+		
+		_purchases.Save( "internal::purchases.json" )
+	
+	end
+
+	'populate listview with products
+	'
+	Method UpdateListView()
+
+		_listView.RemoveAllItems()
+		
+		If _iap.Open
+		
+			For Local product:=Eachin _products
+				
+				'products become valid when store is opened.
+				'
+				'If Not product.Valid Continue
+				
+				_listView.AddItem( product.Title+" - "+product.Description+" for the low price of "+product.Price+" ("+_purchases.GetNumber( product.Identifier )+")" )
+			Next
+			
+		Endif
+			
+		_listView.AddItem( "[RESTORE PURCHASES]" )
+		
+	End
+	
+	Method OnRender( canvas:Canvas ) Override
+		
+		App.RequestRender()
+		
+		canvas.DrawText( "Hello World!",Width/2,Height/2,.5,.5 )
+		
+		canvas.DrawText( "IAP info:"+_info,0,0 )
+	End
+	
+	Method OnMeasure:Vec2i() Override
+		
+		Return New Vec2i( 640,360 )
+	End
+	
+End
+
+Function Main()
+
+	New AppInstance
+	
+	New MyWindow
+	
+	App.Run()
+End

+ 141 - 0
modules/iap/iap.monkey2

@@ -0,0 +1,141 @@
+
+Namespace iap
+
+#If __MOBILE_TARGET__
+
+#Import "<std>"
+#Import "<mojo>"
+
+#If __TARGET__="android"
+
+#Import "iap_android"
+
+#Elseif __TARGET__="ios"
+
+#Import "iap_ios"
+
+#Endif
+
+Using std..
+Using mojo..
+
+Enum ProductType
+	Consumable=1
+	NonConsumable=2
+End
+
+Class IAPStore
+	
+	Field OpenStoreComplete:Void( result:Int,interrupted:Product[],owned:Product[] )
+	
+	Field BuyProductComplete:Void( result:Int,product:Product )
+	
+	Field GetOwnedProductsComplete:Void( result:Int,owned:Product[] )
+	
+	Method New()
+
+		_iap=New IAPStoreRep
+	End
+	
+	Property Open:Bool()
+		
+		Return _state>0
+	End
+	
+	Property Busy:Bool()
+		
+		Return _state>1
+	End
+	
+	Method OpenStore( products:Product[] )
+		
+		If _state>0 Return
+		
+		_products=products
+
+		_state=2
+		
+		App.Idle+=UpdateState
+
+		_iap.OpenStoreAsync( _products )
+	End
+	
+	Method BuyProduct( product:Product )
+		
+		If _state<>1 Return
+		
+		_buying=product
+		
+		_state=3
+		
+		App.Idle+=UpdateState
+		
+		_iap.BuyProductAsync( product )
+	End
+	
+	Method GetOwnedProducts()
+		
+		If _state<>1 Return
+		
+		_state=4
+		
+		App.Idle+=UpdateState
+		
+		_iap.GetOwnedProductsAsync()
+	End
+	
+	Private
+	
+	Field _products:Product[]
+	
+	Field _iap:IAPStoreRep
+	
+	Field _state:=0
+	
+	Field _buying:Product
+
+	Method UpdateState()
+		
+		If _iap.IsRunning() 
+			App.Idle+=UpdateState
+			Return
+		Endif
+		
+		Local result:=_iap.GetResult()
+		Local state:=_state
+		
+		_state=1
+		
+		Select state
+		Case 2	'openstore
+			
+			If result<0 _state=0
+			
+			Local interrupted:=New Stack<Product>
+			Local owned:=New Stack<Product>
+			For Local product:=Eachin _products
+				If product.Interrupted interrupted.Push( product )
+				If product.Owned owned.Push( product )
+			Next
+			OpenStoreComplete( result,interrupted.ToArray(),owned.ToArray() )
+			
+		Case 3	'buyproduct
+			
+			Local buying:=_buying
+			_buying=Null
+			BuyProductComplete( result,buying )
+			
+		Case 4	'GetOwnedProducts
+			
+			Local owned:=New Stack<Product>
+			For Local product:=Eachin _products
+				If product.Owned owned.Push( product )
+			Next
+			GetOwnedProductsComplete( result,owned.ToArray() )
+		End
+		
+	End
+	
+End
+
+#Endif

+ 184 - 0
modules/iap/iap_android.monkey2

@@ -0,0 +1,184 @@
+
+Namespace iap
+
+#Import "<sdl2>"
+#Import "<jni>"
+
+#Import "native/Monkey2IAP.java"
+
+Using jni..
+
+Class Product
+	
+	Method New( identifier:String,type:ProductType )
+		Init()
+		
+		_instance=Env.NewObject( _class,_ctor,New Variant[]( identifier,Cast<Int>( type ) ) )
+		
+		_globalref=Env.NewGlobalRef( _instance )
+	End
+	
+	Property Identifier:String()
+		
+		Return Env.GetStringField( _instance,_identifier )
+	End
+	
+	Property Type:ProductType()
+		
+		Return Cast<ProductType>( Env.GetIntField( _instance,_type ) )
+	End
+
+	Property Valid:Bool()
+		
+		Return Env.GetBooleanField( _instance,_valid )
+	End
+	
+	Property Title:String()
+		
+		Return Env.GetStringField( _instance,_title )
+	End
+	
+	Property Description:String()
+		
+		Return Env.GetStringField( _instance,_description )
+	End
+	
+	Property Price:String()
+		
+		Return Env.GetStringField( _instance,_price )
+	End
+	
+	Internal
+	
+	Property Owned:Bool()
+		
+		Return Env.GetBooleanField( _instance,_owned )
+	End
+	
+	Property Interrupted:Bool()
+		
+		Return Env.GetBooleanField( _instance,_interrupted )
+	End
+	
+	Private
+	
+	Global _class:jclass
+	Global _ctor:jmethodID
+	
+	Global _valid:jfieldID
+	Global _title:jfieldID
+	Global _description:jfieldID
+	Global _price:jfieldID
+	Global _identifier:jfieldID
+	Global _type:jfieldID
+	Global _owned:jfieldID
+	Global _interrupted:jfieldID
+
+	Function Init()
+		
+		If _class Return
+
+		Local env:=sdl2.Android_JNI_GetEnv()
+
+		_class=env.FindClass( "com/monkey2/lib/Monkey2IAP$Product" )
+		_ctor=env.GetMethodID( _class,"<init>","(Ljava/lang/String;I)V" )
+		
+		_valid=env.GetFieldID( _class,"valid","Z" )
+		_title=env.GetFieldID( _class,"title","Ljava/lang/String;" )
+		_description=env.GetFieldID( _class,"description","Ljava/lang/String;" )
+		_price=env.GetFieldID( _class,"price","Ljava/lang/String;" )
+		_identifier=env.GetFieldID( _class,"identifier","Ljava/lang/String;" )
+		_type=env.GetFieldID( _class,"type","I" )
+		_owned=env.GetFieldID( _class,"owned","Z" )
+		_interrupted=env.GetFieldID( _class,"interrupted","Z" )
+	End
+	
+	Field _instance:jobject
+	Field _globalref:jobject
+
+	Property Env:JNIEnv()
+		
+		Return sdl2.Android_JNI_GetEnv()
+	End
+	
+End
+
+Internal
+
+Class IAPStoreRep
+	
+	Method New()
+		
+		Init()
+		
+		_instance=Env.NewObject( _class,_ctor,Null )
+	End
+	
+	Method OpenStoreAsync:Bool( products:Product[] )
+		
+		Local jarray:jobjectArray=Env.NewObjectArray( products.Length,Product._class,Null )
+		
+		For Local i:=0 Until products.Length
+			
+			Env.SetObjectArrayElement( jarray,i,products[i]._instance )
+		Next
+		
+		Return Env.CallBooleanMethod( _instance,_openstoreasync,New Variant[]( Cast<jobject>( jarray ) ) )
+	End
+	
+	Method BuyProductAsync:Bool( product:Product )
+		
+		Return Env.CallBooleanMethod( _instance,_buyproductasync,New Variant[]( product._instance ) )
+	End
+			
+	Method GetOwnedProductsAsync:Bool()
+		
+		Return Env.CallBooleanMethod( _instance,_getownedproductsasync,Null )
+	End
+	
+	Method IsRunning:Bool()
+		
+		Return Env.CallBooleanMethod( _instance,_isrunning,Null )
+	End
+	
+	Method GetResult:Int()
+		
+		Return Env.CallIntMethod( _instance,_getresult,Null )
+	End
+	
+	Private
+	
+	Global _class:jclass
+	Global _ctor:jmethodID
+
+	Global _openstoreasync:jmethodID
+	Global _buyproductasync:jmethodID
+	Global _getownedproductsasync:jmethodID
+	Global _isrunning:jmethodID
+	Global _getresult:jmethodID
+	
+	Method Init()
+		
+		If _class Return
+
+		Local env:=sdl2.Android_JNI_GetEnv()
+
+		_class=env.FindClass( "com/monkey2/lib/Monkey2IAP" )
+		_ctor=env.GetMethodID( _class,"<init>","()V" )
+		
+		_openstoreasync=env.GetMethodID( _class,"OpenStoreAsync","([Lcom/monkey2/lib/Monkey2IAP$Product;)Z" )
+		_buyproductasync=env.GetMethodID( _class,"BuyProductAsync","(Lcom/monkey2/lib/Monkey2IAP$Product;)Z" )
+		_getownedproductsasync=env.GetMethodID( _class,"GetOwnedProductsAsync","()Z" )
+		_isrunning=env.GetMethodID( _class,"IsRunning","()Z" )
+		_getresult=env.GetMethodID( _class,"GetResult","()I" )
+		
+	End
+	
+	Field _instance:jobject
+	
+	Property Env:JNIEnv()
+		
+		Return sdl2.Android_JNI_GetEnv()
+	End
+	
+End

+ 103 - 0
modules/iap/iap_ios.monkey2

@@ -0,0 +1,103 @@
+
+Namespace iap
+
+#Import "native/iap_ios.mm"
+
+#Import "native/iap_ios.h"
+
+Extern Internal
+
+Class BBProduct="BBProduct"
+
+	Field identifier:String
+	Field type:Int
+	Field valid:Bool
+	Field title:String
+	Field description:String
+	Field price:String
+	Field owned:Bool
+End
+
+Class BBIAPStore="BBIAPStore"
+	
+	Method BBOpenStoreAsync:Bool( products:BBProduct[] )="OpenStoreAsync"
+	Method BBBuyProductAsync:Bool( product:BBProduct )="BuyProductAsync"
+	Method GetOwnedProductsAsync:Bool()
+	
+	Method IsRunning:Bool()
+	Method GetResult:Int()
+End
+
+public
+
+Class Product Extends BBProduct
+	
+	Method New( identifier:String,type:Int )
+		
+		Self.identifier=identifier
+		Self.type=type
+	End
+
+	Property Valid:Bool()
+		
+		Return valid
+	End
+		
+	Property Title:String()
+
+		Return title
+	End
+	
+	Property Description:String()
+		
+		Return description
+	End
+	
+	Property Price:String()
+		
+		Return price
+	End
+	
+	Property Identifier:String()
+		
+		Return identifier
+	End
+	
+	Property Type:Int()
+		
+		Return type
+	End
+	
+	Internal
+	
+	Property Interrupted:Bool()
+		
+		Return False
+	end
+	
+	Property Owned:Bool()
+		
+		Return owned
+	End
+	
+End
+
+Class IAPStoreRep Extends BBIAPStore
+
+	Method OpenStoreAsync:Bool( products:Product[] )
+		
+		Local bbproducts:=New BBProduct[products.Length]
+		
+		For Local i:=0 Until bbproducts.Length
+			bbproducts[i]=products[i]
+		Next
+		
+		Return Super.BBOpenStoreAsync( bbproducts )
+	End
+	
+	Method BuyProductAsync:Bool( product:Product )
+		
+		Return Super.BBBuyProductAsync( product )
+	End
+	
+End

+ 8 - 0
modules/iap/module.json

@@ -0,0 +1,8 @@
+{
+	"module":"iap",
+	"about":"In App Purchases",
+	"author":"Mark Sibly",
+	"version":"1.0.04",
+	"support":"http://monkeycoder.co.nz",
+	"depends":["jni","mojo"]
+}

+ 321 - 0
modules/iap/native/Monkey2IAP.java

@@ -0,0 +1,321 @@
+
+package com.monkey2.lib;
+
+import android.os.*;
+import android.app.*;
+import android.content.*;
+
+import java.util.*;
+
+import com.android.vending.billing.*;
+
+import org.json.*;
+
+public class Monkey2IAP extends Monkey2Activity.Delegate implements ServiceConnection{
+
+    private static final String TAG = "Monkey2IAP";
+
+	private static final int ACTIVITY_RESULT_REQUEST_CODE=101;
+
+	public static class Product{
+	
+		public boolean valid;
+		public String title;
+		public String identifier;
+		public String description;
+		public String price;
+		public int type;
+		public boolean owned;
+		public boolean interrupted;
+		
+		Product( String identifier,int type ){
+			this.identifier=identifier;
+			this.type=type;
+		}
+	}
+	
+	public Monkey2IAP(){
+
+		//Log.v( TAG,"Monkey2IAP()" );
+
+		_activity=Monkey2Activity.instance();
+		
+		Intent intent=new Intent( "com.android.vending.billing.InAppBillingService.BIND" );
+
+		intent.setPackage( "com.android.vending" );
+	
+		_activity.bindService( intent,this,Context.BIND_AUTO_CREATE );
+	}
+
+	public boolean OpenStoreAsync( Product[] products ){
+	
+		if( _running ) return false;
+	
+		_products=products;
+		
+		OpenStoreThread thread=new OpenStoreThread();
+		
+		_running=true;
+		
+		_result=-1;
+
+		thread.start();
+		
+		return true;
+	}
+
+	public boolean BuyProductAsync( Product p ){
+	
+		if( _running ) return false;
+	
+		_result=-1;
+	
+		try{
+			Bundle buy=_service.getBuyIntent( 3,_activity.getPackageName(),p.identifier,"inapp","NOP" );
+			int response=buy.getInt( "RESPONSE_CODE" );
+			
+			if( response==0 ){
+				
+				PendingIntent intent=buy.getParcelable( "BUY_INTENT" );
+				if( intent!=null ){
+				
+					Integer zero=Integer.valueOf( 0 );
+					_activity.startIntentSenderForResult( intent.getIntentSender(),ACTIVITY_RESULT_REQUEST_CODE,new Intent(),zero,zero,zero );
+					
+					_running=true;
+					return true;
+				}
+			}
+			switch( response ){
+			case 1:
+				_result=1;	//cancelled
+				break;
+			case 7:
+				_result=0;	//already purchased
+				break;
+			}
+		}catch( IntentSender.SendIntentException ex ){
+		
+		}catch( RemoteException ex ){
+		}
+		
+		return true;
+	}
+	
+	public boolean GetOwnedProductsAsync(){
+	
+		if( _running ) return false;
+	
+		_result=0;
+		
+		return true;
+	}
+	
+	public boolean IsRunning(){
+
+		return _running;
+	}
+	
+	public int GetResult(){
+
+		return _result;
+	}
+	
+	// ***** PRIVATE *****
+
+	Activity _activity;
+	IInAppBillingService _service;
+	Object _mutex=new Object();
+	boolean _running;
+	int _result=-1;
+	Product[] _products;
+	ArrayList unconsumed=new ArrayList();
+	
+	Product FindProduct( String id ){
+		for( int i=0;i<_products.length;++i ){
+			if( id.equals( _products[i].identifier ) ) return _products[i];
+		}
+		return null;
+	}
+	
+	class OpenStoreThread extends Thread{
+	
+		public void run(){
+		
+			//wait for service to start
+			synchronized( _mutex ){
+				while( _service==null ){
+					try{
+						_mutex.wait();
+					}catch( InterruptedException ex ){
+					
+					}catch( IllegalMonitorStateException ex ){
+					
+					}
+				}
+			}
+			
+			int i0=0;
+			while( i0<_products.length ){
+			
+				ArrayList list=new ArrayList();
+				for( int i1=Math.min( i0+20,_products.length );i0<i1;++i0 ){
+					list.add( _products[i0].identifier );
+				}
+
+				Bundle query=new Bundle();
+				query.putStringArrayList( "ITEM_ID_LIST",list );
+				
+				_result=0;
+	
+				try{
+	
+					//Get product details
+					Bundle details=_service.getSkuDetails( 3,_activity.getPackageName(),"inapp",query );
+					ArrayList detailsList=details.getStringArrayList( "DETAILS_LIST" );
+					
+					if( detailsList==null ){
+						_result=-1;
+						_running=false;
+						return;
+					}
+					
+					for( int i=0;i<detailsList.size();++i ){
+					
+						JSONObject jobj=new JSONObject( (String)detailsList.get( i ) );
+	
+						Product p=FindProduct( jobj.getString( "productId" ) );
+						if( p==null ) continue;
+	
+						//strip (APP_NAME) from end of title					
+						String title=jobj.getString( "title" );
+						if( title.endsWith( ")" ) ){
+							int j=title.lastIndexOf( " (" );
+							if( j!=-1 ) title=title.substring( 0,j );
+						}
+						
+						p.valid=true;
+						p.title=title;
+						p.description=jobj.getString( "description" );
+						p.price=jobj.getString( "price" );
+					}
+					
+					//Get owned products and consume consumables
+					Bundle owned=_service.getPurchases( 3,_activity.getPackageName(),"inapp",null );
+					ArrayList itemList=owned.getStringArrayList( "INAPP_PURCHASE_ITEM_LIST" );
+					ArrayList dataList=owned.getStringArrayList( "INAPP_PURCHASE_DATA_LIST" );
+	
+					if( itemList==null || dataList==null ){
+						_result=-1;
+						_running=false;
+						return;
+					}
+					
+					//consume consumables
+					for( int i=0;i<itemList.size();++i ){
+					
+						Product p=FindProduct( (String)itemList.get( i ) );
+						if( p==null ) continue;
+						
+						if( p.type==1 ){
+	
+							JSONObject jobj=new JSONObject( (String)dataList.get( i ) );
+							int response=_service.consumePurchase( 3,_activity.getPackageName(),jobj.getString( "purchaseToken" ) );
+							if( response!=0 ){
+								p.valid=false;
+								_result=-1;
+								break;
+							}
+							p.interrupted=true;
+
+						}else if( p.type==2 ){
+	
+							p.owned=true;
+						}
+					}
+					
+				}catch( RemoteException ex ){
+					_result=-1;
+				}catch( JSONException ex ){
+					_result=-1;
+				}
+			}
+			_running=false;
+		}
+	}
+	
+	class ConsumeProductThread extends Thread{
+	
+		String _token;
+		
+		ConsumeProductThread( String token ){
+			_token=token;
+		}
+		
+		public void run(){
+		
+			try{
+				int response=_service.consumePurchase( 3,_activity.getPackageName(),_token );
+				if( response==0 ) _result=0;
+			}catch( RemoteException ex ){
+			}
+			
+			_running=false;
+		}
+	}
+
+	@Override
+	public void onServiceDisconnected( ComponentName name ){
+		_service=null;
+	}
+
+	@Override
+	public void onServiceConnected( ComponentName name,IBinder service ){
+		_service=IInAppBillingService.Stub.asInterface( service );
+
+		Monkey2Activity.addDelegate( this );
+
+		synchronized( _mutex ){
+			try{
+				_mutex.notify();
+			}catch( IllegalMonitorStateException ex ){
+			}
+		}
+	}
+
+	@Override	
+	public void onActivityResult( int requestCode,int resultCode,Intent data ){
+	
+		if( requestCode!=ACTIVITY_RESULT_REQUEST_CODE ) return;
+
+		int response=data.getIntExtra( "RESPONSE_CODE",0 );
+		
+		switch( response ){
+		case 0:
+			try{
+				JSONObject pdata=new JSONObject( data.getStringExtra( "INAPP_PURCHASE_DATA" ) );
+				Product p=FindProduct( pdata.getString( "productId" ) );
+				if( p!=null ){
+					if( p.type==1 ){
+						ConsumeProductThread thread=new ConsumeProductThread( pdata.getString( "purchaseToken" ) );
+						thread.start();
+						return;
+					}else if( p.type==2 ){
+						p.owned=true;
+						_result=0;
+					}
+				}
+			}catch( JSONException ex ){
+			}
+			break;
+		case 1:
+			_result=1;	//cancelled
+			break;
+		case 7:
+			_result=0;	//already purchased
+			break;
+		}
+
+		_running=false;
+	}
+}

+ 281 - 0
modules/iap/native/aidl/com/android/vending/billing/IInAppBillingService.aidl

@@ -0,0 +1,281 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.billing;
+
+import android.os.Bundle;
+
+/**
+ * InAppBillingService is the service that provides in-app billing version 3 and beyond.
+ * This service provides the following features:
+ * 1. Provides a new API to get details of in-app items published for the app including
+ *    price, type, title and description.
+ * 2. The purchase flow is synchronous and purchase information is available immediately
+ *    after it completes.
+ * 3. Purchase information of in-app purchases is maintained within the Google Play system
+ *    till the purchase is consumed.
+ * 4. An API to consume a purchase of an inapp item. All purchases of one-time
+ *    in-app items are consumable and thereafter can be purchased again.
+ * 5. An API to get current purchases of the user immediately. This will not contain any
+ *    consumed purchases.
+ *
+ * All calls will give a response code with the following possible values
+ * RESULT_OK = 0 - success
+ * RESULT_USER_CANCELED = 1 - User pressed back or canceled a dialog
+ * RESULT_SERVICE_UNAVAILABLE = 2 - The network connection is down
+ * RESULT_BILLING_UNAVAILABLE = 3 - This billing API version is not supported for the type requested
+ * RESULT_ITEM_UNAVAILABLE = 4 - Requested SKU is not available for purchase
+ * RESULT_DEVELOPER_ERROR = 5 - Invalid arguments provided to the API
+ * RESULT_ERROR = 6 - Fatal error during the API action
+ * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
+ * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
+ */
+interface IInAppBillingService {
+    /**
+     * Checks support for the requested billing API version, package and in-app type.
+     * Minimum API version supported by this interface is 3.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName the package name of the calling app
+     * @param type type of the in-app item being purchased ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @return RESULT_OK(0) on success and appropriate response code on failures.
+     */
+    int isBillingSupported(int apiVersion, String packageName, String type);
+
+    /**
+     * Provides details of a list of SKUs
+     * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
+     * with a list JSON strings containing the productId, price, title and description.
+     * This API can be called with a maximum of 20 SKUs.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName the package name of the calling app
+     * @param type of the in-app items ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+     *                         on failures.
+     *         "DETAILS_LIST" with a StringArrayList containing purchase information
+     *                        in JSON format similar to:
+     *                        '{ "productId" : "exampleSku",
+     *                           "type" : "inapp",
+     *                           "price" : "$5.00",
+     *                           "price_currency": "USD",
+     *                           "price_amount_micros": 5000000,
+     *                           "title : "Example Title",
+     *                           "description" : "This is an example description" }'
+     */
+    Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
+
+    /**
+     * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
+     * the type, a unique purchase token and an optional developer payload.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName package name of the calling app
+     * @param sku the SKU of the in-app item as published in the developer console
+     * @param type of the in-app item being purchased ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @param developerPayload optional argument to be sent back with the purchase information
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+     *                         on failures.
+     *         "BUY_INTENT" - PendingIntent to start the purchase flow
+     *
+     * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+     * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+     * If the purchase is successful, the result data will contain the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
+     *                         codes on failures.
+     *         "INAPP_PURCHASE_DATA" - String in JSON format similar to
+     *                                 '{"orderId":"12999763169054705758.1371079406387615",
+     *                                   "packageName":"com.example.app",
+     *                                   "productId":"exampleSku",
+     *                                   "purchaseTime":1345678900000,
+     *                                   "purchaseToken" : "122333444455555",
+     *                                   "developerPayload":"example developer payload" }'
+     *         "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+     *                                  was signed with the private key of the developer
+     */
+    Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
+        String developerPayload);
+
+    /**
+     * Returns the current SKUs owned by the user of the type and package name specified along with
+     * purchase information and a signature of the data to be validated.
+     * This will return all SKUs that have been purchased in V3 and managed items purchased using
+     * V1 and V2 that have not been consumed.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName package name of the calling app
+     * @param type of the in-app items being requested ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @param continuationToken to be set as null for the first call, if the number of owned
+     *        skus are too many, a continuationToken is returned in the response bundle.
+     *        This method can be called again with the continuation token to get the next set of
+     *        owned skus.
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+                               on failures.
+     *         "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
+     *         "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
+     *         "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
+     *                                      of the purchase information
+     *         "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+     *                                      next set of in-app purchases. Only set if the
+     *                                      user has more owned skus than the current list.
+     */
+    Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
+
+    /**
+     * Consume the last purchase of the given SKU. This will result in this item being removed
+     * from all subsequent responses to getPurchases() and allow re-purchase of this item.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName package name of the calling app
+     * @param purchaseToken token in the purchase information JSON that identifies the purchase
+     *        to be consumed
+     * @return RESULT_OK(0) if consumption succeeded, appropriate response codes on failures.
+     */
+    int consumePurchase(int apiVersion, String packageName, String purchaseToken);
+
+    /**
+     * This API is currently under development.
+     */
+    int stub(int apiVersion, String packageName, String type);
+
+    /**
+     * Returns a pending intent to launch the purchase flow for upgrading or downgrading a
+     * subscription. The existing owned SKU(s) should be provided along with the new SKU that
+     * the user is upgrading or downgrading to.
+     * @param apiVersion billing API version that the app is using, must be 5 or later
+     * @param packageName package name of the calling app
+     * @param oldSkus the SKU(s) that the user is upgrading or downgrading from,
+     *        if null or empty this method will behave like {@link #getBuyIntent}
+     * @param newSku the SKU that the user is upgrading or downgrading to
+     * @param type of the item being purchased, currently must be "subs"
+     * @param developerPayload optional argument to be sent back with the purchase information
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response codes
+     *                         on failures.
+     *         "BUY_INTENT" - PendingIntent to start the purchase flow
+     *
+     * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+     * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+     * If the purchase is successful, the result data will contain the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, appropriate response
+     *                         codes on failures.
+     *         "INAPP_PURCHASE_DATA" - String in JSON format similar to
+     *                                 '{"orderId":"12999763169054705758.1371079406387615",
+     *                                   "packageName":"com.example.app",
+     *                                   "productId":"exampleSku",
+     *                                   "purchaseTime":1345678900000,
+     *                                   "purchaseToken" : "122333444455555",
+     *                                   "developerPayload":"example developer payload" }'
+     *         "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+     *                                  was signed with the private key of the developer
+     */
+    Bundle getBuyIntentToReplaceSkus(int apiVersion, String packageName,
+        in List<String> oldSkus, String newSku, String type, String developerPayload);
+
+    /**
+     * Returns a pending intent to launch the purchase flow for an in-app item. This method is
+     * a variant of the {@link #getBuyIntent} method and takes an additional {@code extraParams}
+     * parameter. This parameter is a Bundle of optional keys and values that affect the
+     * operation of the method.
+     * @param apiVersion billing API version that the app is using, must be 6 or later
+     * @param packageName package name of the calling app
+     * @param sku the SKU of the in-app item as published in the developer console
+     * @param type of the in-app item being purchased ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @param developerPayload optional argument to be sent back with the purchase information
+     * @extraParams a Bundle with the following optional keys:
+     *        "skusToReplace" - List<String> - an optional list of SKUs that the user is
+     *                          upgrading or downgrading from.
+     *                          Pass this field if the purchase is upgrading or downgrading
+     *                          existing subscriptions.
+     *                          The specified SKUs are replaced with the SKUs that the user is
+     *                          purchasing. Google Play replaces the specified SKUs at the start of
+     *                          the next billing cycle.
+     * "replaceSkusProration" - Boolean - whether the user should be credited for any unused
+     *                          subscription time on the SKUs they are upgrading or downgrading.
+     *                          If you set this field to true, Google Play swaps out the old SKUs
+     *                          and credits the user with the unused value of their subscription
+     *                          time on a pro-rated basis.
+     *                          Google Play applies this credit to the new subscription, and does
+     *                          not begin billing the user for the new subscription until after
+     *                          the credit is used up.
+     *                          If you set this field to false, the user does not receive credit for
+     *                          any unused subscription time and the recurrence date does not
+     *                          change.
+     *                          Default value is true. Ignored if you do not pass skusToReplace.
+     *            "accountId" - String - an optional obfuscated string that is uniquely
+     *                          associated with the user's account in your app.
+     *                          If you pass this value, Google Play can use it to detect irregular
+     *                          activity, such as many devices making purchases on the same
+     *                          account in a short period of time.
+     *                          Do not use the developer ID or the user's Google ID for this field.
+     *                          In addition, this field should not contain the user's ID in
+     *                          cleartext.
+     *                          We recommend that you use a one-way hash to generate a string from
+     *                          the user's ID, and store the hashed string in this field.
+     *                   "vr" - Boolean - an optional flag indicating whether the returned intent
+     *                          should start a VR purchase flow. The apiVersion must also be 7 or
+     *                          later to use this flag.
+     */
+    Bundle getBuyIntentExtraParams(int apiVersion, String packageName, String sku,
+        String type, String developerPayload, in Bundle extraParams);
+
+    /**
+     * Returns the most recent purchase made by the user for each SKU, even if that purchase is
+     * expired, canceled, or consumed.
+     * @param apiVersion billing API version that the app is using, must be 6 or later
+     * @param packageName package name of the calling app
+     * @param type of the in-app items being requested ("inapp" for one-time purchases
+     *        and "subs" for subscriptions)
+     * @param continuationToken to be set as null for the first call, if the number of owned
+     *        skus is too large, a continuationToken is returned in the response bundle.
+     *        This method can be called again with the continuation token to get the next set of
+     *        owned skus.
+     * @param extraParams a Bundle with extra params that would be appended into http request
+     *        query string. Not used at this moment. Reserved for future functionality.
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value: RESULT_OK(0) if success,
+     *         {@link IabHelper#BILLING_RESPONSE_RESULT_*} response codes on failures.
+     *
+     *         "INAPP_PURCHASE_ITEM_LIST" - ArrayList<String> containing the list of SKUs
+     *         "INAPP_PURCHASE_DATA_LIST" - ArrayList<String> containing the purchase information
+     *         "INAPP_DATA_SIGNATURE_LIST"- ArrayList<String> containing the signatures
+     *                                      of the purchase information
+     *         "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+     *                                      next set of in-app purchases. Only set if the
+     *                                      user has more owned skus than the current list.
+     */
+    Bundle getPurchaseHistory(int apiVersion, String packageName, String type,
+        String continuationToken, in Bundle extraParams);
+
+    /**
+    * This method is a variant of {@link #isBillingSupported}} that takes an additional
+    * {@code extraParams} parameter.
+    * @param apiVersion billing API version that the app is using, must be 7 or later
+    * @param packageName package name of the calling app
+    * @param type of the in-app item being purchased ("inapp" for one-time purchases and "subs"
+    *        for subscriptions)
+    * @param extraParams a Bundle with the following optional keys:
+    *        "vr" - Boolean - an optional flag to indicate whether {link #getBuyIntentExtraParams}
+    *               supports returning a VR purchase flow.
+    * @return RESULT_OK(0) on success and appropriate response code on failures.
+    */
+    int isBillingSupportedExtraParams(int apiVersion, String packageName, String type,
+        in Bundle extraParams);
+}

+ 89 - 0
modules/iap/native/iap_ios.h

@@ -0,0 +1,89 @@
+
+#ifndef BB_IAP_IOS_H
+#define BB_IAP_IOS_H
+
+#import <bbmonkey.h>
+
+#ifdef __OBJC__
+
+#import <StoreKit/StoreKit.h>
+
+class BBIAPStore;
+	
+@interface BBIAPStoreDelegate : NSObject<SKProductsRequestDelegate,SKPaymentTransactionObserver>{
+@private
+BBIAPStore *_peer;
+}
+-(id)initWithPeer:(BBIAPStore*)peer;
+-(void)productsRequest:(SKProductsRequest*)request didReceiveResponse:(SKProductsResponse*)response;
+-(void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transactions;
+-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue*)queue;
+-(void)paymentQueue:(SKPaymentQueue*)queue restoreCompletedTransactionsFailedWithError:(NSError*)error;
+-(void)request:(SKRequest*)request didFailWithError:(NSError*)error;
+
+@end
+
+#else
+
+#include <objc/objc.h>
+
+typedef struct objc_object SKProduct;
+typedef struct objc_object SKProductsRequest;
+typedef struct objc_object SKProductsResponse;
+typedef struct objc_object SKPaymentQueue;
+typedef struct objc_object NSArray;
+typedef struct objc_object NSError;
+typedef struct objc_object SKRequest;
+typedef struct objc_object BBIAPStoreDelegate;
+typedef struct objc_object NSNumberFormatter;
+
+#endif
+
+struct BBProduct : public bbObject{
+	
+	BBProduct();
+	~BBProduct();
+	
+	SKProduct *product;
+
+	bool valid;
+	bbString title;
+	bbString identifier;
+	bbString description;
+	bbString price;
+	int type;
+	bool owned;
+	bool interrupted;
+};
+
+struct BBIAPStore : public bbObject{
+	
+	BBIAPStore();
+	
+	bool OpenStoreAsync( bbArray<bbGCVar<BBProduct>> products );
+	bool BuyProductAsync( BBProduct* product );
+	bool GetOwnedProductsAsync();
+	
+	bool IsRunning();
+	int GetResult();
+	
+	BBProduct *FindProduct( bbString id );
+	void OnRequestProductDataResponse( SKProductsRequest *request,SKProductsResponse *response );
+	void OnUpdatedTransactions( SKPaymentQueue *queue,NSArray *transactions );
+	void OnRestoreTransactionsFinished( SKPaymentQueue *queue,NSError *error );
+	void OnRequestFailed( SKRequest *request,NSError *error );
+	
+	virtual void gcMark();
+	
+	BBIAPStoreDelegate *_delegate;
+
+	NSNumberFormatter *_priceFormatter;
+
+	bbArray<bbGCVar<BBProduct>> _products;
+
+	bool _running;
+	
+	int _result;
+};
+
+#endif

+ 228 - 0
modules/iap/native/iap_ios.mm

@@ -0,0 +1,228 @@
+
+// ***** appstore.h *****
+
+#include "iap_ios.h"
+
+@implementation BBIAPStoreDelegate
+
+-(id)initWithPeer:(BBIAPStore*)peer{
+
+	if( self=[super init] ){
+	
+		_peer=peer;
+	}
+	return self;
+}
+
+-(void)productsRequest:(SKProductsRequest*)request didReceiveResponse:(SKProductsResponse*)response{
+
+	_peer->OnRequestProductDataResponse( request,response );
+}
+
+-(void)paymentQueue:(SKPaymentQueue*)queue updatedTransactions:(NSArray*)transactions{
+
+	_peer->OnUpdatedTransactions( queue,transactions );
+}
+
+-(void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue*)queue{
+
+	_peer->OnRestoreTransactionsFinished( queue,0 );
+}
+
+-(void)paymentQueue:(SKPaymentQueue*)queue restoreCompletedTransactionsFailedWithError:(NSError*)error{
+
+	_peer->OnRestoreTransactionsFinished( queue,error );
+}
+
+-(void)request:(SKRequest*)request didFailWithError:(NSError*)error{
+
+	_peer->OnRequestFailed( request,error );
+}
+
+@end
+
+BBProduct::BBProduct():product(0),valid(false),type(0),owned(false),interrupted(false){
+}
+
+BBProduct::~BBProduct(){
+
+//	[product release];
+}
+
+// ***** IAPStore *****
+
+BBIAPStore::BBIAPStore():_running( false ),_products( 0 ),_result( -1 ){
+
+	_delegate=[[BBIAPStoreDelegate alloc] initWithPeer:this];
+	
+	[[SKPaymentQueue defaultQueue] addTransactionObserver:_delegate];
+	
+	_priceFormatter=[[NSNumberFormatter alloc] init];
+	
+	[_priceFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
+	[_priceFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
+}
+
+bool BBIAPStore::OpenStoreAsync( bbArray<bbGCVar<BBProduct>> products ){
+
+	if( _running || _products.length() ) return false;
+
+	if( ![SKPaymentQueue canMakePayments] ) return false;
+	
+	_products=products;
+	
+	id __strong *objs=new id[products.length()];
+
+	for( int i=0;i<products.length();++i ){
+		objs[i]=products[i]->identifier.ToNSString();
+	}
+	
+	NSSet *set=[NSSet setWithObjects:objs count:products.length()];
+	
+	SKProductsRequest *request=[[SKProductsRequest alloc] initWithProductIdentifiers:set];
+	
+    request.delegate=_delegate;
+    
+    _running=true;
+
+	_result=-1;
+
+    [request start];
+    
+    return true;
+}
+
+bool BBIAPStore::BuyProductAsync( BBProduct *prod ){
+
+	if( _running || !_products.length() || !prod || !prod->valid ) return false; 
+
+	if( ![SKPaymentQueue canMakePayments] ) return false;
+	
+	SKMutablePayment *payment=[SKMutablePayment paymentWithProduct:prod->product];
+	
+	_running=true;
+
+	_result=-1;
+	
+	[[SKPaymentQueue defaultQueue] addPayment:payment];
+	
+	return true;
+}
+
+bool BBIAPStore::GetOwnedProductsAsync(){
+
+	if( _running || !_products.length() ) return false;
+
+	if( ![SKPaymentQueue canMakePayments] ) return false;
+	
+	_running=true;
+	
+	_result=-1;
+	
+	[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
+	
+	return true;
+}
+
+bool BBIAPStore::IsRunning(){
+
+	return _running;
+}
+
+int BBIAPStore::GetResult(){
+
+	return _result;
+}
+
+BBProduct *BBIAPStore::FindProduct( bbString id ){
+
+	for( int i=0;i<_products.length();++i ){
+	
+		BBProduct *p=_products[i];
+		
+		if( p->identifier==id ) return p;
+	}
+
+	return 0;
+}
+
+void BBIAPStore::OnRequestProductDataResponse( SKProductsRequest *request,SKProductsResponse *response ){
+
+	//Get product details
+	for( SKProduct *p in response.products ){
+	
+		printf( "product=%p\n",p );fflush( stdout );
+	
+		BBProduct *prod=FindProduct( p.productIdentifier );
+		if( !prod ) continue;
+		
+		[_priceFormatter setLocale:p.priceLocale];
+		
+		prod->valid=true;
+		prod->product=p;	//[p retain];
+		prod->title=bbString( p.localizedTitle );
+		prod->identifier=bbString( p.productIdentifier );
+		prod->description=bbString( p.localizedDescription );
+		prod->price=bbString( [_priceFormatter stringFromNumber:p.price] );
+	}
+	
+	_result=-1;
+	
+	for( int i=0;i<_products.length();++i ){
+	
+		if( !_products[i]->product ) continue;
+		
+		_result=0;
+		break;
+	}
+	
+	_running=false;
+}
+
+void BBIAPStore::OnUpdatedTransactions( SKPaymentQueue *queue,NSArray *transactions ){
+
+	_result=-1;
+
+	for( SKPaymentTransaction *transaction in transactions ){
+	
+		if( transaction.transactionState==SKPaymentTransactionStatePurchased ){
+		
+			_result=0;
+			
+			_running=false;
+			
+		}else if( transaction.transactionState==SKPaymentTransactionStateFailed ){
+		
+			_result=(transaction.error.code==SKErrorPaymentCancelled) ? 1 : -1;
+			
+			_running=false;
+			
+		}else if( transaction.transactionState==SKPaymentTransactionStateRestored ){
+		
+			if( BBProduct *p=FindProduct( transaction.payment.productIdentifier ) ) p->owned=true;
+		
+		}else{
+		
+			continue;
+		}
+		
+		[queue finishTransaction:transaction];
+	}
+}
+
+void BBIAPStore::OnRestoreTransactionsFinished( SKPaymentQueue *queue,NSError *error ){
+
+	_result=error ? (error.code==SKErrorPaymentCancelled ? 1 : -1) : 0;
+	
+	_running=false;
+}
+
+void BBIAPStore::OnRequestFailed( SKRequest *request,NSError *error ){
+
+	_running=false;
+}
+
+void BBIAPStore::gcMark(){
+
+	bbGCMark( _products );
+}