mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
InAppPurchases: fix a bug on Android when failed purchases would not be propagated to IAP listeners. Make InAppPurchases a Singleton.
This commit is contained in:
parent
4be6679aab
commit
3e66bc69fb
6 changed files with 84 additions and 63 deletions
|
|
@ -1,5 +1,42 @@
|
|||
JUCE breaking changes
|
||||
=====================
|
||||
=====================
|
||||
|
||||
Develop
|
||||
=======
|
||||
|
||||
Change
|
||||
------
|
||||
InAppPurchases class is now a JUCE Singleton. This means that you need
|
||||
to get an instance via InAppPurchases::getInstance(), instead of storing a
|
||||
InAppPurchases object yourself.
|
||||
|
||||
|
||||
Possible Issues
|
||||
---------------
|
||||
Any code using InAppPurchases needs to be updated to retrieve a singleton pointer
|
||||
to InAppPurchases.
|
||||
|
||||
|
||||
Workaround
|
||||
----------
|
||||
Instead of holding a InAppPurchase member yourself, you should get an instance
|
||||
via InAppPurchases::getInstance(), e.g.
|
||||
|
||||
instead of:
|
||||
|
||||
InAppPurchases iap;
|
||||
iap.purchaseProduct (…);
|
||||
|
||||
call:
|
||||
|
||||
InAppPurchases::getInstance()->purchaseProduct (…);
|
||||
|
||||
|
||||
Rationale
|
||||
---------
|
||||
This change was required to fix an issue on Android where on failed transaction
|
||||
a listener would not get called.
|
||||
|
||||
|
||||
Develop
|
||||
=======
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ public:
|
|||
void updateDisplay()
|
||||
{
|
||||
voiceListBox.updateContent();
|
||||
voiceListBox.setEnabled (! getInstance()->getPurchases().isPurchaseInProgress());
|
||||
voiceListBox.repaint();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public:
|
|||
|
||||
~VoicePurchases()
|
||||
{
|
||||
inAppPurchases.removeListener (this);
|
||||
InAppPurchases::getInstance()->removeListener (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -61,9 +61,9 @@ public:
|
|||
if (! havePurchasesBeenRestored)
|
||||
{
|
||||
havePurchasesBeenRestored = true;
|
||||
inAppPurchases.addListener (this);
|
||||
InAppPurchases::getInstance()->addListener (this);
|
||||
|
||||
inAppPurchases.restoreProductsBoughtList (true);
|
||||
InAppPurchases::getInstance()->restoreProductsBoughtList (true);
|
||||
}
|
||||
|
||||
return voiceProducts[voiceIndex];
|
||||
|
|
@ -77,8 +77,12 @@ public:
|
|||
|
||||
if (! product.isPurchased)
|
||||
{
|
||||
purchaseInProgress = true;
|
||||
|
||||
product.purchaseInProgress = true;
|
||||
inAppPurchases.purchaseProduct (product.identifier, false);
|
||||
InAppPurchases::getInstance()->purchaseProduct (product.identifier, false);
|
||||
|
||||
guiUpdater.triggerAsyncUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -93,11 +97,13 @@ public:
|
|||
return names;
|
||||
}
|
||||
|
||||
bool isPurchaseInProgress() const noexcept { return purchaseInProgress; }
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
void productsInfoReturned (const Array<InAppPurchases::Product>& products) override
|
||||
{
|
||||
if (! inAppPurchases.isInAppPurchasesSupported())
|
||||
if (! InAppPurchases::getInstance()->isInAppPurchasesSupported())
|
||||
{
|
||||
for (auto idx = 1; idx < voiceProducts.size(); ++idx)
|
||||
{
|
||||
|
|
@ -142,6 +148,8 @@ private:
|
|||
|
||||
void productPurchaseFinished (const PurchaseInfo& info, bool success, const String&) override
|
||||
{
|
||||
purchaseInProgress = false;
|
||||
|
||||
auto idx = findVoiceIndexFromIdentifier (info.purchase.productId);
|
||||
|
||||
if (isPositiveAndBelow (idx, voiceProducts.size()))
|
||||
|
|
@ -152,6 +160,15 @@ private:
|
|||
voiceProduct.purchaseInProgress = false;
|
||||
guiUpdater.triggerAsyncUpdate();
|
||||
}
|
||||
else
|
||||
{
|
||||
// On failure Play Store will not tell us which purchase failed
|
||||
|
||||
for (auto& voiceProduct : voiceProducts)
|
||||
voiceProduct.purchaseInProgress = false;
|
||||
|
||||
guiUpdater.triggerAsyncUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void purchasesListRestored (const Array<PurchaseInfo>& infos, bool success, const String&) override
|
||||
|
|
@ -181,7 +198,7 @@ private:
|
|||
for (auto& voiceProduct : voiceProducts)
|
||||
identifiers.add (voiceProduct.identifier);
|
||||
|
||||
inAppPurchases.getProductsInformation(identifiers);
|
||||
InAppPurchases::getInstance()->getProductsInformation (identifiers);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,8 +216,7 @@ private:
|
|||
|
||||
//==============================================================================
|
||||
AsyncUpdater& guiUpdater;
|
||||
bool havePurchasesBeenRestored = false, havePricesBeenFetched = false;
|
||||
InAppPurchases inAppPurchases;
|
||||
bool havePurchasesBeenRestored = false, havePricesBeenFetched = false, purchaseInProgress = false;
|
||||
Array<VoiceProduct> voiceProducts;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoicePurchases)
|
||||
|
|
|
|||
|
|
@ -27,13 +27,16 @@
|
|||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
JUCE_IMPLEMENT_SINGLETON (InAppPurchases)
|
||||
|
||||
InAppPurchases::InAppPurchases()
|
||||
#if JUCE_ANDROID || JUCE_IOS || JUCE_MAC
|
||||
: pimpl (new Pimpl (*this))
|
||||
#endif
|
||||
{}
|
||||
|
||||
InAppPurchases::~InAppPurchases() {}
|
||||
InAppPurchases::~InAppPurchases() { clearSingletonInstance(); }
|
||||
|
||||
bool InAppPurchases::isInAppPurchasesSupported() const
|
||||
{
|
||||
|
|
|
|||
|
|
@ -36,9 +36,13 @@ namespace juce
|
|||
|
||||
Once an InAppPurchases object is created, call addListener() to attach listeners.
|
||||
*/
|
||||
class JUCE_API InAppPurchases
|
||||
class JUCE_API InAppPurchases : private DeletedAtShutdown
|
||||
{
|
||||
public:
|
||||
#ifndef DOXYGEN
|
||||
JUCE_DECLARE_SINGLETON (InAppPurchases, false)
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
/** Represents a product available in the store. */
|
||||
struct Product
|
||||
|
|
@ -253,13 +257,13 @@ public:
|
|||
/** iOS only: Cancels downloads of hosted content from the store. */
|
||||
void cancelDownloads (const Array<Download*>& downloads);
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
#ifndef DOXYGEN
|
||||
InAppPurchases();
|
||||
~InAppPurchases();
|
||||
#endif
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
|
|
|
|||
|
|
@ -79,8 +79,6 @@ struct InAppPurchases::Pimpl : private AsyncUpdater,
|
|||
{
|
||||
Pimpl (InAppPurchases& parent) : owner (parent)
|
||||
{
|
||||
getInAppPurchaseInstances().add (this);
|
||||
|
||||
auto* env = getEnv();
|
||||
auto intent = env->NewObject (AndroidIntent, AndroidIntent.constructWithString,
|
||||
javaString ("com.android.vending.billing.InAppBillingService.BIND").get());
|
||||
|
|
@ -103,8 +101,6 @@ struct InAppPurchases::Pimpl : private AsyncUpdater,
|
|||
android.activity.callVoidMethod (JuceAppActivity.unbindService, serviceConnection.get());
|
||||
serviceConnection.clear();
|
||||
}
|
||||
|
||||
getInAppPurchaseInstances().removeFirstMatchingValue (this);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -221,7 +217,7 @@ struct InAppPurchases::Pimpl : private AsyncUpdater,
|
|||
|
||||
auto skuString = javaString (productIdentifier);
|
||||
auto productTypeString = javaString (isSubscription ? "subs" : "inapp");
|
||||
auto devString = javaString (getDeveloperExtraData());
|
||||
auto devString = javaString ("");
|
||||
|
||||
if (subscriptionIdentifiers.isEmpty())
|
||||
return LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getBuyIntent, 3,
|
||||
|
|
@ -769,13 +765,7 @@ struct InAppPurchases::Pimpl : private AsyncUpdater,
|
|||
}
|
||||
|
||||
//==============================================================================
|
||||
static Array<Pimpl*>& getInAppPurchaseInstances() noexcept
|
||||
{
|
||||
static Array<Pimpl*> instances;
|
||||
return instances;
|
||||
}
|
||||
|
||||
static void inAppPurchaseCompleted (jobject intentData)
|
||||
void inAppPurchaseCompleted (jobject intentData)
|
||||
{
|
||||
auto* env = getEnv();
|
||||
|
||||
|
|
@ -811,47 +801,16 @@ struct InAppPurchases::Pimpl : private AsyncUpdater,
|
|||
var purchaseToken = props[purchaseTokenIdentifier];
|
||||
var developerPayload = props[developerPayloadIdentifier];
|
||||
|
||||
if (auto* target = getPimplFromDeveloperExtraData (developerPayload))
|
||||
{
|
||||
auto purchaseTimeString = Time (purchaseTime.toString().getLargeIntValue())
|
||||
.toString (true, true, true, true);
|
||||
auto purchaseTimeString = Time (purchaseTime.toString().getLargeIntValue())
|
||||
.toString (true, true, true, true);
|
||||
|
||||
target->notifyAboutPurchaseResult ({ orderId.toString(), productId.toString(), packageName.toString(),
|
||||
purchaseTimeString, purchaseToken.toString() },
|
||||
true, statusCodeUserString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
String getDeveloperExtraData()
|
||||
{
|
||||
static const Identifier inAppPurchaseInstance ("inAppPurchaseInstance");
|
||||
DynamicObject::Ptr developerString (new DynamicObject());
|
||||
|
||||
developerString->setProperty (inAppPurchaseInstance,
|
||||
"0x" + String::toHexString (reinterpret_cast<pointer_sized_int> (this)));
|
||||
return JSON::toString (var (developerString));
|
||||
}
|
||||
|
||||
static Pimpl* getPimplFromDeveloperExtraData (const String& developerExtra)
|
||||
{
|
||||
static const Identifier inAppPurchaseInstance ("inAppPurchaseInstance");
|
||||
|
||||
if (DynamicObject::Ptr developerData = JSON::fromString (developerExtra).getDynamicObject())
|
||||
{
|
||||
String hexAddr = developerData->getProperty (inAppPurchaseInstance);
|
||||
|
||||
if (hexAddr.startsWith ("0x"))
|
||||
hexAddr = hexAddr.fromFirstOccurrenceOf ("0x", false, false);
|
||||
|
||||
auto* target = reinterpret_cast<Pimpl*> (static_cast<pointer_sized_int> (hexAddr.getHexValue64()));
|
||||
|
||||
if (getInAppPurchaseInstances().contains (target))
|
||||
return target;
|
||||
notifyAboutPurchaseResult ({ orderId.toString(), productId.toString(), packageName.toString(),
|
||||
purchaseTimeString, purchaseToken.toString() },
|
||||
true, statusCodeUserString);
|
||||
return;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
notifyAboutPurchaseResult ({}, false, statusCodeUserString);
|
||||
}
|
||||
|
||||
//==============================================================================
|
||||
|
|
@ -890,7 +849,8 @@ struct InAppPurchases::Pimpl : private AsyncUpdater,
|
|||
//==============================================================================
|
||||
void juce_inAppPurchaseCompleted (void* intentData)
|
||||
{
|
||||
InAppPurchases::Pimpl::inAppPurchaseCompleted (static_cast<jobject> (intentData));
|
||||
if (auto* instance = InAppPurchases::getInstance())
|
||||
instance->pimpl->inAppPurchaseCompleted (static_cast<jobject> (intentData));
|
||||
}
|
||||
|
||||
} // namespace juce
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue