fix macOS build (following Projucer changes made in Windows, which removed /Applications/JUCE/modules from its headers). move JUCE headers under source control, so that Windows and macOS can both build against same version of JUCE. remove AUv3 target (I think it's an iOS thing, so it will never work with this macOS fluidsynth dylib).

This commit is contained in:
Alex Birch
2018-06-17 13:34:53 +01:00
parent a2be47c887
commit dff4d13a1d
1563 changed files with 601601 additions and 3466 deletions

View File

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

View File

@ -0,0 +1,746 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
27th April 2017).
End User License Agreement: www.juce.com/juce-5-licence
Privacy Policy: www.juce.com/juce-5-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
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) {}
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.contentLength longLongValue]; }
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 { [[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, bool, const StringArray&, 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* storeRequest = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: nsStringLiteral (storeURL)]];
[storeRequest setHTTPMethod: nsStringLiteral ("POST")];
[storeRequest setHTTPBody: requestData];
auto* task = [[NSURLSession sharedSession] dataTaskWithRequest: storeRequest
completionHandler:
^(NSData* data, NSURLResponse*, NSError* connectionError)
{
if (connectionError != nil)
{
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