Browse Source

InAppPurchases: Add support for Android BillingClient 5.0.0

pull/22/head
reuk 3 years ago
parent
commit
fab6a072bc
No known key found for this signature in database GPG Key ID: 9ADCD339CFC98A11
7 changed files with 402 additions and 185 deletions
  1. +22
    -16
      examples/Utilities/InAppPurchasesDemo.h
  2. +1
    -1
      extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h
  3. +9
    -0
      modules/juce_gui_basics/native/javaopt/app/com/rmsl/juce/JuceActivity.java
  4. +19
    -7
      modules/juce_gui_basics/native/juce_android_Windowing.cpp
  5. +6
    -6
      modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h
  6. +54
    -43
      modules/juce_product_unlocking/native/javaopt/app/com/rmsl/juce/JuceBillingClient.java
  7. +291
    -112
      modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp

+ 22
- 16
examples/Utilities/InAppPurchasesDemo.h View File

@@ -192,20 +192,23 @@ private:
{
purchaseInProgress = false;
auto idx = findVoiceIndexFromIdentifier (info.purchase.productId);
if (isPositiveAndBelow (idx, voiceProducts.size()))
for (const auto productId : info.purchase.productIds)
{
auto& voiceProduct = voiceProducts.getReference (idx);
auto idx = findVoiceIndexFromIdentifier (productId);
voiceProduct.isPurchased = success;
voiceProduct.purchaseInProgress = false;
}
else
{
// On failure Play Store will not tell us which purchase failed
for (auto& voiceProduct : voiceProducts)
if (isPositiveAndBelow (idx, voiceProducts.size()))
{
auto& voiceProduct = voiceProducts.getReference (idx);
voiceProduct.isPurchased = success;
voiceProduct.purchaseInProgress = false;
}
else
{
// On failure Play Store will not tell us which purchase failed
for (auto& voiceProduct : voiceProducts)
voiceProduct.purchaseInProgress = false;
}
}
guiUpdater.triggerAsyncUpdate();
@@ -217,13 +220,16 @@ private:
{
for (auto& info : infos)
{
auto idx = findVoiceIndexFromIdentifier (info.purchase.productId);
if (isPositiveAndBelow (idx, voiceProducts.size()))
for (const auto productId : info.purchase.productIds)
{
auto& voiceProduct = voiceProducts.getReference (idx);
auto idx = findVoiceIndexFromIdentifier (productId);
if (isPositiveAndBelow (idx, voiceProducts.size()))
{
auto& voiceProduct = voiceProducts.getReference (idx);
voiceProduct.isPurchased = true;
voiceProduct.isPurchased = true;
}
}
}


+ 1
- 1
extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Android.h View File

@@ -867,7 +867,7 @@ private:
mo << " implementation files('libs/" << File (d).getFileName() << "')" << newLine;
if (isInAppBillingEnabled())
mo << " implementation 'com.android.billingclient:billing:2.1.0'" << newLine;
mo << " implementation 'com.android.billingclient:billing:5.0.0'" << newLine;
if (areRemoteNotificationsEnabled())
{


+ 9
- 0
modules/juce_gui_basics/native/javaopt/app/com/rmsl/juce/JuceActivity.java View File

@@ -33,6 +33,7 @@ public class JuceActivity extends Activity
{
//==============================================================================
private native void appNewIntent (Intent intent);
private native void appOnResume();
@Override
protected void onNewIntent (Intent intent)
@@ -42,4 +43,12 @@ public class JuceActivity extends Activity
appNewIntent (intent);
}
@Override
protected void onResume()
{
super.onResume();
appOnResume();
}
}

+ 19
- 7
modules/juce_gui_basics/native/juce_android_Windowing.cpp View File

@@ -126,12 +126,18 @@ static const uint8 javaComponentPeerView[] =
//==============================================================================
#if JUCE_PUSH_NOTIFICATIONS && JUCE_MODULE_AVAILABLE_juce_gui_extra
extern bool juce_handleNotificationIntent (void*);
extern void juce_firebaseDeviceNotificationsTokenRefreshed (void*);
extern void juce_firebaseRemoteNotificationReceived (void*);
extern void juce_firebaseRemoteMessagesDeleted();
extern void juce_firebaseRemoteMessageSent(void*);
extern void juce_firebaseRemoteMessageSendError (void*, void*);
bool juce_handleNotificationIntent (void*);
void juce_firebaseDeviceNotificationsTokenRefreshed (void*);
void juce_firebaseRemoteNotificationReceived (void*);
void juce_firebaseRemoteMessagesDeleted();
void juce_firebaseRemoteMessageSent(void*);
void juce_firebaseRemoteMessageSendError (void*, void*);
#endif
#if JUCE_IN_APP_PURCHASES && JUCE_MODULE_AVAILABLE_juce_product_unlocking
void juce_handleOnResume();
#else
static void juce_handleOnResume() {}
#endif
//==============================================================================
@@ -2091,7 +2097,8 @@ const int KeyPress::rewindKey = extendedKeyModifier + 72;
struct JuceActivityNewIntentListener
{
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
CALLBACK (appNewIntent, "appNewIntent", "(Landroid/content/Intent;)V")
CALLBACK (appNewIntent, "appNewIntent", "(Landroid/content/Intent;)V") \
CALLBACK (appOnResume, "appOnResume", "()V")
DECLARE_JNI_CLASS (JavaActivity, JUCE_PUSH_NOTIFICATIONS_ACTIVITY)
#undef JNI_CLASS_MEMBERS
@@ -2100,6 +2107,11 @@ const int KeyPress::rewindKey = extendedKeyModifier + 72;
{
juce_handleNotificationIntent (static_cast<void*> (intentData));
}
static void JNICALL appOnResume (JNIEnv*, jobject)
{
juce_handleOnResume();
}
};
JuceActivityNewIntentListener::JavaActivity_Class JuceActivityNewIntentListener::JavaActivity;


+ 6
- 6
modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h View File

@@ -71,8 +71,8 @@ public:
/** 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;
/** Unique identifiers of the products that were purchased. */
StringArray productIds;
/** This will be bundle ID on iOS and package name on Android, of the application for which this
in-app product was purchased. */
@@ -147,7 +147,7 @@ public:
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.
which products a user is entitled.
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!
@@ -210,7 +210,7 @@ public:
const String& upgradeOrDowngradeFromSubscriptionWithProductIdentifier = {},
bool creditForUnusedSubscription = true);
/** Asynchronously asks about a list of products that a user has already bought. Upon completion, Listener::purchasesListReceived()
/** Asynchronously asks about a list of products that a user has already bought. Upon completion, Listener::purchasesListRestored()
callback will be invoked. The user may be prompted to login first.
@param includeDownloadInfo (iOS only) if true, then after restoration is successful, the downloads array passed to
@@ -258,8 +258,8 @@ public:
//==============================================================================
#ifndef DOXYGEN
[[deprecated ("On Android, it is no longer necessary to specify whether the product being purchased is a subscription "
"and only a single subscription can be upgraded/downgraded. Use the updated purchaseProduct method "
"which takes a single String argument.")]]
"and only a single subscription can be upgraded/downgraded. Use the updated purchaseProduct method "
"which takes a single String argument.")]]
void purchaseProduct (const String& productIdentifier,
bool isSubscription,
const StringArray& upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers = {},


+ 54
- 43
modules/juce_product_unlocking/native/javaopt/app/com/rmsl/juce/JuceBillingClient.java View File

@@ -27,8 +27,9 @@ package com.rmsl.juce;
import com.android.billingclient.api.*;
public class JuceBillingClient implements PurchasesUpdatedListener, BillingClientStateListener {
private native void skuDetailsQueryCallback(long host, java.util.List<SkuDetails> skuDetails);
public class JuceBillingClient implements PurchasesUpdatedListener,
BillingClientStateListener {
private native void productDetailsQueryCallback(long host, java.util.List<ProductDetails> productDetails);
private native void purchasesListQueryCallback(long host, java.util.List<Purchase> purchases);
private native void purchaseCompletedCallback(long host, Purchase purchase, int responseCode);
private native void purchaseConsumedCallback(long host, String productIdentifier, int responseCode);
@@ -57,36 +58,39 @@ public class JuceBillingClient implements PurchasesUpdatedListener, BillingClien
== BillingClient.BillingResponseCode.OK;
}
public void querySkuDetails(final String[] skusToQuery) {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
final java.util.List<String> skuList = java.util.Arrays.asList(skusToQuery);
public QueryProductDetailsParams getProductListParams(final String[] productsToQuery, String type) {
java.util.ArrayList<QueryProductDetailsParams.Product> productList = new java.util.ArrayList<>();
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP);
for (String product : productsToQuery)
productList.add(QueryProductDetailsParams.Product.newBuilder().setProductId(product).setProductType(type).build());
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, final java.util.List<SkuDetails> inAppSkuDetails) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.SUBS);
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult, java.util.List<SkuDetails> subsSkuDetails) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
subsSkuDetails.addAll(inAppSkuDetails);
skuDetailsQueryCallback(host, subsSkuDetails);
}
}
});
}
return QueryProductDetailsParams.newBuilder().setProductList(productList).build();
}
public void queryProductDetailsImpl(final String[] productsToQuery, java.util.List<String> productTypes, java.util.List<ProductDetails> details) {
if (productTypes == null || productTypes.isEmpty()) {
productDetailsQueryCallback(host, details);
} else {
billingClient.queryProductDetailsAsync(getProductListParams(productsToQuery, productTypes.get(0)), new ProductDetailsResponseListener() {
@Override
public void onProductDetailsResponse(BillingResult billingResult, java.util.List<ProductDetails> newDetails) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
details.addAll(newDetails);
queryProductDetailsImpl(productsToQuery, productTypes.subList(1, productTypes.size()), details);
} else {
queryProductDetailsImpl(productsToQuery, null, details);
}
});
}
});
}
}
public void queryProductDetails(final String[] productsToQuery) {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
String[] toCheck = {BillingClient.ProductType.INAPP, BillingClient.ProductType.SUBS};
queryProductDetailsImpl(productsToQuery, java.util.Arrays.asList(toCheck), new java.util.ArrayList<ProductDetails>());
}
});
}
@@ -100,23 +104,30 @@ public class JuceBillingClient implements PurchasesUpdatedListener, BillingClien
});
}
private void queryPurchasesImpl(java.util.List<String> toCheck, java.util.ArrayList<Purchase> purchases) {
if (toCheck == null || toCheck.isEmpty()) {
purchasesListQueryCallback(host, purchases);
} else {
billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(toCheck.get(0)).build(), new PurchasesResponseListener() {
@Override
public void onQueryPurchasesResponse(BillingResult billingResult, java.util.List<Purchase> list) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
purchases.addAll(list);
queryPurchasesImpl(toCheck.subList(1, toCheck.size()), purchases);
} else {
queryPurchasesImpl(null, purchases);
}
}
});
}
}
public void queryPurchases() {
executeOnBillingClientConnection(new Runnable() {
@Override
public void run() {
Purchase.PurchasesResult inAppPurchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
Purchase.PurchasesResult subsPurchases = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
if (inAppPurchases.getResponseCode() == BillingClient.BillingResponseCode.OK
&& subsPurchases.getResponseCode() == BillingClient.BillingResponseCode.OK) {
java.util.List<Purchase> purchaseList = inAppPurchases.getPurchasesList();
purchaseList.addAll(subsPurchases.getPurchasesList());
purchasesListQueryCallback(host, purchaseList);
return;
}
purchasesListQueryCallback(host, null);
String[] toCheck = {BillingClient.ProductType.INAPP, BillingClient.ProductType.SUBS};
queryPurchasesImpl(java.util.Arrays.asList(toCheck), new java.util.ArrayList<Purchase>());
}
});
}
@@ -211,5 +222,5 @@ public class JuceBillingClient implements PurchasesUpdatedListener, BillingClien
}
private long host = 0;
private BillingClient billingClient;
private final BillingClient billingClient;
}

+ 291
- 112
modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp View File

@@ -27,13 +27,46 @@ namespace juce
{
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getSku, "getSku", "()Ljava/lang/String;") \
METHOD (getTitle, "getTitle", "()Ljava/lang/String;") \
METHOD (getDescription, "getDescription", "()Ljava/lang/String;") \
METHOD (getPrice, "getPrice", "()Ljava/lang/String;") \
METHOD (getPriceCurrencyCode, "getPriceCurrencyCode", "()Ljava/lang/String;")
METHOD (getProductId, "getProductId", "()Ljava/lang/String;") \
METHOD (getTitle, "getTitle", "()Ljava/lang/String;") \
METHOD (getDescription, "getDescription", "()Ljava/lang/String;") \
METHOD (getOneTimePurchaseOfferDetails, "getOneTimePurchaseOfferDetails", "()Lcom/android/billingclient/api/ProductDetails$OneTimePurchaseOfferDetails;") \
METHOD (getSubscriptionOfferDetails, "getSubscriptionOfferDetails", "()Ljava/util/List;")
DECLARE_JNI_CLASS (SkuDetails, "com/android/billingclient/api/SkuDetails")
DECLARE_JNI_CLASS (ProductDetails, "com/android/billingclient/api/ProductDetails")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getFormattedPrice, "getFormattedPrice", "()Ljava/lang/String;") \
METHOD (getPriceCurrencyCode, "getPriceCurrencyCode", "()Ljava/lang/String;")
DECLARE_JNI_CLASS (OneTimePurchaseOfferDetails, "com/android/billingclient/api/ProductDetails$OneTimePurchaseOfferDetails")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getFormattedPrice, "getFormattedPrice", "()Ljava/lang/String;") \
METHOD (getPriceCurrencyCode, "getPriceCurrencyCode", "()Ljava/lang/String;")
DECLARE_JNI_CLASS (PricingPhase, "com/android/billingclient/api/ProductDetails$PricingPhase")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getOfferToken, "getOfferToken", "()Ljava/lang/String;") \
METHOD (getPricingPhases, "getPricingPhases", "()Lcom/android/billingclient/api/ProductDetails$PricingPhases;")
DECLARE_JNI_CLASS (SubscriptionOfferDetails, "com/android/billingclient/api/ProductDetails$SubscriptionOfferDetails")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getPricingPhaseList, "getPricingPhaseList", "()Ljava/util/List;")
DECLARE_JNI_CLASS (PricingPhases, "com/android/billingclient/api/ProductDetails$PricingPhases")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
STATICMETHOD (newBuilder, "newBuilder", "()Lcom/android/billingclient/api/BillingFlowParams$ProductDetailsParams$Builder;")
DECLARE_JNI_CLASS (BillingFlowParamsProductDetailsParams, "com/android/billingclient/api/BillingFlowParams$ProductDetailsParams")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
@@ -43,33 +76,81 @@ DECLARE_JNI_CLASS (BillingFlowParams, "com/android/billingclient/api/BillingFlow
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (build, "build", "()Lcom/android/billingclient/api/BillingFlowParams;") \
METHOD (setOldSku, "setOldSku", "(Ljava/lang/String;Ljava/lang/String;)Lcom/android/billingclient/api/BillingFlowParams$Builder;") \
METHOD (setReplaceSkusProrationMode, "setReplaceSkusProrationMode", "(I)Lcom/android/billingclient/api/BillingFlowParams$Builder;") \
METHOD (setSkuDetails, "setSkuDetails", "(Lcom/android/billingclient/api/SkuDetails;)Lcom/android/billingclient/api/BillingFlowParams$Builder;")
STATICMETHOD (newBuilder, "newBuilder", "()Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder;")
DECLARE_JNI_CLASS (BillingFlowParamsSubscriptionUpdateParams, "com/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (build, "build", "()Lcom/android/billingclient/api/BillingFlowParams;") \
METHOD (setSubscriptionUpdateParams, "setSubscriptionUpdateParams", "(Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams;)Lcom/android/billingclient/api/BillingFlowParams$Builder;") \
METHOD (setProductDetailsParamsList, "setProductDetailsParamsList", "(Ljava/util/List;)Lcom/android/billingclient/api/BillingFlowParams$Builder;")
DECLARE_JNI_CLASS (BillingFlowParamsBuilder, "com/android/billingclient/api/BillingFlowParams$Builder")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (build, "build", "()Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams;") \
METHOD (setOldPurchaseToken, "setOldPurchaseToken", "(Ljava/lang/String;)Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder;") \
METHOD (setReplaceProrationMode, "setReplaceProrationMode", "(I)Lcom/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder;")
DECLARE_JNI_CLASS (BillingFlowParamsSubscriptionUpdateParamsBuilder, "com/android/billingclient/api/BillingFlowParams$SubscriptionUpdateParams$Builder")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (build, "build", "()Lcom/android/billingclient/api/BillingFlowParams$ProductDetailsParams;") \
METHOD (setOfferToken, "setOfferToken", "(Ljava/lang/String;)Lcom/android/billingclient/api/BillingFlowParams$ProductDetailsParams$Builder;") \
METHOD (setProductDetails, "setProductDetails", "(Lcom/android/billingclient/api/ProductDetails;)Lcom/android/billingclient/api/BillingFlowParams$ProductDetailsParams$Builder;")
DECLARE_JNI_CLASS (BillingFlowParamsProductDetailsParamsBuilder, "com/android/billingclient/api/BillingFlowParams$ProductDetailsParams$Builder")
#undef JNI_CLASS_MEMBERS
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (getOrderId, "getOrderId", "()Ljava/lang/String;") \
METHOD (getSku, "getSku", "()Ljava/lang/String;") \
METHOD (getPurchaseState, "getPurchaseState", "()I") \
METHOD (getProducts, "getProducts", "()Ljava/util/List;") \
METHOD (getPackageName, "getPackageName", "()Ljava/lang/String;") \
METHOD (getPurchaseTime, "getPurchaseTime", "()J") \
METHOD (getPurchaseTime, "getPurchaseTime", "()J") \
METHOD (getPurchaseToken, "getPurchaseToken", "()Ljava/lang/String;")
DECLARE_JNI_CLASS (AndroidPurchase, "com/android/billingclient/api/Purchase")
#undef JNI_CLASS_MEMBERS
template <typename Fn>
static void callOnMainThread (Fn&& fn)
{
if (MessageManager::getInstance()->isThisTheMessageThread())
fn();
else
MessageManager::callAsync (std::forward<Fn> (fn));
}
inline StringArray javaListOfStringToJuceStringArray (const LocalRef<jobject>& javaArray)
{
if (javaArray.get() == nullptr)
return {};
auto* env = getEnv();
StringArray result;
const auto size = env->CallIntMethod (javaArray, JavaList.size);
for (int i = 0; i < size; ++i)
result.add (juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (javaArray, JavaList.get, i) }.get()));
return result;
}
//==============================================================================
struct InAppPurchases::Pimpl
{
Pimpl (InAppPurchases& parent)
: owner (parent),
billingClient (LocalRef<jobject> (getEnv()->NewObject (JuceBillingClient,
JuceBillingClient.constructor,
getAppContext().get(),
(jlong) this)))
billingClient (LocalRef<jobject> { getEnv()->NewObject (JuceBillingClient,
JuceBillingClient.constructor,
getAppContext().get(),
(jlong) this) })
{
}
@@ -86,46 +167,52 @@ struct InAppPurchases::Pimpl
void getProductsInformation (const StringArray& productIdentifiers)
{
skuDetailsQueryCallbackQueue.emplace ([this] (LocalRef<jobject> skuDetailsList)
productDetailsQueryCallbackQueue.emplace ([this] (LocalRef<jobject> productDetailsList)
{
if (skuDetailsList != nullptr)
if (productDetailsList != nullptr)
{
auto* env = getEnv();
Array<InAppPurchases::Product> products;
for (int i = 0; i < env->CallIntMethod (skuDetailsList, JavaList.size); ++i)
products.add (buildProduct (LocalRef<jobject> (env->CallObjectMethod (skuDetailsList, JavaList.get, i))));
for (int i = 0; i < env->CallIntMethod (productDetailsList, JavaList.size); ++i)
products.add (buildProduct (LocalRef<jobject> { env->CallObjectMethod (productDetailsList, JavaList.get, i) }));
owner.listeners.call ([&] (Listener& l) { l.productsInfoReturned (products); });
callMemberOnMainThread ([this, products]
{
owner.listeners.call ([&] (Listener& l) { l.productsInfoReturned (products); });
});
}
});
querySkuDetailsAsync (convertToLowerCase (productIdentifiers));
queryProductDetailsAsync (convertToLowerCase (productIdentifiers));
}
void purchaseProduct (const String& productIdentifier,
const String& subscriptionIdentifier,
bool creditForUnusedSubscription)
{
skuDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> skuDetailsList)
productDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> productDetailsList)
{
if (skuDetailsList != nullptr)
if (productDetailsList != nullptr)
{
auto* env = getEnv();
if (env->CallIntMethod (skuDetailsList, JavaList.size) > 0)
if (env->CallIntMethod (productDetailsList, JavaList.size) > 0)
{
LocalRef<jobject> skuDetails (env->CallObjectMethod (skuDetailsList, JavaList.get, 0));
GlobalRef productDetails (LocalRef<jobject> { env->CallObjectMethod (productDetailsList, JavaList.get, 0) });
if (subscriptionIdentifier.isNotEmpty())
changeExistingSubscription (skuDetails, subscriptionIdentifier, creditForUnusedSubscription);
else
purchaseProductWithSkuDetails (skuDetails);
callMemberOnMainThread ([this, productDetails, subscriptionIdentifier, creditForUnusedSubscription]
{
if (subscriptionIdentifier.isNotEmpty())
changeExistingSubscription (productDetails, subscriptionIdentifier, creditForUnusedSubscription);
else
purchaseProductWithProductDetails (productDetails);
});
}
}
});
querySkuDetailsAsync (convertToLowerCase ({ productIdentifier }));
queryProductDetailsAsync (convertToLowerCase ({ productIdentifier }));
}
void restoreProductsBoughtList (bool, const juce::String&)
@@ -139,15 +226,21 @@ struct InAppPurchases::Pimpl
for (int i = 0; i < env->CallIntMethod (purchasesList, JavaArrayList.size); ++i)
{
LocalRef<jobject> purchase (env->CallObjectMethod (purchasesList, JavaArrayList.get, i));
const LocalRef<jobject> purchase { env->CallObjectMethod (purchasesList, JavaArrayList.get, i) };
purchases.add ({ buildPurchase (purchase), {} });
}
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored (purchases, true, NEEDS_TRANS ("Success")); });
callMemberOnMainThread ([this, purchases]
{
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored (purchases, true, NEEDS_TRANS ("Success")); });
});
}
else
{
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Failure")); });
callMemberOnMainThread ([this]
{
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Failure")); });
});
}
});
@@ -158,17 +251,17 @@ struct InAppPurchases::Pimpl
{
if (purchaseToken.isEmpty())
{
skuDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> skuDetailsList)
productDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> productDetailsList)
{
if (skuDetailsList != nullptr)
if (productDetailsList != nullptr)
{
auto* env = getEnv();
if (env->CallIntMethod (skuDetailsList, JavaList.size) > 0)
if (env->CallIntMethod (productDetailsList, JavaList.size) > 0)
{
LocalRef<jobject> sku (env->CallObjectMethod (skuDetailsList, JavaList.get, 0));
const LocalRef<jobject> product { env->CallObjectMethod (productDetailsList, JavaList.get, 0) };
auto token = juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (sku, AndroidPurchase.getSku)));
auto token = juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (product, ProductDetails.getProductId) });
if (token.isNotEmpty())
{
@@ -178,10 +271,13 @@ struct InAppPurchases::Pimpl
}
}
notifyListenersAboutConsume (productIdentifier, false, NEEDS_TRANS ("Item unavailable"));
callMemberOnMainThread ([this, productIdentifier]
{
notifyListenersAboutConsume (productIdentifier, false, NEEDS_TRANS ("Item unavailable"));
});
});
querySkuDetailsAsync (convertToLowerCase ({ productIdentifier }));
queryProductDetailsAsync (convertToLowerCase ({ productIdentifier }));
}
consumePurchaseWithToken (productIdentifier, purchaseToken);
@@ -218,27 +314,27 @@ struct InAppPurchases::Pimpl
private:
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
METHOD (constructor, "<init>", "(Landroid/content/Context;J)V") \
METHOD (endConnection, "endConnection", "()V") \
METHOD (isReady, "isReady", "()Z") \
METHOD (isBillingSupported, "isBillingSupported", "()Z") \
METHOD (querySkuDetails, "querySkuDetails", "([Ljava/lang/String;)V") \
METHOD (launchBillingFlow, "launchBillingFlow", "(Landroid/app/Activity;Lcom/android/billingclient/api/BillingFlowParams;)V") \
METHOD (queryPurchases, "queryPurchases", "()V") \
METHOD (consumePurchase, "consumePurchase", "(Ljava/lang/String;Ljava/lang/String;)V") \
\
CALLBACK (skuDetailsQueryCallback, "skuDetailsQueryCallback", "(JLjava/util/List;)V") \
CALLBACK (purchasesListQueryCallback, "purchasesListQueryCallback", "(JLjava/util/List;)V") \
CALLBACK (purchaseCompletedCallback, "purchaseCompletedCallback", "(JLcom/android/billingclient/api/Purchase;I)V") \
CALLBACK (purchaseConsumedCallback, "purchaseConsumedCallback", "(JLjava/lang/String;I)V")
METHOD (constructor, "<init>", "(Landroid/content/Context;J)V") \
METHOD (endConnection, "endConnection", "()V") \
METHOD (isReady, "isReady", "()Z") \
METHOD (isBillingSupported, "isBillingSupported", "()Z") \
METHOD (queryProductDetails, "queryProductDetails", "([Ljava/lang/String;)V") \
METHOD (launchBillingFlow, "launchBillingFlow", "(Landroid/app/Activity;Lcom/android/billingclient/api/BillingFlowParams;)V") \
METHOD (queryPurchases, "queryPurchases", "()V") \
METHOD (consumePurchase, "consumePurchase", "(Ljava/lang/String;Ljava/lang/String;)V") \
\
CALLBACK (productDetailsQueryCallback, "productDetailsQueryCallback", "(JLjava/util/List;)V") \
CALLBACK (purchasesListQueryCallback, "purchasesListQueryCallback", "(JLjava/util/List;)V") \
CALLBACK (purchaseCompletedCallback, "purchaseCompletedCallback", "(JLcom/android/billingclient/api/Purchase;I)V") \
CALLBACK (purchaseConsumedCallback, "purchaseConsumedCallback", "(JLjava/lang/String;I)V")
DECLARE_JNI_CLASS (JuceBillingClient, "com/rmsl/juce/JuceBillingClient")
#undef JNI_CLASS_MEMBERS
static void JNICALL skuDetailsQueryCallback (JNIEnv*, jobject, jlong host, jobject skuDetailsList)
static void JNICALL productDetailsQueryCallback (JNIEnv*, jobject, jlong host, jobject productDetailsList)
{
if (auto* myself = reinterpret_cast<Pimpl*> (host))
myself->updateSkuDetails (skuDetailsList);
myself->updateProductDetails (productDetailsList);
}
static void JNICALL purchasesListQueryCallback (JNIEnv*, jobject, jlong host, jobject purchasesList)
@@ -289,7 +385,7 @@ private:
return lowerCase;
}
void querySkuDetailsAsync (const StringArray& productIdentifiers)
void queryProductDetailsAsync (const StringArray& productIdentifiers)
{
Thread::launch ([=]
{
@@ -299,7 +395,7 @@ private:
MessageManager::callAsync ([=]
{
getEnv()->CallVoidMethod (billingClient,
JuceBillingClient.querySkuDetails,
JuceBillingClient.queryProductDetails,
juceStringArrayToJava (productIdentifiers).get());
});
});
@@ -331,23 +427,15 @@ private:
owner.listeners.call ([&] (Listener& l) { l.productConsumed (productIdentifier, success, statusDescription); });
}
LocalRef<jobject> createBillingFlowParamsBuilder (LocalRef<jobject> skuDetails)
{
auto* env = getEnv();
auto builder = LocalRef<jobject> (env->CallStaticObjectMethod (BillingFlowParams, BillingFlowParams.newBuilder));
return LocalRef<jobject> (env->CallObjectMethod (builder.get(),
BillingFlowParamsBuilder.setSkuDetails,
skuDetails.get()));
}
void launchBillingFlowWithParameters (LocalRef<jobject> params)
{
LocalRef<jobject> activity (getCurrentActivity());
const auto activity = []
{
if (auto current = getCurrentActivity())
return current;
if (activity == nullptr)
activity = getMainActivity();
return getMainActivity();
}();
getEnv()->CallVoidMethod (billingClient,
JuceBillingClient.launchBillingFlow,
@@ -355,7 +443,7 @@ private:
params.get());
}
void changeExistingSubscription (LocalRef<jobject> skuDetails, const String& subscriptionIdentifier, bool creditForUnusedSubscription)
void changeExistingSubscription (GlobalRef productDetails, const String& subscriptionIdentifier, bool creditForUnusedSubscription)
{
if (! isReady())
{
@@ -371,35 +459,47 @@ private:
for (int i = 0; i < env->CallIntMethod (purchasesList, JavaArrayList.size); ++i)
{
auto purchase = buildPurchase (LocalRef<jobject> (env->CallObjectMethod (purchasesList.get(), JavaArrayList.get, i)));
auto purchase = buildPurchase (LocalRef<jobject> { env->CallObjectMethod (purchasesList.get(), JavaArrayList.get, i) });
if (purchase.productId == subscriptionIdentifier)
if (purchase.productIds.contains (subscriptionIdentifier))
{
auto builder = createBillingFlowParamsBuilder (skuDetails);
builder = LocalRef<jobject> (env->CallObjectMethod (builder.get(),
BillingFlowParamsBuilder.setOldSku,
javaString (subscriptionIdentifier).get(),
javaString (purchase.purchaseToken).get()));
const LocalRef<jobject> subscriptionBuilder { getEnv()->CallStaticObjectMethod (BillingFlowParamsSubscriptionUpdateParams,
BillingFlowParamsSubscriptionUpdateParams.newBuilder) };
env->CallObjectMethod (subscriptionBuilder.get(),
BillingFlowParamsSubscriptionUpdateParamsBuilder.setOldPurchaseToken,
javaString (purchase.purchaseToken).get());
if (! creditForUnusedSubscription)
builder = LocalRef<jobject> (env->CallObjectMethod (builder.get(),
BillingFlowParamsBuilder.setReplaceSkusProrationMode,
3 /*IMMEDIATE_WITHOUT_PRORATION*/));
{
env->CallObjectMethod (subscriptionBuilder.get(),
BillingFlowParamsSubscriptionUpdateParamsBuilder.setReplaceProrationMode,
3 /*IMMEDIATE_WITHOUT_PRORATION*/);
}
const LocalRef<jobject> subscriptionParams { env->CallObjectMethod (subscriptionBuilder.get(),
BillingFlowParamsSubscriptionUpdateParamsBuilder.build) };
launchBillingFlowWithParameters (LocalRef<jobject> (env->CallObjectMethod (builder.get(),
BillingFlowParamsBuilder.build)));
const LocalRef<jobject> builder { env->CallStaticObjectMethod (BillingFlowParams, BillingFlowParams.newBuilder) };
env->CallObjectMethod (builder.get(),
BillingFlowParamsBuilder.setSubscriptionUpdateParams,
subscriptionParams.get());
const LocalRef<jobject> params { env->CallObjectMethod (builder.get(), BillingFlowParamsBuilder.build) };
launchBillingFlowWithParameters (params);
}
}
}
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("Unable to get subscription details"));
callMemberOnMainThread ([this]
{
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("Unable to get subscription details"));
});
});
getProductsBoughtAsync();
}
void purchaseProductWithSkuDetails (LocalRef<jobject> skuDetails)
void purchaseProductWithProductDetails (GlobalRef productDetails)
{
if (! isReady())
{
@@ -407,22 +507,48 @@ private:
return;
}
launchBillingFlowWithParameters (LocalRef<jobject> (getEnv()->CallObjectMethod (createBillingFlowParamsBuilder (skuDetails).get(),
BillingFlowParamsBuilder.build)));
auto* env = getEnv();
const LocalRef<jobject> billingFlowParamsProductDetailsParamsBuilder { env->CallStaticObjectMethod (BillingFlowParamsProductDetailsParams, BillingFlowParamsProductDetailsParams.newBuilder) };
env->CallObjectMethod (billingFlowParamsProductDetailsParamsBuilder, BillingFlowParamsProductDetailsParamsBuilder.setProductDetails, productDetails.get());
if (const LocalRef<jobject> subscriptionDetailsList { env->CallObjectMethod (productDetails, ProductDetails.getSubscriptionOfferDetails) })
{
if (env->CallIntMethod (subscriptionDetailsList, JavaList.size) > 0)
{
const LocalRef<jobject> subscriptionDetails { env->CallObjectMethod (subscriptionDetailsList, JavaList.get, 0) };
const LocalRef<jobject> offerToken { env->CallObjectMethod (subscriptionDetails, SubscriptionOfferDetails.getOfferToken) };
env->CallObjectMethod (billingFlowParamsProductDetailsParamsBuilder, BillingFlowParamsProductDetailsParamsBuilder.setOfferToken, offerToken.get());
}
}
const LocalRef<jobject> billingFlowParamsProductDetailsParams { env->CallObjectMethod (billingFlowParamsProductDetailsParamsBuilder, BillingFlowParamsProductDetailsParamsBuilder.build) };
const LocalRef<jobject> list { env->NewObject (JavaArrayList, JavaArrayList.constructor, 0) };
env->CallBooleanMethod (list, JavaArrayList.add, billingFlowParamsProductDetailsParams.get());
const LocalRef<jobject> billingFlowParamsBuilder { env->CallStaticObjectMethod (BillingFlowParams, BillingFlowParams.newBuilder) };
env->CallObjectMethod (billingFlowParamsBuilder, BillingFlowParamsBuilder.setProductDetailsParamsList, list.get());
const LocalRef<jobject> params { env->CallObjectMethod (billingFlowParamsBuilder, BillingFlowParamsBuilder.build) };
launchBillingFlowWithParameters (params);
}
void consumePurchaseWithToken (const String& productIdentifier, const String& purchaseToken)
{
if (! isReady())
{
notifyListenersAboutConsume (productIdentifier, false, NEEDS_TRANS ("In-App purchases unavailable"));
callMemberOnMainThread ([this, productIdentifier]
{
notifyListenersAboutConsume (productIdentifier, false, NEEDS_TRANS ("In-App purchases unavailable"));
});
return;
}
getEnv()->CallObjectMethod (billingClient,
JuceBillingClient.consumePurchase,
LocalRef<jstring> (javaString (productIdentifier)).get(),
LocalRef<jstring> (javaString (purchaseToken)).get());
LocalRef<jstring> { javaString (productIdentifier) }.get(),
LocalRef<jstring> { javaString (purchaseToken) }.get());
}
//==============================================================================
@@ -433,25 +559,59 @@ private:
auto* env = getEnv();
return { juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getOrderId))),
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getSku))),
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getPackageName))),
if (env->CallIntMethod(purchase, AndroidPurchase.getPurchaseState) != 1 /* PURCHASED */)
return {};
return { juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (purchase, AndroidPurchase.getOrderId) }),
javaListOfStringToJuceStringArray (LocalRef<jobject> { env->CallObjectMethod (purchase, AndroidPurchase.getProducts) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (purchase, AndroidPurchase.getPackageName) }),
Time (env->CallLongMethod (purchase, AndroidPurchase.getPurchaseTime)).toString (true, true, true, true),
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getPurchaseToken))) };
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (purchase, AndroidPurchase.getPurchaseToken) }) };
}
static InAppPurchases::Product buildProduct (LocalRef<jobject> productSkuDetails)
static InAppPurchases::Product buildProduct (LocalRef<jobject> productDetails)
{
if (productSkuDetails == nullptr)
if (productDetails == nullptr)
return {};
auto* env = getEnv();
return { juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getSku))),
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getTitle))),
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getDescription))),
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getPrice))),
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getPriceCurrencyCode))) };
if (LocalRef<jobject> oneTimePurchase { env->CallObjectMethod (productDetails, ProductDetails.getOneTimePurchaseOfferDetails) })
{
return { juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (productDetails, ProductDetails.getProductId) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (productDetails, ProductDetails.getTitle) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (productDetails, ProductDetails.getDescription) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (oneTimePurchase, OneTimePurchaseOfferDetails.getFormattedPrice) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (oneTimePurchase, OneTimePurchaseOfferDetails.getPriceCurrencyCode) }) };
}
LocalRef<jobject> subscription { env->CallObjectMethod (productDetails, ProductDetails.getSubscriptionOfferDetails) };
if (env->CallIntMethod (subscription, JavaList.size) == 0)
return {};
// We can only return a single subscription price for this subscription,
// but the subscription has more than one pricing scheme.
jassert (env->CallIntMethod (subscription, JavaList.size) == 1);
const LocalRef<jobject> offerDetails { env->CallObjectMethod (subscription, JavaList.get, 0) };
const LocalRef<jobject> pricingPhases { env->CallObjectMethod (offerDetails, SubscriptionOfferDetails.getPricingPhases) };
const LocalRef<jobject> phaseList { env->CallObjectMethod (pricingPhases, PricingPhases.getPricingPhaseList) };
if (env->CallIntMethod (phaseList, JavaList.size) == 0)
return {};
// We can only return a single subscription price for this subscription,
// but the pricing scheme for this subscription has more than one phase.
jassert (env->CallIntMethod (phaseList, JavaList.size) == 1);
const LocalRef<jobject> phase { env->CallObjectMethod (phaseList, JavaList.get, 0) };
return { juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (productDetails, ProductDetails.getProductId) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (productDetails, ProductDetails.getTitle) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (productDetails, ProductDetails.getDescription) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (phase, PricingPhase.getFormattedPrice) }),
juceString (LocalRef<jstring> { (jstring) env->CallObjectMethod (phase, PricingPhase.getPriceCurrencyCode) }) };
}
static String getStatusDescriptionFromResponseCode (int responseCode)
@@ -478,29 +638,29 @@ private:
void purchaseCompleted (jobject purchase, int responseCode)
{
notifyListenersAboutPurchase (buildPurchase (LocalRef<jobject> (purchase)),
notifyListenersAboutPurchase (buildPurchase (LocalRef<jobject> { purchase }),
wasSuccessful (responseCode),
getStatusDescriptionFromResponseCode (responseCode));
}
void purchaseConsumed (jstring productIdentifier, int responseCode)
{
notifyListenersAboutConsume (juceString (LocalRef<jstring> (productIdentifier)),
notifyListenersAboutConsume (juceString (LocalRef<jstring> { productIdentifier }),
wasSuccessful (responseCode),
getStatusDescriptionFromResponseCode (responseCode));
}
void updateSkuDetails (jobject skuDetailsList)
void updateProductDetails (jobject productDetailsList)
{
jassert (! skuDetailsQueryCallbackQueue.empty());
skuDetailsQueryCallbackQueue.front() (LocalRef<jobject> (skuDetailsList));
skuDetailsQueryCallbackQueue.pop();
jassert (! productDetailsQueryCallbackQueue.empty());
productDetailsQueryCallbackQueue.front() (LocalRef<jobject> { productDetailsList });
productDetailsQueryCallbackQueue.pop();
}
void updatePurchasesList (jobject purchasesList)
{
jassert (! purchasesListQueryCallbackQueue.empty());
purchasesListQueryCallbackQueue.front() (LocalRef<jobject> (purchasesList));
purchasesListQueryCallbackQueue.front() (LocalRef<jobject> { purchasesList });
purchasesListQueryCallbackQueue.pop();
}
@@ -508,13 +668,32 @@ private:
InAppPurchases& owner;
GlobalRef billingClient;
std::queue<std::function<void (LocalRef<jobject>)>> skuDetailsQueryCallbackQueue,
std::queue<std::function<void (LocalRef<jobject>)>> productDetailsQueryCallbackQueue,
purchasesListQueryCallbackQueue;
//==============================================================================
void callMemberOnMainThread (std::function<void()> callback)
{
callOnMainThread ([ref = WeakReference<Pimpl> (this), callback]
{
if (ref != nullptr)
callback();
});
}
//==============================================================================
JUCE_DECLARE_WEAK_REFERENCEABLE(Pimpl)
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
};
void juce_handleOnResume()
{
callOnMainThread ([]
{
InAppPurchases::getInstance()->restoreProductsBoughtList (false);
});
}
InAppPurchases::Pimpl::JuceBillingClient_Class InAppPurchases::Pimpl::JuceBillingClient;


Loading…
Cancel
Save