Browse Source

Added in-app purchase feature to JUCE

tags/2021-05-28
hogliux 8 years ago
parent
commit
df91b15455
14 changed files with 3115 additions and 79 deletions
  1. +86
    -56
      extras/Projucer/Source/Project Saving/jucer_ProjectExport_Android.h
  2. +1
    -0
      extras/Projucer/Source/Utility/jucer_PresetIDs.h
  3. +971
    -0
      modules/juce_core/native/java/IInAppBillingService.java
  4. +9
    -1
      modules/juce_core/native/java/JuceAppActivity.java
  5. +25
    -21
      modules/juce_core/native/juce_android_JNIHelpers.h
  6. +10
    -0
      modules/juce_core/native/juce_osx_ObjCHelpers.h
  7. +17
    -0
      modules/juce_gui_basics/native/juce_android_Windowing.cpp
  8. +66
    -0
      modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp
  9. +269
    -0
      modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h
  10. +20
    -0
      modules/juce_product_unlocking/juce_product_unlocking.cpp
  11. +9
    -1
      modules/juce_product_unlocking/juce_product_unlocking.h
  12. +23
    -0
      modules/juce_product_unlocking/juce_product_unlocking.mm
  13. +930
    -0
      modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp
  14. +679
    -0
      modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp

+ 86
- 56
extras/Projucer/Source/Project Saving/jucer_ProjectExport_Android.h View File

@@ -101,7 +101,8 @@ public:
androidSharedLibraries, androidStaticLibraries, androidExtraAssetsFolder;
CachedValue<bool> androidInternetNeeded, androidMicNeeded, androidBluetoothNeeded,
androidExternalReadPermission, androidExternalWritePermission;
androidExternalReadPermission, androidExternalWritePermission,
androidInAppBillingPermission;
CachedValue<String> androidOtherPermissions;
CachedValue<String> androidKeyStore, androidKeyStorePass, androidKeyAlias, androidKeyAliasPass;
@@ -128,6 +129,7 @@ public:
androidBluetoothNeeded (settings, Ids::androidBluetoothNeeded, nullptr, true),
androidExternalReadPermission (settings, Ids::androidExternalReadNeeded, nullptr, true),
androidExternalWritePermission (settings, Ids::androidExternalWriteNeeded, nullptr, true),
androidInAppBillingPermission (settings, Ids::androidInAppBilling, nullptr, false),
androidOtherPermissions (settings, Ids::androidOtherPermissions, nullptr),
androidKeyStore (settings, Ids::androidKeyStore, nullptr, "${user.home}/.android/debug.keystore"),
androidKeyStorePass (settings, Ids::androidKeyStorePass, nullptr, "android"),
@@ -201,14 +203,8 @@ public:
removeOldFiles (targetFolder);
{
const String package (getActivityClassPackage());
const String path (package.replaceCharacter ('.', File::separator));
const File javaTarget (targetFolder.getChildFile ("app/src/main/java").getChildFile (path));
if (! isLibrary())
copyActivityJavaFiles (modules, javaTarget, package);
}
if (! isLibrary())
copyJavaFiles (modules);
copyExtraResourceFiles();
@@ -827,6 +823,9 @@ private:
props.add (new BooleanPropertyComponent (androidExternalWritePermission.getPropertyAsValue(), "Write to external storage", "Specify permissions to write to external storage"),
"If enabled, this will set the android.permission.WRITE_EXTERNAL_STORAGE flag in the manifest.");
props.add (new BooleanPropertyComponent (androidInAppBillingPermission.getPropertyAsValue(), "In-App Billing", "Specify In-App Billing permission in the manifest"),
"If enabled, this will set the com.android.vending.BILLING flag in the manifest.");
props.add (new TextPropertyComponent (androidOtherPermissions.getPropertyAsValue(), "Custom permissions", 2048, false),
"A space-separated list of other permission flags that should be added to the manifest.");
@@ -879,78 +878,106 @@ private:
}
//==============================================================================
void copyActivityJavaFiles (const OwnedArray<LibraryModule>& modules, const File& targetFolder, const String& package) const
void copyJavaFiles (const OwnedArray<LibraryModule>& modules) const
{
if (auto* coreModule = getCoreModule (modules))
{
auto package = getActivityClassPackage();
auto targetFolder = getTargetFolder();
auto inAppBillingPath = String ("com.android.vending.billing").replaceCharacter ('.', File::separator);
auto javaSourceFolder = coreModule->getFolder().getChildFile ("native").getChildFile ("java");
auto javaInAppBillingTarget = targetFolder.getChildFile ("app/src/main/java").getChildFile (inAppBillingPath);
auto javaActivityTarget = targetFolder.getChildFile ("app/src/main/java")
.getChildFile (package.replaceCharacter ('.', File::separator));
copyActivityJavaFiles (javaSourceFolder, javaActivityTarget, package);
copyAdditionalJavaFiles (javaSourceFolder, javaInAppBillingTarget);
}
}
void copyActivityJavaFiles (const File& javaSourceFolder, const File& targetFolder, const String& package) const
{
if (androidActivityClass.get().contains ("_"))
throw SaveError ("Your Android activity class name or path may not contain any underscores! Try a project name without underscores.");
const String className (getActivityName());
auto className = getActivityName();
if (className.isEmpty())
throw SaveError ("Invalid Android Activity class name: " + androidActivityClass.get());
createDirectoryOrThrow (targetFolder);
if (auto* coreModule = getCoreModule (modules))
{
File javaDestFile (targetFolder.getChildFile (className + ".java"));
auto javaDestFile = targetFolder.getChildFile (className + ".java");
File javaSourceFolder (coreModule->getFolder().getChildFile ("native")
.getChildFile ("java"));
String juceMidiCode, juceMidiImports, juceRuntimePermissionsCode;
String juceMidiCode, juceMidiImports, juceRuntimePermissionsCode;
juceMidiImports << newLine;
juceMidiImports << newLine;
if (androidMinimumSDK.get().getIntValue() >= 23)
{
File javaAndroidMidi (javaSourceFolder.getChildFile ("AndroidMidi.java"));
File javaRuntimePermissions (javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java"));
if (androidMinimumSDK.get().getIntValue() >= 23)
{
auto javaAndroidMidi = javaSourceFolder.getChildFile ("AndroidMidi.java");
auto javaRuntimePermissions = javaSourceFolder.getChildFile ("AndroidRuntimePermissions.java");
juceMidiImports << "import android.media.midi.*;" << newLine
<< "import android.bluetooth.*;" << newLine
<< "import android.bluetooth.le.*;" << newLine;
juceMidiImports << "import android.media.midi.*;" << newLine
<< "import android.bluetooth.*;" << newLine
<< "import android.bluetooth.le.*;" << newLine;
juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className);
juceMidiCode = javaAndroidMidi.loadFileAsString().replace ("JuceAppActivity", className);
juceRuntimePermissionsCode = javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className);
}
else
juceRuntimePermissionsCode = javaRuntimePermissions.loadFileAsString().replace ("JuceAppActivity", className);
}
else
{
juceMidiCode = javaSourceFolder.getChildFile ("AndroidMidiFallback.java")
.loadFileAsString()
.replace ("JuceAppActivity", className);
}
auto javaSourceFile = javaSourceFolder.getChildFile ("JuceAppActivity.java");
auto javaSourceLines = StringArray::fromLines (javaSourceFile.loadFileAsString());
{
MemoryOutputStream newFile;
for (const auto& line : javaSourceLines)
{
juceMidiCode = javaSourceFolder.getChildFile ("AndroidMidiFallback.java")
.loadFileAsString()
.replace ("JuceAppActivity", className);
if (line.contains ("$$JuceAndroidMidiImports$$"))
newFile << juceMidiImports;
else if (line.contains ("$$JuceAndroidMidiCode$$"))
newFile << juceMidiCode;
else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$"))
newFile << juceRuntimePermissionsCode;
else
newFile << line.replace ("JuceAppActivity", className)
.replace ("package com.juce;", "package " + package + ";") << newLine;
}
auto javaSourceFile = javaSourceFolder.getChildFile ("JuceAppActivity.java");
auto javaSourceLines = StringArray::fromLines (javaSourceFile.loadFileAsString());
javaSourceLines = StringArray::fromLines (newFile.toString());
}
{
MemoryOutputStream newFile;
while (javaSourceLines.size() > 2
&& javaSourceLines[javaSourceLines.size() - 1].trim().isEmpty()
&& javaSourceLines[javaSourceLines.size() - 2].trim().isEmpty())
javaSourceLines.remove (javaSourceLines.size() - 1);
for (const auto& line : javaSourceLines)
{
if (line.contains ("$$JuceAndroidMidiImports$$"))
newFile << juceMidiImports;
else if (line.contains ("$$JuceAndroidMidiCode$$"))
newFile << juceMidiCode;
else if (line.contains ("$$JuceAndroidRuntimePermissionsCode$$"))
newFile << juceRuntimePermissionsCode;
else
newFile << line.replace ("JuceAppActivity", className)
.replace ("package com.juce;", "package " + package + ";") << newLine;
}
overwriteFileIfDifferentOrThrow (javaDestFile, javaSourceLines.joinIntoString (newLine));
}
javaSourceLines = StringArray::fromLines (newFile.toString());
}
void copyAdditionalJavaFiles (const File& sourceFolder, const File& targetFolder) const
{
auto inAppBillingJavaFileName = String ("IInAppBillingService.java");
while (javaSourceLines.size() > 2
&& javaSourceLines[javaSourceLines.size() - 1].trim().isEmpty()
&& javaSourceLines[javaSourceLines.size() - 2].trim().isEmpty())
javaSourceLines.remove (javaSourceLines.size() - 1);
auto inAppBillingJavaSrcFile = sourceFolder.getChildFile (inAppBillingJavaFileName);
auto inAppBillingJavaDestFile = targetFolder.getChildFile (inAppBillingJavaFileName);
overwriteFileIfDifferentOrThrow (javaDestFile, javaSourceLines.joinIntoString (newLine));
}
createDirectoryOrThrow (targetFolder);
jassert (inAppBillingJavaSrcFile.existsAsFile());
if (inAppBillingJavaSrcFile.existsAsFile())
inAppBillingJavaSrcFile.copyFileTo (inAppBillingJavaDestFile);
}
void copyExtraResourceFiles() const
@@ -1422,6 +1449,9 @@ private:
if (androidExternalWritePermission.get())
s.add ("android.permission.WRITE_EXTERNAL_STORAGE");
if (androidInAppBillingPermission.get())
s.add ("com.android.vending.BILLING");
return getCleanedStringArray (s);
}


+ 1
- 0
extras/Projucer/Source/Utility/jucer_PresetIDs.h View File

@@ -180,6 +180,7 @@ namespace Ids
DECLARE_ID (androidBluetoothNeeded);
DECLARE_ID (androidExternalReadNeeded);
DECLARE_ID (androidExternalWriteNeeded);
DECLARE_ID (androidInAppBilling);
DECLARE_ID (androidMinimumSDK);
DECLARE_ID (androidOtherPermissions);
DECLARE_ID (androidKeyStore);


+ 971
- 0
modules/juce_core/native/java/IInAppBillingService.java View File

@@ -0,0 +1,971 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
package com.android.vending.billing;
/**
* 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
*/
public interface IInAppBillingService extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.android.vending.billing.IInAppBillingService
{
private static final java.lang.String DESCRIPTOR = "com.android.vending.billing.IInAppBillingService";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.android.vending.billing.IInAppBillingService interface,
* generating a proxy if needed.
*/
public static com.android.vending.billing.IInAppBillingService asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.android.vending.billing.IInAppBillingService))) {
return ((com.android.vending.billing.IInAppBillingService)iin);
}
return new com.android.vending.billing.IInAppBillingService.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_isBillingSupported:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
int _result = this.isBillingSupported(_arg0, _arg1, _arg2);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_getSkuDetails:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
android.os.Bundle _arg3;
if ((0!=data.readInt())) {
_arg3 = android.os.Bundle.CREATOR.createFromParcel(data);
}
else {
_arg3 = null;
}
android.os.Bundle _result = this.getSkuDetails(_arg0, _arg1, _arg2, _arg3);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_getBuyIntent:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
java.lang.String _arg3;
_arg3 = data.readString();
java.lang.String _arg4;
_arg4 = data.readString();
android.os.Bundle _result = this.getBuyIntent(_arg0, _arg1, _arg2, _arg3, _arg4);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_getPurchases:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
java.lang.String _arg3;
_arg3 = data.readString();
android.os.Bundle _result = this.getPurchases(_arg0, _arg1, _arg2, _arg3);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_consumePurchase:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
int _result = this.consumePurchase(_arg0, _arg1, _arg2);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_stub:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
int _result = this.stub(_arg0, _arg1, _arg2);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_getBuyIntentToReplaceSkus:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.util.List<java.lang.String> _arg2;
_arg2 = data.createStringArrayList();
java.lang.String _arg3;
_arg3 = data.readString();
java.lang.String _arg4;
_arg4 = data.readString();
java.lang.String _arg5;
_arg5 = data.readString();
android.os.Bundle _result = this.getBuyIntentToReplaceSkus(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_getBuyIntentExtraParams:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
java.lang.String _arg3;
_arg3 = data.readString();
java.lang.String _arg4;
_arg4 = data.readString();
android.os.Bundle _arg5;
if ((0!=data.readInt())) {
_arg5 = android.os.Bundle.CREATOR.createFromParcel(data);
}
else {
_arg5 = null;
}
android.os.Bundle _result = this.getBuyIntentExtraParams(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_getPurchaseHistory:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
java.lang.String _arg3;
_arg3 = data.readString();
android.os.Bundle _arg4;
if ((0!=data.readInt())) {
_arg4 = android.os.Bundle.CREATOR.createFromParcel(data);
}
else {
_arg4 = null;
}
android.os.Bundle _result = this.getPurchaseHistory(_arg0, _arg1, _arg2, _arg3, _arg4);
reply.writeNoException();
if ((_result!=null)) {
reply.writeInt(1);
_result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
}
else {
reply.writeInt(0);
}
return true;
}
case TRANSACTION_isBillingSupportedExtraParams:
{
data.enforceInterface(DESCRIPTOR);
int _arg0;
_arg0 = data.readInt();
java.lang.String _arg1;
_arg1 = data.readString();
java.lang.String _arg2;
_arg2 = data.readString();
android.os.Bundle _arg3;
if ((0!=data.readInt())) {
_arg3 = android.os.Bundle.CREATOR.createFromParcel(data);
}
else {
_arg3 = null;
}
int _result = this.isBillingSupportedExtraParams(_arg0, _arg1, _arg2, _arg3);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.android.vending.billing.IInAppBillingService
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
mRemote.transact(Stub.TRANSACTION_isBillingSupported, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* 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" }'
*/
@Override public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
if ((skusBundle!=null)) {
_data.writeInt(1);
skusBundle.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_getSkuDetails, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* 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
*/
@Override public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(sku);
_data.writeString(type);
_data.writeString(developerPayload);
mRemote.transact(Stub.TRANSACTION_getBuyIntent, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* 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.
*/
@Override public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
_data.writeString(continuationToken);
mRemote.transact(Stub.TRANSACTION_getPurchases, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(purchaseToken);
mRemote.transact(Stub.TRANSACTION_consumePurchase, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public int stub(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
mRemote.transact(Stub.TRANSACTION_stub, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* 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
*/
@Override public android.os.Bundle getBuyIntentToReplaceSkus(int apiVersion, java.lang.String packageName, java.util.List<java.lang.String> oldSkus, java.lang.String newSku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeStringList(oldSkus);
_data.writeString(newSku);
_data.writeString(type);
_data.writeString(developerPayload);
mRemote.transact(Stub.TRANSACTION_getBuyIntentToReplaceSkus, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* 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.
*/
@Override public android.os.Bundle getBuyIntentExtraParams(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload, android.os.Bundle extraParams) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(sku);
_data.writeString(type);
_data.writeString(developerPayload);
if ((extraParams!=null)) {
_data.writeInt(1);
extraParams.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_getBuyIntentExtraParams, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
* 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.
*/
@Override public android.os.Bundle getPurchaseHistory(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken, android.os.Bundle extraParams) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
android.os.Bundle _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
_data.writeString(continuationToken);
if ((extraParams!=null)) {
_data.writeInt(1);
extraParams.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_getPurchaseHistory, _data, _reply, 0);
_reply.readException();
if ((0!=_reply.readInt())) {
_result = android.os.Bundle.CREATOR.createFromParcel(_reply);
}
else {
_result = null;
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public int isBillingSupportedExtraParams(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle extraParams) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(apiVersion);
_data.writeString(packageName);
_data.writeString(type);
if ((extraParams!=null)) {
_data.writeInt(1);
extraParams.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_isBillingSupportedExtraParams, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
static final int TRANSACTION_isBillingSupported = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_getSkuDetails = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_getBuyIntent = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_getPurchases = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
static final int TRANSACTION_consumePurchase = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
static final int TRANSACTION_stub = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5);
static final int TRANSACTION_getBuyIntentToReplaceSkus = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6);
static final int TRANSACTION_getBuyIntentExtraParams = (android.os.IBinder.FIRST_CALL_TRANSACTION + 7);
static final int TRANSACTION_getPurchaseHistory = (android.os.IBinder.FIRST_CALL_TRANSACTION + 8);
static final int TRANSACTION_isBillingSupportedExtraParams = (android.os.IBinder.FIRST_CALL_TRANSACTION + 9);
}
public int isBillingSupported(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException;
/**
* 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" }'
*/
public android.os.Bundle getSkuDetails(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle skusBundle) throws android.os.RemoteException;
/**
* 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
*/
public android.os.Bundle getBuyIntent(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException;
/**
* 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.
*/
public android.os.Bundle getPurchases(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken) throws android.os.RemoteException;
public int consumePurchase(int apiVersion, java.lang.String packageName, java.lang.String purchaseToken) throws android.os.RemoteException;
public int stub(int apiVersion, java.lang.String packageName, java.lang.String type) throws android.os.RemoteException;
/**
* 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
*/
public android.os.Bundle getBuyIntentToReplaceSkus(int apiVersion, java.lang.String packageName, java.util.List<java.lang.String> oldSkus, java.lang.String newSku, java.lang.String type, java.lang.String developerPayload) throws android.os.RemoteException;
/**
* 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.
*/
public android.os.Bundle getBuyIntentExtraParams(int apiVersion, java.lang.String packageName, java.lang.String sku, java.lang.String type, java.lang.String developerPayload, android.os.Bundle extraParams) throws android.os.RemoteException;
/**
* 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.
*/
public android.os.Bundle getPurchaseHistory(int apiVersion, java.lang.String packageName, java.lang.String type, java.lang.String continuationToken, android.os.Bundle extraParams) throws android.os.RemoteException;
public int isBillingSupportedExtraParams(int apiVersion, java.lang.String packageName, java.lang.String type, android.os.Bundle extraParams) throws android.os.RemoteException;
}

+ 9
- 1
modules/juce_core/native/java/JuceAppActivity.java View File

@@ -66,7 +66,7 @@ import java.util.concurrent.Callable;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.atomic.*;
$$JuceAndroidMidiImports$$ // If you get an error here, you need to re-save your project with the Projucer!
$$JuceAndroidMidiImports$$ // If you get an error here, you need to re-save your project with the Projucer!
//==============================================================================
@@ -299,6 +299,7 @@ public class JuceAppActivity extends Activity
private native void suspendApp();
private native void resumeApp();
private native void setScreenSize (int screenWidth, int screenHeight, int dpi);
private native void appActivityResult (int requestCode, int resultCode, Intent data);
//==============================================================================
private ViewHolder viewHolder;
@@ -1197,6 +1198,13 @@ public class JuceAppActivity extends Activity
public static final String getMoviesFolder() { return getFileLocation (Environment.DIRECTORY_MOVIES); }
public static final String getDownloadsFolder() { return getFileLocation (Environment.DIRECTORY_DOWNLOADS); }
//==============================================================================
@Override
protected void onActivityResult (int requestCode, int resultCode, Intent data)
{
appActivityResult (requestCode, resultCode, data);
}
//==============================================================================
public final Typeface getTypeFaceFromAsset (String assetName)
{


+ 25
- 21
modules/juce_core/native/juce_android_JNIHelpers.h View File

@@ -279,27 +279,27 @@ extern AndroidSystem android;
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (createNewView, "createNewView", "(ZJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;") \
METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \
METHOD (createNewView, "createNewView", "(ZJ)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;") \
METHOD (deleteView, "deleteView", "(L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$ComponentPeerView;)V") \
METHOD (createNativeSurfaceView, "createNativeSurfaceView", "(J)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$NativeSurfaceView;") \
METHOD (finish, "finish", "()V") \
METHOD (finish, "finish", "()V") \
METHOD (setRequestedOrientation,"setRequestedOrientation", "(I)V") \
METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \
METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \
METHOD (excludeClipRegion, "excludeClipRegion", "(Landroid/graphics/Canvas;FFFF)V") \
METHOD (renderGlyph, "renderGlyph", "(CCLandroid/graphics/Paint;Landroid/graphics/Matrix;Landroid/graphics/Rect;)[I") \
STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;ILjava/lang/String;)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \
METHOD (launchURL, "launchURL", "(Ljava/lang/String;)V") \
METHOD (showMessageBox, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \
METHOD (showOkCancelBox, "showOkCancelBox", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;)V") \
METHOD (showYesNoCancelBox, "showYesNoCancelBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \
STATICMETHOD (getLocaleValue, "getLocaleValue", "(Z)Ljava/lang/String;") \
STATICMETHOD (getDocumentsFolder, "getDocumentsFolder", "()Ljava/lang/String;") \
STATICMETHOD (getPicturesFolder, "getPicturesFolder", "()Ljava/lang/String;") \
STATICMETHOD (getMusicFolder, "getMusicFolder", "()Ljava/lang/String;") \
STATICMETHOD (getDownloadsFolder, "getDownloadsFolder", "()Ljava/lang/String;") \
STATICMETHOD (getMoviesFolder, "getMoviesFolder", "()Ljava/lang/String;") \
METHOD (getTypeFaceFromAsset, "getTypeFaceFromAsset", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \
METHOD (getClipboardContent, "getClipboardContent", "()Ljava/lang/String;") \
METHOD (setClipboardContent, "setClipboardContent", "(Ljava/lang/String;)V") \
METHOD (excludeClipRegion, "excludeClipRegion", "(Landroid/graphics/Canvas;FFFF)V") \
METHOD (renderGlyph, "renderGlyph", "(CCLandroid/graphics/Paint;Landroid/graphics/Matrix;Landroid/graphics/Rect;)[I") \
STATICMETHOD (createHTTPStream, "createHTTPStream", "(Ljava/lang/String;Z[BLjava/lang/String;I[ILjava/lang/StringBuffer;ILjava/lang/String;)L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream;") \
METHOD (launchURL, "launchURL", "(Ljava/lang/String;)V") \
METHOD (showMessageBox, "showMessageBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \
METHOD (showOkCancelBox, "showOkCancelBox", "(Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;Ljava/lang/String;)V") \
METHOD (showYesNoCancelBox, "showYesNoCancelBox", "(Ljava/lang/String;Ljava/lang/String;J)V") \
STATICMETHOD (getLocaleValue, "getLocaleValue", "(Z)Ljava/lang/String;") \
STATICMETHOD (getDocumentsFolder, "getDocumentsFolder", "()Ljava/lang/String;") \
STATICMETHOD (getPicturesFolder, "getPicturesFolder", "()Ljava/lang/String;") \
STATICMETHOD (getMusicFolder, "getMusicFolder", "()Ljava/lang/String;") \
STATICMETHOD (getDownloadsFolder, "getDownloadsFolder", "()Ljava/lang/String;") \
STATICMETHOD (getMoviesFolder, "getMoviesFolder", "()Ljava/lang/String;") \
METHOD (getTypeFaceFromAsset, "getTypeFaceFromAsset", "(Ljava/lang/String;)Landroid/graphics/Typeface;") \
METHOD (getTypeFaceFromByteArray,"getTypeFaceFromByteArray","([B)Landroid/graphics/Typeface;") \
METHOD (setScreenSaver, "setScreenSaver", "(Z)V") \
METHOD (getScreenSaver, "getScreenSaver", "()Z") \
@@ -307,12 +307,16 @@ extern AndroidSystem android;
METHOD (getAndroidBluetoothManager, "getAndroidBluetoothManager", "()L" JUCE_ANDROID_ACTIVITY_CLASSPATH "$BluetoothManager;") \
METHOD (getAndroidSDKVersion, "getAndroidSDKVersion", "()I") \
METHOD (audioManagerGetProperty, "audioManagerGetProperty", "(Ljava/lang/String;)Ljava/lang/String;") \
METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \
METHOD (hasSystemFeature, "hasSystemFeature", "(Ljava/lang/String;)Z" ) \
METHOD (requestRuntimePermission, "requestRuntimePermission", "(IJ)V" ) \
METHOD (isPermissionGranted, "isPermissionGranted", "(I)Z" ) \
METHOD (isPermissionGranted, "isPermissionGranted", "(I)Z" ) \
METHOD (isPermissionDeclaredInManifest, "isPermissionDeclaredInManifest", "(I)Z" ) \
METHOD (getSystemService, "getSystemService", "(Ljava/lang/String;)Ljava/lang/Object;") \
STATICMETHOD (createInvocationHandler, "createInvocationHandler", "(J)Ljava/lang/reflect/InvocationHandler;") \
METHOD (bindService, "bindService", "(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z") \
METHOD (unbindService, "unbindService", "(Landroid/content/ServiceConnection;)V") \
METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \
METHOD (getPackageName, "getPackageName", "()Ljava/lang/String;") \
DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH);
#undef JNI_CLASS_MEMBERS


+ 10
- 0
modules/juce_core/native/juce_osx_ObjCHelpers.h View File

@@ -59,6 +59,16 @@ namespace
return createNSURLFromFile (f.getFullPathName());
}
static inline NSArray* createNSArrayFromStringArray (const StringArray& strings)
{
auto* array = [[NSMutableArray alloc] init];
for (auto string: strings)
[array addObject:juceStringToNS (string)];
return [array autorelease];
}
#if JUCE_MAC
template <typename RectangleType>
static NSRect makeNSRect (const RectangleType& r) noexcept


+ 17
- 0
modules/juce_gui_basics/native/juce_android_Windowing.cpp View File

@@ -31,6 +31,11 @@ extern juce::JUCEApplicationBase* juce_CreateApplication(); // (from START_JUCE_
namespace juce
{
//==============================================================================
#if JUCE_MODULE_AVAILABLE_juce_product_unlocking
extern void juce_inAppPurchaseCompleted (jobject intentData);
#endif
//==============================================================================
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, launchApp, void, (JNIEnv* env, jobject activity,
jstring appFile, jstring appDataDir))
@@ -83,6 +88,18 @@ JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, quitApp, void, (JNIEnv* env,
android.shutdown (env);
}
JUCE_JNI_CALLBACK (JUCE_ANDROID_ACTIVITY_CLASSNAME, appActivityResult, void, (JNIEnv* env, jobject, jint requestCode, jint /*resultCode*/, jobject intentData))
{
setEnv (env);
#if JUCE_MODULE_AVAILABLE_juce_product_unlocking
if (requestCode == 1001)
juce_inAppPurchaseCompleted (intentData);
#else
ignoreUnused (intentData, requestCode);
#endif
}
//==============================================================================
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (drawBitmap, "drawBitmap", "([IIIFFIIZLandroid/graphics/Paint;)V") \


+ 66
- 0
modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp View File

@@ -0,0 +1,66 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
InAppPurchases::InAppPurchases() : pimpl (new Pimpl (*this)) {}
InAppPurchases::~InAppPurchases() {}
bool InAppPurchases::isInAppPurchasesSupported() const
{
return pimpl->isInAppPurchasesSupported();
}
void InAppPurchases::getProductsInformation (const StringArray& productIdentifiers)
{
pimpl->getProductsInformation (productIdentifiers);
}
void InAppPurchases::purchaseProduct (const String& productIdentifier,
bool isSubscription,
const StringArray& upgradeProductIdentifiers,
bool creditForUnusedSubscription)
{
pimpl->purchaseProduct (productIdentifier, isSubscription,
upgradeProductIdentifiers, creditForUnusedSubscription);
}
void InAppPurchases::restoreProductsBoughtList (bool includeDownloadInfo, const String& subscriptionsSharedSecret)
{
pimpl->restoreProductsBoughtList (includeDownloadInfo, subscriptionsSharedSecret);
}
void InAppPurchases::consumePurchase (const String& productIdentifier, const String& purchaseToken)
{
pimpl->consumePurchase (productIdentifier, purchaseToken);
}
void InAppPurchases::addListener (Listener* l) { listeners.add (l); }
void InAppPurchases::removeListener (Listener* l) { listeners.remove (l); }
void InAppPurchases::startDownloads (const Array<Download*>& downloads) { pimpl->startDownloads (downloads); }
void InAppPurchases::pauseDownloads (const Array<Download*>& downloads) { pimpl->pauseDownloads (downloads); }
void InAppPurchases::resumeDownloads (const Array<Download*>& downloads) { pimpl->resumeDownloads (downloads); }
void InAppPurchases::cancelDownloads (const Array<Download*>& downloads) { pimpl->cancelDownloads (downloads); }

+ 269
- 0
modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h View File

@@ -0,0 +1,269 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
/**
Provides in-app purchase functionality.
Your app should create a single instance of this class, and on iOS it should
be created as soon as your app starts. This is because on application startup
any previously pending transactions will be resumed.
Once an InAppPurchases object is created, call addListener() to attach listeners.
*/
class JUCE_API InAppPurchases
{
public:
//==============================================================================
/** Represents a product available in the store. */
struct Product
{
/** Product ID (also known as SKU) that uniquely identifies a product in the store. */
String identifier;
/** Title of the product. */
String title;
/** Description of the product. */
String description;
/** Price of the product in local currency. */
String price;
/** Price locale. */
String priceLocale;
};
//==============================================================================
/** Represents a purchase of a product in the store. */
struct Purchase
{
/** A unique order identifier for the transaction (generated by the store). */
String orderId;
/** A unique identifier of in-app product that was purchased. */
String productId;
/** This will be bundle ID on iOS and package name on Android, of the application for which this
in-app product was purchased. */
String applicationBundleName;
/** Date of the purchase (in ISO8601 format). */
String purchaseTime;
/** Android only: purchase token that should be used to consume purchase, provided that In-App product
is consumable. */
String purchaseToken;
};
//==============================================================================
/** iOS only: represents in-app purchase download. Download will be available only
for purchases that are hosted on the AppStore. */
struct Download
{
enum class Status
{
waiting = 0, /**< The download is waiting to start. Called at the beginning of a download operation. */
active, /**< The download is in progress. */
paused, /**< The download was paused and is awaiting resuming or cancelling. */
finished, /**< The download was finished successfully. */
failed, /**< The download failed (e.g. because of no internet connection). */
cancelled, /**< The download was cancelled. */
};
virtual ~Download() {}
/** A unique identifier for the in-app product to be downloaded. */
virtual String getProductId() const = 0;
/** Content length in bytes. */
virtual int64 getContentLength() const = 0;
/** Content version. */
virtual String getContentVersion() const = 0;
/** Returns current status of the download. */
virtual Status getStatus() const = 0;
};
//==============================================================================
/** Represents an object that gets notified about events such as product info returned or product purchase
finished. */
struct Listener
{
virtual ~Listener() {}
/** Called whenever a product info is returned after a call to InAppPurchases::getProductsInformation(). */
virtual void productsInfoReturned (const Array<Product>& /*products*/) {}
struct PurchaseInfo
{
Purchase purchase;
Array<Download*> downloads;
};
/** Called whenever a purchase is complete, with additional state whether the purchase completed successfully.
For hosted content (iOS only), the downloads array within PurchaseInfo will contain all download objects corresponding
with the purchase. For non-hosted content, the downloads array will be empty.
InAppPurchases class will own downloads and will delete them as soon as they are finished.
NOTE: it is possible to receive this callback for the same purchase multiple times. If that happens,
only the newest set of downloads and the newest orderId will be valid, the old ones should be not used anymore!
*/
virtual void productPurchaseFinished (const PurchaseInfo&, bool /*success*/, const String& /*statusDescription*/) {}
/** Called when a list of all purchases is restored. This can be used to figure out to
which products a user is entitled to.
NOTE: it is possible to receive this callback for the same purchase multiple times. If that happens,
only the newest set of downloads and the newest orderId will be valid, the old ones should be not used anymore!
*/
virtual void purchasesListRestored (const Array<PurchaseInfo>&, bool /*success*/, const String& /*statusDescription*/) {}
/** Called whenever a product consumption finishes. */
virtual void productConsumed (const String& /*productId*/, bool /*success*/, const String& /*statusDescription*/) {}
/** iOS only: Called when a product download progress gets updated. If the download was interrupted in the last
application session, this callback may be called after the application starts.
If the download was in progress and the application was closed, the download may happily continue in the
background by OS. If you open the app and the download is still in progress, you will receive this callback.
If the download finishes in the background before you start the app again, you will receive productDownloadFinished
callback instead. The download will only stop when it is explicitly cancelled or when it is finished.
*/
virtual void productDownloadProgressUpdate (Download&, float /*progress*/, RelativeTime /*timeRemaining*/) {}
/** iOS only: Called when a product download is paused. This may also be called after the application starts, if
the download was in a paused state and the application was closed before finishing the download.
Only after the download is finished successfully or cancelled you will stop receiving this callback on startup.
*/
virtual void productDownloadPaused (Download&) {}
/** iOS only: Called when a product download finishes (successfully or not). Call Download::getStatus()
to check if the downloaded finished successfully.
It is your responsibility to move the download content into your app directory and to clean up
any files that are no longer needed.
After the download is finished, the download object is destroyed and should not be accessed anymore.
*/
virtual void productDownloadFinished (Download&, const URL& /*downloadedContentPath*/) {}
};
//==============================================================================
/** Checks whether in-app purchases is supported on current platform. On iOS this always returns true. */
bool isInAppPurchasesSupported() const;
/** Asynchronously requests information for products with given ids. Upon completion, for each enquired product
there is going to be a corresponding @class Product object.
If there is no information available for the given product identifier, it will be ignored.
*/
void getProductsInformation (const StringArray& productIdentifiers);
/** Asynchronously requests to buy a product with given id.
@param isSubscription (Android only) defines if a product a user wants to buy is a subscription or a one-time purchase.
On iOS, type of the product is derived implicitly.
@param upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers (Android only) specifies subscriptions that will be replaced by the
one being purchased now. Used only when buying a subscription
that is an upgrade or downgrade from other ones.
@param creditForUnusedSubscription (Android only) controls whether a user should be credited for any unused subscription time on
the products that are being upgraded or downgraded.
*/
void purchaseProduct (const String& productIdentifier,
bool isSubscription,
const StringArray& upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers = {},
bool creditForUnusedSubscription = true);
/** Asynchronously asks about a list of products that a user has already bought. Upon completion, Listener::purchasesListReceived()
callback will be invoked. The user may be prompted to login first.
@param includeDownloadInfo (iOS only) if true, then after restoration is successfull, the downloads array passed to
Listener::purchasesListReceived() callback will contain all the download objects corresponding with
the purchase. In the opposite case, the downloads array will be empty.
@param subscriptionsSharedSecret (iOS only) required when not including download information and when there are
auto-renewable subscription set up with this app. Refer to In-App-Purchase settings in the store.
*/
void restoreProductsBoughtList (bool includeDownloadInfo, const juce::String& subscriptionsSharedSecret = {});
/** Android only: asynchronously sends a request to mark a purchase with given identifier as consumed.
To consume a product, provide product identifier as well as a purchase token that was generated when
the product was purchased. The purchase token can also be retrieved by using getProductsInformation().
In general if it is available on hand, it is better to use it, because otherwise another async
request will be sent to the store, to first retrieve the token.
After successful consumption, a product will no longer be returned in getProductsBought() and
it will be available for purchase.
On iOS consumption happens automatically. If the product was set as consumable, this function is a no-op.
*/
void consumePurchase (const String& productIdentifier, const String& purchaseToken = {});
//==============================================================================
/** Adds a listener. */
void addListener (Listener*);
/** Removes a listener. */
void removeListener (Listener*);
//==============================================================================
/** iOS only: Starts downloads of hosted content from the store. */
void startDownloads (const Array<Download*>& downloads);
/** iOS only: Pauses downloads of hosted content from the store. */
void pauseDownloads (const Array<Download*>& downloads);
/** iOS only: Resumes downloads of hosted content from the store. */
void resumeDownloads (const Array<Download*>& downloads);
/** iOS only: Cancels downloads of hosted content from the store. */
void cancelDownloads (const Array<Download*>& downloads);
//==============================================================================
#ifndef DOXYGEN
InAppPurchases();
~InAppPurchases();
#endif
private:
//==============================================================================
ListenerList<Listener> listeners;
#if JUCE_ANDROID
friend void juce_inAppPurchaseCompleted (jobject);
#endif
struct Pimpl;
friend struct Pimpl;
ScopedPointer<Pimpl> pimpl;
};

+ 20
- 0
modules/juce_product_unlocking/juce_product_unlocking.cpp View File

@@ -33,10 +33,30 @@
#error "Incorrect use of JUCE cpp file"
#endif
#define JUCE_CORE_INCLUDE_JNI_HELPERS 1
#define JUCE_CORE_INCLUDE_OBJC_HELPERS 1
#define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1
// Set this flag to 1 to use test servers on iOS
#ifndef JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT
#define JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT 0
#endif
#include "juce_product_unlocking.h"
#if JUCE_IOS || JUCE_MAC
#import <StoreKit/StoreKit.h>
#endif
namespace juce
{
#if JUCE_ANDROID
#include "native/juce_android_InAppPurchases.cpp"
#elif JUCE_IOS
#include "native/juce_ios_InAppPurchases.cpp"
#endif
#include "in_app_purchases/juce_InAppPurchases.cpp"
#include "marketplace/juce_OnlineUnlockStatus.cpp"
#if JUCE_MODULE_AVAILABLE_juce_data_structures


+ 9
- 1
modules/juce_product_unlocking/juce_product_unlocking.h View File

@@ -41,7 +41,7 @@
website: http://www.juce.com/juce
license: GPL/Commercial
dependencies: juce_cryptography
dependencies: juce_cryptography juce_core
END_JUCE_MODULE_DECLARATION
@@ -60,9 +60,13 @@
Tracktion Marketplace web-store, the module itself is fully open, and can
be used to connect to your own web-store instead, if you implement your
own compatible web-server back-end.
In additional, the module supports in-app purchases both on iOS and Android
platforms.
*/
//==============================================================================
#include <juce_core/juce_core.h>
#include <juce_cryptography/juce_cryptography.h>
#if JUCE_MODULE_AVAILABLE_juce_data_structures
@@ -75,6 +79,10 @@
namespace juce
{
#if JUCE_ANDROID || JUCE_IOS
#include "in_app_purchases/juce_InAppPurchases.h"
#endif
#if JUCE_MODULE_AVAILABLE_juce_data_structures
#include "marketplace/juce_OnlineUnlockStatus.h"
#include "marketplace/juce_TracktionMarketplaceStatus.h"


+ 23
- 0
modules/juce_product_unlocking/juce_product_unlocking.mm View File

@@ -0,0 +1,23 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
The code included in this file is provided under the terms of the ISC license
http://www.isc.org/downloads/software-support-policy/isc-license. Permission
To use, copy, modify, and/or distribute this software for any purpose with or
without fee is hereby granted provided that the above copyright notice and
this permission notice appear in all copies.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#include "juce_product_unlocking.cpp"

+ 930
- 0
modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp View File

@@ -0,0 +1,930 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (isBillingSupported, "isBillingSupported", "(ILjava/lang/String;Ljava/lang/String;)I") \
METHOD (getSkuDetails, "getSkuDetails", "(ILjava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;") \
METHOD (getBuyIntent, "getBuyIntent", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/os/Bundle;") \
METHOD (getBuyIntentExtraParams, "getBuyIntentExtraParams", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;") \
METHOD (getPurchases, "getPurchases", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/os/Bundle;") \
METHOD (consumePurchase, "consumePurchase", "(ILjava/lang/String;Ljava/lang/String;)I") \
METHOD (getPurchaseHistory, "getPurchaseHistory", "(ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;Landroid/os/Bundle;)Landroid/os/Bundle;")
DECLARE_JNI_CLASS (IInAppBillingService, "com/android/vending/billing/IInAppBillingService");
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
STATICMETHOD (asInterface, "asInterface", "(Landroid/os/IBinder;)Lcom/android/vending/billing/IInAppBillingService;") \
DECLARE_JNI_CLASS (IInAppBillingServiceStub, "com/android/vending/billing/IInAppBillingService$Stub");
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (constructor, "<init>", "(I)V") \
METHOD (add, "add", "(Ljava/lang/Object;)Z") \
METHOD (iterator, "iterator", "()Ljava/util/Iterator;") \
METHOD (get, "get", "(I)Ljava/lang/Object;") \
METHOD (size, "size", "()I")
DECLARE_JNI_CLASS (ArrayList, "java/util/ArrayList");
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (constructor, "<init>", "()V") \
METHOD (putBoolean, "putBoolean", "(Ljava/lang/String;Z)V") \
METHOD (putStringArrayList, "putStringArrayList", "(Ljava/lang/String;Ljava/util/ArrayList;)V") \
METHOD (getInt, "getInt", "(Ljava/lang/String;)I") \
METHOD (getStringArrayList, "getStringArrayList", "(Ljava/lang/String;)Ljava/util/ArrayList;") \
METHOD (getString, "getString", "(Ljava/lang/String;)Ljava/lang/String;") \
METHOD (getParcelable, "getParcelable", "(Ljava/lang/String;)Landroid/os/Parcelable;")
DECLARE_JNI_CLASS (Bundle, "android/os/Bundle");
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
STATICMETHOD (valueOf, "valueOf", "(I)Ljava/lang/Integer;")
DECLARE_JNI_CLASS (Integer, "java/lang/Integer");
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (constructor, "<init>", "()V") \
METHOD (constructWithString, "<init>", "(Ljava/lang/String;)V") \
METHOD (setPackage, "setPackage", "(Ljava/lang/String;)Landroid/content/Intent;") \
METHOD (getIntExtra, "getIntExtra", "(Ljava/lang/String;I)I") \
METHOD (getStringExtra, "getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;")
DECLARE_JNI_CLASS (Intent, "android/content/Intent");
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (hasNext, "hasNext", "()Z") \
METHOD (next, "next", "()Ljava/lang/Object;")
DECLARE_JNI_CLASS (Iterator, "java/util/Iterator");
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \
METHOD (getIntentSender, "getIntentSender", "()Landroid/content/IntentSender;")
DECLARE_JNI_CLASS (PendingIntent, "android/app/PendingIntent");
#undef JNI_CLASS_MEMBERS
//==============================================================================
struct ServiceConnection : public AndroidInterfaceImplementer
{
virtual void onServiceConnected (jobject component, jobject iBinder) = 0;
virtual void onServiceDisconnected (jobject component) = 0;
jobject invoke (jobject proxy, jobject method, jobjectArray args) override
{
auto* env = getEnv();
auto methodName = juceString ((jstring) env->CallObjectMethod (method, Method.getName));
if (methodName == "onServiceConnected")
{
onServiceConnected (env->GetObjectArrayElement (args, 0),
env->GetObjectArrayElement (args, 1));
return nullptr;
}
if (methodName == "onServiceDisconnected")
{
onServiceDisconnected (env->GetObjectArrayElement (args, 0));
return nullptr;
}
return AndroidInterfaceImplementer::invoke (proxy, method, args);
}
};
//==============================================================================
struct InAppPurchases::Pimpl : private AsyncUpdater,
private ServiceConnection
{
Pimpl (InAppPurchases& parent) : owner (parent)
{
getInAppPurchaseInstances().add (this);
auto* env = getEnv();
auto intent = env->NewObject (Intent, Intent.constructWithString,
javaString ("com.android.vending.billing.InAppBillingService.BIND").get());
env->CallObjectMethod (intent, Intent.setPackage, javaString ("com.android.vending").get());
serviceConnection = GlobalRef (CreateJavaInterface (this, "android/content/ServiceConnection").get());
android.activity.callBooleanMethod (JuceAppActivity.bindService, intent,
serviceConnection.get(), 1 /*BIND_AUTO_CREATE*/);
}
~Pimpl()
{
if (serviceConnection != nullptr)
{
android.activity.callVoidMethod (JuceAppActivity.unbindService, serviceConnection.get());
serviceConnection.clear();
}
getInAppPurchaseInstances().removeFirstMatchingValue (this);
}
//==============================================================================
bool isInAppPurchasesSupported() { return isInAppPurchasesSupported (inAppBillingService); }
void getProductsInformation (const StringArray& productIdentifiers)
{
if (! checkIsReady())
return;
auto callback = [this](const Array<InAppPurchases::Product>& products)
{
const ScopedLock lock (getProductsInformationJobResultsLock);
getProductsInformationJobResults.insert (0, products);
triggerAsyncUpdate();
};
threadPool->addJob (new GetProductsInformationJob (inAppBillingService, getPackageName(),
productIdentifiers, callback), true);
}
void purchaseProduct (const String& productIdentifier, bool isSubscription,
const StringArray& subscriptionIdentifiers, bool creditForUnusedSubscription)
{
if (! checkIsReady())
return;
// Upgrading/downgrading only makes sense for subscriptions!
jassert (subscriptionIdentifiers.isEmpty() || isSubscription);
auto buyIntentBundle = getBuyIntentBundle (productIdentifier, isSubscription,
subscriptionIdentifiers, creditForUnusedSubscription);
auto* env = getEnv();
auto responseCodeString = javaString ("RESPONSE_CODE");
auto responseCode = env->CallIntMethod (buyIntentBundle.get(), Bundle.getInt, responseCodeString.get());
if (responseCode == 0)
{
auto buyIntentString = javaString ("BUY_INTENT");
auto pendingIntent = LocalRef<jobject> (env->CallObjectMethod (buyIntentBundle.get(), Bundle.getParcelable, buyIntentString.get()));
auto requestCode = 1001;
auto intentSender = LocalRef<jobject> (env->CallObjectMethod (pendingIntent.get(), PendingIntent.getIntentSender));
auto fillInIntent = LocalRef<jobject> (env->NewObject (Intent, Intent.constructor));
auto flagsMask = LocalRef<jobject> (env->CallStaticObjectMethod (Integer, Integer.valueOf, 0));
auto flagsValues = LocalRef<jobject> (env->CallStaticObjectMethod (Integer, Integer.valueOf, 0));
auto extraFlags = LocalRef<jobject> (env->CallStaticObjectMethod (Integer, Integer.valueOf, 0));
android.activity.callVoidMethod (JuceAppActivity.startIntentSenderForResult, intentSender.get(), requestCode,
fillInIntent.get(), flagsMask.get(), flagsValues.get(), extraFlags.get());
}
}
void restoreProductsBoughtList (bool, const juce::String&)
{
if (! checkIsReady())
return;
auto callback = [this](const Array<InAppPurchases::Listener::PurchaseInfo>& purchases)
{
const ScopedLock lock (getProductsBoughtJobResultsLock);
getProductsBoughtJobResults.insert (0, purchases);
triggerAsyncUpdate();
};
threadPool->addJob (new GetProductsBoughtJob (inAppBillingService,
getPackageName(), callback), true);
}
void consumePurchase (const String& productIdentifier, const String& purchaseToken)
{
if (! checkIsReady())
return;
auto callback = [this](const ConsumePurchaseJob::Result& r)
{
const ScopedLock lock (consumePurchaseJobResultsLock);
consumePurchaseJobResults.insert (0, r);
triggerAsyncUpdate();
};
threadPool->addJob (new ConsumePurchaseJob (inAppBillingService, getPackageName(), productIdentifier,
purchaseToken, callback), true);
}
//==============================================================================
void startDownloads (const Array<Download*>& downloads)
{
// Not available on this platform.
ignoreUnused (downloads);
jassertfalse;
}
void pauseDownloads (const Array<Download*>& downloads)
{
// Not available on this platform.
ignoreUnused (downloads);
jassertfalse;
}
void resumeDownloads (const Array<Download*>& downloads)
{
// Not available on this platform.
ignoreUnused (downloads);
jassertfalse;
}
void cancelDownloads (const Array<Download*>& downloads)
{
// Not available on this platform.
ignoreUnused (downloads);
jassertfalse;
}
//==============================================================================
LocalRef<jobject> getBuyIntentBundle (const String& productIdentifier, bool isSubscription,
const StringArray& subscriptionIdentifiers, bool creditForUnusedSubscription)
{
auto* env = getEnv();
auto skuString = javaString (productIdentifier);
auto productTypeString = javaString (isSubscription ? "subs" : "inapp");
auto devString = javaString (getDeveloperExtraData());
if (subscriptionIdentifiers.isEmpty())
return LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getBuyIntent, 3,
getPackageName().get(), skuString.get(),
productTypeString.get(), devString.get()));
auto skuList = LocalRef<jobject> (env->NewObject (ArrayList, ArrayList.constructor,
(int) subscriptionIdentifiers.size()));
if (skuList.get() == 0)
{
jassertfalse;
return LocalRef<jobject> (0);
}
for (const auto& identifier : subscriptionIdentifiers)
env->CallBooleanMethod (skuList.get(), ArrayList.add, javaString (identifier).get());
auto extraParams = LocalRef<jobject> (env->NewObject (Bundle, Bundle.constructor));
if (extraParams.get() == 0)
{
jassertfalse;
return LocalRef<jobject> (0);
}
auto skusToReplaceString = javaString ("skusToReplace");
auto replaceSkusProrationString = javaString ("replaceSkusProration");
env->CallVoidMethod (extraParams.get(), Bundle.putStringArrayList, skusToReplaceString.get(), skuList.get());
env->CallVoidMethod (extraParams.get(), Bundle.putBoolean, replaceSkusProrationString.get(), creditForUnusedSubscription);
return LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getBuyIntentExtraParams, 6,
getPackageName().get(), skuString.get(),
productTypeString.get(), devString.get(),
extraParams.get()));
}
//==============================================================================
void notifyAboutPurchaseResult (const InAppPurchases::Purchase& purchase, bool success, const String& statusDescription)
{
owner.listeners.call (&Listener::productPurchaseFinished, { purchase, {} }, success, statusDescription);
}
//==============================================================================
bool checkIsReady()
{
return (inAppBillingService.get() != 0);
}
static bool isInAppPurchasesSupported (jobject iapService)
{
if (iapService != nullptr)
{
auto* env = getEnv();
auto inAppString = javaString ("inapp");
auto subsString = javaString ("subs");
if (env->CallIntMethod (iapService, IInAppBillingService.isBillingSupported, 3,
getPackageName().get(), inAppString.get()) != 0)
return false;
if (env->CallIntMethod (iapService, IInAppBillingService.isBillingSupported, 3,
getPackageName().get(), subsString.get()) != 0)
return false;
return true;
}
// Connecting to the in-app purchase server failed! This could have multiple reasons:
// 1) Your phone/emulator must support the google play store
// 2) Your phone must be logged into the google play store and be able to receive updates
return false;
}
//==============================================================================
void onServiceConnected (jobject, jobject iBinder) override
{
auto* env = getEnv();
LocalRef<jobject> iapService (env->CallStaticObjectMethod (IInAppBillingServiceStub,
IInAppBillingServiceStub.asInterface,
iBinder));
if (isInAppPurchasesSupported (iapService))
{
if (threadPool == nullptr)
threadPool = new ThreadPool (1);
inAppBillingService = GlobalRef (iapService);
}
// If you hit this assert, then in-app purchases is not available on your device,
// most likely due to too old version of Google Play API (hint: update Google Play on the device).
jassert (isInAppPurchasesSupported());
}
void onServiceDisconnected (jobject) override
{
threadPool = nullptr;
inAppBillingService.clear();
}
//==============================================================================
static LocalRef<jstring> getPackageName()
{
return LocalRef<jstring> ((jstring) (android.activity.callObjectMethod (JuceAppActivity.getPackageName)));
}
//==============================================================================
struct GetProductsInformationJob : public ThreadPoolJob
{
using Callback = std::function<void(const Array<InAppPurchases::Product>&)>;
GetProductsInformationJob (const GlobalRef& inAppBillingServiceToUse,
const LocalRef<jstring>& packageNameToUse,
const StringArray& productIdentifiersToUse,
const Callback& callbackToUse)
: ThreadPoolJob ("GetProductsInformationJob"),
inAppBillingService (inAppBillingServiceToUse),
packageName (packageNameToUse.get()),
productIdentifiers (productIdentifiersToUse),
callback (callbackToUse)
{}
ThreadPoolJob::JobStatus runJob() override
{
jassert (callback);
if (inAppBillingService.get() != 0)
{
// Google's Billing API limitation
auto maxQuerySize = 20;
auto pi = 0;
Array<InAppPurchases::Product> results;
StringArray identifiersToUse;
for (auto i = 0; i < productIdentifiers.size(); ++i)
{
identifiersToUse.add (productIdentifiers[i].toLowerCase());
++pi;
if (pi == maxQuerySize || i == productIdentifiers.size() - 1)
{
auto inAppProducts = processRetrievedProducts (queryProductsInformationFromService (identifiersToUse, "inapp"));
auto subsProducts = processRetrievedProducts (queryProductsInformationFromService (identifiersToUse, "subs"));
results.addArray (inAppProducts);
results.addArray (subsProducts);
identifiersToUse.clear();
pi = 0;
}
}
if (callback)
callback (results);
}
else
{
if (callback)
callback ({});
}
return jobHasFinished;
}
private:
LocalRef<jobject> queryProductsInformationFromService (const StringArray& productIdentifiersToQuery, const String& productType)
{
auto* env = getEnv();
auto skuList = LocalRef<jobject> (env->NewObject (ArrayList, ArrayList.constructor, productIdentifiersToQuery.size()));
if (skuList.get() == 0)
return LocalRef<jobject> (0);
for (const auto& pi : productIdentifiersToQuery)
env->CallBooleanMethod (skuList.get(), ArrayList.add, javaString (pi).get());
auto querySkus = LocalRef<jobject> (env->NewObject (Bundle, Bundle.constructor));
if (querySkus.get() == 0)
return LocalRef<jobject> (0);
auto itemIdListString = javaString ("ITEM_ID_LIST");
env->CallVoidMethod (querySkus.get(), Bundle.putStringArrayList, itemIdListString.get(), skuList.get());
auto productTypeString = javaString (productType);
auto productDetails = LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getSkuDetails,
3, (jstring) packageName.get(),
productTypeString.get(), querySkus.get()));
return productDetails;
}
Array<InAppPurchases::Product> processRetrievedProducts (LocalRef<jobject> retrievedProducts)
{
Array<InAppPurchases::Product> products;
if (retrievedProducts.get() != 0)
{
auto* env = getEnv();
auto responseCodeString = javaString ("RESPONSE_CODE");
auto responseCode = env->CallIntMethod (retrievedProducts.get(), Bundle.getInt, responseCodeString.get());
if (responseCode == 0)
{
auto detailsListString = javaString ("DETAILS_LIST");
auto responseList = LocalRef<jobject> (env->CallObjectMethod (retrievedProducts.get(), Bundle.getStringArrayList,
detailsListString.get()));
if (responseList != 0)
{
auto iterator = LocalRef<jobject> (env->CallObjectMethod (responseList.get(), ArrayList.iterator));
if (iterator.get() != 0)
{
for (;;)
{
if (! env->CallBooleanMethod (iterator, Iterator.hasNext))
break;
auto response = juce::LocalRef<jstring> ((jstring)env->CallObjectMethod (iterator, Iterator.next));
if (response.get() != 0)
{
var responseData = JSON::parse (juceString (response.get()));
if (DynamicObject* object = responseData.getDynamicObject())
{
NamedValueSet& props = object->getProperties();
static Identifier productIdIdentifier ("productId");
static Identifier titleIdentifier ("title");
static Identifier descriptionIdentifier ("description");
static Identifier priceIdentifier ("price");
static Identifier priceCurrencyCodeIdentifier ("price_currency_code");
var productId = props[productIdIdentifier];
var title = props[titleIdentifier];
var description = props[descriptionIdentifier];
var price = props[priceIdentifier];
var priceCurrencyCode = props[priceCurrencyCodeIdentifier];
products.add ( { productId.toString(),
title.toString(),
description.toString(),
price.toString(),
priceCurrencyCode.toString() } );
}
}
}
}
}
}
}
return products;
}
GlobalRef inAppBillingService, packageName;
const StringArray productIdentifiers;
Callback callback;
};
//==============================================================================
struct GetProductsBoughtJob : public ThreadPoolJob
{
using Callback = std::function<void(const Array<InAppPurchases::Listener::PurchaseInfo>&)>;
GetProductsBoughtJob (const GlobalRef& inAppBillingServiceToUse,
const LocalRef<jstring>& packageNameToUse,
const Callback& callbackToUse)
: ThreadPoolJob ("GetProductsBoughtJob"),
inAppBillingService (inAppBillingServiceToUse),
packageName (packageNameToUse.get()),
callback (callbackToUse)
{}
ThreadPoolJob::JobStatus runJob() override
{
jassert (callback);
if (inAppBillingService.get() != 0)
{
auto inAppPurchases = getProductsBought ("inapp", 0);
auto subsPurchases = getProductsBought ("subs", 0);
inAppPurchases.addArray (subsPurchases);
Array<InAppPurchases::Listener::PurchaseInfo> purchases;
for (const auto& purchase : inAppPurchases)
purchases.add ({ purchase, {} });
if (callback)
callback (purchases);
}
else
{
if (callback)
callback ({});
}
return jobHasFinished;
}
private:
Array<InAppPurchases::Purchase> getProductsBought (const String& productType, jstring continuationToken)
{
Array<InAppPurchases::Purchase> purchases;
auto* env = getEnv();
auto productTypeString = javaString (productType);
auto ownedItems = LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getPurchases, 3,
(jstring) packageName.get(), productTypeString.get(),
continuationToken));
if (ownedItems.get() != 0)
{
auto responseCodeString = javaString ("RESPONSE_CODE");
auto responseCode = env->CallIntMethod (ownedItems.get(), Bundle.getInt, responseCodeString.get());
if (responseCode == 0)
{
auto itemListString = javaString ("INAPP_PURCHASE_ITEM_LIST");
auto dataListString = javaString ("INAPP_PURCHASE_DATA_LIST");
auto signatureListString = javaString ("INAPP_DATA_SIGNATURE_LIST");
auto continuationTokenString = javaString ("INAPP_CONTINUATION_TOKEN");
auto ownedSkus = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, itemListString.get()));
auto purchaseDataList = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, dataListString.get()));
auto signatureList = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, signatureListString.get()));
auto newContinuationToken = LocalRef<jstring> ((jstring) env->CallObjectMethod (ownedItems.get(), Bundle.getString, continuationTokenString.get()));
for (auto i = 0; i < env->CallIntMethod (purchaseDataList.get(), ArrayList.size); ++i)
{
auto sku = juceString ((jstring) (env->CallObjectMethod (ownedSkus.get(), ArrayList.get, i)));
auto purchaseData = juceString ((jstring) (env->CallObjectMethod (purchaseDataList.get(), ArrayList.get, i)));
auto signature = juceString ((jstring) (env->CallObjectMethod (signatureList.get(), ArrayList.get, i)));
var responseData = JSON::parse (purchaseData);
if (auto* object = responseData.getDynamicObject())
{
auto& props = object->getProperties();
static const Identifier orderIdIdentifier ("orderId"),
packageNameIdentifier ("packageName"),
productIdIdentifier ("productId"),
purchaseTimeIdentifier ("purchaseTime"),
purchaseTokenIdentifier ("purchaseToken");
var orderId = props[orderIdIdentifier];
var appPackageName = props[packageNameIdentifier];
var productId = props[productIdIdentifier];
var purchaseTime = props[purchaseTimeIdentifier];
var purchaseToken = props[purchaseTokenIdentifier];
String purchaseTimeString = Time (purchaseTime.toString().getLargeIntValue()).toString (true, true, true, true);
purchases.add ({ orderId.toString(), productId.toString(), appPackageName.toString(), purchaseTimeString, purchaseToken.toString() });
}
}
if (newContinuationToken.get() != 0)
getProductsBought (productType, newContinuationToken.get());
}
}
return purchases;
}
GlobalRef inAppBillingService, packageName;
Callback callback;
};
//==============================================================================
class ConsumePurchaseJob : public ThreadPoolJob
{
public:
struct Result
{
String productIdentifier;
bool success;
String statusDescription;
};
using Callback = std::function<void(const Result&)>;
ConsumePurchaseJob (const GlobalRef& inAppBillingServiceToUse,
const LocalRef<jstring>& packageNameToUse,
const String& productIdentifierToUse,
const String& purchaseTokenToUse,
const Callback& callbackToUse)
: ThreadPoolJob ("ConsumePurchaseJob"),
inAppBillingService (inAppBillingServiceToUse),
packageName (packageNameToUse.get()),
productIdentifier (productIdentifierToUse),
purchaseToken (purchaseTokenToUse),
callback (callbackToUse)
{}
ThreadPoolJob::JobStatus runJob() override
{
jassert (callback);
auto token = (! purchaseToken.isEmpty() ? purchaseToken : getPurchaseTokenForProductId (productIdentifier, false, 0));
if (token.isEmpty())
{
if (callback)
callback ({ productIdentifier, false, NEEDS_TRANS ("Item not owned") });
return jobHasFinished;
}
auto responseCode = inAppBillingService.callIntMethod (IInAppBillingService.consumePurchase, 3,
(jstring)packageName.get(), javaString (token).get());
if (callback)
callback ({ productIdentifier, responseCode == 0, statusCodeToUserString (responseCode) });
return jobHasFinished;
}
private:
String getPurchaseTokenForProductId (const String productIdToLookFor, bool isSubscription, jstring continuationToken)
{
auto productTypeString = javaString (isSubscription ? "subs" : "inapp");
auto ownedItems = LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getPurchases, 3,
(jstring) packageName.get(), productTypeString.get(),
continuationToken));
if (ownedItems.get() != 0)
{
auto* env = getEnv();
auto responseCodeString = javaString ("RESPONSE_CODE");
auto responseCode = env->CallIntMethod (ownedItems.get(), Bundle.getInt, responseCodeString.get());
if (responseCode == 0)
{
auto dataListString = javaString ("INAPP_PURCHASE_DATA_LIST");
auto continuationTokenString = javaString ("INAPP_CONTINUATION_TOKEN");
auto purchaseDataList = LocalRef<jobject> (env->CallObjectMethod (ownedItems.get(), Bundle.getStringArrayList, dataListString.get()));
auto newContinuationToken = LocalRef<jstring> ((jstring) env->CallObjectMethod (ownedItems.get(), Bundle.getString, continuationTokenString.get()));
for (auto i = 0; i < env->CallIntMethod (purchaseDataList.get(), ArrayList.size); ++i)
{
auto purchaseData = juceString ((jstring) (env->CallObjectMethod (purchaseDataList.get(), ArrayList.get, i)));
var responseData = JSON::parse (purchaseData);
if (auto* object = responseData.getDynamicObject())
{
static const Identifier productIdIdentifier ("productId"),
purchaseTokenIdentifier ("purchaseToken");
auto& props = object->getProperties();
var productId = props[productIdIdentifier];
if (productId.toString() == productIdToLookFor)
return props[purchaseTokenIdentifier].toString();
}
}
if (newContinuationToken.get() != 0)
return getPurchaseTokenForProductId (productIdToLookFor, isSubscription, newContinuationToken.get());
}
}
return {};
}
GlobalRef inAppBillingService, packageName;
const String productIdentifier, purchaseToken;
Callback callback;
};
//==============================================================================
void handleAsyncUpdate() override
{
{
const ScopedLock lock (getProductsInformationJobResultsLock);
for (int i = getProductsInformationJobResults.size(); --i >= 0;)
{
const auto& result = getProductsInformationJobResults.getReference (i);
owner.listeners.call (&Listener::productsInfoReturned, result);
getProductsInformationJobResults.remove (i);
}
}
{
const ScopedLock lock (getProductsBoughtJobResultsLock);
for (int i = getProductsBoughtJobResults.size(); --i >= 0;)
{
const auto& result = getProductsBoughtJobResults.getReference (i);
owner.listeners.call (&Listener::purchasesListRestored, result, true, NEEDS_TRANS ("Success"));
getProductsBoughtJobResults.remove (i);
}
}
{
const ScopedLock lock (consumePurchaseJobResultsLock);
for (int i = consumePurchaseJobResults.size(); --i >= 0;)
{
const auto& result = consumePurchaseJobResults.getReference (i);
owner.listeners.call (&Listener::productConsumed, result.productIdentifier,
result.success, result.statusDescription);
consumePurchaseJobResults.remove (i);
}
}
}
//==============================================================================
static Array<Pimpl*>& getInAppPurchaseInstances() noexcept
{
static Array<Pimpl*> instances;
return instances;
}
static void inAppPurchaseCompleted (jobject intentData)
{
auto* env = getEnv();
auto inAppPurchaseDataString = javaString ("INAPP_PURCHASE_DATA");
auto inAppDataSignatureString = javaString ("INAPP_DATA_SIGNATURE");
auto responseCodeString = javaString ("RESPONSE_CODE");
auto pd = LocalRef<jstring> ((jstring) env->CallObjectMethod (intentData, Intent.getStringExtra, inAppPurchaseDataString.get()));
auto sig = LocalRef<jstring> ((jstring) env->CallObjectMethod (intentData, Intent.getStringExtra, inAppDataSignatureString.get()));
auto purchaseDataString = pd.get() != 0 ? juceString (pd.get()) : String();
auto dataSignatureString = sig.get() != 0 ? juceString (sig.get()) : String();
var responseData = JSON::parse (purchaseDataString);
auto responseCode = env->CallIntMethod (intentData, Intent.getIntExtra, responseCodeString.get());
auto statusCodeUserString = statusCodeToUserString (responseCode);
if (auto* object = responseData.getDynamicObject())
{
auto& props = object->getProperties();
static const Identifier orderIdIdentifier ("orderId"),
packageNameIdentifier ("packageName"),
productIdIdentifier ("productId"),
purchaseTimeIdentifier ("purchaseTime"),
purchaseTokenIdentifier ("purchaseToken"),
developerPayloadIdentifier ("developerPayload");
var orderId = props[orderIdIdentifier];
var packageName = props[packageNameIdentifier];
var productId = props[productIdIdentifier];
var purchaseTime = props[purchaseTimeIdentifier];
var purchaseToken = props[purchaseTokenIdentifier];
var developerPayload = props[developerPayloadIdentifier];
if (auto* target = getPimplFromDeveloperExtraData (developerPayload))
{
auto purchaseTimeString = Time (purchaseTime.toString().getLargeIntValue())
.toString (true, true, true, true);
target->notifyAboutPurchaseResult ({ orderId.toString(), productId.toString(), packageName.toString(),
purchaseTimeString, purchaseToken.toString() },
true, statusCodeUserString);
}
}
}
//==============================================================================
String getDeveloperExtraData()
{
static const Identifier inAppPurchaseInstance ("inAppPurchaseInstance");
DynamicObject::Ptr developerString (new DynamicObject());
developerString->setProperty (inAppPurchaseInstance,
"0x" + String::toHexString (reinterpret_cast<pointer_sized_int> (this)));
return JSON::toString (var (developerString));
}
static Pimpl* getPimplFromDeveloperExtraData (const String& developerExtra)
{
static const Identifier inAppPurchaseInstance ("inAppPurchaseInstance");
if (DynamicObject::Ptr developerData = JSON::fromString (developerExtra).getDynamicObject())
{
String hexAddr = developerData->getProperty (inAppPurchaseInstance);
if (hexAddr.startsWith ("0x"))
hexAddr = hexAddr.fromFirstOccurrenceOf ("0x", false, false);
auto* target = reinterpret_cast<Pimpl*> (static_cast<pointer_sized_int> (hexAddr.getHexValue64()));
if (getInAppPurchaseInstances().contains (target))
return target;
}
return nullptr;
}
//==============================================================================
static String statusCodeToUserString (int statusCode)
{
switch (statusCode)
{
case 0: return NEEDS_TRANS ("Success");
case 1: return NEEDS_TRANS ("Cancelled by user");
case 2: return NEEDS_TRANS ("Service unavailable");
case 3: return NEEDS_TRANS ("Billing unavailable");
case 4: return NEEDS_TRANS ("Item unavailable");
case 5: return NEEDS_TRANS ("Internal error");
case 6: return NEEDS_TRANS ("Generic error");
case 7: return NEEDS_TRANS ("Item already owned");
case 8: return NEEDS_TRANS ("Item not owned");
default: jassertfalse; return NEEDS_TRANS ("Unknown status");
}
}
//==============================================================================
InAppPurchases& owner;
GlobalRef inAppBillingService, serviceConnection;
ScopedPointer<ThreadPool> threadPool;
CriticalSection getProductsInformationJobResultsLock,
getProductsBoughtJobResultsLock,
consumePurchaseJobResultsLock;
Array<Array<InAppPurchases::Product>> getProductsInformationJobResults;
Array<Array<InAppPurchases::Listener::PurchaseInfo>> getProductsBoughtJobResults;
Array<ConsumePurchaseJob::Result> consumePurchaseJobResults;
};
//==============================================================================
void juce_inAppPurchaseCompleted (jobject intentData)
{
InAppPurchases::Pimpl::inAppPurchaseCompleted (intentData);
}

+ 679
- 0
modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp View File

@@ -0,0 +1,679 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
template <> struct ContainerDeletePolicy<SKProductsRequest> { static void destroy (NSObject* o) { [o release]; } };
template <> struct ContainerDeletePolicy<SKReceiptRefreshRequest> { static void destroy (NSObject* o) { [o release]; } };
template <> struct ContainerDeletePolicy<NSObject<SKProductsRequestDelegate,SKPaymentTransactionObserver>> { static void destroy (NSObject* o) { [o release]; } };
//==============================================================================
struct SKDelegateAndPaymentObserver
{
SKDelegateAndPaymentObserver() : delegate ([getClass().createInstance() init])
{
Class::setThis (delegate, this);
}
virtual ~SKDelegateAndPaymentObserver() {}
virtual void didReceiveResponse (SKProductsRequest*, SKProductsResponse*) = 0;
virtual void requestDidFinish (SKRequest*) = 0;
virtual void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>*) = 0;
virtual void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError*) = 0;
virtual void restoreCompletedTransactionsFinished (SKPaymentQueue*) = 0;
virtual void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>*) = 0;
protected:
ScopedPointer<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> delegate;
private:
struct Class : public ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>>
{
//==============================================================================
Class() : ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> ("SKDelegateAndPaymentObserverBase_")
{
addIvar<SKDelegateAndPaymentObserver*> ("self");
addMethod (@selector (productsRequest:didReceiveResponse:), didReceiveResponse, "v@:@@");
addMethod (@selector (requestDidFinish:), requestDidFinish, "v@:@");
addMethod (@selector (paymentQueue:updatedTransactions:), updatedTransactions, "v@:@@");
addMethod (@selector (paymentQueue:restoreCompletedTransactionsFailedWithError:), restoreCompletedTransactionsFailedWithError, "v@:@@");
addMethod (@selector (paymentQueueRestoreCompletedTransactionsFinished:), restoreCompletedTransactionsFinished, "v@:@");
addMethod (@selector (paymentQueue:updatedDownloads:), updatedDownloads, "v@:@@");
registerClass();
}
//==============================================================================
static SKDelegateAndPaymentObserver& getThis (id self) { return *getIvar<SKDelegateAndPaymentObserver*> (self, "self"); }
static void setThis (id self, SKDelegateAndPaymentObserver* s) { object_setInstanceVariable (self, "self", s); }
//==============================================================================
static void didReceiveResponse (id self, SEL, SKProductsRequest* request, SKProductsResponse* response) { getThis (self).didReceiveResponse (request, response); }
static void requestDidFinish (id self, SEL, SKRequest* request) { getThis (self).requestDidFinish (request); }
static void updatedTransactions (id self, SEL, SKPaymentQueue* queue, NSArray<SKPaymentTransaction*>* trans) { getThis (self).updatedTransactions (queue, trans); }
static void restoreCompletedTransactionsFailedWithError (id self, SEL, SKPaymentQueue* q, NSError* err) { getThis (self).restoreCompletedTransactionsFailedWithError (q, err); }
static void restoreCompletedTransactionsFinished (id self, SEL, SKPaymentQueue* queue) { getThis (self).restoreCompletedTransactionsFinished (queue); }
static void updatedDownloads (id self, SEL, SKPaymentQueue* queue, NSArray<SKDownload*>* downloads) { getThis (self).updatedDownloads (queue, downloads); }
};
//==============================================================================
static Class& getClass()
{
static Class c;
return c;
}
};
//==============================================================================
struct InAppPurchases::Pimpl : public SKDelegateAndPaymentObserver
{
/** AppStore implementation of hosted content download. */
struct DownloadImpl : public Download
{
DownloadImpl (SKDownload* downloadToUse) : download (downloadToUse) {}
String getProductId() const override { return nsStringToJuce (download.contentIdentifier); }
int64 getContentLength() const override { return download.contentLength; }
String getContentVersion() const override { return nsStringToJuce (download.contentVersion); }
Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.downloadState); }
SKDownload* download;
};
/** Represents a pending request initialised with [SKProductRequest start]. */
struct PendingProductInfoRequest
{
enum class Type
{
query = 0,
purchase
};
Type type;
ScopedPointer<SKProductsRequest> request;
};
/** Represents a pending request started from [SKReceiptRefreshRequest start]. */
struct PendingReceiptRefreshRequest
{
String subscriptionsSharedSecret;
ScopedPointer<SKReceiptRefreshRequest> request;
};
/** Represents a transaction with pending downloads. Only after all downloads
are finished, the transaction is marked as finished. */
struct PendingDownloadsTransaction
{
PendingDownloadsTransaction (SKPaymentTransaction* t) : transaction (t)
{
addDownloadsFromSKTransaction (transaction);
}
void addDownloadsFromSKTransaction (SKPaymentTransaction* transactionToUse)
{
for (SKDownload* download in transactionToUse.downloads)
downloads.add (new DownloadImpl (download));
}
bool canBeMarkedAsFinished() const
{
for (SKDownload* d in transaction.downloads)
{
if (d.downloadState != SKDownloadStateFinished
&& d.downloadState != SKDownloadStateFailed
&& d.downloadState != SKDownloadStateCancelled)
{
return false;
}
}
return true;
}
OwnedArray<DownloadImpl> downloads;
SKPaymentTransaction* const transaction;
};
//==============================================================================
Pimpl (InAppPurchases& p) : owner (p) { [[SKPaymentQueue defaultQueue] addTransactionObserver: delegate]; }
~Pimpl() noexcept { [[SKPaymentQueue defaultQueue] removeTransactionObserver: delegate]; }
//==============================================================================
bool isInAppPurchasesSupported() const { return true; }
void getProductsInformation (const StringArray& productIdentifiers)
{
auto* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithArray: createNSArrayFromStringArray (productIdentifiers)]];
pendingProductInfoRequests.add (new PendingProductInfoRequest {PendingProductInfoRequest::Type::query, productsRequest});
productsRequest.delegate = delegate;
[productsRequest start];
}
void purchaseProduct (const String& productIdentifier, bool, const StringArray&, bool)
{
if (! [SKPaymentQueue canMakePayments])
{
owner.listeners.call (&Listener::productPurchaseFinished, {}, false, NEEDS_TRANS ("Payments not allowed"));
return;
}
auto* productIdentifiers = [NSArray arrayWithObject: juceStringToNS (productIdentifier)];
auto* productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]];
pendingProductInfoRequests.add (new PendingProductInfoRequest {PendingProductInfoRequest::Type::purchase, productsRequest});
productsRequest.delegate = delegate;
[productsRequest start];
}
void restoreProductsBoughtList (bool includeDownloadInfo, const String& subscriptionsSharedSecret)
{
if (includeDownloadInfo)
{
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
else
{
auto* receiptRequest = [[SKReceiptRefreshRequest alloc] init];
pendingReceiptRefreshRequests.add (new PendingReceiptRefreshRequest {subscriptionsSharedSecret,
[receiptRequest retain]});
receiptRequest.delegate = delegate;
[receiptRequest start];
}
}
void consumePurchase (const String&, const String&) {}
//==============================================================================
void startDownloads (const Array<Download*>& downloads)
{
[[SKPaymentQueue defaultQueue] startDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
}
void pauseDownloads (const Array<Download*>& downloads)
{
[[SKPaymentQueue defaultQueue] pauseDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
}
void resumeDownloads (const Array<Download*>& downloads)
{
[[SKPaymentQueue defaultQueue] resumeDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
}
void cancelDownloads (const Array<Download*>& downloads)
{
[[SKPaymentQueue defaultQueue] cancelDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
}
//==============================================================================
void didReceiveResponse (SKProductsRequest* request, SKProductsResponse* response) override
{
for (auto i = 0; i < pendingProductInfoRequests.size(); ++i)
{
auto& pendingRequest = *pendingProductInfoRequests[i];
if (pendingRequest.request == request)
{
if (pendingRequest.type == PendingProductInfoRequest::Type::query) notifyProductsInfoReceived (response.products);
else if (pendingRequest.type == PendingProductInfoRequest::Type::purchase) startPurchase (response.products);
else break;
pendingProductInfoRequests.remove (i);
return;
}
}
// Unknown request received!
jassertfalse;
}
void requestDidFinish (SKRequest* request) override
{
if (auto receiptRefreshRequest = getAs<SKReceiptRefreshRequest> (request))
{
for (auto i = 0; i < pendingReceiptRefreshRequests.size(); ++i)
{
auto& pendingRequest = *pendingReceiptRefreshRequests[i];
if (pendingRequest.request == receiptRefreshRequest)
{
processReceiptRefreshResponseWithSubscriptionsSharedSecret (pendingRequest.subscriptionsSharedSecret);
pendingReceiptRefreshRequests.remove (i);
return;
}
}
}
}
void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>* transactions) override
{
for (SKPaymentTransaction* transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchasing: break;
case SKPaymentTransactionStateDeferred: break;
case SKPaymentTransactionStateFailed: processTransactionFinish (transaction, false); break;
case SKPaymentTransactionStatePurchased: processTransactionFinish (transaction, true); break;
case SKPaymentTransactionStateRestored: processTransactionFinish (transaction, true); break;
default: jassertfalse; break; // Unexpected transaction state
}
}
}
void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError* error) override
{
owner.listeners.call (&Listener::purchasesListRestored, {}, false, nsStringToJuce (error.localizedDescription));
}
void restoreCompletedTransactionsFinished (SKPaymentQueue*) override
{
owner.listeners.call (&Listener::purchasesListRestored, restoredPurchases, true, NEEDS_TRANS ("Success"));
restoredPurchases.clear();
}
void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>* downloads) override
{
for (SKDownload* download in downloads)
{
if (auto* pendingDownload = getPendingDownloadFor (download))
{
switch (download.downloadState)
{
case SKDownloadStateWaiting: break;
case SKDownloadStatePaused: owner.listeners.call (&Listener::productDownloadPaused, *pendingDownload); break;
case SKDownloadStateActive: owner.listeners.call (&Listener::productDownloadProgressUpdate, *pendingDownload,
download.progress, RelativeTime (download.timeRemaining)); break;
case SKDownloadStateFinished:
case SKDownloadStateFailed:
case SKDownloadStateCancelled: processDownloadFinish (pendingDownload, download); break;
default: jassertfalse; break; // Unexpected download state
}
}
}
}
//==============================================================================
void notifyProductsInfoReceived (NSArray<SKProduct*>* products)
{
Array<Product> productsToReturn;
for (SKProduct* skProduct in products)
productsToReturn.add (SKProductToIAPProduct (skProduct));
owner.listeners.call (&Listener::productsInfoReturned, productsToReturn);
}
void startPurchase (NSArray<SKProduct*>* products)
{
if ([products count] > 0)
{
// Only one product can be bought at once!
jassert ([products count] == 1);
auto* product = products[0];
auto* payment = [SKPayment paymentWithProduct: product];
[[SKPaymentQueue defaultQueue] addPayment: payment];
}
else
{
owner.listeners.call (&Listener::productPurchaseFinished, {}, false,
NEEDS_TRANS ("Your app is not setup for payments"));
}
}
//==============================================================================
Array<Download*> removeInvalidDownloads (const Array<Download*>& downloadsToUse)
{
Array<Download*> downloads (downloadsToUse);
for (int i = downloads.size(); --i >= 0;)
{
auto hasPendingDownload = hasDownloadInPendingDownloadsTransaction (*downloads[i]);
// Invalid download passed, it does not exist in pending downloads list
jassert (hasPendingDownload);
if (! hasPendingDownload)
downloads.remove (i);
}
return downloads;
}
bool hasDownloadInPendingDownloadsTransaction (const Download& download)
{
for (auto* pdt : pendingDownloadsTransactions)
for (auto* pendingDownload : pdt->downloads)
if (pendingDownload == &download)
return true;
return false;
}
//==============================================================================
void processTransactionFinish (SKPaymentTransaction* transaction, bool success)
{
auto orderId = nsStringToJuce (transaction.transactionIdentifier);
auto packageName = nsStringToJuce ([[NSBundle mainBundle] bundleIdentifier]);
auto productId = nsStringToJuce (transaction.payment.productIdentifier);
auto purchaseTime = Time (1000 * (int64) transaction.transactionDate.timeIntervalSince1970)
.toString (true, true, true, true);
Purchase purchase { orderId, productId, packageName, purchaseTime, {} };
Array<Download*> downloads;
// If transaction failed or there are no downloads, finish the transaction immediately, otherwise
// finish the transaction only after all downloads are finished.
if (transaction.transactionState == SKPaymentTransactionStateFailed
|| transaction.downloads == nil
|| [transaction.downloads count] == 0)
{
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
}
else
{
// On application startup or when the app is resumed we may receive multiple
// "purchased" callbacks with the same underlying transaction. Sadly, only
// the last set of downloads will be valid.
auto* pdt = getPendingDownloadsTransactionForSKTransaction (transaction);
if (pdt == nullptr)
{
pdt = pendingDownloadsTransactions.add (new PendingDownloadsTransaction (transaction));
}
else
{
pdt->downloads.clear();
pdt->addDownloadsFromSKTransaction (transaction);
}
for (auto* download : pdt->downloads)
downloads.add (download);
}
if (transaction.transactionState == SKPaymentTransactionStateRestored)
restoredPurchases.add ({ purchase, downloads });
else
owner.listeners.call (&Listener::productPurchaseFinished, { purchase, downloads }, success,
SKPaymentTransactionStateToString (transaction.transactionState));
}
PendingDownloadsTransaction* getPendingDownloadsTransactionForSKTransaction (SKPaymentTransaction* transaction)
{
for (auto* pdt : pendingDownloadsTransactions)
if (pdt->transaction == transaction)
return pdt;
return nullptr;
}
//==============================================================================
PendingDownloadsTransaction* getPendingDownloadsTransactionSKDownloadFor (SKDownload* download)
{
for (auto* pdt : pendingDownloadsTransactions)
for (auto* pendingDownload : pdt->downloads)
if (pendingDownload->download == download)
return pdt;
jassertfalse;
return nullptr;
}
Download* getPendingDownloadFor (SKDownload* download)
{
if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download))
for (auto* pendingDownload : pdt->downloads)
if (pendingDownload->download == download)
return pendingDownload;
jassertfalse;
return nullptr;
}
void processDownloadFinish (Download* pendingDownload, SKDownload* download)
{
if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download))
{
auto contentURL = download.downloadState == SKDownloadStateFinished
? URL (nsStringToJuce (download.contentURL.absoluteString))
: URL();
owner.listeners.call (&Listener::productDownloadFinished, *pendingDownload, contentURL);
if (pdt->canBeMarkedAsFinished())
{
// All downloads finished, mark transaction as finished too.
[[SKPaymentQueue defaultQueue] finishTransaction: pdt->transaction];
pendingDownloadsTransactions.removeObject (pdt);
}
}
}
//==============================================================================
void processReceiptRefreshResponseWithSubscriptionsSharedSecret (const String& secret)
{
auto* receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
if (auto* receiptData = [NSData dataWithContentsOfURL: receiptURL])
fetchReceiptDetailsFromAppStore (receiptData, secret);
else
owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed"));
}
void fetchReceiptDetailsFromAppStore (NSData* receiptData, const String& secret)
{
auto* requestContents = [NSMutableDictionary dictionaryWithCapacity: (NSUInteger) (secret.isNotEmpty() ? 2 : 1)];
[requestContents setObject: [receiptData base64EncodedStringWithOptions:0] forKey: nsStringLiteral ("receipt-data")];
if (secret.isNotEmpty())
[requestContents setObject: juceStringToNS (secret) forKey: nsStringLiteral ("password")];
NSError* error;
auto* requestData = [NSJSONSerialization dataWithJSONObject: requestContents
options: 0
error: &error];
if (requestData == nil)
{
owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed"));
return;
}
#if JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT
auto storeURL = "https://sandbox.itunes.apple.com/verifyReceipt";
#else
auto storeURL = "https://buy.itunes.apple.com/verifyReceipt";
#endif
// TODO: use juce URL here
auto* storeRequest = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: nsStringLiteral (storeURL)]];
[storeRequest setHTTPMethod: nsStringLiteral ("POST")];
[storeRequest setHTTPBody: requestData];
auto* task = [[NSURLSession sharedSession] dataTaskWithRequest: storeRequest
completionHandler:
^(NSData* data, NSURLResponse*, NSError* connectionError)
{
if (connectionError != nil)
{
owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed"));
}
else
{
NSError* err;
if (NSDictionary* receiptDetails = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &err])
processReceiptDetails (receiptDetails);
else
owner.listeners.call (&Listener::purchasesListRestored, {}, false, NEEDS_TRANS ("Receipt fetch failed"));
}
}];
[task resume];
}
void processReceiptDetails (NSDictionary* receiptDetails)
{
if (auto receipt = getAs<NSDictionary> (receiptDetails[nsStringLiteral ("receipt")]))
{
if (auto bundleId = getAs<NSString> (receipt[nsStringLiteral ("bundle_id")]))
{
if (auto inAppPurchases = getAs<NSArray> (receipt[nsStringLiteral ("in_app")]))
{
Array<Listener::PurchaseInfo> purchases;
for (id inAppPurchaseData in inAppPurchases)
{
if (auto* purchaseData = getAs<NSDictionary> (inAppPurchaseData))
{
// Ignore products that were cancelled.
if (purchaseData[nsStringLiteral ("cancellation_date")] != nil)
continue;
if (auto transactionId = getAs<NSString> (purchaseData[nsStringLiteral ("original_transaction_id")]))
{
if (auto productId = getAs<NSString> (purchaseData[nsStringLiteral ("product_id")]))
{
if (auto purchaseTime = getAs<NSNumber> (purchaseData[nsStringLiteral ("purchase_date_ms")]))
{
purchases.add ({ { nsStringToJuce (transactionId),
nsStringToJuce (productId),
nsStringToJuce (bundleId),
Time ([purchaseTime integerValue]).toString (true, true, true, true),
{} }, {} });
}
else
{
return sendReceiptFetchFail();
}
}
}
}
else
{
return sendReceiptFetchFail();
}
}
MessageManager::callAsync ([this, purchases]() { owner.listeners.call (&Listener::purchasesListRestored,
purchases, true, NEEDS_TRANS ("Success")); });
return;
}
}
}
sendReceiptFetchFail();
}
void sendReceiptFetchFail()
{
MessageManager::callAsync ([this]() { owner.listeners.call (&Listener::purchasesListRestored,
{}, false, NEEDS_TRANS ("Receipt fetch failed")); });
}
//==============================================================================
static Product SKProductToIAPProduct (SKProduct* skProduct)
{
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle: NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale: skProduct.priceLocale];
auto identifier = nsStringToJuce (skProduct.productIdentifier);
auto title = nsStringToJuce (skProduct.localizedTitle);
auto description = nsStringToJuce (skProduct.localizedDescription);
auto priceLocale = nsStringToJuce (skProduct.priceLocale.languageCode);
auto price = nsStringToJuce ([numberFormatter stringFromNumber: skProduct.price]);
[numberFormatter release];
return { identifier, title, description, price, priceLocale };
}
static String SKPaymentTransactionStateToString (SKPaymentTransactionState state)
{
switch (state)
{
case SKPaymentTransactionStatePurchasing: return NEEDS_TRANS ("Purchasing");
case SKPaymentTransactionStatePurchased: return NEEDS_TRANS ("Success");
case SKPaymentTransactionStateFailed: return NEEDS_TRANS ("Failure");
case SKPaymentTransactionStateRestored: return NEEDS_TRANS ("Restored");
case SKPaymentTransactionStateDeferred: return NEEDS_TRANS ("Deferred");
default: jassertfalse; return NEEDS_TRANS ("Unknown status");
}
}
static Download::Status SKDownloadStateToDownloadStatus (SKDownloadState state)
{
switch (state)
{
case SKDownloadStateWaiting: return Download::Status::waiting;
case SKDownloadStateActive: return Download::Status::active;
case SKDownloadStatePaused: return Download::Status::paused;
case SKDownloadStateFinished: return Download::Status::finished;
case SKDownloadStateFailed: return Download::Status::failed;
case SKDownloadStateCancelled: return Download::Status::cancelled;
default: jassertfalse; return Download::Status::waiting;
}
}
static NSArray<SKDownload*>* downloadsToSKDownloads (const Array<Download*>& downloads)
{
NSMutableArray<SKDownload*>* skDownloads = [NSMutableArray arrayWithCapacity: (NSUInteger) downloads.size()];
for (const auto& d : downloads)
if (auto impl = dynamic_cast<DownloadImpl*>(d))
[skDownloads addObject: impl->download];
return skDownloads;
}
template <typename ObjCType>
static ObjCType* getAs (id o)
{
if (o == nil || ! [o isKindOfClass: [ObjCType class]])
return nil;
return (ObjCType*) o;
}
//==============================================================================
InAppPurchases& owner;
OwnedArray<PendingProductInfoRequest> pendingProductInfoRequests;
OwnedArray<PendingReceiptRefreshRequest> pendingReceiptRefreshRequests;
OwnedArray<PendingDownloadsTransaction> pendingDownloadsTransactions;
Array<Listener::PurchaseInfo> restoredPurchases;
};

Loading…
Cancel
Save