From cb54044c1c8e1a0ecd5456246af2ef8b6fce0a2f Mon Sep 17 00:00:00 2001 From: reuk Date: Tue, 28 Feb 2023 14:04:33 +0000 Subject: [PATCH] InAppPurchases: Ensure that the iOS implementation notifies on failure --- examples/Utilities/InAppPurchasesDemo.h | 8 +- .../native/juce_ios_InAppPurchases.cpp | 136 ++++++++++++------ 2 files changed, 96 insertions(+), 48 deletions(-) diff --git a/examples/Utilities/InAppPurchasesDemo.h b/examples/Utilities/InAppPurchasesDemo.h index 1cdc782a85..d0503f36e4 100644 --- a/examples/Utilities/InAppPurchasesDemo.h +++ b/examples/Utilities/InAppPurchasesDemo.h @@ -190,7 +190,7 @@ private: guiUpdater.triggerAsyncUpdate(); } - void productPurchaseFinished (const PurchaseInfo& info, bool success, const String&) override + void productPurchaseFinished (const PurchaseInfo& info, bool success, const String& error) override { purchaseInProgress = false; @@ -213,6 +213,12 @@ private: } } + if (! success) + { + auto options = MessageBoxOptions::makeOptionsOk (MessageBoxIconType::WarningIcon, "Purchase failed", error); + messageBox = AlertWindow::showScopedAsync (options, nullptr); + } + guiUpdater.triggerAsyncUpdate(); } diff --git a/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp b/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp index 56fdb0be89..26c26fae8c 100644 --- a/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp +++ b/modules/juce_product_unlocking/native/juce_ios_InAppPurchases.cpp @@ -73,14 +73,14 @@ struct InAppPurchases::Pimpl }; Type type; - std::unique_ptr request; + NSUniquePtr request; }; /** Represents a pending request started from [SKReceiptRefreshRequest start]. */ struct PendingReceiptRefreshRequest { String subscriptionsSharedSecret; - std::unique_ptr request; + NSUniquePtr request; }; /** Represents a transaction with pending downloads. Only after all downloads @@ -142,8 +142,8 @@ struct InAppPurchases::Pimpl { auto productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithArray: createNSArrayFromStringArray (productIdentifiers)]]; - pendingProductInfoRequests.add (new PendingProductInfoRequest { PendingProductInfoRequest::Type::query, - std::unique_ptr (productsRequest) }); + pendingProductInfoRequests.emplace_back (new PendingProductInfoRequest { PendingProductInfoRequest::Type::query, + NSUniquePtr (productsRequest) }); productsRequest.delegate = delegate.get(); [productsRequest start]; @@ -160,8 +160,8 @@ struct InAppPurchases::Pimpl auto productIdentifiers = [NSArray arrayWithObject: juceStringToNS (productIdentifier)]; auto productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]]; - pendingProductInfoRequests.add (new PendingProductInfoRequest { PendingProductInfoRequest::Type::purchase, - std::unique_ptr (productsRequest) }); + pendingProductInfoRequests.emplace_back (new PendingProductInfoRequest { PendingProductInfoRequest::Type::purchase, + NSUniquePtr (productsRequest) }); productsRequest.delegate = delegate.get(); [productsRequest start]; @@ -177,8 +177,8 @@ struct InAppPurchases::Pimpl { auto receiptRequest = [[SKReceiptRefreshRequest alloc] init]; - pendingReceiptRefreshRequests.add (new PendingReceiptRefreshRequest { subscriptionsSharedSecret, - std::unique_ptr ([receiptRequest retain]) }); + pendingReceiptRefreshRequests.emplace_back (new PendingReceiptRefreshRequest { subscriptionsSharedSecret, + NSUniquePtr ([receiptRequest retain]) }); receiptRequest.delegate = delegate.get(); [receiptRequest start]; } @@ -615,8 +615,23 @@ struct InAppPurchases::Pimpl return (ObjCType*) o; } - private: + auto findPendingProductInfoRequest (SKRequest* r) const + { + const auto request = getAs (r); + return std::find_if (pendingProductInfoRequests.begin(), + pendingProductInfoRequests.end(), + [&] (const auto& pending) { return pending->request.get() == request; }); + } + + auto findPendingReceiptRefreshRequest (SKRequest* r) const + { + const auto request = getAs (r); + return std::find_if (pendingReceiptRefreshRequests.begin(), + pendingReceiptRefreshRequests.end(), + [&] (const auto& pending) { return pending->request.get() == request; }); + } + struct Class : public ObjCClass> { //============================================================================== @@ -629,42 +644,40 @@ private: { auto& t = getThis (self); - for (auto i = 0; i < t.pendingProductInfoRequests.size(); ++i) + if (const auto iter = t.findPendingProductInfoRequest (request); iter != t.pendingProductInfoRequests.end()) { - auto& pendingRequest = *t.pendingProductInfoRequests[i]; - - if (pendingRequest.request.get() == request) + switch (iter->get()->type) { - if (pendingRequest.type == PendingProductInfoRequest::Type::query) t.notifyProductsInfoReceived (response.products); - else if (pendingRequest.type == PendingProductInfoRequest::Type::purchase) t.startPurchase (response.products); - else break; - - t.pendingProductInfoRequests.remove (i); - return; + case PendingProductInfoRequest::Type::query: + t.notifyProductsInfoReceived (response.products); + break; + case PendingProductInfoRequest::Type::purchase: + t.startPurchase (response.products); + break; } - } - // Unknown request received! - jassertfalse; + t.pendingProductInfoRequests.erase (iter); + } + else + { + // Unknown request received! + jassertfalse; + } }); addMethod (@selector (requestDidFinish:), [] (id self, SEL, SKRequest* request) { auto& t = getThis (self); - if (auto receiptRefreshRequest = getAs (request)) + if (const auto iter = t.findPendingReceiptRefreshRequest (request); iter != t.pendingReceiptRefreshRequests.end()) { - for (auto i = 0; i < t.pendingReceiptRefreshRequests.size(); ++i) - { - auto& pendingRequest = *t.pendingReceiptRefreshRequests[i]; + t.processReceiptRefreshResponseWithSubscriptionsSharedSecret (iter->get()->subscriptionsSharedSecret); + t.pendingReceiptRefreshRequests.erase (iter); + } - if (pendingRequest.request.get() == receiptRefreshRequest) - { - t.processReceiptRefreshResponseWithSubscriptionsSharedSecret (pendingRequest.subscriptionsSharedSecret); - t.pendingReceiptRefreshRequests.remove (i); - return; - } - } + if (const auto iter = t.findPendingProductInfoRequest (request); iter != t.pendingProductInfoRequests.end()) + { + t.pendingProductInfoRequests.erase (iter); } }); @@ -672,20 +685,49 @@ private: { auto& t = getThis (self); - if (auto receiptRefreshRequest = getAs (request)) + const auto errorDetails = [&] { - for (auto i = 0; i < t.pendingReceiptRefreshRequests.size(); ++i) - { - auto& pendingRequest = *t.pendingReceiptRefreshRequests[i]; + if (error == nil) + return String(); - if (pendingRequest.request.get() == receiptRefreshRequest) - { - auto errorDetails = error != nil ? (", " + nsStringToJuce ([error localizedDescription])) : String(); - t.owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed") + errorDetails); }); - t.pendingReceiptRefreshRequests.remove (i); - return; - } + const auto description = nsStringToJuce ([error localizedDescription]); + + if (description.isEmpty()) + return String(); + + return ": " + description; + }(); + + if (const auto iter = t.findPendingReceiptRefreshRequest (request); iter != t.pendingReceiptRefreshRequests.end()) + { + t.owner.listeners.call ([&] (Listener& l) + { + l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed") + errorDetails); + }); + t.pendingReceiptRefreshRequests.erase (iter); + return; + } + + if (const auto iter = t.findPendingProductInfoRequest (request); iter != t.pendingProductInfoRequests.end()) + { + switch (iter->get()->type) + { + case PendingProductInfoRequest::Type::query: + t.owner.listeners.call ([&] (Listener& l) + { + l.purchasesListRestored ({}, false, NEEDS_TRANS ("Product query failed") + errorDetails); + }); + break; + case PendingProductInfoRequest::Type::purchase: + t.owner.listeners.call ([&] (Listener& l) + { + l.productPurchaseFinished ({}, false, NEEDS_TRANS ("Purchase request failed") + errorDetails); + }); + break; } + + t.pendingProductInfoRequests.erase (iter); + return; } }); @@ -779,10 +821,10 @@ private: //============================================================================== InAppPurchases& owner; - std::unique_ptr, NSObjectDeleter> delegate { [Class::get().createInstance() init] }; + NSUniquePtr> delegate { [Class::get().createInstance() init] }; - OwnedArray pendingProductInfoRequests; - OwnedArray pendingReceiptRefreshRequests; + std::vector> pendingProductInfoRequests; + std::vector> pendingReceiptRefreshRequests; OwnedArray pendingDownloadsTransactions; Array restoredPurchases;