InAppPurchase thread safety

Hi,

It seems some InAppPurchase listener callback happen on threads that are not the juce message thread. For example, I was trying to call ‘restoreProductsBoughtList’ on a device with no internet access. At that point, ‘purchasesListRestored’ is called for my InAppPurchases::Listener with the ‘Product query failed’ error, but not from the message thread ( JUCE_ASSERT_MESSAGE_MANAGER_IS_LOCKED asserts).

It can be silenced by replacing:

                       case PendingProductInfoRequest::Type::query:
                            t.owner.listeners.call ([&] (Listener& l)
                            {
                                l.purchasesListRestored ({}, false, NEEDS_TRANS ("Product query failed") + errorDetails);
                            });
                        break;

with

                       case PendingProductInfoRequest::Type::query:
                          MessageManager::callAsync ([&t, errorDetails]
                          {
                            t.owner.listeners.call ([errorDetails] (Listener& l)
                            {
                                l.purchasesListRestored ({}, false, NEEDS_TRANS ("Product query failed") + errorDetails);
                            });
                          });
                            break;

Maybe the other places where listeners are called should be reviewed in order to ensure that we stay on the message thread.

probably related: the red warning in SKProductsRequestDelegate | Apple Developer Documentation

Responses received by the SKProductsRequestDelegate may not be returned on a specific thread. If you make assumptions about which queue will handle delegate responses, you may encounter unintended performance and compatibility issues in the future.

(bump)

the issue is still there btw. I’m wrapping the juce InAppPurchase::Listener with MessageManager::callAsync in order to avoid issues:

struct SafeJuceInAppPurchasesListener {
  using Product = juce::InAppPurchases::Product;
  using PurchaseInfo = juce::InAppPurchases::Listener::PurchaseInfo;
private:
  struct UnsafeListener : public juce::InAppPurchases::Listener {
    SafeJuceInAppPurchasesListener *owner;
    UnsafeListener(SafeJuceInAppPurchasesListener *owner) : owner(owner) {}

    static void callAsyncIfNotOnMessageThread(std::function<void()> fn) {
      if (juce::MessageManager::getInstance()->isThisTheMessageThread()) {
        fn();
      } else {
        juce::MessageManager::callAsync(std::move(fn));
      }
    }

    void productsInfoReturned (const Array<Product>& products) override {
      callAsyncIfNotOnMessageThread([this, products]() { owner->productsInfoReturned(products); });
    }
    void productPurchaseFinished (const PurchaseInfo& info, bool success, const String& statusDescription) override {
      callAsyncIfNotOnMessageThread([this, info, success, statusDescription]() {
        owner->productPurchaseFinished(info, success, statusDescription);
      });
    }
    void purchasesListRestored (const Array<PurchaseInfo> &info, bool success, const String& statusDescription) override {
      callAsyncIfNotOnMessageThread([this, info, success, statusDescription]() {
        owner->purchasesListRestored(info, success, statusDescription);
      });
    }
  };
  UnsafeListener juce_listener{this};
public:
  juce::InAppPurchases::Listener *getUnsafeListener() { return &juce_listener; }
  virtual void productsInfoReturned (const Array<Product>& /*products*/) {}
  virtual void productPurchaseFinished (const PurchaseInfo&, bool /*success*/, const String& /*statusDescription*/) {}
  virtual void purchasesListRestored (const Array<PurchaseInfo>&, bool /*success*/, const String& /*statusDescription*/) {}
};
2 Likes