| @@ -1,5 +1,42 @@ | |||||
| JUCE breaking changes | 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 | Develop | ||||
| ======= | ======= | ||||
| @@ -109,6 +109,7 @@ public: | |||||
| void updateDisplay() | void updateDisplay() | ||||
| { | { | ||||
| voiceListBox.updateContent(); | voiceListBox.updateContent(); | ||||
| voiceListBox.setEnabled (! getInstance()->getPurchases().isPurchaseInProgress()); | |||||
| voiceListBox.repaint(); | voiceListBox.repaint(); | ||||
| } | } | ||||
| @@ -52,7 +52,7 @@ public: | |||||
| ~VoicePurchases() | ~VoicePurchases() | ||||
| { | { | ||||
| inAppPurchases.removeListener (this); | |||||
| InAppPurchases::getInstance()->removeListener (this); | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -61,9 +61,9 @@ public: | |||||
| if (! havePurchasesBeenRestored) | if (! havePurchasesBeenRestored) | ||||
| { | { | ||||
| havePurchasesBeenRestored = true; | havePurchasesBeenRestored = true; | ||||
| inAppPurchases.addListener (this); | |||||
| InAppPurchases::getInstance()->addListener (this); | |||||
| inAppPurchases.restoreProductsBoughtList (true); | |||||
| InAppPurchases::getInstance()->restoreProductsBoughtList (true); | |||||
| } | } | ||||
| return voiceProducts[voiceIndex]; | return voiceProducts[voiceIndex]; | ||||
| @@ -77,8 +77,12 @@ public: | |||||
| if (! product.isPurchased) | if (! product.isPurchased) | ||||
| { | { | ||||
| purchaseInProgress = true; | |||||
| product.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; | return names; | ||||
| } | } | ||||
| bool isPurchaseInProgress() const noexcept { return purchaseInProgress; } | |||||
| private: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| void productsInfoReturned (const Array<InAppPurchases::Product>& products) override | void productsInfoReturned (const Array<InAppPurchases::Product>& products) override | ||||
| { | { | ||||
| if (! inAppPurchases.isInAppPurchasesSupported()) | |||||
| if (! InAppPurchases::getInstance()->isInAppPurchasesSupported()) | |||||
| { | { | ||||
| for (auto idx = 1; idx < voiceProducts.size(); ++idx) | for (auto idx = 1; idx < voiceProducts.size(); ++idx) | ||||
| { | { | ||||
| @@ -142,6 +148,8 @@ private: | |||||
| void productPurchaseFinished (const PurchaseInfo& info, bool success, const String&) override | void productPurchaseFinished (const PurchaseInfo& info, bool success, const String&) override | ||||
| { | { | ||||
| purchaseInProgress = false; | |||||
| auto idx = findVoiceIndexFromIdentifier (info.purchase.productId); | auto idx = findVoiceIndexFromIdentifier (info.purchase.productId); | ||||
| if (isPositiveAndBelow (idx, voiceProducts.size())) | if (isPositiveAndBelow (idx, voiceProducts.size())) | ||||
| @@ -152,6 +160,15 @@ private: | |||||
| voiceProduct.purchaseInProgress = false; | voiceProduct.purchaseInProgress = false; | ||||
| guiUpdater.triggerAsyncUpdate(); | 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 | void purchasesListRestored (const Array<PurchaseInfo>& infos, bool success, const String&) override | ||||
| @@ -181,7 +198,7 @@ private: | |||||
| for (auto& voiceProduct : voiceProducts) | for (auto& voiceProduct : voiceProducts) | ||||
| identifiers.add (voiceProduct.identifier); | identifiers.add (voiceProduct.identifier); | ||||
| inAppPurchases.getProductsInformation(identifiers); | |||||
| InAppPurchases::getInstance()->getProductsInformation (identifiers); | |||||
| } | } | ||||
| } | } | ||||
| @@ -199,8 +216,7 @@ private: | |||||
| //============================================================================== | //============================================================================== | ||||
| AsyncUpdater& guiUpdater; | AsyncUpdater& guiUpdater; | ||||
| bool havePurchasesBeenRestored = false, havePricesBeenFetched = false; | |||||
| InAppPurchases inAppPurchases; | |||||
| bool havePurchasesBeenRestored = false, havePricesBeenFetched = false, purchaseInProgress = false; | |||||
| Array<VoiceProduct> voiceProducts; | Array<VoiceProduct> voiceProducts; | ||||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoicePurchases) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VoicePurchases) | ||||
| @@ -27,13 +27,16 @@ | |||||
| namespace juce | namespace juce | ||||
| { | { | ||||
| //============================================================================== | |||||
| JUCE_IMPLEMENT_SINGLETON (InAppPurchases) | |||||
| InAppPurchases::InAppPurchases() | InAppPurchases::InAppPurchases() | ||||
| #if JUCE_ANDROID || JUCE_IOS || JUCE_MAC | #if JUCE_ANDROID || JUCE_IOS || JUCE_MAC | ||||
| : pimpl (new Pimpl (*this)) | : pimpl (new Pimpl (*this)) | ||||
| #endif | #endif | ||||
| {} | {} | ||||
| InAppPurchases::~InAppPurchases() {} | |||||
| InAppPurchases::~InAppPurchases() { clearSingletonInstance(); } | |||||
| bool InAppPurchases::isInAppPurchasesSupported() const | bool InAppPurchases::isInAppPurchasesSupported() const | ||||
| { | { | ||||
| @@ -36,9 +36,13 @@ namespace juce | |||||
| Once an InAppPurchases object is created, call addListener() to attach listeners. | Once an InAppPurchases object is created, call addListener() to attach listeners. | ||||
| */ | */ | ||||
| class JUCE_API InAppPurchases | |||||
| class JUCE_API InAppPurchases : private DeletedAtShutdown | |||||
| { | { | ||||
| public: | public: | ||||
| #ifndef DOXYGEN | |||||
| JUCE_DECLARE_SINGLETON (InAppPurchases, false) | |||||
| #endif | |||||
| //============================================================================== | //============================================================================== | ||||
| /** Represents a product available in the store. */ | /** Represents a product available in the store. */ | ||||
| struct Product | struct Product | ||||
| @@ -253,13 +257,13 @@ public: | |||||
| /** iOS only: Cancels downloads of hosted content from the store. */ | /** iOS only: Cancels downloads of hosted content from the store. */ | ||||
| void cancelDownloads (const Array<Download*>& downloads); | void cancelDownloads (const Array<Download*>& downloads); | ||||
| private: | |||||
| //============================================================================== | //============================================================================== | ||||
| #ifndef DOXYGEN | #ifndef DOXYGEN | ||||
| InAppPurchases(); | InAppPurchases(); | ||||
| ~InAppPurchases(); | ~InAppPurchases(); | ||||
| #endif | #endif | ||||
| private: | |||||
| //============================================================================== | //============================================================================== | ||||
| ListenerList<Listener> listeners; | ListenerList<Listener> listeners; | ||||
| @@ -79,8 +79,6 @@ struct InAppPurchases::Pimpl : private AsyncUpdater, | |||||
| { | { | ||||
| Pimpl (InAppPurchases& parent) : owner (parent) | Pimpl (InAppPurchases& parent) : owner (parent) | ||||
| { | { | ||||
| getInAppPurchaseInstances().add (this); | |||||
| auto* env = getEnv(); | auto* env = getEnv(); | ||||
| auto intent = env->NewObject (AndroidIntent, AndroidIntent.constructWithString, | auto intent = env->NewObject (AndroidIntent, AndroidIntent.constructWithString, | ||||
| javaString ("com.android.vending.billing.InAppBillingService.BIND").get()); | javaString ("com.android.vending.billing.InAppBillingService.BIND").get()); | ||||
| @@ -103,8 +101,6 @@ struct InAppPurchases::Pimpl : private AsyncUpdater, | |||||
| android.activity.callVoidMethod (JuceAppActivity.unbindService, serviceConnection.get()); | android.activity.callVoidMethod (JuceAppActivity.unbindService, serviceConnection.get()); | ||||
| serviceConnection.clear(); | serviceConnection.clear(); | ||||
| } | } | ||||
| getInAppPurchaseInstances().removeFirstMatchingValue (this); | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -221,7 +217,7 @@ struct InAppPurchases::Pimpl : private AsyncUpdater, | |||||
| auto skuString = javaString (productIdentifier); | auto skuString = javaString (productIdentifier); | ||||
| auto productTypeString = javaString (isSubscription ? "subs" : "inapp"); | auto productTypeString = javaString (isSubscription ? "subs" : "inapp"); | ||||
| auto devString = javaString (getDeveloperExtraData()); | |||||
| auto devString = javaString (""); | |||||
| if (subscriptionIdentifiers.isEmpty()) | if (subscriptionIdentifiers.isEmpty()) | ||||
| return LocalRef<jobject> (inAppBillingService.callObjectMethod (IInAppBillingService.getBuyIntent, 3, | 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(); | auto* env = getEnv(); | ||||
| @@ -811,47 +801,16 @@ struct InAppPurchases::Pimpl : private AsyncUpdater, | |||||
| var purchaseToken = props[purchaseTokenIdentifier]; | var purchaseToken = props[purchaseTokenIdentifier]; | ||||
| var developerPayload = props[developerPayloadIdentifier]; | 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) | 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 | } // namespace juce | ||||