jpo
January 8, 2024, 5:11pm
1
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.
jpo
January 9, 2024, 8:58am
2
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.
jpo
October 3, 2025, 2:38pm
4
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