git subrepo clone --branch=sono6good https://github.com/essej/JUCE.git deps/juce
subrepo: subdir: "deps/juce" merged: "b13f9084e" upstream: origin: "https://github.com/essej/JUCE.git" branch: "sono6good" commit: "b13f9084e" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo.git" commit: "2f68596"
This commit is contained in:
215
deps/juce/modules/juce_product_unlocking/native/javaopt/app/com/rmsl/juce/JuceBillingClient.java
vendored
Normal file
215
deps/juce/modules/juce_product_unlocking/native/javaopt/app/com/rmsl/juce/JuceBillingClient.java
vendored
Normal file
@ -0,0 +1,215 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
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 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
public JuceBillingClient(android.content.Context context, long hostToUse) {
|
||||
host = hostToUse;
|
||||
|
||||
billingClient = BillingClient.newBuilder(context)
|
||||
.enablePendingPurchases()
|
||||
.setListener(this)
|
||||
.build();
|
||||
|
||||
billingClient.startConnection(this);
|
||||
}
|
||||
|
||||
public void endConnection() {
|
||||
billingClient.endConnection();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return billingClient.isReady();
|
||||
}
|
||||
|
||||
public boolean isBillingSupported() {
|
||||
return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).getResponseCode()
|
||||
== 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);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void launchBillingFlow(final android.app.Activity activity, final BillingFlowParams params) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BillingResult r = billingClient.launchBillingFlow(activity, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void consumePurchase(final String productIdentifier, final String purchaseToken) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
||||
.setPurchaseToken(purchaseToken)
|
||||
.build();
|
||||
|
||||
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
|
||||
@Override
|
||||
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||
purchaseConsumedCallback(host, productIdentifier, billingResult.getResponseCode());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(BillingResult result, java.util.List<Purchase> purchases) {
|
||||
int responseCode = result.getResponseCode();
|
||||
|
||||
if (purchases != null) {
|
||||
for (Purchase purchase : purchases) {
|
||||
handlePurchase(purchase, responseCode);
|
||||
}
|
||||
} else {
|
||||
purchaseCompletedCallback(host, null, responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResult)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void executeOnBillingClientConnection(Runnable runnable) {
|
||||
if (billingClient.isReady()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
connectAndExecute(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectAndExecute(final Runnable executeOnSuccess) {
|
||||
billingClient.startConnection(new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResponse) {
|
||||
if (billingResponse.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
if (executeOnSuccess != null) {
|
||||
executeOnSuccess.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handlePurchase(final Purchase purchase, final int responseCode) {
|
||||
purchaseCompletedCallback(host, purchase, responseCode);
|
||||
|
||||
if (responseCode == BillingClient.BillingResponseCode.OK
|
||||
&& purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED
|
||||
&& !purchase.isAcknowledged()) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
|
||||
billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private long host = 0;
|
||||
private BillingClient billingClient;
|
||||
}
|
521
deps/juce/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp
vendored
Normal file
521
deps/juce/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp
vendored
Normal file
@ -0,0 +1,521 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
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 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
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;")
|
||||
|
||||
DECLARE_JNI_CLASS (SkuDetails, "com/android/billingclient/api/SkuDetails")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
STATICMETHOD (newBuilder, "newBuilder", "()Lcom/android/billingclient/api/BillingFlowParams$Builder;")
|
||||
|
||||
DECLARE_JNI_CLASS (BillingFlowParams, "com/android/billingclient/api/BillingFlowParams")
|
||||
#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;")
|
||||
|
||||
DECLARE_JNI_CLASS (BillingFlowParamsBuilder, "com/android/billingclient/api/BillingFlowParams$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 (getPackageName, "getPackageName", "()Ljava/lang/String;") \
|
||||
METHOD (getPurchaseTime, "getPurchaseTime", "()J") \
|
||||
METHOD (getPurchaseToken, "getPurchaseToken", "()Ljava/lang/String;")
|
||||
|
||||
DECLARE_JNI_CLASS (AndroidPurchase, "com/android/billingclient/api/Purchase")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
//==============================================================================
|
||||
struct InAppPurchases::Pimpl
|
||||
{
|
||||
Pimpl (InAppPurchases& parent)
|
||||
: owner (parent),
|
||||
billingClient (LocalRef<jobject> (getEnv()->NewObject (JuceBillingClient,
|
||||
JuceBillingClient.constructor,
|
||||
getAppContext().get(),
|
||||
(jlong) this)))
|
||||
{
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
getEnv()->CallVoidMethod (billingClient, JuceBillingClient.endConnection);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isInAppPurchasesSupported() const
|
||||
{
|
||||
return isReady() && getEnv()->CallBooleanMethod (billingClient, JuceBillingClient.isBillingSupported);
|
||||
}
|
||||
|
||||
void getProductsInformation (const StringArray& productIdentifiers)
|
||||
{
|
||||
skuDetailsQueryCallbackQueue.emplace ([this] (LocalRef<jobject> skuDetailsList)
|
||||
{
|
||||
if (skuDetailsList != 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))));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.productsInfoReturned (products); });
|
||||
}
|
||||
});
|
||||
|
||||
querySkuDetailsAsync (convertToLowerCase (productIdentifiers));
|
||||
}
|
||||
|
||||
void purchaseProduct (const String& productIdentifier,
|
||||
const String& subscriptionIdentifier,
|
||||
bool creditForUnusedSubscription)
|
||||
{
|
||||
skuDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> skuDetailsList)
|
||||
{
|
||||
if (skuDetailsList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
if (env->CallIntMethod (skuDetailsList, JavaList.size) > 0)
|
||||
{
|
||||
LocalRef<jobject> skuDetails (env->CallObjectMethod (skuDetailsList, JavaList.get, 0));
|
||||
|
||||
if (subscriptionIdentifier.isNotEmpty())
|
||||
changeExistingSubscription (skuDetails, subscriptionIdentifier, creditForUnusedSubscription);
|
||||
else
|
||||
purchaseProductWithSkuDetails (skuDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
querySkuDetailsAsync (convertToLowerCase ({ productIdentifier }));
|
||||
}
|
||||
|
||||
void restoreProductsBoughtList (bool, const juce::String&)
|
||||
{
|
||||
purchasesListQueryCallbackQueue.emplace ([this] (LocalRef<jobject> purchasesList)
|
||||
{
|
||||
if (purchasesList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
Array<InAppPurchases::Listener::PurchaseInfo> purchases;
|
||||
|
||||
for (int i = 0; i < env->CallIntMethod (purchasesList, JavaArrayList.size); ++i)
|
||||
{
|
||||
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")); });
|
||||
}
|
||||
else
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Failure")); });
|
||||
}
|
||||
});
|
||||
|
||||
getProductsBoughtAsync();
|
||||
}
|
||||
|
||||
void consumePurchase (const String& productIdentifier, const String& purchaseToken)
|
||||
{
|
||||
if (purchaseToken.isEmpty())
|
||||
{
|
||||
skuDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> skuDetailsList)
|
||||
{
|
||||
if (skuDetailsList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
if (env->CallIntMethod (skuDetailsList, JavaList.size) > 0)
|
||||
{
|
||||
LocalRef<jobject> sku (env->CallObjectMethod (skuDetailsList, JavaList.get, 0));
|
||||
|
||||
auto token = juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (sku, AndroidPurchase.getSku)));
|
||||
|
||||
if (token.isNotEmpty())
|
||||
{
|
||||
consumePurchaseWithToken (productIdentifier, token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyListenersAboutConsume (productIdentifier, false, NEEDS_TRANS ("Item unavailable"));
|
||||
});
|
||||
|
||||
querySkuDetailsAsync (convertToLowerCase ({ productIdentifier }));
|
||||
}
|
||||
|
||||
consumePurchaseWithToken (productIdentifier, purchaseToken);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
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;
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
DECLARE_JNI_CLASS (JuceBillingClient, "com/rmsl/juce/JuceBillingClient")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
static void JNICALL skuDetailsQueryCallback (JNIEnv*, jobject, jlong host, jobject skuDetailsList)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->updateSkuDetails (skuDetailsList);
|
||||
}
|
||||
|
||||
static void JNICALL purchasesListQueryCallback (JNIEnv*, jobject, jlong host, jobject purchasesList)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->updatePurchasesList (purchasesList);
|
||||
}
|
||||
|
||||
static void JNICALL purchaseCompletedCallback (JNIEnv*, jobject, jlong host, jobject purchase, int responseCode)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->purchaseCompleted (purchase, responseCode);
|
||||
}
|
||||
|
||||
static void JNICALL purchaseConsumedCallback (JNIEnv*, jobject, jlong host, jstring productIdentifier, int responseCode)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->purchaseConsumed (productIdentifier, responseCode);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isReady() const
|
||||
{
|
||||
return getEnv()->CallBooleanMethod (billingClient, JuceBillingClient.isReady);
|
||||
}
|
||||
|
||||
bool checkIsReady() const
|
||||
{
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
if (isReady())
|
||||
return true;
|
||||
|
||||
Thread::sleep (500);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static StringArray convertToLowerCase (const StringArray& stringsToConvert)
|
||||
{
|
||||
StringArray lowerCase;
|
||||
|
||||
for (auto& s : stringsToConvert)
|
||||
lowerCase.add (s.toLowerCase());
|
||||
|
||||
return lowerCase;
|
||||
}
|
||||
|
||||
void querySkuDetailsAsync (const StringArray& productIdentifiers)
|
||||
{
|
||||
Thread::launch ([=]
|
||||
{
|
||||
if (! checkIsReady())
|
||||
return;
|
||||
|
||||
MessageManager::callAsync ([=]
|
||||
{
|
||||
getEnv()->CallVoidMethod (billingClient,
|
||||
JuceBillingClient.querySkuDetails,
|
||||
juceStringArrayToJava (productIdentifiers).get());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void getProductsBoughtAsync()
|
||||
{
|
||||
Thread::launch ([=]
|
||||
{
|
||||
if (! checkIsReady())
|
||||
return;
|
||||
|
||||
MessageManager::callAsync ([=]
|
||||
{
|
||||
getEnv()->CallVoidMethod (billingClient,
|
||||
JuceBillingClient.queryPurchases);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void notifyListenersAboutPurchase (const InAppPurchases::Purchase& purchase, bool success, const String& statusDescription)
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.productPurchaseFinished ({ purchase, {} }, success, statusDescription); });
|
||||
}
|
||||
|
||||
void notifyListenersAboutConsume (const String& productIdentifier, bool success, const String& statusDescription)
|
||||
{
|
||||
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());
|
||||
|
||||
if (activity == nullptr)
|
||||
activity = getMainActivity();
|
||||
|
||||
getEnv()->CallVoidMethod (billingClient,
|
||||
JuceBillingClient.launchBillingFlow,
|
||||
activity.get(),
|
||||
params.get());
|
||||
}
|
||||
|
||||
void changeExistingSubscription (LocalRef<jobject> skuDetails, const String& subscriptionIdentifier, bool creditForUnusedSubscription)
|
||||
{
|
||||
if (! isReady())
|
||||
{
|
||||
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("In-App purchases unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
purchasesListQueryCallbackQueue.emplace ([=] (LocalRef<jobject> purchasesList)
|
||||
{
|
||||
if (purchasesList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
for (int i = 0; i < env->CallIntMethod (purchasesList, JavaArrayList.size); ++i)
|
||||
{
|
||||
auto purchase = buildPurchase (LocalRef<jobject> (env->CallObjectMethod (purchasesList.get(), JavaArrayList.get, i)));
|
||||
|
||||
if (purchase.productId == subscriptionIdentifier)
|
||||
{
|
||||
auto builder = createBillingFlowParamsBuilder (skuDetails);
|
||||
|
||||
builder = LocalRef<jobject> (env->CallObjectMethod (builder.get(),
|
||||
BillingFlowParamsBuilder.setOldSku,
|
||||
javaString (subscriptionIdentifier).get(),
|
||||
javaString (purchase.purchaseToken).get()));
|
||||
|
||||
if (! creditForUnusedSubscription)
|
||||
builder = LocalRef<jobject> (env->CallObjectMethod (builder.get(),
|
||||
BillingFlowParamsBuilder.setReplaceSkusProrationMode,
|
||||
3 /*IMMEDIATE_WITHOUT_PRORATION*/));
|
||||
|
||||
launchBillingFlowWithParameters (LocalRef<jobject> (env->CallObjectMethod (builder.get(),
|
||||
BillingFlowParamsBuilder.build)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("Unable to get subscription details"));
|
||||
});
|
||||
|
||||
getProductsBoughtAsync();
|
||||
}
|
||||
|
||||
void purchaseProductWithSkuDetails (LocalRef<jobject> skuDetails)
|
||||
{
|
||||
if (! isReady())
|
||||
{
|
||||
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("In-App purchases unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
launchBillingFlowWithParameters (LocalRef<jobject> (getEnv()->CallObjectMethod (createBillingFlowParamsBuilder (skuDetails).get(),
|
||||
BillingFlowParamsBuilder.build)));
|
||||
}
|
||||
|
||||
void consumePurchaseWithToken (const String& productIdentifier, const String& purchaseToken)
|
||||
{
|
||||
if (! isReady())
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static InAppPurchases::Purchase buildPurchase (LocalRef<jobject> purchase)
|
||||
{
|
||||
if (purchase == nullptr)
|
||||
return {};
|
||||
|
||||
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))),
|
||||
Time (env->CallLongMethod (purchase, AndroidPurchase.getPurchaseTime)).toString (true, true, true, true),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getPurchaseToken))) };
|
||||
}
|
||||
|
||||
static InAppPurchases::Product buildProduct (LocalRef<jobject> productSkuDetails)
|
||||
{
|
||||
if (productSkuDetails == 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))) };
|
||||
}
|
||||
|
||||
static String getStatusDescriptionFromResponseCode (int responseCode)
|
||||
{
|
||||
switch (responseCode)
|
||||
{
|
||||
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: return NEEDS_TRANS ("Unknown status");
|
||||
}
|
||||
}
|
||||
|
||||
static bool wasSuccessful (int responseCode)
|
||||
{
|
||||
return responseCode == 0;
|
||||
}
|
||||
|
||||
void purchaseCompleted (jobject purchase, int responseCode)
|
||||
{
|
||||
notifyListenersAboutPurchase (buildPurchase (LocalRef<jobject> (purchase)),
|
||||
wasSuccessful (responseCode),
|
||||
getStatusDescriptionFromResponseCode (responseCode));
|
||||
}
|
||||
|
||||
void purchaseConsumed (jstring productIdentifier, int responseCode)
|
||||
{
|
||||
notifyListenersAboutConsume (juceString (LocalRef<jstring> (productIdentifier)),
|
||||
wasSuccessful (responseCode),
|
||||
getStatusDescriptionFromResponseCode (responseCode));
|
||||
}
|
||||
|
||||
void updateSkuDetails (jobject skuDetailsList)
|
||||
{
|
||||
jassert (! skuDetailsQueryCallbackQueue.empty());
|
||||
skuDetailsQueryCallbackQueue.front() (LocalRef<jobject> (skuDetailsList));
|
||||
skuDetailsQueryCallbackQueue.pop();
|
||||
}
|
||||
|
||||
void updatePurchasesList (jobject purchasesList)
|
||||
{
|
||||
jassert (! purchasesListQueryCallbackQueue.empty());
|
||||
purchasesListQueryCallbackQueue.front() (LocalRef<jobject> (purchasesList));
|
||||
purchasesListQueryCallbackQueue.pop();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
InAppPurchases& owner;
|
||||
GlobalRef billingClient;
|
||||
|
||||
std::queue<std::function<void (LocalRef<jobject>)>> skuDetailsQueryCallbackQueue,
|
||||
purchasesListQueryCallbackQueue;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
|
||||
InAppPurchases::Pimpl::JuceBillingClient_Class InAppPurchases::Pimpl::JuceBillingClient;
|
||||
|
||||
} // namespace juce
|
755
deps/juce/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp
vendored
Normal file
755
deps/juce/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp
vendored
Normal file
@ -0,0 +1,755 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
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 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
struct SKDelegateAndPaymentObserver
|
||||
{
|
||||
SKDelegateAndPaymentObserver() : delegate ([getClass().createInstance() init])
|
||||
{
|
||||
Class::setThis (delegate.get(), this);
|
||||
}
|
||||
|
||||
virtual ~SKDelegateAndPaymentObserver() {}
|
||||
|
||||
virtual void didReceiveResponse (SKProductsRequest*, SKProductsResponse*) = 0;
|
||||
virtual void requestDidFinish (SKRequest*) = 0;
|
||||
virtual void requestDidFailWithError (SKRequest*, NSError*) = 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:
|
||||
std::unique_ptr<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>, NSObjectDeleter> 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 (request:didFailWithError:), requestDidFailWithError, "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 requestDidFailWithError (id self, SEL, SKRequest* request, NSError* err) { getThis (self).requestDidFailWithError (request, err); }
|
||||
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)
|
||||
{
|
||||
[download retain];
|
||||
}
|
||||
|
||||
~DownloadImpl() override
|
||||
{
|
||||
[download release];
|
||||
}
|
||||
|
||||
String getProductId() const override { return nsStringToJuce (download.contentIdentifier); }
|
||||
String getContentVersion() const override { return nsStringToJuce (download.contentVersion); }
|
||||
|
||||
#if JUCE_IOS
|
||||
int64 getContentLength() const override { return download.contentLength; }
|
||||
Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.downloadState); }
|
||||
#else
|
||||
int64 getContentLength() const override { return download.expectedContentLength; }
|
||||
Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.state); }
|
||||
#endif
|
||||
|
||||
SKDownload* download;
|
||||
};
|
||||
|
||||
/** Represents a pending request initialised with [SKProductRequest start]. */
|
||||
struct PendingProductInfoRequest
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
query = 0,
|
||||
purchase
|
||||
};
|
||||
|
||||
Type type;
|
||||
std::unique_ptr<SKProductsRequest, NSObjectDeleter> request;
|
||||
};
|
||||
|
||||
/** Represents a pending request started from [SKReceiptRefreshRequest start]. */
|
||||
struct PendingReceiptRefreshRequest
|
||||
{
|
||||
String subscriptionsSharedSecret;
|
||||
std::unique_ptr<SKReceiptRefreshRequest, NSObjectDeleter> 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 JUCE_IOS
|
||||
SKDownloadState state = d.downloadState;
|
||||
#else
|
||||
SKDownloadState state = d.state;
|
||||
#endif
|
||||
if (state != SKDownloadStateFinished
|
||||
&& state != SKDownloadStateFailed
|
||||
&& state != SKDownloadStateCancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
OwnedArray<DownloadImpl> downloads;
|
||||
SKPaymentTransaction* const transaction;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
Pimpl (InAppPurchases& p) : owner (p) { [[SKPaymentQueue defaultQueue] addTransactionObserver: delegate.get()]; }
|
||||
~Pimpl() noexcept override { [[SKPaymentQueue defaultQueue] removeTransactionObserver: delegate.get()]; }
|
||||
|
||||
//==============================================================================
|
||||
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,
|
||||
std::unique_ptr<SKProductsRequest, NSObjectDeleter> (productsRequest) });
|
||||
|
||||
productsRequest.delegate = delegate.get();
|
||||
[productsRequest start];
|
||||
}
|
||||
|
||||
void purchaseProduct (const String& productIdentifier, const String&, bool)
|
||||
{
|
||||
if (! [SKPaymentQueue canMakePayments])
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.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,
|
||||
std::unique_ptr<SKProductsRequest, NSObjectDeleter> (productsRequest) });
|
||||
|
||||
productsRequest.delegate = delegate.get();
|
||||
[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,
|
||||
std::unique_ptr<SKReceiptRefreshRequest, NSObjectDeleter> ([receiptRequest retain]) });
|
||||
receiptRequest.delegate = delegate.get();
|
||||
[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.get() == 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.get() == receiptRefreshRequest)
|
||||
{
|
||||
processReceiptRefreshResponseWithSubscriptionsSharedSecret (pendingRequest.subscriptionsSharedSecret);
|
||||
pendingReceiptRefreshRequests.remove (i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void requestDidFailWithError (SKRequest* request, NSError* error) override
|
||||
{
|
||||
if (auto receiptRefreshRequest = getAs<SKReceiptRefreshRequest> (request))
|
||||
{
|
||||
for (auto i = 0; i < pendingReceiptRefreshRequests.size(); ++i)
|
||||
{
|
||||
auto& pendingRequest = *pendingReceiptRefreshRequests[i];
|
||||
|
||||
if (pendingRequest.request.get() == receiptRefreshRequest)
|
||||
{
|
||||
auto errorDetails = error != nil ? (", " + nsStringToJuce ([error localizedDescription])) : String();
|
||||
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed") + errorDetails); });
|
||||
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& l) { l.purchasesListRestored ({}, false, nsStringToJuce (error.localizedDescription)); });
|
||||
}
|
||||
|
||||
void restoreCompletedTransactionsFinished (SKPaymentQueue*) override
|
||||
{
|
||||
owner.listeners.call ([this] (Listener& l) { l.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))
|
||||
{
|
||||
#if JUCE_IOS
|
||||
switch (download.downloadState)
|
||||
#else
|
||||
switch (download.state)
|
||||
#endif
|
||||
{
|
||||
case SKDownloadStateWaiting: break;
|
||||
case SKDownloadStatePaused: owner.listeners.call ([&] (Listener& l) { l.productDownloadPaused (*pendingDownload); }); break;
|
||||
case SKDownloadStateActive: owner.listeners.call ([&] (Listener& l) { l.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& l) { l.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& l) { l.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& l) { l.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))
|
||||
{
|
||||
#if JUCE_IOS
|
||||
SKDownloadState state = download.downloadState;
|
||||
#else
|
||||
SKDownloadState state = download.state;
|
||||
#endif
|
||||
|
||||
auto contentURL = state == SKDownloadStateFinished
|
||||
? URL (nsStringToJuce (download.contentURL.absoluteString))
|
||||
: URL();
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.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& l) { l.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)
|
||||
{
|
||||
sendReceiptFetchFail();
|
||||
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* urlPtr = [NSURL URLWithString: nsStringLiteral (storeURL)];
|
||||
auto storeRequest = [NSMutableURLRequest requestWithURL: urlPtr];
|
||||
[storeRequest setHTTPMethod: nsStringLiteral ("POST")];
|
||||
[storeRequest setHTTPBody: requestData];
|
||||
|
||||
auto task = [[NSURLSession sharedSession] dataTaskWithRequest: storeRequest
|
||||
completionHandler:
|
||||
^(NSData* data, NSURLResponse*, NSError* connectionError)
|
||||
{
|
||||
if (connectionError != nil)
|
||||
{
|
||||
sendReceiptFetchFail();
|
||||
}
|
||||
else
|
||||
{
|
||||
NSError* err;
|
||||
|
||||
if (NSDictionary* receiptDetails = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &err])
|
||||
processReceiptDetails (receiptDetails);
|
||||
else
|
||||
sendReceiptFetchFail();
|
||||
}
|
||||
}];
|
||||
|
||||
[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")]))
|
||||
{
|
||||
auto purchaseTime = getPurchaseDateMs (purchaseData[nsStringLiteral ("purchase_date_ms")]);
|
||||
|
||||
if (purchaseTime > 0)
|
||||
{
|
||||
purchases.add ({ { nsStringToJuce (transactionId),
|
||||
nsStringToJuce (productId),
|
||||
nsStringToJuce (bundleId),
|
||||
Time (purchaseTime).toString (true, true, true, true),
|
||||
{} }, {} });
|
||||
}
|
||||
else
|
||||
{
|
||||
return sendReceiptFetchFailAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return sendReceiptFetchFailAsync();
|
||||
}
|
||||
}
|
||||
|
||||
MessageManager::callAsync ([this, purchases] { owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored (purchases, true, NEEDS_TRANS ("Success")); }); });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendReceiptFetchFailAsync();
|
||||
}
|
||||
|
||||
void sendReceiptFetchFail()
|
||||
{
|
||||
owner.listeners.call ([] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed")); });
|
||||
}
|
||||
|
||||
void sendReceiptFetchFailAsync()
|
||||
{
|
||||
MessageManager::callAsync ([this] { sendReceiptFetchFail(); });
|
||||
}
|
||||
|
||||
static int64 getPurchaseDateMs (id date)
|
||||
{
|
||||
if (auto dateAsNumber = getAs<NSNumber> (date))
|
||||
{
|
||||
return [dateAsNumber longLongValue];
|
||||
}
|
||||
else if (auto dateAsString = getAs<NSString> (date))
|
||||
{
|
||||
auto formatter = [[NSNumberFormatter alloc] init];
|
||||
[formatter setNumberStyle: NSNumberFormatterDecimalStyle];
|
||||
dateAsNumber = [formatter numberFromString: dateAsString];
|
||||
[formatter release];
|
||||
return [dateAsNumber longLongValue];
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
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 objectForKey: NSLocaleLanguageCode]);
|
||||
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;
|
||||
};
|
||||
|
||||
} // namespace juce
|
Reference in New Issue
Block a user