git subrepo clone --branch=sono6good https://github.com/essej/JUCE.git deps/juce
subrepo: subdir: "deps/juce" merged: "b13f9084e" upstream: origin: "https://github.com/essej/JUCE.git" branch: "sono6good" commit: "b13f9084e" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo.git" commit: "2f68596"
This commit is contained in:
135
deps/juce/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp
vendored
Normal file
135
deps/juce/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp
vendored
Normal file
@ -0,0 +1,135 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
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,
|
||||
const String& upgradeProductIdentifier,
|
||||
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"); });
|
||||
ignoreUnused (upgradeProductIdentifier, creditForUnusedSubscription);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::restoreProductsBoughtList (bool includeDownloadInfo, 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"); });
|
||||
ignoreUnused (includeDownloadInfo, subscriptionsSharedSecret);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::consumePurchase (const String& productIdentifier, 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"); });
|
||||
ignoreUnused (purchaseToken);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::addListener (Listener* l) { listeners.add (l); }
|
||||
void InAppPurchases::removeListener (Listener* l) { listeners.remove (l); }
|
||||
|
||||
void InAppPurchases::startDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->startDownloads (downloads);
|
||||
#else
|
||||
ignoreUnused (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::pauseDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->pauseDownloads (downloads);
|
||||
#else
|
||||
ignoreUnused (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::resumeDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->resumeDownloads (downloads);
|
||||
#else
|
||||
ignoreUnused (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InAppPurchases::cancelDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
pimpl->cancelDownloads (downloads);
|
||||
#else
|
||||
ignoreUnused (downloads);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace juce
|
298
deps/juce/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h
vendored
Normal file
298
deps/juce/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h
vendored
Normal file
@ -0,0 +1,298 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
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;
|
||||
|
||||
/** A unique identifier of in-app product that was purchased. */
|
||||
String productId;
|
||||
|
||||
/** 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 to.
|
||||
|
||||
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::purchasesListReceived()
|
||||
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,
|
||||
bool isSubscription,
|
||||
const StringArray& upgradeOrDowngradeFromSubscriptionsWithProductIdentifiers = {},
|
||||
bool creditForUnusedSubscription = true)
|
||||
{
|
||||
|
||||
ignoreUnused (isSubscription);
|
||||
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
|
68
deps/juce/modules/juce_product_unlocking/juce_product_unlocking.cpp
vendored
Normal file
68
deps/juce/modules/juce_product_unlocking/juce_product_unlocking.cpp
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#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_android_InAppPurchases.cpp"
|
||||
#elif JUCE_IOS || JUCE_MAC
|
||||
#include "native/juce_ios_InAppPurchases.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
|
95
deps/juce/modules/juce_product_unlocking/juce_product_unlocking.h
vendored
Normal file
95
deps/juce/modules/juce_product_unlocking/juce_product_unlocking.h
vendored
Normal file
@ -0,0 +1,95 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
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: 6.1.2
|
||||
name: JUCE Online marketplace support
|
||||
description: Classes for online product authentication
|
||||
website: http://www.juce.com/juce
|
||||
license: GPL/Commercial
|
||||
minimumCppStandard: 14
|
||||
|
||||
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
|
26
deps/juce/modules/juce_product_unlocking/juce_product_unlocking.mm
vendored
Normal file
26
deps/juce/modules/juce_product_unlocking/juce_product_unlocking.mm
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "juce_product_unlocking.cpp"
|
115
deps/juce/modules/juce_product_unlocking/marketplace/juce_KeyFileGeneration.h
vendored
Normal file
115
deps/juce/modules/juce_product_unlocking/marketplace/juce_KeyFileGeneration.h
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
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 Joe_Bloggs joebloggs@foobar.com 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
|
319
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockForm.cpp
vendored
Normal file
319
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockForm.cpp
vendored
Normal file
@ -0,0 +1,319 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
struct Spinner : 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 : 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 (4);
|
||||
}
|
||||
|
||||
~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())
|
||||
{
|
||||
AlertWindow::showMessageBoxAsync (MessageBoxIconType::WarningIcon,
|
||||
TRANS("Registration Failed"),
|
||||
result.errorMessage);
|
||||
}
|
||||
else if (result.informativeMessage.isNotEmpty())
|
||||
{
|
||||
AlertWindow::showMessageBoxAsync (MessageBoxIconType::InfoIcon,
|
||||
TRANS("Registration Complete!"),
|
||||
result.informativeMessage);
|
||||
}
|
||||
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
|
99
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockForm.h
vendored
Normal file
99
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockForm.h
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/** 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;
|
||||
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
|
506
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp
vendored
Normal file
506
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.cpp
vendored
Normal file
@ -0,0 +1,506 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/* 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()));
|
||||
}
|
||||
|
||||
StringArray OnlineUnlockStatus::MachineIDUtilities::getLocalMachineIDs()
|
||||
{
|
||||
auto identifiers = SystemStats::getDeviceIdentifiers();
|
||||
|
||||
for (auto& identifier : identifiers)
|
||||
identifier = getEncodedIDString (identifier);
|
||||
|
||||
return identifiers;
|
||||
}
|
||||
|
||||
StringArray OnlineUnlockStatus::getLocalMachineIDs()
|
||||
{
|
||||
return MachineIDUtilities::getLocalMachineIDs();
|
||||
}
|
||||
|
||||
void OnlineUnlockStatus::userCancelled()
|
||||
{
|
||||
}
|
||||
|
||||
void OnlineUnlockStatus::setUserEmail (const String& usernameOrEmail)
|
||||
{
|
||||
status.setProperty (userNameProp, usernameOrEmail, nullptr);
|
||||
}
|
||||
|
||||
String OnlineUnlockStatus::getUserEmail() const
|
||||
{
|
||||
return status[userNameProp].toString();
|
||||
}
|
||||
|
||||
bool OnlineUnlockStatus::applyKeyFile (String keyFileContent)
|
||||
{
|
||||
KeyFileUtils::KeyFileData data;
|
||||
data = KeyFileUtils::getDataFromKeyFile (KeyFileUtils::getXmlFromKeyFile (keyFileContent, getPublicKey()));
|
||||
|
||||
if (data.licensee.isNotEmpty() && data.email.isNotEmpty() && doesProductIDMatch (data.appID))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if ((! dummyResult) && actualResult)
|
||||
status.setProperty (unlockedProp, actualResult, nullptr);
|
||||
|
||||
return isUnlocked();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool canConnectToWebsite (const URL& url)
|
||||
{
|
||||
return url.createInputStream (URL::InputStreamOptions (URL::ParameterHandling::inAddress)
|
||||
.withConnectionTimeoutMs (2000)) != nullptr;
|
||||
}
|
||||
|
||||
static bool areMajorWebsitesAvailable()
|
||||
{
|
||||
const char* urlsToTry[] = { "http://google.com", "http://bing.com", "http://amazon.com",
|
||||
"https://google.com", "https://bing.com", "https://amazon.com", nullptr};
|
||||
|
||||
for (const char** url = urlsToTry; *url != nullptr; ++url)
|
||||
if (canConnectToWebsite (URL (*url)))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
OnlineUnlockStatus::UnlockResult OnlineUnlockStatus::handleXmlReply (XmlElement xml)
|
||||
{
|
||||
UnlockResult r;
|
||||
|
||||
if (auto keyNode = xml.getChildByName ("KEY"))
|
||||
{
|
||||
const String keyText (keyNode->getAllSubText().trim());
|
||||
r.succeeded = keyText.length() > 10 && applyKeyFile (keyText);
|
||||
}
|
||||
else
|
||||
{
|
||||
r.succeeded = false;
|
||||
}
|
||||
|
||||
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
|
277
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.h
vendored
Normal file
277
deps/juce/modules/juce_product_unlocking/marketplace/juce_OnlineUnlockStatus.h
vendored
Normal file
@ -0,0 +1,277 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
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;
|
||||
|
||||
/** 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.
|
||||
*/
|
||||
bool applyKeyFile (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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
static StringArray getLocalMachineIDs();
|
||||
};
|
||||
|
||||
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
|
90
deps/juce/modules/juce_product_unlocking/marketplace/juce_TracktionMarketplaceStatus.cpp
vendored
Normal file
90
deps/juce/modules/juce_product_unlocking/marketplace/juce_TracktionMarketplaceStatus.cpp
vendored
Normal file
@ -0,0 +1,90 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
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
|
64
deps/juce/modules/juce_product_unlocking/marketplace/juce_TracktionMarketplaceStatus.h
vendored
Normal file
64
deps/juce/modules/juce_product_unlocking/marketplace/juce_TracktionMarketplaceStatus.h
vendored
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
/**
|
||||
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
|
215
deps/juce/modules/juce_product_unlocking/native/javaopt/app/com/rmsl/juce/JuceBillingClient.java
vendored
Normal file
215
deps/juce/modules/juce_product_unlocking/native/javaopt/app/com/rmsl/juce/JuceBillingClient.java
vendored
Normal file
@ -0,0 +1,215 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
package com.rmsl.juce;
|
||||
|
||||
import com.android.billingclient.api.*;
|
||||
|
||||
public class JuceBillingClient implements PurchasesUpdatedListener, BillingClientStateListener {
|
||||
private native void skuDetailsQueryCallback(long host, java.util.List<SkuDetails> skuDetails);
|
||||
private native void purchasesListQueryCallback(long host, java.util.List<Purchase> purchases);
|
||||
private native void purchaseCompletedCallback(long host, Purchase purchase, int responseCode);
|
||||
private native void purchaseConsumedCallback(long host, String productIdentifier, int responseCode);
|
||||
|
||||
public JuceBillingClient(android.content.Context context, long hostToUse) {
|
||||
host = hostToUse;
|
||||
|
||||
billingClient = BillingClient.newBuilder(context)
|
||||
.enablePendingPurchases()
|
||||
.setListener(this)
|
||||
.build();
|
||||
|
||||
billingClient.startConnection(this);
|
||||
}
|
||||
|
||||
public void endConnection() {
|
||||
billingClient.endConnection();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return billingClient.isReady();
|
||||
}
|
||||
|
||||
public boolean isBillingSupported() {
|
||||
return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).getResponseCode()
|
||||
== BillingClient.BillingResponseCode.OK;
|
||||
}
|
||||
|
||||
public void querySkuDetails(final String[] skusToQuery) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final java.util.List<String> skuList = java.util.Arrays.asList(skusToQuery);
|
||||
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
|
||||
.setSkusList(skuList)
|
||||
.setType(BillingClient.SkuType.INAPP);
|
||||
|
||||
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, final java.util.List<SkuDetails> inAppSkuDetails) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder()
|
||||
.setSkusList(skuList)
|
||||
.setType(BillingClient.SkuType.SUBS);
|
||||
|
||||
billingClient.querySkuDetailsAsync(params.build(), new SkuDetailsResponseListener() {
|
||||
@Override
|
||||
public void onSkuDetailsResponse(BillingResult billingResult, java.util.List<SkuDetails> subsSkuDetails) {
|
||||
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
subsSkuDetails.addAll(inAppSkuDetails);
|
||||
skuDetailsQueryCallback(host, subsSkuDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void launchBillingFlow(final android.app.Activity activity, final BillingFlowParams params) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
BillingResult r = billingClient.launchBillingFlow(activity, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void queryPurchases() {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Purchase.PurchasesResult inAppPurchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
|
||||
Purchase.PurchasesResult subsPurchases = billingClient.queryPurchases(BillingClient.SkuType.SUBS);
|
||||
|
||||
if (inAppPurchases.getResponseCode() == BillingClient.BillingResponseCode.OK
|
||||
&& subsPurchases.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
java.util.List<Purchase> purchaseList = inAppPurchases.getPurchasesList();
|
||||
purchaseList.addAll(subsPurchases.getPurchasesList());
|
||||
|
||||
purchasesListQueryCallback(host, purchaseList);
|
||||
return;
|
||||
}
|
||||
|
||||
purchasesListQueryCallback(host, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void consumePurchase(final String productIdentifier, final String purchaseToken) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ConsumeParams consumeParams = ConsumeParams.newBuilder()
|
||||
.setPurchaseToken(purchaseToken)
|
||||
.build();
|
||||
|
||||
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
|
||||
@Override
|
||||
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
|
||||
purchaseConsumedCallback(host, productIdentifier, billingResult.getResponseCode());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPurchasesUpdated(BillingResult result, java.util.List<Purchase> purchases) {
|
||||
int responseCode = result.getResponseCode();
|
||||
|
||||
if (purchases != null) {
|
||||
for (Purchase purchase : purchases) {
|
||||
handlePurchase(purchase, responseCode);
|
||||
}
|
||||
} else {
|
||||
purchaseCompletedCallback(host, null, responseCode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResult)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void executeOnBillingClientConnection(Runnable runnable) {
|
||||
if (billingClient.isReady()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
connectAndExecute(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectAndExecute(final Runnable executeOnSuccess) {
|
||||
billingClient.startConnection(new BillingClientStateListener() {
|
||||
@Override
|
||||
public void onBillingSetupFinished(BillingResult billingResponse) {
|
||||
if (billingResponse.getResponseCode() == BillingClient.BillingResponseCode.OK) {
|
||||
if (executeOnSuccess != null) {
|
||||
executeOnSuccess.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBillingServiceDisconnected() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handlePurchase(final Purchase purchase, final int responseCode) {
|
||||
purchaseCompletedCallback(host, purchase, responseCode);
|
||||
|
||||
if (responseCode == BillingClient.BillingResponseCode.OK
|
||||
&& purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED
|
||||
&& !purchase.isAcknowledged()) {
|
||||
executeOnBillingClientConnection(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
AcknowledgePurchaseParams acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchase.getPurchaseToken()).build();
|
||||
billingClient.acknowledgePurchase(acknowledgePurchaseParams, new AcknowledgePurchaseResponseListener() {
|
||||
@Override
|
||||
public void onAcknowledgePurchaseResponse(BillingResult billingResult) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private long host = 0;
|
||||
private BillingClient billingClient;
|
||||
}
|
521
deps/juce/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp
vendored
Normal file
521
deps/juce/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp
vendored
Normal file
@ -0,0 +1,521 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (getSku, "getSku", "()Ljava/lang/String;") \
|
||||
METHOD (getTitle, "getTitle", "()Ljava/lang/String;") \
|
||||
METHOD (getDescription, "getDescription", "()Ljava/lang/String;") \
|
||||
METHOD (getPrice, "getPrice", "()Ljava/lang/String;") \
|
||||
METHOD (getPriceCurrencyCode, "getPriceCurrencyCode", "()Ljava/lang/String;")
|
||||
|
||||
DECLARE_JNI_CLASS (SkuDetails, "com/android/billingclient/api/SkuDetails")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
STATICMETHOD (newBuilder, "newBuilder", "()Lcom/android/billingclient/api/BillingFlowParams$Builder;")
|
||||
|
||||
DECLARE_JNI_CLASS (BillingFlowParams, "com/android/billingclient/api/BillingFlowParams")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (build, "build", "()Lcom/android/billingclient/api/BillingFlowParams;") \
|
||||
METHOD (setOldSku, "setOldSku", "(Ljava/lang/String;Ljava/lang/String;)Lcom/android/billingclient/api/BillingFlowParams$Builder;") \
|
||||
METHOD (setReplaceSkusProrationMode, "setReplaceSkusProrationMode", "(I)Lcom/android/billingclient/api/BillingFlowParams$Builder;") \
|
||||
METHOD (setSkuDetails, "setSkuDetails", "(Lcom/android/billingclient/api/SkuDetails;)Lcom/android/billingclient/api/BillingFlowParams$Builder;")
|
||||
|
||||
DECLARE_JNI_CLASS (BillingFlowParamsBuilder, "com/android/billingclient/api/BillingFlowParams$Builder")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (getOrderId, "getOrderId", "()Ljava/lang/String;") \
|
||||
METHOD (getSku, "getSku", "()Ljava/lang/String;") \
|
||||
METHOD (getPackageName, "getPackageName", "()Ljava/lang/String;") \
|
||||
METHOD (getPurchaseTime, "getPurchaseTime", "()J") \
|
||||
METHOD (getPurchaseToken, "getPurchaseToken", "()Ljava/lang/String;")
|
||||
|
||||
DECLARE_JNI_CLASS (AndroidPurchase, "com/android/billingclient/api/Purchase")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
//==============================================================================
|
||||
struct InAppPurchases::Pimpl
|
||||
{
|
||||
Pimpl (InAppPurchases& parent)
|
||||
: owner (parent),
|
||||
billingClient (LocalRef<jobject> (getEnv()->NewObject (JuceBillingClient,
|
||||
JuceBillingClient.constructor,
|
||||
getAppContext().get(),
|
||||
(jlong) this)))
|
||||
{
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
getEnv()->CallVoidMethod (billingClient, JuceBillingClient.endConnection);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isInAppPurchasesSupported() const
|
||||
{
|
||||
return isReady() && getEnv()->CallBooleanMethod (billingClient, JuceBillingClient.isBillingSupported);
|
||||
}
|
||||
|
||||
void getProductsInformation (const StringArray& productIdentifiers)
|
||||
{
|
||||
skuDetailsQueryCallbackQueue.emplace ([this] (LocalRef<jobject> skuDetailsList)
|
||||
{
|
||||
if (skuDetailsList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
Array<InAppPurchases::Product> products;
|
||||
|
||||
for (int i = 0; i < env->CallIntMethod (skuDetailsList, JavaList.size); ++i)
|
||||
products.add (buildProduct (LocalRef<jobject> (env->CallObjectMethod (skuDetailsList, JavaList.get, i))));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.productsInfoReturned (products); });
|
||||
}
|
||||
});
|
||||
|
||||
querySkuDetailsAsync (convertToLowerCase (productIdentifiers));
|
||||
}
|
||||
|
||||
void purchaseProduct (const String& productIdentifier,
|
||||
const String& subscriptionIdentifier,
|
||||
bool creditForUnusedSubscription)
|
||||
{
|
||||
skuDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> skuDetailsList)
|
||||
{
|
||||
if (skuDetailsList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
if (env->CallIntMethod (skuDetailsList, JavaList.size) > 0)
|
||||
{
|
||||
LocalRef<jobject> skuDetails (env->CallObjectMethod (skuDetailsList, JavaList.get, 0));
|
||||
|
||||
if (subscriptionIdentifier.isNotEmpty())
|
||||
changeExistingSubscription (skuDetails, subscriptionIdentifier, creditForUnusedSubscription);
|
||||
else
|
||||
purchaseProductWithSkuDetails (skuDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
querySkuDetailsAsync (convertToLowerCase ({ productIdentifier }));
|
||||
}
|
||||
|
||||
void restoreProductsBoughtList (bool, const juce::String&)
|
||||
{
|
||||
purchasesListQueryCallbackQueue.emplace ([this] (LocalRef<jobject> purchasesList)
|
||||
{
|
||||
if (purchasesList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
Array<InAppPurchases::Listener::PurchaseInfo> purchases;
|
||||
|
||||
for (int i = 0; i < env->CallIntMethod (purchasesList, JavaArrayList.size); ++i)
|
||||
{
|
||||
LocalRef<jobject> purchase (env->CallObjectMethod (purchasesList, JavaArrayList.get, i));
|
||||
purchases.add ({ buildPurchase (purchase), {} });
|
||||
}
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored (purchases, true, NEEDS_TRANS ("Success")); });
|
||||
}
|
||||
else
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Failure")); });
|
||||
}
|
||||
});
|
||||
|
||||
getProductsBoughtAsync();
|
||||
}
|
||||
|
||||
void consumePurchase (const String& productIdentifier, const String& purchaseToken)
|
||||
{
|
||||
if (purchaseToken.isEmpty())
|
||||
{
|
||||
skuDetailsQueryCallbackQueue.emplace ([=] (LocalRef<jobject> skuDetailsList)
|
||||
{
|
||||
if (skuDetailsList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
if (env->CallIntMethod (skuDetailsList, JavaList.size) > 0)
|
||||
{
|
||||
LocalRef<jobject> sku (env->CallObjectMethod (skuDetailsList, JavaList.get, 0));
|
||||
|
||||
auto token = juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (sku, AndroidPurchase.getSku)));
|
||||
|
||||
if (token.isNotEmpty())
|
||||
{
|
||||
consumePurchaseWithToken (productIdentifier, token);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyListenersAboutConsume (productIdentifier, false, NEEDS_TRANS ("Item unavailable"));
|
||||
});
|
||||
|
||||
querySkuDetailsAsync (convertToLowerCase ({ productIdentifier }));
|
||||
}
|
||||
|
||||
consumePurchaseWithToken (productIdentifier, purchaseToken);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void startDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
// Not available on this platform.
|
||||
ignoreUnused (downloads);
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
void pauseDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
// Not available on this platform.
|
||||
ignoreUnused (downloads);
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
void resumeDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
// Not available on this platform.
|
||||
ignoreUnused (downloads);
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
void cancelDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
// Not available on this platform.
|
||||
ignoreUnused (downloads);
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
private:
|
||||
#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \
|
||||
METHOD (constructor, "<init>", "(Landroid/content/Context;J)V") \
|
||||
METHOD (endConnection, "endConnection", "()V") \
|
||||
METHOD (isReady, "isReady", "()Z") \
|
||||
METHOD (isBillingSupported, "isBillingSupported", "()Z") \
|
||||
METHOD (querySkuDetails, "querySkuDetails", "([Ljava/lang/String;)V") \
|
||||
METHOD (launchBillingFlow, "launchBillingFlow", "(Landroid/app/Activity;Lcom/android/billingclient/api/BillingFlowParams;)V") \
|
||||
METHOD (queryPurchases, "queryPurchases", "()V") \
|
||||
METHOD (consumePurchase, "consumePurchase", "(Ljava/lang/String;Ljava/lang/String;)V") \
|
||||
\
|
||||
CALLBACK (skuDetailsQueryCallback, "skuDetailsQueryCallback", "(JLjava/util/List;)V") \
|
||||
CALLBACK (purchasesListQueryCallback, "purchasesListQueryCallback", "(JLjava/util/List;)V") \
|
||||
CALLBACK (purchaseCompletedCallback, "purchaseCompletedCallback", "(JLcom/android/billingclient/api/Purchase;I)V") \
|
||||
CALLBACK (purchaseConsumedCallback, "purchaseConsumedCallback", "(JLjava/lang/String;I)V")
|
||||
|
||||
DECLARE_JNI_CLASS (JuceBillingClient, "com/rmsl/juce/JuceBillingClient")
|
||||
#undef JNI_CLASS_MEMBERS
|
||||
|
||||
static void JNICALL skuDetailsQueryCallback (JNIEnv*, jobject, jlong host, jobject skuDetailsList)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->updateSkuDetails (skuDetailsList);
|
||||
}
|
||||
|
||||
static void JNICALL purchasesListQueryCallback (JNIEnv*, jobject, jlong host, jobject purchasesList)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->updatePurchasesList (purchasesList);
|
||||
}
|
||||
|
||||
static void JNICALL purchaseCompletedCallback (JNIEnv*, jobject, jlong host, jobject purchase, int responseCode)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->purchaseCompleted (purchase, responseCode);
|
||||
}
|
||||
|
||||
static void JNICALL purchaseConsumedCallback (JNIEnv*, jobject, jlong host, jstring productIdentifier, int responseCode)
|
||||
{
|
||||
if (auto* myself = reinterpret_cast<Pimpl*> (host))
|
||||
myself->purchaseConsumed (productIdentifier, responseCode);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
bool isReady() const
|
||||
{
|
||||
return getEnv()->CallBooleanMethod (billingClient, JuceBillingClient.isReady);
|
||||
}
|
||||
|
||||
bool checkIsReady() const
|
||||
{
|
||||
for (int i = 0; i < 10; ++i)
|
||||
{
|
||||
if (isReady())
|
||||
return true;
|
||||
|
||||
Thread::sleep (500);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static StringArray convertToLowerCase (const StringArray& stringsToConvert)
|
||||
{
|
||||
StringArray lowerCase;
|
||||
|
||||
for (auto& s : stringsToConvert)
|
||||
lowerCase.add (s.toLowerCase());
|
||||
|
||||
return lowerCase;
|
||||
}
|
||||
|
||||
void querySkuDetailsAsync (const StringArray& productIdentifiers)
|
||||
{
|
||||
Thread::launch ([=]
|
||||
{
|
||||
if (! checkIsReady())
|
||||
return;
|
||||
|
||||
MessageManager::callAsync ([=]
|
||||
{
|
||||
getEnv()->CallVoidMethod (billingClient,
|
||||
JuceBillingClient.querySkuDetails,
|
||||
juceStringArrayToJava (productIdentifiers).get());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void getProductsBoughtAsync()
|
||||
{
|
||||
Thread::launch ([=]
|
||||
{
|
||||
if (! checkIsReady())
|
||||
return;
|
||||
|
||||
MessageManager::callAsync ([=]
|
||||
{
|
||||
getEnv()->CallVoidMethod (billingClient,
|
||||
JuceBillingClient.queryPurchases);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void notifyListenersAboutPurchase (const InAppPurchases::Purchase& purchase, bool success, const String& statusDescription)
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.productPurchaseFinished ({ purchase, {} }, success, statusDescription); });
|
||||
}
|
||||
|
||||
void notifyListenersAboutConsume (const String& productIdentifier, bool success, const String& statusDescription)
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.productConsumed (productIdentifier, success, statusDescription); });
|
||||
}
|
||||
|
||||
LocalRef<jobject> createBillingFlowParamsBuilder (LocalRef<jobject> skuDetails)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
auto builder = LocalRef<jobject> (env->CallStaticObjectMethod (BillingFlowParams, BillingFlowParams.newBuilder));
|
||||
|
||||
return LocalRef<jobject> (env->CallObjectMethod (builder.get(),
|
||||
BillingFlowParamsBuilder.setSkuDetails,
|
||||
skuDetails.get()));
|
||||
}
|
||||
|
||||
void launchBillingFlowWithParameters (LocalRef<jobject> params)
|
||||
{
|
||||
LocalRef<jobject> activity (getCurrentActivity());
|
||||
|
||||
if (activity == nullptr)
|
||||
activity = getMainActivity();
|
||||
|
||||
getEnv()->CallVoidMethod (billingClient,
|
||||
JuceBillingClient.launchBillingFlow,
|
||||
activity.get(),
|
||||
params.get());
|
||||
}
|
||||
|
||||
void changeExistingSubscription (LocalRef<jobject> skuDetails, const String& subscriptionIdentifier, bool creditForUnusedSubscription)
|
||||
{
|
||||
if (! isReady())
|
||||
{
|
||||
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("In-App purchases unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
purchasesListQueryCallbackQueue.emplace ([=] (LocalRef<jobject> purchasesList)
|
||||
{
|
||||
if (purchasesList != nullptr)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
for (int i = 0; i < env->CallIntMethod (purchasesList, JavaArrayList.size); ++i)
|
||||
{
|
||||
auto purchase = buildPurchase (LocalRef<jobject> (env->CallObjectMethod (purchasesList.get(), JavaArrayList.get, i)));
|
||||
|
||||
if (purchase.productId == subscriptionIdentifier)
|
||||
{
|
||||
auto builder = createBillingFlowParamsBuilder (skuDetails);
|
||||
|
||||
builder = LocalRef<jobject> (env->CallObjectMethod (builder.get(),
|
||||
BillingFlowParamsBuilder.setOldSku,
|
||||
javaString (subscriptionIdentifier).get(),
|
||||
javaString (purchase.purchaseToken).get()));
|
||||
|
||||
if (! creditForUnusedSubscription)
|
||||
builder = LocalRef<jobject> (env->CallObjectMethod (builder.get(),
|
||||
BillingFlowParamsBuilder.setReplaceSkusProrationMode,
|
||||
3 /*IMMEDIATE_WITHOUT_PRORATION*/));
|
||||
|
||||
launchBillingFlowWithParameters (LocalRef<jobject> (env->CallObjectMethod (builder.get(),
|
||||
BillingFlowParamsBuilder.build)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("Unable to get subscription details"));
|
||||
});
|
||||
|
||||
getProductsBoughtAsync();
|
||||
}
|
||||
|
||||
void purchaseProductWithSkuDetails (LocalRef<jobject> skuDetails)
|
||||
{
|
||||
if (! isReady())
|
||||
{
|
||||
notifyListenersAboutPurchase ({}, false, NEEDS_TRANS ("In-App purchases unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
launchBillingFlowWithParameters (LocalRef<jobject> (getEnv()->CallObjectMethod (createBillingFlowParamsBuilder (skuDetails).get(),
|
||||
BillingFlowParamsBuilder.build)));
|
||||
}
|
||||
|
||||
void consumePurchaseWithToken (const String& productIdentifier, const String& purchaseToken)
|
||||
{
|
||||
if (! isReady())
|
||||
{
|
||||
notifyListenersAboutConsume (productIdentifier, false, NEEDS_TRANS ("In-App purchases unavailable"));
|
||||
return;
|
||||
}
|
||||
|
||||
getEnv()->CallObjectMethod (billingClient,
|
||||
JuceBillingClient.consumePurchase,
|
||||
LocalRef<jstring> (javaString (productIdentifier)).get(),
|
||||
LocalRef<jstring> (javaString (purchaseToken)).get());
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static InAppPurchases::Purchase buildPurchase (LocalRef<jobject> purchase)
|
||||
{
|
||||
if (purchase == nullptr)
|
||||
return {};
|
||||
|
||||
auto* env = getEnv();
|
||||
|
||||
return { juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getOrderId))),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getSku))),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getPackageName))),
|
||||
Time (env->CallLongMethod (purchase, AndroidPurchase.getPurchaseTime)).toString (true, true, true, true),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (purchase, AndroidPurchase.getPurchaseToken))) };
|
||||
}
|
||||
|
||||
static InAppPurchases::Product buildProduct (LocalRef<jobject> productSkuDetails)
|
||||
{
|
||||
if (productSkuDetails == nullptr)
|
||||
return {};
|
||||
|
||||
auto* env = getEnv();
|
||||
|
||||
return { juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getSku))),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getTitle))),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getDescription))),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getPrice))),
|
||||
juceString (LocalRef<jstring> ((jstring) env->CallObjectMethod (productSkuDetails, SkuDetails.getPriceCurrencyCode))) };
|
||||
}
|
||||
|
||||
static String getStatusDescriptionFromResponseCode (int responseCode)
|
||||
{
|
||||
switch (responseCode)
|
||||
{
|
||||
case 0: return NEEDS_TRANS ("Success");
|
||||
case 1: return NEEDS_TRANS ("Cancelled by user");
|
||||
case 2: return NEEDS_TRANS ("Service unavailable");
|
||||
case 3: return NEEDS_TRANS ("Billing unavailable");
|
||||
case 4: return NEEDS_TRANS ("Item unavailable");
|
||||
case 5: return NEEDS_TRANS ("Internal error");
|
||||
case 6: return NEEDS_TRANS ("Generic error");
|
||||
case 7: return NEEDS_TRANS ("Item already owned");
|
||||
case 8: return NEEDS_TRANS ("Item not owned");
|
||||
default: return NEEDS_TRANS ("Unknown status");
|
||||
}
|
||||
}
|
||||
|
||||
static bool wasSuccessful (int responseCode)
|
||||
{
|
||||
return responseCode == 0;
|
||||
}
|
||||
|
||||
void purchaseCompleted (jobject purchase, int responseCode)
|
||||
{
|
||||
notifyListenersAboutPurchase (buildPurchase (LocalRef<jobject> (purchase)),
|
||||
wasSuccessful (responseCode),
|
||||
getStatusDescriptionFromResponseCode (responseCode));
|
||||
}
|
||||
|
||||
void purchaseConsumed (jstring productIdentifier, int responseCode)
|
||||
{
|
||||
notifyListenersAboutConsume (juceString (LocalRef<jstring> (productIdentifier)),
|
||||
wasSuccessful (responseCode),
|
||||
getStatusDescriptionFromResponseCode (responseCode));
|
||||
}
|
||||
|
||||
void updateSkuDetails (jobject skuDetailsList)
|
||||
{
|
||||
jassert (! skuDetailsQueryCallbackQueue.empty());
|
||||
skuDetailsQueryCallbackQueue.front() (LocalRef<jobject> (skuDetailsList));
|
||||
skuDetailsQueryCallbackQueue.pop();
|
||||
}
|
||||
|
||||
void updatePurchasesList (jobject purchasesList)
|
||||
{
|
||||
jassert (! purchasesListQueryCallbackQueue.empty());
|
||||
purchasesListQueryCallbackQueue.front() (LocalRef<jobject> (purchasesList));
|
||||
purchasesListQueryCallbackQueue.pop();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
InAppPurchases& owner;
|
||||
GlobalRef billingClient;
|
||||
|
||||
std::queue<std::function<void (LocalRef<jobject>)>> skuDetailsQueryCallbackQueue,
|
||||
purchasesListQueryCallbackQueue;
|
||||
|
||||
//==============================================================================
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
|
||||
InAppPurchases::Pimpl::JuceBillingClient_Class InAppPurchases::Pimpl::JuceBillingClient;
|
||||
|
||||
} // namespace juce
|
755
deps/juce/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp
vendored
Normal file
755
deps/juce/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp
vendored
Normal file
@ -0,0 +1,755 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2020 - Raw Material Software Limited
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
|
||||
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-6-licence
|
||||
Privacy Policy: www.juce.com/juce-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
struct SKDelegateAndPaymentObserver
|
||||
{
|
||||
SKDelegateAndPaymentObserver() : delegate ([getClass().createInstance() init])
|
||||
{
|
||||
Class::setThis (delegate.get(), this);
|
||||
}
|
||||
|
||||
virtual ~SKDelegateAndPaymentObserver() {}
|
||||
|
||||
virtual void didReceiveResponse (SKProductsRequest*, SKProductsResponse*) = 0;
|
||||
virtual void requestDidFinish (SKRequest*) = 0;
|
||||
virtual void requestDidFailWithError (SKRequest*, NSError*) = 0;
|
||||
virtual void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>*) = 0;
|
||||
virtual void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError*) = 0;
|
||||
virtual void restoreCompletedTransactionsFinished (SKPaymentQueue*) = 0;
|
||||
virtual void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>*) = 0;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>, NSObjectDeleter> delegate;
|
||||
|
||||
private:
|
||||
struct Class : public ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>>
|
||||
{
|
||||
//==============================================================================
|
||||
Class() : ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> ("SKDelegateAndPaymentObserverBase_")
|
||||
{
|
||||
addIvar<SKDelegateAndPaymentObserver*> ("self");
|
||||
|
||||
addMethod (@selector (productsRequest:didReceiveResponse:), didReceiveResponse, "v@:@@");
|
||||
addMethod (@selector (requestDidFinish:), requestDidFinish, "v@:@");
|
||||
addMethod (@selector (request:didFailWithError:), requestDidFailWithError, "v@:@@");
|
||||
addMethod (@selector (paymentQueue:updatedTransactions:), updatedTransactions, "v@:@@");
|
||||
addMethod (@selector (paymentQueue:restoreCompletedTransactionsFailedWithError:), restoreCompletedTransactionsFailedWithError, "v@:@@");
|
||||
addMethod (@selector (paymentQueueRestoreCompletedTransactionsFinished:), restoreCompletedTransactionsFinished, "v@:@");
|
||||
addMethod (@selector (paymentQueue:updatedDownloads:), updatedDownloads, "v@:@@");
|
||||
|
||||
registerClass();
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static SKDelegateAndPaymentObserver& getThis (id self) { return *getIvar<SKDelegateAndPaymentObserver*> (self, "self"); }
|
||||
static void setThis (id self, SKDelegateAndPaymentObserver* s) { object_setInstanceVariable (self, "self", s); }
|
||||
|
||||
//==============================================================================
|
||||
static void didReceiveResponse (id self, SEL, SKProductsRequest* request, SKProductsResponse* response) { getThis (self).didReceiveResponse (request, response); }
|
||||
static void requestDidFinish (id self, SEL, SKRequest* request) { getThis (self).requestDidFinish (request); }
|
||||
static void requestDidFailWithError (id self, SEL, SKRequest* request, NSError* err) { getThis (self).requestDidFailWithError (request, err); }
|
||||
static void updatedTransactions (id self, SEL, SKPaymentQueue* queue, NSArray<SKPaymentTransaction*>* trans) { getThis (self).updatedTransactions (queue, trans); }
|
||||
static void restoreCompletedTransactionsFailedWithError (id self, SEL, SKPaymentQueue* q, NSError* err) { getThis (self).restoreCompletedTransactionsFailedWithError (q, err); }
|
||||
static void restoreCompletedTransactionsFinished (id self, SEL, SKPaymentQueue* queue) { getThis (self).restoreCompletedTransactionsFinished (queue); }
|
||||
static void updatedDownloads (id self, SEL, SKPaymentQueue* queue, NSArray<SKDownload*>* downloads) { getThis (self).updatedDownloads (queue, downloads); }
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
static Class& getClass()
|
||||
{
|
||||
static Class c;
|
||||
return c;
|
||||
}
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct InAppPurchases::Pimpl : public SKDelegateAndPaymentObserver
|
||||
{
|
||||
/** AppStore implementation of hosted content download. */
|
||||
struct DownloadImpl : public Download
|
||||
{
|
||||
DownloadImpl (SKDownload* downloadToUse)
|
||||
: download (downloadToUse)
|
||||
{
|
||||
[download retain];
|
||||
}
|
||||
|
||||
~DownloadImpl() override
|
||||
{
|
||||
[download release];
|
||||
}
|
||||
|
||||
String getProductId() const override { return nsStringToJuce (download.contentIdentifier); }
|
||||
String getContentVersion() const override { return nsStringToJuce (download.contentVersion); }
|
||||
|
||||
#if JUCE_IOS
|
||||
int64 getContentLength() const override { return download.contentLength; }
|
||||
Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.downloadState); }
|
||||
#else
|
||||
int64 getContentLength() const override { return download.expectedContentLength; }
|
||||
Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.state); }
|
||||
#endif
|
||||
|
||||
SKDownload* download;
|
||||
};
|
||||
|
||||
/** Represents a pending request initialised with [SKProductRequest start]. */
|
||||
struct PendingProductInfoRequest
|
||||
{
|
||||
enum class Type
|
||||
{
|
||||
query = 0,
|
||||
purchase
|
||||
};
|
||||
|
||||
Type type;
|
||||
std::unique_ptr<SKProductsRequest, NSObjectDeleter> request;
|
||||
};
|
||||
|
||||
/** Represents a pending request started from [SKReceiptRefreshRequest start]. */
|
||||
struct PendingReceiptRefreshRequest
|
||||
{
|
||||
String subscriptionsSharedSecret;
|
||||
std::unique_ptr<SKReceiptRefreshRequest, NSObjectDeleter> request;
|
||||
};
|
||||
|
||||
/** Represents a transaction with pending downloads. Only after all downloads
|
||||
are finished, the transaction is marked as finished. */
|
||||
struct PendingDownloadsTransaction
|
||||
{
|
||||
PendingDownloadsTransaction (SKPaymentTransaction* t) : transaction (t)
|
||||
{
|
||||
addDownloadsFromSKTransaction (transaction);
|
||||
}
|
||||
|
||||
void addDownloadsFromSKTransaction (SKPaymentTransaction* transactionToUse)
|
||||
{
|
||||
for (SKDownload* download in transactionToUse.downloads)
|
||||
downloads.add (new DownloadImpl (download));
|
||||
}
|
||||
|
||||
bool canBeMarkedAsFinished() const
|
||||
{
|
||||
for (SKDownload* d in transaction.downloads)
|
||||
{
|
||||
#if JUCE_IOS
|
||||
SKDownloadState state = d.downloadState;
|
||||
#else
|
||||
SKDownloadState state = d.state;
|
||||
#endif
|
||||
if (state != SKDownloadStateFinished
|
||||
&& state != SKDownloadStateFailed
|
||||
&& state != SKDownloadStateCancelled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
OwnedArray<DownloadImpl> downloads;
|
||||
SKPaymentTransaction* const transaction;
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
Pimpl (InAppPurchases& p) : owner (p) { [[SKPaymentQueue defaultQueue] addTransactionObserver: delegate.get()]; }
|
||||
~Pimpl() noexcept override { [[SKPaymentQueue defaultQueue] removeTransactionObserver: delegate.get()]; }
|
||||
|
||||
//==============================================================================
|
||||
bool isInAppPurchasesSupported() const { return true; }
|
||||
|
||||
void getProductsInformation (const StringArray& productIdentifiers)
|
||||
{
|
||||
auto productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithArray: createNSArrayFromStringArray (productIdentifiers)]];
|
||||
|
||||
pendingProductInfoRequests.add (new PendingProductInfoRequest { PendingProductInfoRequest::Type::query,
|
||||
std::unique_ptr<SKProductsRequest, NSObjectDeleter> (productsRequest) });
|
||||
|
||||
productsRequest.delegate = delegate.get();
|
||||
[productsRequest start];
|
||||
}
|
||||
|
||||
void purchaseProduct (const String& productIdentifier, const String&, bool)
|
||||
{
|
||||
if (! [SKPaymentQueue canMakePayments])
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.productPurchaseFinished ({}, false, NEEDS_TRANS ("Payments not allowed")); });
|
||||
return;
|
||||
}
|
||||
|
||||
auto productIdentifiers = [NSArray arrayWithObject: juceStringToNS (productIdentifier)];
|
||||
auto productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]];
|
||||
|
||||
pendingProductInfoRequests.add (new PendingProductInfoRequest { PendingProductInfoRequest::Type::purchase,
|
||||
std::unique_ptr<SKProductsRequest, NSObjectDeleter> (productsRequest) });
|
||||
|
||||
productsRequest.delegate = delegate.get();
|
||||
[productsRequest start];
|
||||
}
|
||||
|
||||
void restoreProductsBoughtList (bool includeDownloadInfo, const String& subscriptionsSharedSecret)
|
||||
{
|
||||
if (includeDownloadInfo)
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
|
||||
}
|
||||
else
|
||||
{
|
||||
auto receiptRequest = [[SKReceiptRefreshRequest alloc] init];
|
||||
|
||||
pendingReceiptRefreshRequests.add (new PendingReceiptRefreshRequest { subscriptionsSharedSecret,
|
||||
std::unique_ptr<SKReceiptRefreshRequest, NSObjectDeleter> ([receiptRequest retain]) });
|
||||
receiptRequest.delegate = delegate.get();
|
||||
[receiptRequest start];
|
||||
}
|
||||
}
|
||||
|
||||
void consumePurchase (const String&, const String&) {}
|
||||
|
||||
//==============================================================================
|
||||
void startDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] startDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
|
||||
}
|
||||
|
||||
void pauseDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] pauseDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
|
||||
}
|
||||
|
||||
void resumeDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] resumeDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
|
||||
}
|
||||
|
||||
void cancelDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] cancelDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))];
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void didReceiveResponse (SKProductsRequest* request, SKProductsResponse* response) override
|
||||
{
|
||||
for (auto i = 0; i < pendingProductInfoRequests.size(); ++i)
|
||||
{
|
||||
auto& pendingRequest = *pendingProductInfoRequests[i];
|
||||
|
||||
if (pendingRequest.request.get() == request)
|
||||
{
|
||||
if (pendingRequest.type == PendingProductInfoRequest::Type::query) notifyProductsInfoReceived (response.products);
|
||||
else if (pendingRequest.type == PendingProductInfoRequest::Type::purchase) startPurchase (response.products);
|
||||
else break;
|
||||
|
||||
pendingProductInfoRequests.remove (i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Unknown request received!
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
void requestDidFinish (SKRequest* request) override
|
||||
{
|
||||
if (auto receiptRefreshRequest = getAs<SKReceiptRefreshRequest> (request))
|
||||
{
|
||||
for (auto i = 0; i < pendingReceiptRefreshRequests.size(); ++i)
|
||||
{
|
||||
auto& pendingRequest = *pendingReceiptRefreshRequests[i];
|
||||
|
||||
if (pendingRequest.request.get() == receiptRefreshRequest)
|
||||
{
|
||||
processReceiptRefreshResponseWithSubscriptionsSharedSecret (pendingRequest.subscriptionsSharedSecret);
|
||||
pendingReceiptRefreshRequests.remove (i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void requestDidFailWithError (SKRequest* request, NSError* error) override
|
||||
{
|
||||
if (auto receiptRefreshRequest = getAs<SKReceiptRefreshRequest> (request))
|
||||
{
|
||||
for (auto i = 0; i < pendingReceiptRefreshRequests.size(); ++i)
|
||||
{
|
||||
auto& pendingRequest = *pendingReceiptRefreshRequests[i];
|
||||
|
||||
if (pendingRequest.request.get() == receiptRefreshRequest)
|
||||
{
|
||||
auto errorDetails = error != nil ? (", " + nsStringToJuce ([error localizedDescription])) : String();
|
||||
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed") + errorDetails); });
|
||||
pendingReceiptRefreshRequests.remove (i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>* transactions) override
|
||||
{
|
||||
for (SKPaymentTransaction* transaction in transactions)
|
||||
{
|
||||
switch (transaction.transactionState)
|
||||
{
|
||||
case SKPaymentTransactionStatePurchasing: break;
|
||||
case SKPaymentTransactionStateDeferred: break;
|
||||
case SKPaymentTransactionStateFailed: processTransactionFinish (transaction, false); break;
|
||||
case SKPaymentTransactionStatePurchased: processTransactionFinish (transaction, true); break;
|
||||
case SKPaymentTransactionStateRestored: processTransactionFinish (transaction, true); break;
|
||||
default: jassertfalse; break; // Unexpected transaction state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError* error) override
|
||||
{
|
||||
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, nsStringToJuce (error.localizedDescription)); });
|
||||
}
|
||||
|
||||
void restoreCompletedTransactionsFinished (SKPaymentQueue*) override
|
||||
{
|
||||
owner.listeners.call ([this] (Listener& l) { l.purchasesListRestored (restoredPurchases, true, NEEDS_TRANS ("Success")); });
|
||||
restoredPurchases.clear();
|
||||
}
|
||||
|
||||
void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>* downloads) override
|
||||
{
|
||||
for (SKDownload* download in downloads)
|
||||
{
|
||||
if (auto* pendingDownload = getPendingDownloadFor (download))
|
||||
{
|
||||
#if JUCE_IOS
|
||||
switch (download.downloadState)
|
||||
#else
|
||||
switch (download.state)
|
||||
#endif
|
||||
{
|
||||
case SKDownloadStateWaiting: break;
|
||||
case SKDownloadStatePaused: owner.listeners.call ([&] (Listener& l) { l.productDownloadPaused (*pendingDownload); }); break;
|
||||
case SKDownloadStateActive: owner.listeners.call ([&] (Listener& l) { l.productDownloadProgressUpdate (*pendingDownload,
|
||||
download.progress,
|
||||
RelativeTime (download.timeRemaining)); }); break;
|
||||
case SKDownloadStateFinished:
|
||||
case SKDownloadStateFailed:
|
||||
case SKDownloadStateCancelled: processDownloadFinish (pendingDownload, download); break;
|
||||
|
||||
default: jassertfalse; break; // Unexpected download state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void notifyProductsInfoReceived (NSArray<SKProduct*>* products)
|
||||
{
|
||||
Array<Product> productsToReturn;
|
||||
|
||||
for (SKProduct* skProduct in products)
|
||||
productsToReturn.add (SKProductToIAPProduct (skProduct));
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.productsInfoReturned (productsToReturn); });
|
||||
}
|
||||
|
||||
void startPurchase (NSArray<SKProduct*>* products)
|
||||
{
|
||||
if ([products count] > 0)
|
||||
{
|
||||
// Only one product can be bought at once!
|
||||
jassert ([products count] == 1);
|
||||
|
||||
auto* product = products[0];
|
||||
auto payment = [SKPayment paymentWithProduct: product];
|
||||
[[SKPaymentQueue defaultQueue] addPayment: payment];
|
||||
}
|
||||
else
|
||||
{
|
||||
owner.listeners.call ([] (Listener& l) { l.productPurchaseFinished ({}, false, NEEDS_TRANS ("Your app is not setup for payments")); });
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
Array<Download*> removeInvalidDownloads (const Array<Download*>& downloadsToUse)
|
||||
{
|
||||
Array<Download*> downloads (downloadsToUse);
|
||||
|
||||
for (int i = downloads.size(); --i >= 0;)
|
||||
{
|
||||
auto hasPendingDownload = hasDownloadInPendingDownloadsTransaction (*downloads[i]);
|
||||
|
||||
// Invalid download passed, it does not exist in pending downloads list
|
||||
jassert (hasPendingDownload);
|
||||
|
||||
if (! hasPendingDownload)
|
||||
downloads.remove (i);
|
||||
}
|
||||
|
||||
return downloads;
|
||||
}
|
||||
|
||||
bool hasDownloadInPendingDownloadsTransaction (const Download& download)
|
||||
{
|
||||
for (auto* pdt : pendingDownloadsTransactions)
|
||||
for (auto* pendingDownload : pdt->downloads)
|
||||
if (pendingDownload == &download)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void processTransactionFinish (SKPaymentTransaction* transaction, bool success)
|
||||
{
|
||||
auto orderId = nsStringToJuce (transaction.transactionIdentifier);
|
||||
auto packageName = nsStringToJuce ([[NSBundle mainBundle] bundleIdentifier]);
|
||||
auto productId = nsStringToJuce (transaction.payment.productIdentifier);
|
||||
auto purchaseTime = Time (1000 * (int64) transaction.transactionDate.timeIntervalSince1970)
|
||||
.toString (true, true, true, true);
|
||||
|
||||
Purchase purchase { orderId, productId, packageName, purchaseTime, {} };
|
||||
|
||||
Array<Download*> downloads;
|
||||
|
||||
// If transaction failed or there are no downloads, finish the transaction immediately, otherwise
|
||||
// finish the transaction only after all downloads are finished.
|
||||
if (transaction.transactionState == SKPaymentTransactionStateFailed
|
||||
|| transaction.downloads == nil
|
||||
|| [transaction.downloads count] == 0)
|
||||
{
|
||||
[[SKPaymentQueue defaultQueue] finishTransaction: transaction];
|
||||
}
|
||||
else
|
||||
{
|
||||
// On application startup or when the app is resumed we may receive multiple
|
||||
// "purchased" callbacks with the same underlying transaction. Sadly, only
|
||||
// the last set of downloads will be valid.
|
||||
auto* pdt = getPendingDownloadsTransactionForSKTransaction (transaction);
|
||||
|
||||
if (pdt == nullptr)
|
||||
{
|
||||
pdt = pendingDownloadsTransactions.add (new PendingDownloadsTransaction (transaction));
|
||||
}
|
||||
else
|
||||
{
|
||||
pdt->downloads.clear();
|
||||
pdt->addDownloadsFromSKTransaction (transaction);
|
||||
}
|
||||
|
||||
for (auto* download : pdt->downloads)
|
||||
downloads.add (download);
|
||||
}
|
||||
|
||||
if (transaction.transactionState == SKPaymentTransactionStateRestored)
|
||||
restoredPurchases.add ({ purchase, downloads });
|
||||
else
|
||||
owner.listeners.call ([&] (Listener& l) { l.productPurchaseFinished ({ purchase, downloads }, success,
|
||||
SKPaymentTransactionStateToString (transaction.transactionState)); });
|
||||
}
|
||||
|
||||
PendingDownloadsTransaction* getPendingDownloadsTransactionForSKTransaction (SKPaymentTransaction* transaction)
|
||||
{
|
||||
for (auto* pdt : pendingDownloadsTransactions)
|
||||
if (pdt->transaction == transaction)
|
||||
return pdt;
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
PendingDownloadsTransaction* getPendingDownloadsTransactionSKDownloadFor (SKDownload* download)
|
||||
{
|
||||
for (auto* pdt : pendingDownloadsTransactions)
|
||||
for (auto* pendingDownload : pdt->downloads)
|
||||
if (pendingDownload->download == download)
|
||||
return pdt;
|
||||
|
||||
jassertfalse;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Download* getPendingDownloadFor (SKDownload* download)
|
||||
{
|
||||
if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download))
|
||||
for (auto* pendingDownload : pdt->downloads)
|
||||
if (pendingDownload->download == download)
|
||||
return pendingDownload;
|
||||
|
||||
jassertfalse;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void processDownloadFinish (Download* pendingDownload, SKDownload* download)
|
||||
{
|
||||
if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download))
|
||||
{
|
||||
#if JUCE_IOS
|
||||
SKDownloadState state = download.downloadState;
|
||||
#else
|
||||
SKDownloadState state = download.state;
|
||||
#endif
|
||||
|
||||
auto contentURL = state == SKDownloadStateFinished
|
||||
? URL (nsStringToJuce (download.contentURL.absoluteString))
|
||||
: URL();
|
||||
|
||||
owner.listeners.call ([&] (Listener& l) { l.productDownloadFinished (*pendingDownload, contentURL); });
|
||||
|
||||
if (pdt->canBeMarkedAsFinished())
|
||||
{
|
||||
// All downloads finished, mark transaction as finished too.
|
||||
[[SKPaymentQueue defaultQueue] finishTransaction: pdt->transaction];
|
||||
|
||||
pendingDownloadsTransactions.removeObject (pdt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
void processReceiptRefreshResponseWithSubscriptionsSharedSecret (const String& secret)
|
||||
{
|
||||
auto receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
|
||||
|
||||
if (auto receiptData = [NSData dataWithContentsOfURL: receiptURL])
|
||||
fetchReceiptDetailsFromAppStore (receiptData, secret);
|
||||
else
|
||||
owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed")); });
|
||||
}
|
||||
|
||||
void fetchReceiptDetailsFromAppStore (NSData* receiptData, const String& secret)
|
||||
{
|
||||
auto requestContents = [NSMutableDictionary dictionaryWithCapacity: (NSUInteger) (secret.isNotEmpty() ? 2 : 1)];
|
||||
[requestContents setObject: [receiptData base64EncodedStringWithOptions:0] forKey: nsStringLiteral ("receipt-data")];
|
||||
|
||||
if (secret.isNotEmpty())
|
||||
[requestContents setObject: juceStringToNS (secret) forKey: nsStringLiteral ("password")];
|
||||
|
||||
NSError* error;
|
||||
auto requestData = [NSJSONSerialization dataWithJSONObject: requestContents
|
||||
options: 0
|
||||
error: &error];
|
||||
if (requestData == nil)
|
||||
{
|
||||
sendReceiptFetchFail();
|
||||
return;
|
||||
}
|
||||
|
||||
#if JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT
|
||||
auto storeURL = "https://sandbox.itunes.apple.com/verifyReceipt";
|
||||
#else
|
||||
auto storeURL = "https://buy.itunes.apple.com/verifyReceipt";
|
||||
#endif
|
||||
|
||||
// TODO: use juce URL here
|
||||
auto* urlPtr = [NSURL URLWithString: nsStringLiteral (storeURL)];
|
||||
auto storeRequest = [NSMutableURLRequest requestWithURL: urlPtr];
|
||||
[storeRequest setHTTPMethod: nsStringLiteral ("POST")];
|
||||
[storeRequest setHTTPBody: requestData];
|
||||
|
||||
auto task = [[NSURLSession sharedSession] dataTaskWithRequest: storeRequest
|
||||
completionHandler:
|
||||
^(NSData* data, NSURLResponse*, NSError* connectionError)
|
||||
{
|
||||
if (connectionError != nil)
|
||||
{
|
||||
sendReceiptFetchFail();
|
||||
}
|
||||
else
|
||||
{
|
||||
NSError* err;
|
||||
|
||||
if (NSDictionary* receiptDetails = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &err])
|
||||
processReceiptDetails (receiptDetails);
|
||||
else
|
||||
sendReceiptFetchFail();
|
||||
}
|
||||
}];
|
||||
|
||||
[task resume];
|
||||
}
|
||||
|
||||
void processReceiptDetails (NSDictionary* receiptDetails)
|
||||
{
|
||||
if (auto receipt = getAs<NSDictionary> (receiptDetails[nsStringLiteral ("receipt")]))
|
||||
{
|
||||
if (auto bundleId = getAs<NSString> (receipt[nsStringLiteral ("bundle_id")]))
|
||||
{
|
||||
if (auto inAppPurchases = getAs<NSArray> (receipt[nsStringLiteral ("in_app")]))
|
||||
{
|
||||
Array<Listener::PurchaseInfo> purchases;
|
||||
|
||||
for (id inAppPurchaseData in inAppPurchases)
|
||||
{
|
||||
if (auto* purchaseData = getAs<NSDictionary> (inAppPurchaseData))
|
||||
{
|
||||
// Ignore products that were cancelled.
|
||||
if (purchaseData[nsStringLiteral ("cancellation_date")] != nil)
|
||||
continue;
|
||||
|
||||
if (auto transactionId = getAs<NSString> (purchaseData[nsStringLiteral ("original_transaction_id")]))
|
||||
{
|
||||
if (auto productId = getAs<NSString> (purchaseData[nsStringLiteral ("product_id")]))
|
||||
{
|
||||
auto purchaseTime = getPurchaseDateMs (purchaseData[nsStringLiteral ("purchase_date_ms")]);
|
||||
|
||||
if (purchaseTime > 0)
|
||||
{
|
||||
purchases.add ({ { nsStringToJuce (transactionId),
|
||||
nsStringToJuce (productId),
|
||||
nsStringToJuce (bundleId),
|
||||
Time (purchaseTime).toString (true, true, true, true),
|
||||
{} }, {} });
|
||||
}
|
||||
else
|
||||
{
|
||||
return sendReceiptFetchFailAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return sendReceiptFetchFailAsync();
|
||||
}
|
||||
}
|
||||
|
||||
MessageManager::callAsync ([this, purchases] { owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored (purchases, true, NEEDS_TRANS ("Success")); }); });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendReceiptFetchFailAsync();
|
||||
}
|
||||
|
||||
void sendReceiptFetchFail()
|
||||
{
|
||||
owner.listeners.call ([] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed")); });
|
||||
}
|
||||
|
||||
void sendReceiptFetchFailAsync()
|
||||
{
|
||||
MessageManager::callAsync ([this] { sendReceiptFetchFail(); });
|
||||
}
|
||||
|
||||
static int64 getPurchaseDateMs (id date)
|
||||
{
|
||||
if (auto dateAsNumber = getAs<NSNumber> (date))
|
||||
{
|
||||
return [dateAsNumber longLongValue];
|
||||
}
|
||||
else if (auto dateAsString = getAs<NSString> (date))
|
||||
{
|
||||
auto formatter = [[NSNumberFormatter alloc] init];
|
||||
[formatter setNumberStyle: NSNumberFormatterDecimalStyle];
|
||||
dateAsNumber = [formatter numberFromString: dateAsString];
|
||||
[formatter release];
|
||||
return [dateAsNumber longLongValue];
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
static Product SKProductToIAPProduct (SKProduct* skProduct)
|
||||
{
|
||||
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
|
||||
[numberFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4];
|
||||
[numberFormatter setNumberStyle: NSNumberFormatterCurrencyStyle];
|
||||
[numberFormatter setLocale: skProduct.priceLocale];
|
||||
|
||||
auto identifier = nsStringToJuce (skProduct.productIdentifier);
|
||||
auto title = nsStringToJuce (skProduct.localizedTitle);
|
||||
auto description = nsStringToJuce (skProduct.localizedDescription);
|
||||
auto priceLocale = nsStringToJuce ([skProduct.priceLocale objectForKey: NSLocaleLanguageCode]);
|
||||
auto price = nsStringToJuce ([numberFormatter stringFromNumber: skProduct.price]);
|
||||
|
||||
[numberFormatter release];
|
||||
|
||||
return { identifier, title, description, price, priceLocale };
|
||||
}
|
||||
|
||||
static String SKPaymentTransactionStateToString (SKPaymentTransactionState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case SKPaymentTransactionStatePurchasing: return NEEDS_TRANS ("Purchasing");
|
||||
case SKPaymentTransactionStatePurchased: return NEEDS_TRANS ("Success");
|
||||
case SKPaymentTransactionStateFailed: return NEEDS_TRANS ("Failure");
|
||||
case SKPaymentTransactionStateRestored: return NEEDS_TRANS ("Restored");
|
||||
case SKPaymentTransactionStateDeferred: return NEEDS_TRANS ("Deferred");
|
||||
default: jassertfalse; return NEEDS_TRANS ("Unknown status");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static Download::Status SKDownloadStateToDownloadStatus (SKDownloadState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case SKDownloadStateWaiting: return Download::Status::waiting;
|
||||
case SKDownloadStateActive: return Download::Status::active;
|
||||
case SKDownloadStatePaused: return Download::Status::paused;
|
||||
case SKDownloadStateFinished: return Download::Status::finished;
|
||||
case SKDownloadStateFailed: return Download::Status::failed;
|
||||
case SKDownloadStateCancelled: return Download::Status::cancelled;
|
||||
default: jassertfalse; return Download::Status::waiting;
|
||||
}
|
||||
}
|
||||
|
||||
static NSArray<SKDownload*>* downloadsToSKDownloads (const Array<Download*>& downloads)
|
||||
{
|
||||
NSMutableArray<SKDownload*>* skDownloads = [NSMutableArray arrayWithCapacity: (NSUInteger) downloads.size()];
|
||||
|
||||
for (const auto& d : downloads)
|
||||
if (auto impl = dynamic_cast<DownloadImpl*>(d))
|
||||
[skDownloads addObject: impl->download];
|
||||
|
||||
return skDownloads;
|
||||
}
|
||||
|
||||
template <typename ObjCType>
|
||||
static ObjCType* getAs (id o)
|
||||
{
|
||||
if (o == nil || ! [o isKindOfClass: [ObjCType class]])
|
||||
return nil;
|
||||
|
||||
return (ObjCType*) o;
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
InAppPurchases& owner;
|
||||
|
||||
OwnedArray<PendingProductInfoRequest> pendingProductInfoRequests;
|
||||
OwnedArray<PendingReceiptRefreshRequest> pendingReceiptRefreshRequests;
|
||||
|
||||
OwnedArray<PendingDownloadsTransaction> pendingDownloadsTransactions;
|
||||
Array<Listener::PurchaseInfo> restoredPurchases;
|
||||
};
|
||||
|
||||
} // namespace juce
|
Reference in New Issue
Block a user