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:
essej
2022-04-18 17:51:22 -04:00
parent 63e175fee6
commit 25bd5d8adb
3210 changed files with 1045392 additions and 0 deletions

View 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;
}

View 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

View 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