1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

InAppPurchases: Add support for Android BillingClient 5.0.0

This commit is contained in:
reuk 2022-02-15 20:09:34 +00:00
parent 7c66dc8e15
commit 6375f640db
No known key found for this signature in database
GPG key ID: 9ADCD339CFC98A11
7 changed files with 402 additions and 185 deletions

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) {
public QueryProductDetailsParams getProductListParams(final String[] productsToQuery, String type) {
java.util.ArrayList<QueryProductDetailsParams.Product> productList = new java.util.ArrayList<>();
for (String product : productsToQuery)
productList.add(QueryProductDetailsParams.Product.newBuilder().setProductId(product).setProductType(type).build());
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() {
final java.util.List<String> skuList = java.util.Arrays.asList(skusToQuery);
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
.setSkusList(skuList)
.setType(BillingClient.SkuType.INAPP);
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);
}
}
});
}
}
});
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;
}

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*/);
}
launchBillingFlowWithParameters (LocalRef<jobject> (env->CallObjectMethod (builder.get(),
BillingFlowParamsBuilder.build)));
const LocalRef<jobject> subscriptionParams { env->CallObjectMethod (subscriptionBuilder.get(),
BillingFlowParamsSubscriptionUpdateParamsBuilder.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;