build(deps): vendor JUCE 7.0.12
This commit is contained in:
+124
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
JUCE_IMPLEMENT_SINGLETON (InAppPurchases)
|
||||
|
||||
InAppPurchases::InAppPurchases()
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
: pimpl (new Pimpl (*this))
|
||||
#endif
|
||||
{}
|
||||
|
||||
InAppPurchases::~InAppPurchases() { clearSingletonInstance(); }
|
||||
|
||||
bool InAppPurchases::isInAppPurchasesSupported() const
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
return pimpl->isInAppPurchasesSupported();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::getProductsInformation (const StringArray& productIdentifiers)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->getProductsInformation (productIdentifiers);
|
||||
#else
|
||||
Array<Product> products;
|
||||
for (auto productId : productIdentifiers)
|
||||
products.add (Product { productId, {}, {}, {}, {} });
|
||||
|
||||
listeners.call ([&] (Listener& l) { l.productsInfoReturned (products); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::purchaseProduct (const String& productIdentifier,
|
||||
[[maybe_unused]] const String& upgradeProductIdentifier,
|
||||
[[maybe_unused]] bool creditForUnusedSubscription)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->purchaseProduct (productIdentifier, upgradeProductIdentifier, creditForUnusedSubscription);
|
||||
#else
|
||||
Listener::PurchaseInfo purchaseInfo { Purchase { "", productIdentifier, {}, {}, {} }, {} };
|
||||
|
||||
listeners.call ([&] (Listener& l) { l.productPurchaseFinished (purchaseInfo, false, "In-app purchases unavailable"); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::restoreProductsBoughtList ([[maybe_unused]] bool includeDownloadInfo, [[maybe_unused]] const String& subscriptionsSharedSecret)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->restoreProductsBoughtList (includeDownloadInfo, subscriptionsSharedSecret);
|
||||
#else
|
||||
listeners.call ([] (Listener& l) { l.purchasesListRestored ({}, false, "In-app purchases unavailable"); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::consumePurchase (const String& productIdentifier, [[maybe_unused]] const String& purchaseToken)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->consumePurchase (productIdentifier, purchaseToken);
|
||||
#else
|
||||
listeners.call ([&] (Listener& l) { l.productConsumed (productIdentifier, false, "In-app purchases unavailable"); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::addListener (Listener* l) { listeners.add (l); }
|
||||
void InAppPurchases::removeListener (Listener* l) { listeners.remove (l); }
|
||||
|
||||
void InAppPurchases::startDownloads ([[maybe_unused]] const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->startDownloads (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::pauseDownloads ([[maybe_unused]] const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->pauseDownloads (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::resumeDownloads ([[maybe_unused]] const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->resumeDownloads (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::cancelDownloads ([[maybe_unused]] const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->cancelDownloads (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
+296
@@ -0,0 +1,296 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
/**
|
||||
Provides in-app purchase functionality.
|
||||
|
||||
Your app should create a single instance of this class, and on iOS it should
|
||||
be created as soon as your app starts. This is because on application startup
|
||||
any previously pending transactions will be resumed.
|
||||
|
||||
Once an InAppPurchases object is created, call addListener() to attach listeners.
|
||||
|
||||
@tags{ProductUnlocking}
|
||||
*/
|
||||
class JUCE_API InAppPurchases : private DeletedAtShutdown
|
||||
{
|
||||
public:
|
||||
#ifndef DOXYGEN
|
||||
JUCE_DECLARE_SINGLETON (InAppPurchases, false)
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
/** Represents a product available in the store. */
|
||||
struct Product
|
||||
{
|
||||
/** Product ID (also known as SKU) that uniquely identifies a product in the store. */
|
||||
String identifier;
|
||||
|
||||
/** Title of the product. */
|
||||
String title;
|
||||
|
||||
/** Description of the product. */
|
||||
String description;
|
||||
|
||||
/** Price of the product in local currency. */
|
||||
String price;
|
||||
|
||||
/** Price locale. */
|
||||
String priceLocale;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Represents a purchase of a product in the store. */
|
||||
struct Purchase
|
||||
{
|
||||
/** A unique order identifier for the transaction (generated by the store). */
|
||||
String orderId;
|
||||
|
||||
/** Unique identifiers of the products that were purchased. */
|
||||
StringArray productIds;
|
||||
|
||||
/** This will be bundle ID on iOS and package name on Android, of the application for which this
|
||||
in-app product was purchased. */
|
||||
String applicationBundleName;
|
||||
|
||||
/** Date of the purchase (in ISO8601 format). */
|
||||
String purchaseTime;
|
||||
|
||||
/** Android only: purchase token that should be used to consume purchase, provided that In-App product
|
||||
is consumable. */
|
||||
String purchaseToken;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** iOS only: represents in-app purchase download. Download will be available only
|
||||
for purchases that are hosted on the AppStore. */
|
||||
struct Download
|
||||
{
|
||||
enum class Status
|
||||
{
|
||||
waiting = 0, /**< The download is waiting to start. Called at the beginning of a download operation. */
|
||||
active, /**< The download is in progress. */
|
||||
paused, /**< The download was paused and is awaiting resuming or cancelling. */
|
||||
finished, /**< The download was finished successfully. */
|
||||
failed, /**< The download failed (e.g. because of no internet connection). */
|
||||
cancelled, /**< The download was cancelled. */
|
||||
};
|
||||
|
||||
virtual ~Download() {}
|
||||
|
||||
/** A unique identifier for the in-app product to be downloaded. */
|
||||
virtual String getProductId() const = 0;
|
||||
|
||||
/** Content length in bytes. */
|
||||
virtual int64 getContentLength() const = 0;
|
||||
|
||||
/** Content version. */
|
||||
virtual String getContentVersion() const = 0;
|
||||
|
||||
/** Returns current status of the download. */
|
||||
virtual Status getStatus() const = 0;
|
||||
};
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/** Represents an object that gets notified about events such as product info returned or product purchase
|
||||
finished. */
|
||||
struct Listener
|
||||
{
|
||||
virtual ~Listener() {}
|
||||
|
||||
/** Called whenever a product info is returned after a call to InAppPurchases::getProductsInformation(). */
|
||||
virtual void productsInfoReturned (const Array<Product>& /*products*/) {}
|
||||
|
||||
/** Structure holding purchase information */
|
||||
struct PurchaseInfo
|
||||
{
|
||||
Purchase purchase;
|
||||
Array<Download*> downloads;
|
||||
};
|
||||
|
||||
/** Called whenever a purchase is complete, with additional state whether the purchase completed successfully.
|
||||
|
||||
For hosted content (iOS only), the downloads array within PurchaseInfo will contain all download objects corresponding
|
||||
with the purchase. For non-hosted content, the downloads array will be empty.
|
||||
|
||||
InAppPurchases class will own downloads and will delete them as soon as they are finished.
|
||||
|
||||
NOTE: It is possible to receive this callback for the same purchase multiple times. If that happens,
|
||||
only the newest set of downloads and the newest orderId will be valid, the old ones should be not used anymore!
|
||||
*/
|
||||
virtual void productPurchaseFinished (const PurchaseInfo&, bool /*success*/, const String& /*statusDescription*/) {}
|
||||
|
||||
/** Called when a list of all purchases is restored. This can be used to figure out to
|
||||
which products a user is entitled.
|
||||
|
||||
NOTE: It is possible to receive this callback for the same purchase multiple times. If that happens,
|
||||
only the newest set of downloads and the newest orderId will be valid, the old ones should be not used anymore!
|
||||
*/
|
||||
virtual void purchasesListRestored (const Array<PurchaseInfo>&, bool /*success*/, const String& /*statusDescription*/) {}
|
||||
|
||||
/** Called whenever a product consumption finishes. */
|
||||
virtual void productConsumed (const String& /*productId*/, bool /*success*/, const String& /*statusDescription*/) {}
|
||||
|
||||
/** iOS only: Called when a product download progress gets updated. If the download was interrupted in the last
|
||||
application session, this callback may be called after the application starts.
|
||||
|
||||
If the download was in progress and the application was closed, the download may happily continue in the
|
||||
background by OS. If you open the app and the download is still in progress, you will receive this callback.
|
||||
If the download finishes in the background before you start the app again, you will receive productDownloadFinished
|
||||
callback instead. The download will only stop when it is explicitly cancelled or when it is finished.
|
||||
*/
|
||||
virtual void productDownloadProgressUpdate (Download&, float /*progress*/, RelativeTime /*timeRemaining*/) {}
|
||||
|
||||
/** iOS only: Called when a product download is paused. This may also be called after the application starts, if
|
||||
the download was in a paused state and the application was closed before finishing the download.
|
||||
|
||||
Only after the download is finished successfully or cancelled you will stop receiving this callback on startup.
|
||||
*/
|
||||
virtual void productDownloadPaused (Download&) {}
|
||||
|
||||
/** iOS only: Called when a product download finishes (successfully or not). Call Download::getStatus()
|
||||
to check if the downloaded finished successfully.
|
||||
|
||||
It is your responsibility to move the download content into your app directory and to clean up
|
||||
any files that are no longer needed.
|
||||
|
||||
After the download is finished, the download object is destroyed and should not be accessed anymore.
|
||||
*/
|
||||
virtual void productDownloadFinished (Download&, const URL& /*downloadedContentPath*/) {}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
/** Checks whether in-app purchases is supported on current platform. On iOS this always returns true. */
|
||||
bool isInAppPurchasesSupported() const;
|
||||
|
||||
/** Asynchronously requests information for products with given ids. Upon completion, for each enquired product
|
||||
there is going to be a corresponding Product object.
|
||||
If there is no information available for the given product identifier, it will be ignored.
|
||||
*/
|
||||
void getProductsInformation (const StringArray& productIdentifiers);
|
||||
|
||||
/** Asynchronously requests to buy a product with given id.
|
||||
|
||||
@param productIdentifier The product identifier.
|
||||
|
||||
@param upgradeOrDowngradeFromSubscriptionWithProductIdentifier (Android only) specifies the subscription that will be replaced by
|
||||
the one being purchased now. Used only when buying a subscription
|
||||
that is an upgrade or downgrade from another.
|
||||
|
||||
@param creditForUnusedSubscription (Android only) controls whether a user should be credited for any unused subscription time on
|
||||
the product that is being upgraded or downgraded.
|
||||
*/
|
||||
void purchaseProduct (const String& productIdentifier,
|
||||
const String& upgradeOrDowngradeFromSubscriptionWithProductIdentifier = {},
|
||||
bool creditForUnusedSubscription = true);
|
||||
|
||||
/** Asynchronously asks about a list of products that a user has already bought. Upon completion, Listener::purchasesListRestored()
|
||||
callback will be invoked. The user may be prompted to login first.
|
||||
|
||||
@param includeDownloadInfo (iOS only) if true, then after restoration is successful, the downloads array passed to
|
||||
Listener::purchasesListReceived() callback will contain all the download objects corresponding with
|
||||
the purchase. In the opposite case, the downloads array will be empty.
|
||||
|
||||
@param subscriptionsSharedSecret (iOS only) required when not including download information and when there are
|
||||
auto-renewable subscription set up with this app. Refer to In-App-Purchase settings in the store.
|
||||
*/
|
||||
void restoreProductsBoughtList (bool includeDownloadInfo, const juce::String& subscriptionsSharedSecret = {});
|
||||
|
||||
/** Android only: asynchronously sends a request to mark a purchase with given identifier as consumed.
|
||||
To consume a product, provide product identifier as well as a purchase token that was generated when
|
||||
the product was purchased. The purchase token can also be retrieved by using getProductsInformation().
|
||||
In general if it is available on hand, it is better to use it, because otherwise another async
|
||||
request will be sent to the store, to first retrieve the token.
|
||||
|
||||
After successful consumption, a product will no longer be returned in getProductsBought() and
|
||||
it will be available for purchase.
|
||||
|
||||
On iOS consumption happens automatically. If the product was set as consumable, this function is a no-op.
|
||||
*/
|
||||
void consumePurchase (const String& productIdentifier, const String& purchaseToken = {});
|
||||
|
||||
//==============================================================================
|
||||
/** Adds a listener. */
|
||||
void addListener (Listener*);
|
||||
|
||||
/** Removes a listener. */
|
||||
void removeListener (Listener*);
|
||||
|
||||
//==============================================================================
|
||||
/** iOS only: Starts downloads of hosted content from the store. */
|
||||
void startDownloads (const Array<Download*>& downloads);
|
||||
|
||||
/** iOS only: Pauses downloads of hosted content from the store. */
|
||||
void pauseDownloads (const Array<Download*>& downloads);
|
||||
|
||||
/** iOS only: Resumes downloads of hosted content from the store. */
|
||||
void resumeDownloads (const Array<Download*>& downloads);
|
||||
|
||||
/** iOS only: Cancels downloads of hosted content from the store. */
|
||||
void cancelDownloads (const Array<Download*>& downloads);
|
||||
|
||||
//==============================================================================
|
||||
#ifndef DOXYGEN
|
||||
[[deprecated ("On Android, it is no longer necessary to specify whether the product being purchased is a subscription "
|
||||
"and only a single subscription can be upgraded/downgraded. Use the updated purchaseProduct method "
|
||||
"which takes a single String argument.")]]
|
||||
void purchaseProduct (const String& productIdentifier,
|
||||
[[maybe_unused]] bool isSubscription,
|
||||
const StringArray& upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers = {},
|
||||
bool creditForUnusedSubscription = true)
|
||||
{
|
||||
purchaseProduct (productIdentifier,
|
||||
upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers[0],
|
||||
creditForUnusedSubscription);
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
#ifndef DOXYGEN
|
||||
InAppPurchases();
|
||||
~InAppPurchases();
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
#if JUCE_ANDROID
|
||||
friend void juce_inAppPurchaseCompleted (void*);
|
||||
#endif
|
||||
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
struct Pimpl;
|
||||
friend struct Pimpl;
|
||||
|
||||
std::unique_ptr<Pimpl> pimpl;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#ifdef JUCE_PRODUCT_UNLOCKING_H_INCLUDED
|
||||
/* When you add this cpp file to your project, you mustn't include it in a file where you've
|
||||
already included any other headers - just put it inside a file on its own, possibly with your config
|
||||
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
|
||||
header files that the compiler may be using.
|
||||
*/
|
||||
#error "Incorrect use of JUCE cpp file"
|
||||
#endif
|
||||
|
||||
#define JUCE_CORE_INCLUDE_JNI_HELPERS 1
|
||||
#define JUCE_CORE_INCLUDE_OBJC_HELPERS 1
|
||||
#define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1
|
||||
|
||||
// Set this flag to 1 to use test servers on iOS
|
||||
#ifndef JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT
|
||||
#define JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT 0
|
||||
#endif
|
||||
|
||||
#include "juce_product_unlocking.h"
|
||||
|
||||
#if JUCE_IOS || JUCE_MAC
|
||||
#import <StoreKit/StoreKit.h>
|
||||
#endif
|
||||
|
||||
#if JUCE_IN_APP_PURCHASES
|
||||
#if JUCE_ANDROID
|
||||
#include "native/juce_InAppPurchases_android.cpp"
|
||||
#elif JUCE_IOS || JUCE_MAC
|
||||
#include "native/juce_InAppPurchases_ios.cpp"
|
||||
#endif
|
||||
|
||||
#include "in_app_purchases/juce_InAppPurchases.cpp"
|
||||
#endif
|
||||
|
||||
#include "marketplace/juce_OnlineUnlockStatus.cpp"
|
||||
|
||||
#if JUCE_MODULE_AVAILABLE_juce_data_structures
|
||||
#include "marketplace/juce_TracktionMarketplaceStatus.cpp"
|
||||
#endif
|
||||
|
||||
#if JUCE_MODULE_AVAILABLE_juce_gui_extra
|
||||
#include "marketplace/juce_OnlineUnlockForm.cpp"
|
||||
#endif
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this module, and is read by
|
||||
the Projucer to automatically generate project code that uses it.
|
||||
For details about the syntax and how to create or use a module, see the
|
||||
JUCE Module Format.md file.
|
||||
|
||||
|
||||
BEGIN_JUCE_MODULE_DECLARATION
|
||||
|
||||
ID: juce_product_unlocking
|
||||
vendor: juce
|
||||
version: 7.0.12
|
||||
name: JUCE Online marketplace support
|
||||
description: Classes for online product authentication
|
||||
website: http://www.juce.com/juce
|
||||
license: GPL/Commercial
|
||||
minimumCppStandard: 17
|
||||
|
||||
dependencies: juce_cryptography
|
||||
|
||||
END_JUCE_MODULE_DECLARATION
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
#pragma once
|
||||
#define JUCE_PRODUCT_UNLOCKING_H_INCLUDED
|
||||
|
||||
/**
|
||||
The juce_product_unlocking module provides simple user-registration classes
|
||||
for allowing you to build apps/plugins with features that are unlocked by a
|
||||
user having a suitable account on a webserver.
|
||||
|
||||
Although originally designed for use with products that are sold on the
|
||||
Tracktion Marketplace web-store, the module itself is fully open, and can
|
||||
be used to connect to your own web-store instead, if you implement your
|
||||
own compatible web-server back-end.
|
||||
|
||||
In additional, the module supports in-app purchases both on iOS and Android
|
||||
platforms.
|
||||
*/
|
||||
|
||||
//==============================================================================
|
||||
#include <juce_core/juce_core.h>
|
||||
#include <juce_cryptography/juce_cryptography.h>
|
||||
#include <juce_events/juce_events.h>
|
||||
|
||||
#if JUCE_MODULE_AVAILABLE_juce_data_structures
|
||||
#include <juce_data_structures/juce_data_structures.h>
|
||||
#endif
|
||||
|
||||
#if JUCE_MODULE_AVAILABLE_juce_gui_extra
|
||||
#include <juce_gui_extra/juce_gui_extra.h>
|
||||
#endif
|
||||
|
||||
#if JUCE_IN_APP_PURCHASES
|
||||
#include "in_app_purchases/juce_InAppPurchases.h"
|
||||
#endif
|
||||
|
||||
#if JUCE_MODULE_AVAILABLE_juce_data_structures
|
||||
#include "marketplace/juce_OnlineUnlockStatus.h"
|
||||
#include "marketplace/juce_TracktionMarketplaceStatus.h"
|
||||
#endif
|
||||
|
||||
#include "marketplace/juce_KeyFileGeneration.h"
|
||||
|
||||
#if JUCE_MODULE_AVAILABLE_juce_gui_extra
|
||||
#include "marketplace/juce_OnlineUnlockForm.h"
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "juce_product_unlocking.cpp"
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
/**
|
||||
Contains static utilities for generating key-files that can be unlocked by
|
||||
the OnlineUnlockStatus class.
|
||||
|
||||
@tags{ProductUnlocking}
|
||||
*/
|
||||
class JUCE_API KeyGeneration
|
||||
{
|
||||
public:
|
||||
/**
|
||||
Generates the content of a key-file which can be sent to a user's machine to
|
||||
unlock a product.
|
||||
|
||||
The returned value is a block of text containing an RSA-encoded block, followed
|
||||
by some human-readable details. If you pass this block of text to
|
||||
OnlineUnlockStatus::applyKeyFile(), it will decrypt it, and if the
|
||||
key matches and the machine numbers match, it will unlock that machine.
|
||||
|
||||
Typically the way you'd use this on a server would be to build a small executable
|
||||
that simply calls this method and prints the result, so that the webserver can
|
||||
use this as a reply to the product's auto-registration mechanism. The
|
||||
keyGenerationAppMain() function is an example of how to build such a function.
|
||||
|
||||
@see OnlineUnlockStatus
|
||||
*/
|
||||
static String JUCE_CALLTYPE generateKeyFile (const String& appName,
|
||||
const String& userEmail,
|
||||
const String& userName,
|
||||
const String& machineNumbers,
|
||||
const RSAKey& privateKey);
|
||||
|
||||
/** Similar to the above key file generation method but with an expiry time.
|
||||
You must supply a Time after which this key file should no longer be considered as active.
|
||||
|
||||
N.B. when an app is unlocked with an expiring key file, OnlineUnlockStatus::isUnlocked will
|
||||
still return false. You must then check OnlineUnlockStatus::getExpiryTime to see if this
|
||||
expiring key file is still in date and act accordingly.
|
||||
|
||||
@see OnlineUnlockStatus
|
||||
*/
|
||||
static String JUCE_CALLTYPE generateExpiringKeyFile (const String& appName,
|
||||
const String& userEmail,
|
||||
const String& userName,
|
||||
const String& machineNumbers,
|
||||
const Time expiryTime,
|
||||
const RSAKey& privateKey);
|
||||
|
||||
//==============================================================================
|
||||
/** This is a simple implementation of a key-generator that you could easily wrap in
|
||||
a command-line main() function for use on your server.
|
||||
|
||||
So for example you might use this in a command line app called "unlocker" and
|
||||
then call it like this:
|
||||
|
||||
unlocker MyGreatApp joebloggs@foobar.com Joe_Bloggs 1234abcd,95432ff 22d9aec92d986dd1,923ad49e9e7ff294c
|
||||
*/
|
||||
static int keyGenerationAppMain (int argc, char* argv[])
|
||||
{
|
||||
StringArray args;
|
||||
for (int i = 1; i < argc; ++i)
|
||||
args.add (argv[i]);
|
||||
|
||||
if (args.size() != 5)
|
||||
{
|
||||
std::cout << "Requires 5 arguments: app-name user-email username machine-numbers private-key" << std::endl
|
||||
<< " app-name: name of the product being unlocked" << std::endl
|
||||
<< " user-email: user's email address" << std::endl
|
||||
<< " username: name of the user. Careful not to allow any spaces!" << std::endl
|
||||
<< " machine-numbers: a comma- or semicolon-separated list of all machine ID strings this user can run this product on (no whitespace between items!)" << std::endl
|
||||
<< " private-key: the RSA private key corresponding to the public key you've used in the app" << std::endl
|
||||
<< std::endl;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (! args[4].containsChar (','))
|
||||
{
|
||||
std::cout << "Not a valid RSA key!" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << generateKeyFile (args[0], args[1], args[2], args[3], RSAKey (args[4])) << std::endl;
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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 Spinner final : public Component,
|
||||
private Timer
|
||||
{
|
||||
Spinner() { startTimer (1000 / 50); }
|
||||
void timerCallback() override { repaint(); }
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
getLookAndFeel().drawSpinningWaitAnimation (g, Colours::darkgrey, 0, 0, getWidth(), getHeight());
|
||||
}
|
||||
};
|
||||
|
||||
struct OnlineUnlockForm::OverlayComp final : public Component,
|
||||
private Thread,
|
||||
private Timer,
|
||||
private Button::Listener
|
||||
{
|
||||
OverlayComp (OnlineUnlockForm& f, bool hasCancelButton = false)
|
||||
: Thread (String()), form (f)
|
||||
{
|
||||
result.succeeded = false;
|
||||
email = form.emailBox.getText();
|
||||
password = form.passwordBox.getText();
|
||||
addAndMakeVisible (spinner);
|
||||
|
||||
if (hasCancelButton)
|
||||
{
|
||||
cancelButton.reset (new TextButton (TRANS ("Cancel")));
|
||||
addAndMakeVisible (cancelButton.get());
|
||||
cancelButton->addListener (this);
|
||||
}
|
||||
|
||||
startThread (Priority::normal);
|
||||
}
|
||||
|
||||
~OverlayComp() override
|
||||
{
|
||||
stopThread (10000);
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.fillAll (Colours::white.withAlpha (0.97f));
|
||||
|
||||
g.setColour (Colours::black);
|
||||
g.setFont (15.0f);
|
||||
|
||||
g.drawFittedText (TRANS ("Contacting XYZ...").replace ("XYZ", form.status.getWebsiteName()),
|
||||
getLocalBounds().reduced (20, 0).removeFromTop (proportionOfHeight (0.6f)),
|
||||
Justification::centred, 5);
|
||||
}
|
||||
|
||||
void resized() override
|
||||
{
|
||||
const int spinnerSize = 40;
|
||||
spinner.setBounds ((getWidth() - spinnerSize) / 2, proportionOfHeight (0.6f), spinnerSize, spinnerSize);
|
||||
|
||||
if (cancelButton != nullptr)
|
||||
cancelButton->setBounds (getLocalBounds().removeFromBottom (50).reduced (getWidth() / 4, 5));
|
||||
}
|
||||
|
||||
void run() override
|
||||
{
|
||||
result = form.status.attemptWebserverUnlock (email, password);
|
||||
startTimer (100);
|
||||
}
|
||||
|
||||
void timerCallback() override
|
||||
{
|
||||
spinner.setVisible (false);
|
||||
stopTimer();
|
||||
|
||||
if (result.errorMessage.isNotEmpty())
|
||||
{
|
||||
auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon,
|
||||
TRANS ("Registration Failed"),
|
||||
result.errorMessage,
|
||||
{},
|
||||
&form);
|
||||
form.messageBox = AlertWindow::showScopedAsync (options, nullptr);
|
||||
}
|
||||
else if (result.informativeMessage.isNotEmpty())
|
||||
{
|
||||
auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::InfoIcon,
|
||||
TRANS ("Registration Complete!"),
|
||||
result.informativeMessage,
|
||||
{},
|
||||
&form);
|
||||
form.messageBox = AlertWindow::showScopedAsync (options, nullptr);
|
||||
}
|
||||
else if (result.urlToLaunch.isNotEmpty())
|
||||
{
|
||||
URL url (result.urlToLaunch);
|
||||
url.launchInDefaultBrowser();
|
||||
}
|
||||
|
||||
// (local copies because we're about to delete this)
|
||||
const bool worked = result.succeeded;
|
||||
OnlineUnlockForm& f = form;
|
||||
|
||||
delete this;
|
||||
|
||||
if (worked)
|
||||
f.dismiss();
|
||||
}
|
||||
|
||||
void buttonClicked (Button* button) override
|
||||
{
|
||||
if (button == cancelButton.get())
|
||||
{
|
||||
form.status.userCancelled();
|
||||
|
||||
spinner.setVisible (false);
|
||||
stopTimer();
|
||||
|
||||
delete this;
|
||||
}
|
||||
}
|
||||
|
||||
OnlineUnlockForm& form;
|
||||
Spinner spinner;
|
||||
OnlineUnlockStatus::UnlockResult result;
|
||||
String email, password;
|
||||
|
||||
std::unique_ptr<TextButton> cancelButton;
|
||||
|
||||
JUCE_LEAK_DETECTOR (OnlineUnlockForm::OverlayComp)
|
||||
};
|
||||
|
||||
static juce_wchar getDefaultPasswordChar() noexcept
|
||||
{
|
||||
#if JUCE_LINUX || JUCE_BSD
|
||||
return 0x2022;
|
||||
#else
|
||||
return 0x25cf;
|
||||
#endif
|
||||
}
|
||||
|
||||
OnlineUnlockForm::OnlineUnlockForm (OnlineUnlockStatus& s,
|
||||
const String& userInstructions,
|
||||
bool hasCancelButton,
|
||||
bool overlayHasCancelButton)
|
||||
: message (String(), userInstructions),
|
||||
passwordBox (String(), getDefaultPasswordChar()),
|
||||
registerButton (TRANS ("Register")),
|
||||
cancelButton (TRANS ("Cancel")),
|
||||
status (s),
|
||||
showOverlayCancelButton (overlayHasCancelButton)
|
||||
{
|
||||
// Please supply a message to tell your users what to do!
|
||||
jassert (userInstructions.isNotEmpty());
|
||||
|
||||
setOpaque (true);
|
||||
|
||||
emailBox.setText (status.getUserEmail());
|
||||
message.setJustificationType (Justification::centred);
|
||||
|
||||
addAndMakeVisible (message);
|
||||
addAndMakeVisible (emailBox);
|
||||
addAndMakeVisible (passwordBox);
|
||||
addAndMakeVisible (registerButton);
|
||||
|
||||
if (hasCancelButton)
|
||||
addAndMakeVisible (cancelButton);
|
||||
|
||||
emailBox.setEscapeAndReturnKeysConsumed (false);
|
||||
passwordBox.setEscapeAndReturnKeysConsumed (false);
|
||||
|
||||
registerButton.addShortcut (KeyPress (KeyPress::returnKey));
|
||||
|
||||
registerButton.addListener (this);
|
||||
cancelButton.addListener (this);
|
||||
|
||||
lookAndFeelChanged();
|
||||
setSize (500, 250);
|
||||
}
|
||||
|
||||
OnlineUnlockForm::~OnlineUnlockForm()
|
||||
{
|
||||
unlockingOverlay.deleteAndZero();
|
||||
}
|
||||
|
||||
void OnlineUnlockForm::paint (Graphics& g)
|
||||
{
|
||||
g.fillAll (Colours::lightgrey);
|
||||
}
|
||||
|
||||
void OnlineUnlockForm::resized()
|
||||
{
|
||||
/* If you're writing a plugin, then DO NOT USE A POP-UP A DIALOG WINDOW!
|
||||
Plugins that create external windows are incredibly annoying for users, and
|
||||
cause all sorts of headaches for hosts. Don't be the person who writes that
|
||||
plugin that irritates everyone with a nagging dialog box every time they scan!
|
||||
*/
|
||||
jassert (JUCEApplicationBase::isStandaloneApp() || findParentComponentOfClass<DialogWindow>() == nullptr);
|
||||
|
||||
const int buttonHeight = 22;
|
||||
|
||||
auto r = getLocalBounds().reduced (10, 20);
|
||||
|
||||
auto buttonArea = r.removeFromBottom (buttonHeight);
|
||||
registerButton.changeWidthToFitText (buttonHeight);
|
||||
cancelButton.changeWidthToFitText (buttonHeight);
|
||||
|
||||
const int gap = 20;
|
||||
buttonArea = buttonArea.withSizeKeepingCentre (registerButton.getWidth()
|
||||
+ (cancelButton.isVisible() ? gap + cancelButton.getWidth() : 0),
|
||||
buttonHeight);
|
||||
registerButton.setBounds (buttonArea.removeFromLeft (registerButton.getWidth()));
|
||||
buttonArea.removeFromLeft (gap);
|
||||
cancelButton.setBounds (buttonArea);
|
||||
|
||||
r.removeFromBottom (20);
|
||||
|
||||
// (force use of a default system font to make sure it has the password blob character)
|
||||
Font font (Font::getDefaultTypefaceForFont (Font (Font::getDefaultSansSerifFontName(),
|
||||
Font::getDefaultStyle(),
|
||||
5.0f)));
|
||||
|
||||
const int boxHeight = 24;
|
||||
passwordBox.setBounds (r.removeFromBottom (boxHeight));
|
||||
passwordBox.setInputRestrictions (64);
|
||||
passwordBox.setFont (font);
|
||||
|
||||
r.removeFromBottom (20);
|
||||
emailBox.setBounds (r.removeFromBottom (boxHeight));
|
||||
emailBox.setInputRestrictions (512);
|
||||
emailBox.setFont (font);
|
||||
|
||||
r.removeFromBottom (20);
|
||||
|
||||
message.setBounds (r);
|
||||
|
||||
if (unlockingOverlay != nullptr)
|
||||
unlockingOverlay->setBounds (getLocalBounds());
|
||||
}
|
||||
|
||||
void OnlineUnlockForm::lookAndFeelChanged()
|
||||
{
|
||||
Colour labelCol (findColour (TextEditor::backgroundColourId).contrasting (0.5f));
|
||||
|
||||
emailBox.setTextToShowWhenEmpty (TRANS ("Email Address"), labelCol);
|
||||
passwordBox.setTextToShowWhenEmpty (TRANS ("Password"), labelCol);
|
||||
}
|
||||
|
||||
void OnlineUnlockForm::showBubbleMessage (const String& text, Component& target)
|
||||
{
|
||||
bubble.reset (new BubbleMessageComponent (500));
|
||||
addChildComponent (bubble.get());
|
||||
|
||||
AttributedString attString;
|
||||
attString.append (text, Font (16.0f));
|
||||
|
||||
bubble->showAt (getLocalArea (&target, target.getLocalBounds()),
|
||||
attString, 500, // numMillisecondsBeforeRemoving
|
||||
true, // removeWhenMouseClicked
|
||||
false); // deleteSelfAfterUse
|
||||
}
|
||||
|
||||
void OnlineUnlockForm::buttonClicked (Button* b)
|
||||
{
|
||||
if (b == ®isterButton)
|
||||
attemptRegistration();
|
||||
else if (b == &cancelButton)
|
||||
dismiss();
|
||||
}
|
||||
|
||||
void OnlineUnlockForm::attemptRegistration()
|
||||
{
|
||||
if (unlockingOverlay == nullptr)
|
||||
{
|
||||
if (emailBox.getText().trim().length() < 3)
|
||||
{
|
||||
showBubbleMessage (TRANS ("Please enter a valid email address!"), emailBox);
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordBox.getText().trim().length() < 3)
|
||||
{
|
||||
showBubbleMessage (TRANS ("Please enter a valid password!"), passwordBox);
|
||||
return;
|
||||
}
|
||||
|
||||
status.setUserEmail (emailBox.getText());
|
||||
|
||||
addAndMakeVisible (unlockingOverlay = new OverlayComp (*this, showOverlayCancelButton));
|
||||
resized();
|
||||
unlockingOverlay->enterModalState();
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineUnlockForm::dismiss()
|
||||
{
|
||||
delete this;
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
/** Acts as a GUI which asks the user for their details, and calls the appropriate
|
||||
methods on your OnlineUnlockStatus object to attempt to register the app.
|
||||
|
||||
You should create one of these components and add it to your parent window,
|
||||
or use a DialogWindow to display it as a pop-up. But if you're writing a plugin,
|
||||
then DO NOT USE A DIALOG WINDOW! Add it as a child component of your plugin's editor
|
||||
component instead. Plugins that pop up external registration windows are incredibly
|
||||
annoying, and cause all sorts of headaches for hosts. Don't be the person who
|
||||
writes that plugin that irritates everyone with a dialog box every time they
|
||||
try to scan for new plugins!
|
||||
|
||||
Note that after adding it, you should put the component into a modal state,
|
||||
and it will automatically delete itself when it has completed.
|
||||
|
||||
Although it deletes itself, it's also OK to delete it manually yourself
|
||||
if you need to get rid of it sooner.
|
||||
|
||||
@see OnlineUnlockStatus
|
||||
|
||||
@tags{ProductUnlocking}
|
||||
*/
|
||||
class JUCE_API OnlineUnlockForm : public Component,
|
||||
private Button::Listener
|
||||
{
|
||||
public:
|
||||
/** Creates an unlock form that will work with the given status object.
|
||||
The userInstructions will be displayed above the email and password boxes.
|
||||
*/
|
||||
OnlineUnlockForm (OnlineUnlockStatus&,
|
||||
const String& userInstructions,
|
||||
bool hasCancelButton = true,
|
||||
bool overlayHasCancelButton = false);
|
||||
|
||||
/** Destructor. */
|
||||
~OnlineUnlockForm() override;
|
||||
|
||||
/** This is called when the form is dismissed (either cancelled or when registration
|
||||
succeeds).
|
||||
By default it will delete this, but you can override it to do other things.
|
||||
*/
|
||||
virtual void dismiss();
|
||||
|
||||
/** @internal */
|
||||
void paint (Graphics&) override;
|
||||
/** @internal */
|
||||
void resized() override;
|
||||
/** @internal */
|
||||
void lookAndFeelChanged() override;
|
||||
|
||||
Label message;
|
||||
TextEditor emailBox, passwordBox;
|
||||
TextButton registerButton, cancelButton;
|
||||
|
||||
private:
|
||||
OnlineUnlockStatus& status;
|
||||
std::unique_ptr<BubbleMessageComponent> bubble;
|
||||
|
||||
bool showOverlayCancelButton;
|
||||
|
||||
struct OverlayComp;
|
||||
friend struct OverlayComp;
|
||||
ScopedMessageBox messageBox;
|
||||
Component::SafePointer<Component> unlockingOverlay;
|
||||
|
||||
void buttonClicked (Button*) override;
|
||||
void attemptRegistration();
|
||||
void showBubbleMessage (const String&, Component&);
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OnlineUnlockForm)
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
+532
@@ -0,0 +1,532 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
/* Note: There's a bit of light obfuscation in this code, just to make things
|
||||
a bit more annoying for crackers who try to reverse-engineer your binaries, but
|
||||
nothing particularly foolproof.
|
||||
*/
|
||||
|
||||
struct KeyFileUtils
|
||||
{
|
||||
static XmlElement createKeyFileContent (const String& appName,
|
||||
const String& userEmail,
|
||||
const String& userName,
|
||||
const String& machineNumbers,
|
||||
const String& machineNumbersAttributeName)
|
||||
{
|
||||
XmlElement xml ("key");
|
||||
|
||||
xml.setAttribute ("user", userName);
|
||||
xml.setAttribute ("email", userEmail);
|
||||
xml.setAttribute (machineNumbersAttributeName, machineNumbers);
|
||||
xml.setAttribute ("app", appName);
|
||||
xml.setAttribute ("date", String::toHexString (Time::getCurrentTime().toMilliseconds()));
|
||||
|
||||
return xml;
|
||||
}
|
||||
|
||||
static String createKeyFileComment (const String& appName,
|
||||
const String& userEmail,
|
||||
const String& userName,
|
||||
const String& machineNumbers)
|
||||
{
|
||||
String comment;
|
||||
comment << "Keyfile for " << appName << newLine;
|
||||
|
||||
if (userName.isNotEmpty())
|
||||
comment << "User: " << userName << newLine;
|
||||
|
||||
comment << "Email: " << userEmail << newLine
|
||||
<< "Machine numbers: " << machineNumbers << newLine
|
||||
<< "Created: " << Time::getCurrentTime().toString (true, true);
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static String encryptXML (const XmlElement& xml, RSAKey privateKey)
|
||||
{
|
||||
MemoryOutputStream text;
|
||||
text << xml.toString (XmlElement::TextFormat().singleLine());
|
||||
|
||||
BigInteger val;
|
||||
val.loadFromMemoryBlock (text.getMemoryBlock());
|
||||
|
||||
privateKey.applyToValue (val);
|
||||
|
||||
return val.toString (16);
|
||||
}
|
||||
|
||||
static String createKeyFile (String comment,
|
||||
const XmlElement& xml,
|
||||
RSAKey rsaPrivateKey)
|
||||
{
|
||||
String asHex ("#" + encryptXML (xml, rsaPrivateKey));
|
||||
|
||||
StringArray lines;
|
||||
lines.add (comment);
|
||||
lines.add (String());
|
||||
|
||||
const int charsPerLine = 70;
|
||||
while (asHex.length() > 0)
|
||||
{
|
||||
lines.add (asHex.substring (0, charsPerLine));
|
||||
asHex = asHex.substring (charsPerLine);
|
||||
}
|
||||
|
||||
lines.add (String());
|
||||
|
||||
return lines.joinIntoString ("\r\n");
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static XmlElement decryptXML (String hexData, RSAKey rsaPublicKey)
|
||||
{
|
||||
BigInteger val;
|
||||
val.parseString (hexData, 16);
|
||||
|
||||
RSAKey key (rsaPublicKey);
|
||||
jassert (key.isValid());
|
||||
|
||||
std::unique_ptr<XmlElement> xml;
|
||||
|
||||
if (! val.isZero())
|
||||
{
|
||||
key.applyToValue (val);
|
||||
|
||||
auto mb = val.toMemoryBlock();
|
||||
|
||||
if (CharPointer_UTF8::isValidString (static_cast<const char*> (mb.getData()), (int) mb.getSize()))
|
||||
xml = parseXML (mb.toString());
|
||||
}
|
||||
|
||||
return xml != nullptr ? *xml : XmlElement ("key");
|
||||
}
|
||||
|
||||
static XmlElement getXmlFromKeyFile (String keyFileText, RSAKey rsaPublicKey)
|
||||
{
|
||||
return decryptXML (keyFileText.fromLastOccurrenceOf ("#", false, false).trim(), rsaPublicKey);
|
||||
}
|
||||
|
||||
static StringArray getMachineNumbers (XmlElement xml, StringRef attributeName)
|
||||
{
|
||||
StringArray numbers;
|
||||
numbers.addTokens (xml.getStringAttribute (attributeName), ",; ", StringRef());
|
||||
numbers.trim();
|
||||
numbers.removeEmptyStrings();
|
||||
return numbers;
|
||||
}
|
||||
|
||||
static String getLicensee (const XmlElement& xml) { return xml.getStringAttribute ("user"); }
|
||||
static String getEmail (const XmlElement& xml) { return xml.getStringAttribute ("email"); }
|
||||
static String getAppID (const XmlElement& xml) { return xml.getStringAttribute ("app"); }
|
||||
|
||||
struct KeyFileData
|
||||
{
|
||||
String licensee, email, appID;
|
||||
StringArray machineNumbers;
|
||||
|
||||
bool keyFileExpires;
|
||||
Time expiryTime;
|
||||
};
|
||||
|
||||
static KeyFileData getDataFromKeyFile (XmlElement xml)
|
||||
{
|
||||
KeyFileData data;
|
||||
|
||||
data.licensee = getLicensee (xml);
|
||||
data.email = getEmail (xml);
|
||||
data.appID = getAppID (xml);
|
||||
|
||||
if (xml.hasAttribute ("expiryTime") && xml.hasAttribute ("expiring_mach"))
|
||||
{
|
||||
data.keyFileExpires = true;
|
||||
data.machineNumbers.addArray (getMachineNumbers (xml, "expiring_mach"));
|
||||
data.expiryTime = Time (xml.getStringAttribute ("expiryTime").getHexValue64());
|
||||
}
|
||||
else
|
||||
{
|
||||
data.keyFileExpires = false;
|
||||
data.machineNumbers.addArray (getMachineNumbers (xml, "mach"));
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
#if JUCE_MODULE_AVAILABLE_juce_data_structures
|
||||
const char* OnlineUnlockStatus::unlockedProp = "u";
|
||||
const char* OnlineUnlockStatus::expiryTimeProp = "t";
|
||||
static const char* stateTagName = "REG";
|
||||
static const char* userNameProp = "user";
|
||||
static const char* keyfileDataProp = "key";
|
||||
|
||||
static var machineNumberAllowed (StringArray numbersFromKeyFile,
|
||||
StringArray localMachineNumbers)
|
||||
{
|
||||
var result;
|
||||
|
||||
for (int i = 0; i < localMachineNumbers.size(); ++i)
|
||||
{
|
||||
auto localNumber = localMachineNumbers[i].trim();
|
||||
|
||||
if (localNumber.isNotEmpty())
|
||||
{
|
||||
for (int j = numbersFromKeyFile.size(); --j >= 0;)
|
||||
{
|
||||
var ok (localNumber.trim().equalsIgnoreCase (numbersFromKeyFile[j].trim()));
|
||||
result.swapWith (ok);
|
||||
|
||||
if (result)
|
||||
break;
|
||||
}
|
||||
|
||||
if (result)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
OnlineUnlockStatus::OnlineUnlockStatus() : status (stateTagName)
|
||||
{
|
||||
}
|
||||
|
||||
OnlineUnlockStatus::~OnlineUnlockStatus()
|
||||
{
|
||||
}
|
||||
|
||||
void OnlineUnlockStatus::load()
|
||||
{
|
||||
MemoryBlock mb;
|
||||
mb.fromBase64Encoding (getState());
|
||||
|
||||
if (! mb.isEmpty())
|
||||
status = ValueTree::readFromGZIPData (mb.getData(), mb.getSize());
|
||||
else
|
||||
status = ValueTree (stateTagName);
|
||||
|
||||
StringArray localMachineNums (getLocalMachineIDs());
|
||||
|
||||
if (machineNumberAllowed (StringArray ("1234"), localMachineNums))
|
||||
status.removeProperty (unlockedProp, nullptr);
|
||||
|
||||
KeyFileUtils::KeyFileData data;
|
||||
data = KeyFileUtils::getDataFromKeyFile (KeyFileUtils::getXmlFromKeyFile (status[keyfileDataProp], getPublicKey()));
|
||||
|
||||
if (data.keyFileExpires)
|
||||
{
|
||||
if (! doesProductIDMatch (data.appID))
|
||||
status.removeProperty (expiryTimeProp, nullptr);
|
||||
|
||||
if (! machineNumberAllowed (data.machineNumbers, localMachineNums))
|
||||
status.removeProperty (expiryTimeProp, nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (! doesProductIDMatch (data.appID))
|
||||
status.removeProperty (unlockedProp, nullptr);
|
||||
|
||||
if (! machineNumberAllowed (data.machineNumbers, localMachineNums))
|
||||
status.removeProperty (unlockedProp, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void OnlineUnlockStatus::save()
|
||||
{
|
||||
MemoryOutputStream mo;
|
||||
|
||||
{
|
||||
GZIPCompressorOutputStream gzipStream (mo, 9);
|
||||
status.writeToStream (gzipStream);
|
||||
}
|
||||
|
||||
saveState (mo.getMemoryBlock().toBase64Encoding());
|
||||
}
|
||||
|
||||
char OnlineUnlockStatus::MachineIDUtilities::getPlatformPrefix()
|
||||
{
|
||||
#if JUCE_MAC
|
||||
return 'M';
|
||||
#elif JUCE_WINDOWS
|
||||
return 'W';
|
||||
#elif JUCE_LINUX
|
||||
return 'L';
|
||||
#elif JUCE_BSD
|
||||
return 'B';
|
||||
#elif JUCE_IOS
|
||||
return 'I';
|
||||
#elif JUCE_ANDROID
|
||||
return 'A';
|
||||
#endif
|
||||
}
|
||||
|
||||
String OnlineUnlockStatus::MachineIDUtilities::getEncodedIDString (const String& input)
|
||||
{
|
||||
auto platform = String::charToString (static_cast<juce_wchar> (getPlatformPrefix()));
|
||||
|
||||
return platform + MD5 ((input + "salt_1" + platform).toUTF8())
|
||||
.toHexString().substring (0, 9).toUpperCase();
|
||||
}
|
||||
|
||||
bool OnlineUnlockStatus::MachineIDUtilities::addFileIDToList (StringArray& ids, const File& f)
|
||||
{
|
||||
if (auto num = f.getFileIdentifier())
|
||||
{
|
||||
ids.add (getEncodedIDString (String::toHexString ((int64) num)));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void OnlineUnlockStatus::MachineIDUtilities::addMACAddressesToList (StringArray& ids)
|
||||
{
|
||||
for (auto& address : MACAddress::getAllAddresses())
|
||||
ids.add (getEncodedIDString (address.toString()));
|
||||
}
|
||||
|
||||
String OnlineUnlockStatus::MachineIDUtilities::getUniqueMachineID()
|
||||
{
|
||||
return getEncodedIDString (SystemStats::getUniqueDeviceID());
|
||||
}
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996)
|
||||
|
||||
StringArray OnlineUnlockStatus::MachineIDUtilities::getLocalMachineIDs()
|
||||
{
|
||||
auto flags = SystemStats::MachineIdFlags::macAddresses
|
||||
| SystemStats::MachineIdFlags::fileSystemId
|
||||
| SystemStats::MachineIdFlags::legacyUniqueId
|
||||
| SystemStats::MachineIdFlags::uniqueId;
|
||||
auto identifiers = SystemStats::getMachineIdentifiers (flags);
|
||||
|
||||
for (auto& identifier : identifiers)
|
||||
identifier = getEncodedIDString (identifier);
|
||||
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
StringArray OnlineUnlockStatus::getLocalMachineIDs()
|
||||
{
|
||||
return MachineIDUtilities::getLocalMachineIDs();
|
||||
}
|
||||
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
JUCE_END_IGNORE_WARNINGS_MSVC
|
||||
|
||||
void OnlineUnlockStatus::userCancelled()
|
||||
{
|
||||
}
|
||||
|
||||
void OnlineUnlockStatus::setUserEmail (const String& usernameOrEmail)
|
||||
{
|
||||
status.setProperty (userNameProp, usernameOrEmail, nullptr);
|
||||
}
|
||||
|
||||
String OnlineUnlockStatus::getUserEmail() const
|
||||
{
|
||||
return status[userNameProp].toString();
|
||||
}
|
||||
|
||||
Result OnlineUnlockStatus::applyKeyFile (const String& keyFileContent)
|
||||
{
|
||||
KeyFileUtils::KeyFileData data;
|
||||
data = KeyFileUtils::getDataFromKeyFile (KeyFileUtils::getXmlFromKeyFile (keyFileContent, getPublicKey()));
|
||||
|
||||
if (data.licensee.isEmpty() || data.email.isEmpty())
|
||||
return Result::fail (LicenseResult::badCredentials);
|
||||
|
||||
if (! doesProductIDMatch (data.appID))
|
||||
return Result::fail (LicenseResult::badProductID);
|
||||
|
||||
if (MachineIDUtilities::getUniqueMachineID().isEmpty())
|
||||
return Result::fail (LicenseResult::notReady);
|
||||
|
||||
setUserEmail (data.email);
|
||||
status.setProperty (keyfileDataProp, keyFileContent, nullptr);
|
||||
status.removeProperty (data.keyFileExpires ? expiryTimeProp : unlockedProp, nullptr);
|
||||
|
||||
var actualResult (0), dummyResult (1.0);
|
||||
var v (machineNumberAllowed (data.machineNumbers, getLocalMachineIDs()));
|
||||
actualResult.swapWith (v);
|
||||
v = machineNumberAllowed (StringArray ("01"), getLocalMachineIDs());
|
||||
dummyResult.swapWith (v);
|
||||
jassert (! dummyResult);
|
||||
|
||||
if (data.keyFileExpires)
|
||||
{
|
||||
if ((! dummyResult) && actualResult)
|
||||
status.setProperty (expiryTimeProp, data.expiryTime.toMilliseconds(), nullptr);
|
||||
|
||||
return getExpiryTime().toMilliseconds() > 0 ? Result::ok()
|
||||
: Result::fail (LicenseResult::licenseExpired);
|
||||
}
|
||||
|
||||
if ((! dummyResult) && actualResult)
|
||||
status.setProperty (unlockedProp, actualResult, nullptr);
|
||||
|
||||
return isUnlocked() ? Result::ok()
|
||||
: Result::fail (LicenseResult::unlockFailed);
|
||||
}
|
||||
|
||||
static bool areMajorWebsitesAvailable()
|
||||
{
|
||||
static constexpr const char* const urlsToTry[] = { "http://google.com", "http://bing.com", "http://amazon.com",
|
||||
"https://google.com", "https://bing.com", "https://amazon.com" };
|
||||
const auto canConnectToWebsite = [] (auto url)
|
||||
{
|
||||
return URL (url).createInputStream (URL::InputStreamOptions (URL::ParameterHandling::inAddress)
|
||||
.withConnectionTimeoutMs (2000)) != nullptr;
|
||||
};
|
||||
|
||||
return std::any_of (std::begin (urlsToTry),
|
||||
std::end (urlsToTry),
|
||||
canConnectToWebsite);
|
||||
}
|
||||
|
||||
OnlineUnlockStatus::UnlockResult OnlineUnlockStatus::handleXmlReply (XmlElement xml)
|
||||
{
|
||||
UnlockResult r;
|
||||
|
||||
r.succeeded = false;
|
||||
|
||||
if (const auto keyNode = xml.getChildByName ("KEY"))
|
||||
{
|
||||
if (const auto keyText = keyNode->getAllSubText().trim(); keyText.length() > 10)
|
||||
{
|
||||
const auto keyFileResult = applyKeyFile (keyText);
|
||||
|
||||
if (keyFileResult.failed())
|
||||
{
|
||||
r.errorMessage = keyFileResult.getErrorMessage();
|
||||
return r;
|
||||
}
|
||||
|
||||
r.succeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (xml.hasTagName ("MESSAGE"))
|
||||
r.informativeMessage = xml.getStringAttribute ("message").trim();
|
||||
|
||||
if (xml.hasTagName ("ERROR"))
|
||||
r.errorMessage = xml.getStringAttribute ("error").trim();
|
||||
|
||||
if (xml.getStringAttribute ("url").isNotEmpty())
|
||||
r.urlToLaunch = xml.getStringAttribute ("url").trim();
|
||||
|
||||
if (r.errorMessage.isEmpty() && r.informativeMessage.isEmpty() && r.urlToLaunch.isEmpty() && ! r.succeeded)
|
||||
r.errorMessage = getMessageForUnexpectedReply();
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
OnlineUnlockStatus::UnlockResult OnlineUnlockStatus::handleFailedConnection()
|
||||
{
|
||||
UnlockResult r;
|
||||
r.succeeded = false;
|
||||
r.errorMessage = getMessageForConnectionFailure (areMajorWebsitesAvailable());
|
||||
return r;
|
||||
}
|
||||
|
||||
String OnlineUnlockStatus::getMessageForConnectionFailure (bool isInternetConnectionWorking)
|
||||
{
|
||||
String message = TRANS ("Couldn't connect to XYZ").replace ("XYZ", getWebsiteName()) + "...\n\n";
|
||||
|
||||
if (isInternetConnectionWorking)
|
||||
message << TRANS ("Your internet connection seems to be OK, but our webserver "
|
||||
"didn't respond... This is most likely a temporary problem, so try "
|
||||
"again in a few minutes, but if it persists, please contact us for support!");
|
||||
else
|
||||
message << TRANS ("No internet sites seem to be accessible from your computer.. Before trying again, "
|
||||
"please check that your network is working correctly, and make sure "
|
||||
"that any firewall/security software installed on your machine isn't "
|
||||
"blocking your web connection.");
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
String OnlineUnlockStatus::getMessageForUnexpectedReply()
|
||||
{
|
||||
return TRANS ("Unexpected or corrupted reply from XYZ").replace ("XYZ", getWebsiteName()) + "...\n\n"
|
||||
+ TRANS ("Please try again in a few minutes, and contact us for support if this message appears again.");
|
||||
}
|
||||
|
||||
OnlineUnlockStatus::UnlockResult OnlineUnlockStatus::attemptWebserverUnlock (const String& email,
|
||||
const String& password)
|
||||
{
|
||||
// This method will block while it contacts the server, so you must run it on a background thread!
|
||||
jassert (! MessageManager::getInstance()->isThisTheMessageThread());
|
||||
|
||||
auto reply = readReplyFromWebserver (email, password);
|
||||
|
||||
DBG ("Reply from server: " << reply);
|
||||
|
||||
if (auto xml = parseXML (reply))
|
||||
return handleXmlReply (*xml);
|
||||
|
||||
return handleFailedConnection();
|
||||
}
|
||||
|
||||
#endif // JUCE_MODULE_AVAILABLE_juce_data_structures
|
||||
|
||||
//==============================================================================
|
||||
String KeyGeneration::generateKeyFile (const String& appName,
|
||||
const String& userEmail,
|
||||
const String& userName,
|
||||
const String& machineNumbers,
|
||||
const RSAKey& privateKey)
|
||||
{
|
||||
auto xml = KeyFileUtils::createKeyFileContent (appName, userEmail, userName, machineNumbers, "mach");
|
||||
auto comment = KeyFileUtils::createKeyFileComment (appName, userEmail, userName, machineNumbers);
|
||||
|
||||
return KeyFileUtils::createKeyFile (comment, xml, privateKey);
|
||||
}
|
||||
|
||||
String KeyGeneration::generateExpiringKeyFile (const String& appName,
|
||||
const String& userEmail,
|
||||
const String& userName,
|
||||
const String& machineNumbers,
|
||||
const Time expiryTime,
|
||||
const RSAKey& privateKey)
|
||||
{
|
||||
auto xml = KeyFileUtils::createKeyFileContent (appName, userEmail, userName, machineNumbers, "expiring_mach");
|
||||
xml.setAttribute ("expiryTime", String::toHexString (expiryTime.toMilliseconds()));
|
||||
|
||||
auto comment = KeyFileUtils::createKeyFileComment (appName, userEmail, userName, machineNumbers);
|
||||
comment << newLine << "Expires: " << expiryTime.toString (true, true);
|
||||
|
||||
return KeyFileUtils::createKeyFile (comment, xml, privateKey);
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
/**
|
||||
A base class for online unlocking systems.
|
||||
|
||||
This class stores information about whether your app has been unlocked for the
|
||||
current machine, and handles communication with a web-store to perform the
|
||||
unlock procedure.
|
||||
|
||||
You probably won't ever use this base class directly, but rather a store-specific
|
||||
subclass such as TracktionMarketplaceStatus, which knows how to talk to the particular
|
||||
online store that you're using.
|
||||
|
||||
To use it, you create a subclass which implements all the pure virtual methods
|
||||
(see their comments to find out what you'll need to make them do).
|
||||
|
||||
Then you can create an instance of your subclass which will hold the registration
|
||||
state. Typically, you'll want to just keep a single instance of the class around for
|
||||
the duration of your app. You can then call its methods to handle the various
|
||||
registration tasks.
|
||||
|
||||
Areas of your code that need to know whether the user is registered (e.g. to decide
|
||||
whether a particular feature is available) should call isUnlocked() to find out.
|
||||
|
||||
If you want to create a GUI that allows your users to enter their details and
|
||||
register, see the OnlineUnlockForm class.
|
||||
|
||||
@see OnlineUnlockForm, KeyGeneration
|
||||
|
||||
@tags{ProductUnlocking}
|
||||
*/
|
||||
class JUCE_API OnlineUnlockStatus
|
||||
{
|
||||
public:
|
||||
OnlineUnlockStatus();
|
||||
|
||||
/** Destructor. */
|
||||
virtual ~OnlineUnlockStatus();
|
||||
|
||||
//==============================================================================
|
||||
/** This must return your product's ID, as allocated by the store. */
|
||||
virtual String getProductID() = 0;
|
||||
|
||||
/** This must check whether a product ID string that the server returned is OK for
|
||||
unlocking the current app.
|
||||
*/
|
||||
virtual bool doesProductIDMatch (const String& returnedIDFromServer) = 0;
|
||||
|
||||
/** This must return the RSA public key for authenticating responses from
|
||||
the server for this app. You can get this key from your marketplace
|
||||
account page.
|
||||
*/
|
||||
virtual RSAKey getPublicKey() = 0;
|
||||
|
||||
/** This method must store the given string somewhere in your app's
|
||||
persistent properties, so it can be retrieved later by getState().
|
||||
*/
|
||||
virtual void saveState (const String&) = 0;
|
||||
|
||||
/** This method must retrieve the last state that was provided by the
|
||||
saveState method.
|
||||
|
||||
On first-run, it should just return an empty string.
|
||||
*/
|
||||
virtual String getState() = 0;
|
||||
|
||||
/** Returns the name of the web-store website, not for communication, but for
|
||||
presenting to the user.
|
||||
*/
|
||||
virtual String getWebsiteName() = 0;
|
||||
|
||||
/** Returns the URL of the authentication API. */
|
||||
virtual URL getServerAuthenticationURL() = 0;
|
||||
|
||||
/** Subclasses that talk to a particular web-store will implement this method
|
||||
to contact their webserver and attempt to unlock the current machine for
|
||||
the given username and password. The return value is the XML text from the
|
||||
server which contains error information and/or the encrypted keyfile.
|
||||
*/
|
||||
virtual String readReplyFromWebserver (const String& email, const String& password) = 0;
|
||||
|
||||
/** Returns a list of strings, any of which should be unique to this
|
||||
physical computer.
|
||||
|
||||
When testing whether the user is allowed to use the product on this
|
||||
machine, this list of tokens is compared to the ones that were stored
|
||||
on the webserver.
|
||||
|
||||
The default implementation of this method will simply call
|
||||
MachineIDUtilities::getLocalMachineIDs(), which provides a default
|
||||
version of this functionality.
|
||||
*/
|
||||
virtual StringArray getLocalMachineIDs();
|
||||
|
||||
/** This method will be called if the user cancels the connection to the webserver
|
||||
by clicking the cancel button in OnlineUnlockForm::OverlayComp.
|
||||
|
||||
The default implementation of this method does nothing but you should use it to
|
||||
cancel any WebInputStreams that may be connecting.
|
||||
*/
|
||||
virtual void userCancelled();
|
||||
|
||||
virtual String getMessageForConnectionFailure (bool isInternetConnectionWorking);
|
||||
virtual String getMessageForUnexpectedReply();
|
||||
|
||||
//==============================================================================
|
||||
// The following methods can be called by your app:
|
||||
|
||||
/** Returns true if the product has been successfully authorised for this machine.
|
||||
|
||||
The reason it returns a variant rather than a bool is just to make it marginally
|
||||
more tedious for crackers to work around. Hopefully if this method gets inlined
|
||||
they'll need to hack all the places where you call it, rather than just the
|
||||
function itself.
|
||||
|
||||
Bear in mind that each place where you check this return value will need to be
|
||||
changed by a cracker in order to unlock your app, so the more places you call this
|
||||
method, the more hassle it will be for them to find and crack them all.
|
||||
*/
|
||||
inline var isUnlocked() const { return status[unlockedProp]; }
|
||||
|
||||
/** Returns the Time when the keyfile expires.
|
||||
|
||||
If a the key file obtained has an expiry time, isUnlocked will return false and this
|
||||
will return a non-zero time. The interpretation of this is up to your app but could
|
||||
be used for subscription based models or trial periods.
|
||||
*/
|
||||
inline Time getExpiryTime() const { return Time (static_cast<int64> (status[expiryTimeProp])); }
|
||||
|
||||
/** Optionally allows the app to provide the user's email address if
|
||||
it is known.
|
||||
You don't need to call this, but if you do it may save the user
|
||||
typing it in.
|
||||
*/
|
||||
void setUserEmail (const String& usernameOrEmail);
|
||||
|
||||
/** Returns the user's email address if known. */
|
||||
String getUserEmail() const;
|
||||
|
||||
/** The possible error strings that can applyKeyFile() can return on failure. */
|
||||
struct LicenseResult
|
||||
{
|
||||
static constexpr auto notReady = "ID generator is not ready, try again later.";
|
||||
static constexpr auto badCredentials = "Credentials are invalid.";
|
||||
static constexpr auto badProductID = "ProductID is incorrect.";
|
||||
static constexpr auto licenseExpired = "License has expired.";
|
||||
static constexpr auto unlockFailed = "Generic unlock failure.";
|
||||
};
|
||||
|
||||
/** Attempts to perform an unlock using a block of key-file data provided.
|
||||
You may wish to use this as a way of allowing a user to unlock your app
|
||||
by drag-and-dropping a file containing the key data, or by letting them
|
||||
select such a file. This is often needed for allowing registration on
|
||||
machines without internet access.
|
||||
|
||||
You can find the possible string values Result can return in LicenseResult.
|
||||
*/
|
||||
Result applyKeyFile (const String& keyFileContent);
|
||||
|
||||
/** This provides some details about the reply that the server gave in a call
|
||||
to attemptWebserverUnlock().
|
||||
*/
|
||||
struct UnlockResult
|
||||
{
|
||||
/** If an unlock operation fails, this is the error message that the webserver
|
||||
supplied (or a message saying that the server couldn't be contacted)
|
||||
*/
|
||||
String errorMessage;
|
||||
|
||||
/** This is a message that the webserver returned, and which the user should
|
||||
be shown.
|
||||
|
||||
It's not necessarily an error message, e.g. it might say that there's a
|
||||
new version of the app available or some other status update.
|
||||
*/
|
||||
String informativeMessage;
|
||||
|
||||
/** If the webserver wants the user to be directed to a web-page for further
|
||||
information, this is the URL that it would like them to go to.
|
||||
*/
|
||||
String urlToLaunch;
|
||||
|
||||
/** If the unlock operation succeeded, this will be set to true. */
|
||||
bool succeeded;
|
||||
};
|
||||
|
||||
/** Contacts the webserver and attempts to perform a registration with the
|
||||
given user details.
|
||||
|
||||
The return value will either be a success, or a failure with an error message
|
||||
from the server, so you should show this message to your user.
|
||||
|
||||
Because this method blocks while it contacts the server, you must run it on
|
||||
a background thread, not on the message thread. For an easier way to create
|
||||
a GUI to do the unlocking, see OnlineUnlockForm.
|
||||
*/
|
||||
UnlockResult attemptWebserverUnlock (const String& email, const String& password);
|
||||
|
||||
/** Attempts to load the status from the state retrieved by getState().
|
||||
Call this somewhere in your app's startup code.
|
||||
*/
|
||||
void load();
|
||||
|
||||
/** Triggers a call to saveState which you can use to store the current unlock status
|
||||
in your app's settings.
|
||||
*/
|
||||
void save();
|
||||
|
||||
/** This class contains some utility functions that might help with machine ID generation. */
|
||||
struct MachineIDUtilities
|
||||
{
|
||||
/** Returns a character that represents the current OS.
|
||||
E.g. 'M' for Mac, 'W' for windows, etc
|
||||
*/
|
||||
static char getPlatformPrefix();
|
||||
|
||||
/** Returns an encoded hash string from the given input string, prefixing it with
|
||||
a letter to represent the current OS type.
|
||||
*/
|
||||
static String getEncodedIDString (const String& inputString);
|
||||
|
||||
/** Utility function that you may want to use in your machine-ID generation code.
|
||||
This adds an ID string to the given array which is a hash of the filesystem ID of the
|
||||
given file.
|
||||
*/
|
||||
static bool addFileIDToList (StringArray& result, const File& file);
|
||||
|
||||
/** Utility function that you may want to use in your machine-ID generation code.
|
||||
This adds some ID strings to the given array which represent each MAC address of the machine.
|
||||
*/
|
||||
[[deprecated ("MAC addresses are no longer reliable methods of ID generation. You should use getUniqueMachineID() instead, You can still get a list of this device's MAC addresses with MACAddress::findAllAddresses().")]]
|
||||
static void addMACAddressesToList (StringArray& result);
|
||||
|
||||
/** This method calculates some machine IDs based on things like network
|
||||
MAC addresses, hard-disk IDs, etc, but if you want, you can overload
|
||||
it to generate your own list of IDs.
|
||||
|
||||
The IDs that are returned should be short alphanumeric strings
|
||||
without any punctuation characters. Since users may need to type
|
||||
them, case is ignored when comparing them.
|
||||
|
||||
Note that the first item in the list is considered to be the
|
||||
"main" ID, and this will be the one that is displayed to the user
|
||||
and registered with the marketplace webserver. Subsequent IDs are
|
||||
just used as fallback to avoid false negatives when checking for
|
||||
registration on machines which have had hardware added/removed
|
||||
since the product was first registered.
|
||||
*/
|
||||
[[deprecated ("The identifiers generated by this function are no longer reliable. Use getUniqueMachineID() instead.")]]
|
||||
static StringArray getLocalMachineIDs();
|
||||
|
||||
/** Returns an encoded unique machine ID.
|
||||
|
||||
@see SystemStats::getUniqueDeviceID
|
||||
*/
|
||||
static String getUniqueMachineID();
|
||||
};
|
||||
|
||||
private:
|
||||
ValueTree status;
|
||||
|
||||
UnlockResult handleXmlReply (XmlElement);
|
||||
UnlockResult handleFailedConnection();
|
||||
|
||||
static const char* unlockedProp;
|
||||
static const char* expiryTimeProp;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OnlineUnlockStatus)
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
TracktionMarketplaceStatus::TracktionMarketplaceStatus() {}
|
||||
|
||||
URL TracktionMarketplaceStatus::getServerAuthenticationURL()
|
||||
{
|
||||
return URL ("https://www.tracktion.com/marketplace/authenticate.php");
|
||||
}
|
||||
|
||||
String TracktionMarketplaceStatus::getWebsiteName()
|
||||
{
|
||||
return "tracktion.com";
|
||||
}
|
||||
|
||||
bool TracktionMarketplaceStatus::doesProductIDMatch (const String& returnedIDFromServer)
|
||||
{
|
||||
return getProductID() == returnedIDFromServer;
|
||||
}
|
||||
|
||||
String TracktionMarketplaceStatus::readReplyFromWebserver (const String& email, const String& password)
|
||||
{
|
||||
URL url (getServerAuthenticationURL()
|
||||
.withParameter ("product", getProductID())
|
||||
.withParameter ("email", email)
|
||||
.withParameter ("pw", password)
|
||||
.withParameter ("os", SystemStats::getOperatingSystemName())
|
||||
.withParameter ("mach", getLocalMachineIDs()[0]));
|
||||
|
||||
DBG ("Trying to unlock via URL: " << url.toString (true));
|
||||
|
||||
{
|
||||
ScopedLock lock (streamCreationLock);
|
||||
stream.reset (new WebInputStream (url, true));
|
||||
}
|
||||
|
||||
if (stream->connect (nullptr))
|
||||
{
|
||||
auto thread = Thread::getCurrentThread();
|
||||
MemoryOutputStream result;
|
||||
|
||||
while (! (stream->isExhausted() || stream->isError()
|
||||
|| (thread != nullptr && thread->threadShouldExit())))
|
||||
{
|
||||
auto bytesRead = result.writeFromInputStream (*stream, 8192);
|
||||
|
||||
if (bytesRead < 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void TracktionMarketplaceStatus::userCancelled()
|
||||
{
|
||||
ScopedLock lock (streamCreationLock);
|
||||
|
||||
if (stream != nullptr)
|
||||
stream->cancel();
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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
|
||||
{
|
||||
|
||||
/**
|
||||
An implementation of the OnlineUnlockStatus class which talks to the
|
||||
Tracktion Marketplace server.
|
||||
|
||||
For details about how to use this class, see the docs for the base
|
||||
class: OnlineUnlockStatus. Basically, you need to inherit from it, and
|
||||
implement all the pure virtual methods to tell it about your product.
|
||||
|
||||
@see OnlineUnlockStatus, OnlineUnlockForm, KeyGeneration
|
||||
|
||||
@tags{ProductUnlocking}
|
||||
*/
|
||||
class JUCE_API TracktionMarketplaceStatus : public OnlineUnlockStatus
|
||||
{
|
||||
public:
|
||||
TracktionMarketplaceStatus();
|
||||
|
||||
/** @internal */
|
||||
bool doesProductIDMatch (const String& returnedIDFromServer) override;
|
||||
/** @internal */
|
||||
URL getServerAuthenticationURL() override;
|
||||
/** @internal */
|
||||
String getWebsiteName() override;
|
||||
/** @internal */
|
||||
String readReplyFromWebserver (const String& email, const String& password) override;
|
||||
/** @internal */
|
||||
void userCancelled() override;
|
||||
|
||||
private:
|
||||
CriticalSection streamCreationLock;
|
||||
std::unique_ptr<WebInputStream> stream;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TracktionMarketplaceStatus)
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
third_party/JUCE/modules/juce_product_unlocking/native/java/app/com/rmsl/juce/JuceBillingClient.java
Vendored
+226
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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 productDetailsQueryCallback(long host, java.util.List<ProductDetails> productDetails);
|
||||
private native void purchasesListQueryCallback(long host, java.util.List<Purchase> purchases);
|
||||
private native void purchaseCompletedCallback(long host, Purchase purchase, int responseCode);
|
||||
private native void purchaseConsumedCallback(long host, String productIdentifier, int responseCode);
|
||||
|
||||
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 QueryProductDetailsParams getProductListParams(final String[] productsToQuery, String type) {
|
||||
java.util.ArrayList<QueryProductDetailsParams.Product> productList = new java.util.ArrayList<>();
|
||||
|
||||
for (String product : productsToQuery)
|
||||
productList.add(QueryProductDetailsParams.Product.newBuilder().setProductId(product).setProductType(type).build());
|
||||
|
||||
return QueryProductDetailsParams.newBuilder().setProductList(productList).build();
|
||||
}
|
||||
|
||||
public void queryProductDetailsImpl(final String[] productsToQuery, java.util.List<String> productTypes, java.util.List<ProductDetails> details) {
|
||||
if (productTypes == null || productTypes.isEmpty()) {
|
||||
productDetailsQueryCallback(host, details);
|
||||
} else {
|
||||
billingClient.queryProductDetailsAsync(getProductListParams(productsToQuery, productTypes.get(0)), new ProductDetailsResponseListener() {
|
||||
@Override
|
||||
public void onProductDetailsResponse(BillingResult billingResult, java.util.List<ProductDetails> newDetails) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
details.addAll(newDetails);
|
||||
queryProductDetailsImpl(productsToQuery, productTypes.subList(1, productTypes.size()), details);
|
||||
} else {
|
||||
queryProductDetailsImpl(productsToQuery, null, details);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void queryProductDetails(final String[] productsToQuery) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String[] toCheck = {BillingClient.ProductType.INAPP, BillingClient.ProductType.SUBS};
|
||||
queryProductDetailsImpl(productsToQuery, java.util.Arrays.asList(toCheck), new java.util.ArrayList<ProductDetails>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void launchBillingFlow(final android.app.Activity activity, final BillingFlowParams params) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BillingResult r = billingClient.launchBillingFlow(activity, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void queryPurchasesImpl(java.util.List<String> toCheck, java.util.ArrayList<Purchase> purchases) {
|
||||
if (toCheck == null || toCheck.isEmpty()) {
|
||||
purchasesListQueryCallback(host, purchases);
|
||||
} else {
|
||||
billingClient.queryPurchasesAsync(QueryPurchasesParams.newBuilder().setProductType(toCheck.get(0)).build(), new PurchasesResponseListener() {
|
||||
@Override
|
||||
public void onQueryPurchasesResponse(BillingResult billingResult, java.util.List<Purchase> list) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
purchases.addAll(list);
|
||||
queryPurchasesImpl(toCheck.subList(1, toCheck.size()), purchases);
|
||||
} else {
|
||||
queryPurchasesImpl(null, purchases);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void queryPurchases() {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String[] toCheck = {BillingClient.ProductType.INAPP, BillingClient.ProductType.SUBS};
|
||||
queryPurchasesImpl(java.util.Arrays.asList(toCheck), new java.util.ArrayList<Purchase>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 final BillingClient billingClient;
|
||||
}
|
||||
+1073
File diff suppressed because it is too large
Load Diff
+833
@@ -0,0 +1,833 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2022 - 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 7 End-User License
|
||||
Agreement and JUCE Privacy Policy.
|
||||
|
||||
End User License Agreement: www.juce.com/juce-7-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 InAppPurchases::Pimpl
|
||||
{
|
||||
/** AppStore implementation of hosted content download. */
|
||||
struct DownloadImpl : public Download
|
||||
{
|
||||
explicit 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
|
||||
{
|
||||
if (@available (macOS 10.15, *))
|
||||
return download.expectedContentLength;
|
||||
|
||||
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;
|
||||
NSUniquePtr<SKProductsRequest> request;
|
||||
};
|
||||
|
||||
/** Represents a pending request started from [SKReceiptRefreshRequest start]. */
|
||||
struct PendingReceiptRefreshRequest
|
||||
{
|
||||
String subscriptionsSharedSecret;
|
||||
NSUniquePtr<SKReceiptRefreshRequest> 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;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
explicit Pimpl (InAppPurchases& p)
|
||||
: owner (p)
|
||||
{
|
||||
Class::setThis (delegate.get(), this);
|
||||
[[SKPaymentQueue defaultQueue] addTransactionObserver: delegate.get()];
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
[[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.emplace_back (new PendingProductInfoRequest { PendingProductInfoRequest::Type::query,
|
||||
NSUniquePtr<SKProductsRequest> (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.emplace_back (new PendingProductInfoRequest { PendingProductInfoRequest::Type::purchase,
|
||||
NSUniquePtr<SKProductsRequest> (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.emplace_back (new PendingReceiptRefreshRequest { subscriptionsSharedSecret,
|
||||
NSUniquePtr<SKReceiptRefreshRequest> ([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 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)
|
||||
{
|
||||
const auto succeeded = [&]
|
||||
{
|
||||
auto receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
|
||||
|
||||
if (receiptURL == nullptr)
|
||||
return false;
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnullable-to-nonnull-conversion")
|
||||
auto receiptData = [NSData dataWithContentsOfURL: receiptURL];
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
if (receiptData == nullptr)
|
||||
return false;
|
||||
|
||||
fetchReceiptDetailsFromAppStore (receiptData, secret);
|
||||
return true;
|
||||
}();
|
||||
|
||||
if (! succeeded)
|
||||
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: @"receipt-data"];
|
||||
|
||||
if (secret.isNotEmpty())
|
||||
[requestContents setObject: juceStringToNS (secret) forKey: @"password"];
|
||||
|
||||
NSError* error;
|
||||
auto requestData = [NSJSONSerialization dataWithJSONObject: requestContents
|
||||
options: 0
|
||||
error: &error];
|
||||
if (requestData == nil)
|
||||
{
|
||||
sendReceiptFetchFail();
|
||||
return;
|
||||
}
|
||||
|
||||
verifyReceipt ("https://buy.itunes.apple.com/verifyReceipt", requestData);
|
||||
}
|
||||
|
||||
void verifyReceipt (const String& endpoint, NSData* requestData)
|
||||
{
|
||||
const auto nsurl = [NSURL URLWithString: juceStringToNS (endpoint)];
|
||||
|
||||
if (nsurl == nullptr)
|
||||
return;
|
||||
|
||||
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wnullable-to-nonnull-conversion")
|
||||
const auto storeRequest = [NSMutableURLRequest requestWithURL: nsurl];
|
||||
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
|
||||
|
||||
[storeRequest setHTTPMethod: @"POST"];
|
||||
[storeRequest setHTTPBody: requestData];
|
||||
|
||||
constexpr auto sandboxURL = "https://sandbox.itunes.apple.com/verifyReceipt";
|
||||
|
||||
const auto shouldRetryWithSandboxUrl = [isProduction = (endpoint != sandboxURL)] (NSDictionary* receiptDetails)
|
||||
{
|
||||
if (isProduction)
|
||||
if (const auto* status = getAs<NSNumber> (receiptDetails[@"status"]))
|
||||
return [status intValue] == 21007;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
[[[NSURLSession sharedSession] dataTaskWithRequest: storeRequest
|
||||
completionHandler: ^(NSData* responseData, NSURLResponse*, NSError* connectionError)
|
||||
{
|
||||
if (connectionError == nullptr)
|
||||
{
|
||||
NSError* err = nullptr;
|
||||
|
||||
if (NSDictionary* receiptDetails = [NSJSONSerialization JSONObjectWithData: responseData options: 0 error: &err])
|
||||
{
|
||||
if (shouldRetryWithSandboxUrl (receiptDetails))
|
||||
{
|
||||
verifyReceipt (sandboxURL, requestData);
|
||||
return;
|
||||
}
|
||||
|
||||
processReceiptDetails (receiptDetails);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
sendReceiptFetchFailAsync();
|
||||
}] resume];
|
||||
}
|
||||
|
||||
void processReceiptDetails (NSDictionary* receiptDetails)
|
||||
{
|
||||
if (auto receipt = getAs<NSDictionary> (receiptDetails[@"receipt"]))
|
||||
{
|
||||
if (auto bundleId = getAs<NSString> (receipt[@"bundle_id"]))
|
||||
{
|
||||
if (auto inAppPurchases = getAs<NSArray> (receipt[@"in_app"]))
|
||||
{
|
||||
Array<Listener::PurchaseInfo> purchases;
|
||||
|
||||
for (id inAppPurchaseData in inAppPurchases)
|
||||
{
|
||||
auto* purchaseData = getAs<NSDictionary> (inAppPurchaseData);
|
||||
|
||||
if (purchaseData == nullptr)
|
||||
return sendReceiptFetchFailAsync();
|
||||
|
||||
// Ignore products that were cancelled.
|
||||
if (purchaseData[@"cancellation_date"] != nil)
|
||||
continue;
|
||||
|
||||
if (auto transactionId = getAs<NSString> (purchaseData[@"original_transaction_id"]))
|
||||
{
|
||||
if (auto productId = getAs<NSString> (purchaseData[@"product_id"]))
|
||||
{
|
||||
auto purchaseTime = getPurchaseDateMs (purchaseData[@"purchase_date_ms"]);
|
||||
|
||||
if (purchaseTime <= 0)
|
||||
return sendReceiptFetchFailAsync();
|
||||
|
||||
purchases.add ({ { nsStringToJuce (transactionId),
|
||||
nsStringToJuce (productId),
|
||||
nsStringToJuce (bundleId),
|
||||
Time (purchaseTime).toString (true, true, true, true),
|
||||
{} }, {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
if (auto dateAsString = getAs<NSString> (date))
|
||||
{
|
||||
auto formatter = [[NSNumberFormatter alloc] init];
|
||||
[formatter setNumberStyle: NSNumberFormatterDecimalStyle];
|
||||
auto 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;
|
||||
}
|
||||
|
||||
private:
|
||||
auto findPendingProductInfoRequest (SKRequest* r) const
|
||||
{
|
||||
const auto request = getAs<SKProductsRequest> (r);
|
||||
return std::find_if (pendingProductInfoRequests.begin(),
|
||||
pendingProductInfoRequests.end(),
|
||||
[&] (const auto& pending) { return pending->request.get() == request; });
|
||||
}
|
||||
|
||||
auto findPendingReceiptRefreshRequest (SKRequest* r) const
|
||||
{
|
||||
const auto request = getAs<SKReceiptRefreshRequest> (r);
|
||||
return std::find_if (pendingReceiptRefreshRequests.begin(),
|
||||
pendingReceiptRefreshRequests.end(),
|
||||
[&] (const auto& pending) { return pending->request.get() == request; });
|
||||
}
|
||||
|
||||
struct Class final : public ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>>
|
||||
{
|
||||
//==============================================================================
|
||||
Class()
|
||||
: ObjCClass ("SKDelegateAndPaymentObserverBase_")
|
||||
{
|
||||
addIvar<Pimpl*> ("self");
|
||||
|
||||
addMethod (@selector (productsRequest:didReceiveResponse:), [] (id self, SEL, SKProductsRequest* request, SKProductsResponse* response)
|
||||
{
|
||||
auto& t = getThis (self);
|
||||
|
||||
if (const auto iter = t.findPendingProductInfoRequest (request); iter != t.pendingProductInfoRequests.end())
|
||||
{
|
||||
switch (iter->get()->type)
|
||||
{
|
||||
case PendingProductInfoRequest::Type::query:
|
||||
t.notifyProductsInfoReceived (response.products);
|
||||
break;
|
||||
case PendingProductInfoRequest::Type::purchase:
|
||||
t.startPurchase (response.products);
|
||||
break;
|
||||
}
|
||||
|
||||
t.pendingProductInfoRequests.erase (iter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unknown request received!
|
||||
jassertfalse;
|
||||
}
|
||||
});
|
||||
|
||||
addMethod (@selector (requestDidFinish:), [] (id self, SEL, SKRequest* request)
|
||||
{
|
||||
auto& t = getThis (self);
|
||||
|
||||
if (const auto iter = t.findPendingReceiptRefreshRequest (request); iter != t.pendingReceiptRefreshRequests.end())
|
||||
{
|
||||
t.processReceiptRefreshResponseWithSubscriptionsSharedSecret (iter->get()->subscriptionsSharedSecret);
|
||||
t.pendingReceiptRefreshRequests.erase (iter);
|
||||
}
|
||||
|
||||
if (const auto iter = t.findPendingProductInfoRequest (request); iter != t.pendingProductInfoRequests.end())
|
||||
{
|
||||
t.pendingProductInfoRequests.erase (iter);
|
||||
}
|
||||
});
|
||||
|
||||
addMethod (@selector (request:didFailWithError:), [] (id self, SEL, SKRequest* request, NSError* error)
|
||||
{
|
||||
auto& t = getThis (self);
|
||||
|
||||
const auto errorDetails = [&]
|
||||
{
|
||||
if (error == nil)
|
||||
return String();
|
||||
|
||||
const auto description = nsStringToJuce ([error localizedDescription]);
|
||||
|
||||
if (description.isEmpty())
|
||||
return String();
|
||||
|
||||
return ": " + description;
|
||||
}();
|
||||
|
||||
if (const auto iter = t.findPendingReceiptRefreshRequest (request); iter != t.pendingReceiptRefreshRequests.end())
|
||||
{
|
||||
t.owner.listeners.call ([&] (Listener& l)
|
||||
{
|
||||
l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed") + errorDetails);
|
||||
});
|
||||
t.pendingReceiptRefreshRequests.erase (iter);
|
||||
return;
|
||||
}
|
||||
|
||||
if (const auto iter = t.findPendingProductInfoRequest (request); iter != t.pendingProductInfoRequests.end())
|
||||
{
|
||||
switch (iter->get()->type)
|
||||
{
|
||||
case PendingProductInfoRequest::Type::query:
|
||||
t.owner.listeners.call ([&] (Listener& l)
|
||||
{
|
||||
l.purchasesListRestored ({}, false, NEEDS_TRANS ("Product query failed") + errorDetails);
|
||||
});
|
||||
break;
|
||||
case PendingProductInfoRequest::Type::purchase:
|
||||
t.owner.listeners.call ([&] (Listener& l)
|
||||
{
|
||||
l.productPurchaseFinished ({}, false, NEEDS_TRANS ("Purchase request failed") + errorDetails);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
t.pendingProductInfoRequests.erase (iter);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
addMethod (@selector (paymentQueue:updatedTransactions:), [] (id self, SEL, SKPaymentQueue*, NSArray<SKPaymentTransaction*>* transactions)
|
||||
{
|
||||
auto& t = getThis (self);
|
||||
|
||||
for (SKPaymentTransaction* transaction in transactions)
|
||||
{
|
||||
switch (transaction.transactionState)
|
||||
{
|
||||
case SKPaymentTransactionStatePurchasing: break;
|
||||
case SKPaymentTransactionStateDeferred: break;
|
||||
case SKPaymentTransactionStateFailed: t.processTransactionFinish (transaction, false); break;
|
||||
case SKPaymentTransactionStatePurchased: t.processTransactionFinish (transaction, true); break;
|
||||
case SKPaymentTransactionStateRestored: t.processTransactionFinish (transaction, true); break;
|
||||
default: jassertfalse; break; // Unexpected transaction state
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addMethod (@selector (paymentQueue:restoreCompletedTransactionsFailedWithError:), [] (id self, SEL, SKPaymentQueue*, NSError* error)
|
||||
{
|
||||
auto& t = getThis (self);
|
||||
t.owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, nsStringToJuce (error.localizedDescription)); });
|
||||
});
|
||||
|
||||
addMethod (@selector (paymentQueueRestoreCompletedTransactionsFinished:), [] (id self, SEL, SKPaymentQueue*)
|
||||
{
|
||||
auto& t = getThis (self);
|
||||
t.owner.listeners.call ([&t] (Listener& l) { l.purchasesListRestored (t.restoredPurchases, true, NEEDS_TRANS ("Success")); });
|
||||
t.restoredPurchases.clear();
|
||||
});
|
||||
|
||||
addMethod (@selector (paymentQueue:updatedDownloads:), [] (id self, SEL, SKPaymentQueue*, NSArray<SKDownload*>* downloads)
|
||||
{
|
||||
auto& t = getThis (self);
|
||||
|
||||
for (SKDownload* download in downloads)
|
||||
{
|
||||
if (auto* pendingDownload = t.getPendingDownloadFor (download))
|
||||
{
|
||||
#if JUCE_IOS
|
||||
switch (download.downloadState)
|
||||
#else
|
||||
switch (download.state)
|
||||
#endif
|
||||
{
|
||||
case SKDownloadStateWaiting:
|
||||
break;
|
||||
case SKDownloadStatePaused:
|
||||
t.owner.listeners.call ([&] (Listener& l) { l.productDownloadPaused (*pendingDownload); });
|
||||
break;
|
||||
case SKDownloadStateActive:
|
||||
t.owner.listeners.call ([&] (Listener& l)
|
||||
{
|
||||
l.productDownloadProgressUpdate (*pendingDownload,
|
||||
download.progress,
|
||||
RelativeTime (download.timeRemaining));
|
||||
});
|
||||
break;
|
||||
case SKDownloadStateFinished:
|
||||
case SKDownloadStateFailed:
|
||||
case SKDownloadStateCancelled:
|
||||
t.processDownloadFinish (pendingDownload, download);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unexpected download state
|
||||
jassertfalse;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static Pimpl& getThis (id self) { return *getIvar<Pimpl*> (self, "self"); }
|
||||
static void setThis (id self, Pimpl* s) { object_setInstanceVariable (self, "self", s); }
|
||||
|
||||
static Class& get()
|
||||
{
|
||||
static Class c;
|
||||
return c;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
InAppPurchases& owner;
|
||||
|
||||
NSUniquePtr<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> delegate { [Class::get().createInstance() init] };
|
||||
|
||||
std::vector<std::unique_ptr<PendingProductInfoRequest>> pendingProductInfoRequests;
|
||||
std::vector<std::unique_ptr<PendingReceiptRefreshRequest>> pendingReceiptRefreshRequests;
|
||||
|
||||
OwnedArray<PendingDownloadsTransaction> pendingDownloadsTransactions;
|
||||
Array<Listener::PurchaseInfo> restoredPurchases;
|
||||
};
|
||||
|
||||
} // namespace juce
|
||||
Reference in New Issue
Block a user