| @@ -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); | |||
| 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())); | |||
| auto purchaseTimeString = Time (purchaseTime.toString().getLargeIntValue()) | |||
| .toString (true, true, true, true); | |||
| 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 | |||