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

Bumping as I would really like the JUCE team to investigate thread safety of iOS in-app purchase code. Most of my iOS crash logs in Xcode are still something like that (happening in a thread that is not the message thread), so the workaround I previously posted is not enough:

std::__1::vector<std::__1::unique_ptr<juce::InAppPurchases::Pimpl::PendingProductInfoRequest, std::__1::default_delete<juce::InAppPurchases::Pimpl::PendingProductInfoRequest>>, std::__1::allocator<... + 72
0x0000000101099f68 juce::InAppPurchases::Pimpl::Class::Class()::'lambda'(objc_object*, objc_selector*, SKProductsRequest*, SKProductsResponse*)::operator()(objc_object*, objc_selector*, SKProductsRequest*, SKProducts... + 304
0x0000000101099fa0 juce::InAppPurchases::Pimpl::Class::Class()::'lambda'(objc_object*, objc_selector*, SKProductsRequest*, SKProductsResponse*)::__invoke(objc_object*, objc_selector*, SKProductsRequest*, SKProductsRe... + 36