From 3e66bc69fb3f90fcc1d91e52a4f031295068d1f4 Mon Sep 17 00:00:00 2001 From: Lukasz Kozakiewicz Date: Wed, 21 Feb 2018 17:28:48 +0100 Subject: [PATCH] InAppPurchases: fix a bug on Android when failed purchases would not be propagated to IAP listeners. Make InAppPurchases a Singleton. --- BREAKING-CHANGES.txt | 39 +++++++++++- examples/InAppPurchase/Source/Main.cpp | 1 + .../InAppPurchase/Source/VoicePurchases.h | 32 +++++++--- .../in_app_purchases/juce_InAppPurchases.cpp | 5 +- .../in_app_purchases/juce_InAppPurchases.h | 8 ++- .../native/juce_android_InAppPurchases.cpp | 62 ++++--------------- 6 files changed, 84 insertions(+), 63 deletions(-) diff --git a/BREAKING-CHANGES.txt b/BREAKING-CHANGES.txt index beda4704b5..8396e38ad0 100644 --- a/BREAKING-CHANGES.txt +++ b/BREAKING-CHANGES.txt @@ -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 ======= diff --git a/examples/InAppPurchase/Source/Main.cpp b/examples/InAppPurchase/Source/Main.cpp index 80045b41a1..2a83477c0b 100644 --- a/examples/InAppPurchase/Source/Main.cpp +++ b/examples/InAppPurchase/Source/Main.cpp @@ -109,6 +109,7 @@ public: void updateDisplay() { voiceListBox.updateContent(); + voiceListBox.setEnabled (! getInstance()->getPurchases().isPurchaseInProgress()); voiceListBox.repaint(); } diff --git a/examples/InAppPurchase/Source/VoicePurchases.h b/examples/InAppPurchase/Source/VoicePurchases.h index e3052eb628..2d846b6362 100644 --- a/examples/InAppPurchase/Source/VoicePurchases.h +++ b/examples/InAppPurchase/Source/VoicePurchases.h @@ -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& 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& 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 voiceProducts; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoicePurchases) diff --git a/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp b/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp index 7a13e5c2a8..ca53510a07 100644 --- a/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp +++ b/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.cpp @@ -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 { diff --git a/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h b/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h index bb5e2c7586..93409591f4 100644 --- a/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h +++ b/modules/juce_product_unlocking/in_app_purchases/juce_InAppPurchases.h @@ -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& downloads); +private: //============================================================================== #ifndef DOXYGEN InAppPurchases(); ~InAppPurchases(); #endif -private: //============================================================================== ListenerList listeners; diff --git a/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp b/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp index b227f650c6..8fb17ef8a7 100644 --- a/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp +++ b/modules/juce_product_unlocking/native/juce_android_InAppPurchases.cpp @@ -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 (inAppBillingService.callObjectMethod (IInAppBillingService.getBuyIntent, 3, @@ -769,13 +765,7 @@ struct InAppPurchases::Pimpl : private AsyncUpdater, } //============================================================================== - static Array& getInAppPurchaseInstances() noexcept - { - static Array 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 (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 (static_cast (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 (intentData)); + if (auto* instance = InAppPurchases::getInstance()) + instance->pimpl->inAppPurchaseCompleted (static_cast (intentData)); } } // namespace juce