1 /* 2 ============================================================================== 3 4 This file is part of the JUCE library. 5 Copyright (c) 2020 - Raw Material Software Limited 6 7 JUCE is an open source library subject to commercial or open-source 8 licensing. 9 10 By using JUCE, you agree to the terms of both the JUCE 6 End-User License 11 Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). 12 13 End User License Agreement: www.juce.com/juce-6-licence 14 Privacy Policy: www.juce.com/juce-privacy-policy 15 16 Or: You may also use this code under the terms of the GPL v3 (see 17 www.gnu.org/licenses). 18 19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER 20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE 21 DISCLAIMED. 22 23 ============================================================================== 24 */ 25 26 namespace juce 27 { 28 29 struct SKDelegateAndPaymentObserver 30 { SKDelegateAndPaymentObserverjuce::SKDelegateAndPaymentObserver31 SKDelegateAndPaymentObserver() : delegate ([getClass().createInstance() init]) 32 { 33 Class::setThis (delegate.get(), this); 34 } 35 ~SKDelegateAndPaymentObserverjuce::SKDelegateAndPaymentObserver36 virtual ~SKDelegateAndPaymentObserver() {} 37 38 virtual void didReceiveResponse (SKProductsRequest*, SKProductsResponse*) = 0; 39 virtual void requestDidFinish (SKRequest*) = 0; 40 virtual void requestDidFailWithError (SKRequest*, NSError*) = 0; 41 virtual void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>*) = 0; 42 virtual void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError*) = 0; 43 virtual void restoreCompletedTransactionsFinished (SKPaymentQueue*) = 0; 44 virtual void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>*) = 0; 45 46 protected: 47 std::unique_ptr<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>, NSObjectDeleter> delegate; 48 49 private: 50 struct Class : public ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> 51 { 52 //============================================================================== Classjuce::SKDelegateAndPaymentObserver::Class53 Class() : ObjCClass<NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>> ("SKDelegateAndPaymentObserverBase_") 54 { 55 addIvar<SKDelegateAndPaymentObserver*> ("self"); 56 57 addMethod (@selector (productsRequest:didReceiveResponse:), didReceiveResponse, "v@:@@"); 58 addMethod (@selector (requestDidFinish:), requestDidFinish, "v@:@"); 59 addMethod (@selector (request:didFailWithError:), requestDidFailWithError, "v@:@@"); 60 addMethod (@selector (paymentQueue:updatedTransactions:), updatedTransactions, "v@:@@"); 61 addMethod (@selector (paymentQueue:restoreCompletedTransactionsFailedWithError:), restoreCompletedTransactionsFailedWithError, "v@:@@"); 62 addMethod (@selector (paymentQueueRestoreCompletedTransactionsFinished:), restoreCompletedTransactionsFinished, "v@:@"); 63 addMethod (@selector (paymentQueue:updatedDownloads:), updatedDownloads, "v@:@@"); 64 65 registerClass(); 66 } 67 68 //============================================================================== getThisjuce::SKDelegateAndPaymentObserver::Class69 static SKDelegateAndPaymentObserver& getThis (id self) { return *getIvar<SKDelegateAndPaymentObserver*> (self, "self"); } setThisjuce::SKDelegateAndPaymentObserver::Class70 static void setThis (id self, SKDelegateAndPaymentObserver* s) { object_setInstanceVariable (self, "self", s); } 71 72 //============================================================================== didReceiveResponsejuce::SKDelegateAndPaymentObserver::Class73 static void didReceiveResponse (id self, SEL, SKProductsRequest* request, SKProductsResponse* response) { getThis (self).didReceiveResponse (request, response); } requestDidFinishjuce::SKDelegateAndPaymentObserver::Class74 static void requestDidFinish (id self, SEL, SKRequest* request) { getThis (self).requestDidFinish (request); } requestDidFailWithErrorjuce::SKDelegateAndPaymentObserver::Class75 static void requestDidFailWithError (id self, SEL, SKRequest* request, NSError* err) { getThis (self).requestDidFailWithError (request, err); } updatedTransactionsjuce::SKDelegateAndPaymentObserver::Class76 static void updatedTransactions (id self, SEL, SKPaymentQueue* queue, NSArray<SKPaymentTransaction*>* trans) { getThis (self).updatedTransactions (queue, trans); } restoreCompletedTransactionsFailedWithErrorjuce::SKDelegateAndPaymentObserver::Class77 static void restoreCompletedTransactionsFailedWithError (id self, SEL, SKPaymentQueue* q, NSError* err) { getThis (self).restoreCompletedTransactionsFailedWithError (q, err); } restoreCompletedTransactionsFinishedjuce::SKDelegateAndPaymentObserver::Class78 static void restoreCompletedTransactionsFinished (id self, SEL, SKPaymentQueue* queue) { getThis (self).restoreCompletedTransactionsFinished (queue); } updatedDownloadsjuce::SKDelegateAndPaymentObserver::Class79 static void updatedDownloads (id self, SEL, SKPaymentQueue* queue, NSArray<SKDownload*>* downloads) { getThis (self).updatedDownloads (queue, downloads); } 80 }; 81 82 //============================================================================== getClassjuce::SKDelegateAndPaymentObserver83 static Class& getClass() 84 { 85 static Class c; 86 return c; 87 } 88 }; 89 90 //============================================================================== 91 struct InAppPurchases::Pimpl : public SKDelegateAndPaymentObserver 92 { 93 /** AppStore implementation of hosted content download. */ 94 struct DownloadImpl : public Download 95 { DownloadImpljuce::InAppPurchases::Pimpl::DownloadImpl96 DownloadImpl (SKDownload* downloadToUse) 97 : download (downloadToUse) 98 { 99 [download retain]; 100 } 101 ~DownloadImpljuce::InAppPurchases::Pimpl::DownloadImpl102 ~DownloadImpl() override 103 { 104 [download release]; 105 } 106 getProductIdjuce::InAppPurchases::Pimpl::DownloadImpl107 String getProductId() const override { return nsStringToJuce (download.contentIdentifier); } getContentVersionjuce::InAppPurchases::Pimpl::DownloadImpl108 String getContentVersion() const override { return nsStringToJuce (download.contentVersion); } 109 110 #if JUCE_IOS getContentLengthjuce::InAppPurchases::Pimpl::DownloadImpl111 int64 getContentLength() const override { return download.contentLength; } getStatusjuce::InAppPurchases::Pimpl::DownloadImpl112 Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.downloadState); } 113 #else getContentLengthjuce::InAppPurchases::Pimpl::DownloadImpl114 int64 getContentLength() const override { return download.expectedContentLength; } getStatusjuce::InAppPurchases::Pimpl::DownloadImpl115 Status getStatus() const override { return SKDownloadStateToDownloadStatus (download.state); } 116 #endif 117 118 SKDownload* download; 119 }; 120 121 /** Represents a pending request initialised with [SKProductRequest start]. */ 122 struct PendingProductInfoRequest 123 { 124 enum class Type 125 { 126 query = 0, 127 purchase 128 }; 129 130 Type type; 131 std::unique_ptr<SKProductsRequest, NSObjectDeleter> request; 132 }; 133 134 /** Represents a pending request started from [SKReceiptRefreshRequest start]. */ 135 struct PendingReceiptRefreshRequest 136 { 137 String subscriptionsSharedSecret; 138 std::unique_ptr<SKReceiptRefreshRequest, NSObjectDeleter> request; 139 }; 140 141 /** Represents a transaction with pending downloads. Only after all downloads 142 are finished, the transaction is marked as finished. */ 143 struct PendingDownloadsTransaction 144 { PendingDownloadsTransactionjuce::InAppPurchases::Pimpl::PendingDownloadsTransaction145 PendingDownloadsTransaction (SKPaymentTransaction* t) : transaction (t) 146 { 147 addDownloadsFromSKTransaction (transaction); 148 } 149 addDownloadsFromSKTransactionjuce::InAppPurchases::Pimpl::PendingDownloadsTransaction150 void addDownloadsFromSKTransaction (SKPaymentTransaction* transactionToUse) 151 { 152 for (SKDownload* download in transactionToUse.downloads) 153 downloads.add (new DownloadImpl (download)); 154 } 155 canBeMarkedAsFinishedjuce::InAppPurchases::Pimpl::PendingDownloadsTransaction156 bool canBeMarkedAsFinished() const 157 { 158 for (SKDownload* d in transaction.downloads) 159 { 160 #if JUCE_IOS 161 SKDownloadState state = d.downloadState; 162 #else 163 SKDownloadState state = d.state; 164 #endif 165 if (state != SKDownloadStateFinished 166 && state != SKDownloadStateFailed 167 && state != SKDownloadStateCancelled) 168 { 169 return false; 170 } 171 } 172 173 return true; 174 } 175 176 OwnedArray<DownloadImpl> downloads; 177 SKPaymentTransaction* const transaction; 178 }; 179 180 //============================================================================== Pimpljuce::InAppPurchases::Pimpl181 Pimpl (InAppPurchases& p) : owner (p) { [[SKPaymentQueue defaultQueue] addTransactionObserver: delegate.get()]; } ~Pimpljuce::InAppPurchases::Pimpl182 ~Pimpl() noexcept override { [[SKPaymentQueue defaultQueue] removeTransactionObserver: delegate.get()]; } 183 184 //============================================================================== isInAppPurchasesSupportedjuce::InAppPurchases::Pimpl185 bool isInAppPurchasesSupported() const { return true; } 186 getProductsInformationjuce::InAppPurchases::Pimpl187 void getProductsInformation (const StringArray& productIdentifiers) 188 { 189 auto productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers: [NSSet setWithArray: createNSArrayFromStringArray (productIdentifiers)]]; 190 191 pendingProductInfoRequests.add (new PendingProductInfoRequest { PendingProductInfoRequest::Type::query, 192 std::unique_ptr<SKProductsRequest, NSObjectDeleter> (productsRequest) }); 193 194 productsRequest.delegate = delegate.get(); 195 [productsRequest start]; 196 } 197 purchaseProductjuce::InAppPurchases::Pimpl198 void purchaseProduct (const String& productIdentifier, const String&, bool) 199 { 200 if (! [SKPaymentQueue canMakePayments]) 201 { 202 owner.listeners.call ([&] (Listener& l) { l.productPurchaseFinished ({}, false, NEEDS_TRANS ("Payments not allowed")); }); 203 return; 204 } 205 206 auto productIdentifiers = [NSArray arrayWithObject: juceStringToNS (productIdentifier)]; 207 auto productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIdentifiers]]; 208 209 pendingProductInfoRequests.add (new PendingProductInfoRequest { PendingProductInfoRequest::Type::purchase, 210 std::unique_ptr<SKProductsRequest, NSObjectDeleter> (productsRequest) }); 211 212 productsRequest.delegate = delegate.get(); 213 [productsRequest start]; 214 } 215 restoreProductsBoughtListjuce::InAppPurchases::Pimpl216 void restoreProductsBoughtList (bool includeDownloadInfo, const String& subscriptionsSharedSecret) 217 { 218 if (includeDownloadInfo) 219 { 220 [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; 221 } 222 else 223 { 224 auto receiptRequest = [[SKReceiptRefreshRequest alloc] init]; 225 226 pendingReceiptRefreshRequests.add (new PendingReceiptRefreshRequest { subscriptionsSharedSecret, 227 std::unique_ptr<SKReceiptRefreshRequest, NSObjectDeleter> ([receiptRequest retain]) }); 228 receiptRequest.delegate = delegate.get(); 229 [receiptRequest start]; 230 } 231 } 232 consumePurchasejuce::InAppPurchases::Pimpl233 void consumePurchase (const String&, const String&) {} 234 235 //============================================================================== startDownloadsjuce::InAppPurchases::Pimpl236 void startDownloads (const Array<Download*>& downloads) 237 { 238 [[SKPaymentQueue defaultQueue] startDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; 239 } 240 pauseDownloadsjuce::InAppPurchases::Pimpl241 void pauseDownloads (const Array<Download*>& downloads) 242 { 243 [[SKPaymentQueue defaultQueue] pauseDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; 244 } 245 resumeDownloadsjuce::InAppPurchases::Pimpl246 void resumeDownloads (const Array<Download*>& downloads) 247 { 248 [[SKPaymentQueue defaultQueue] resumeDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; 249 } 250 cancelDownloadsjuce::InAppPurchases::Pimpl251 void cancelDownloads (const Array<Download*>& downloads) 252 { 253 [[SKPaymentQueue defaultQueue] cancelDownloads: downloadsToSKDownloads (removeInvalidDownloads (downloads))]; 254 } 255 256 //============================================================================== didReceiveResponsejuce::InAppPurchases::Pimpl257 void didReceiveResponse (SKProductsRequest* request, SKProductsResponse* response) override 258 { 259 for (auto i = 0; i < pendingProductInfoRequests.size(); ++i) 260 { 261 auto& pendingRequest = *pendingProductInfoRequests[i]; 262 263 if (pendingRequest.request.get() == request) 264 { 265 if (pendingRequest.type == PendingProductInfoRequest::Type::query) notifyProductsInfoReceived (response.products); 266 else if (pendingRequest.type == PendingProductInfoRequest::Type::purchase) startPurchase (response.products); 267 else break; 268 269 pendingProductInfoRequests.remove (i); 270 return; 271 } 272 } 273 274 // Unknown request received! 275 jassertfalse; 276 } 277 requestDidFinishjuce::InAppPurchases::Pimpl278 void requestDidFinish (SKRequest* request) override 279 { 280 if (auto receiptRefreshRequest = getAs<SKReceiptRefreshRequest> (request)) 281 { 282 for (auto i = 0; i < pendingReceiptRefreshRequests.size(); ++i) 283 { 284 auto& pendingRequest = *pendingReceiptRefreshRequests[i]; 285 286 if (pendingRequest.request.get() == receiptRefreshRequest) 287 { 288 processReceiptRefreshResponseWithSubscriptionsSharedSecret (pendingRequest.subscriptionsSharedSecret); 289 pendingReceiptRefreshRequests.remove (i); 290 return; 291 } 292 } 293 } 294 } 295 requestDidFailWithErrorjuce::InAppPurchases::Pimpl296 void requestDidFailWithError (SKRequest* request, NSError* error) override 297 { 298 if (auto receiptRefreshRequest = getAs<SKReceiptRefreshRequest> (request)) 299 { 300 for (auto i = 0; i < pendingReceiptRefreshRequests.size(); ++i) 301 { 302 auto& pendingRequest = *pendingReceiptRefreshRequests[i]; 303 304 if (pendingRequest.request.get() == receiptRefreshRequest) 305 { 306 auto errorDetails = error != nil ? (", " + nsStringToJuce ([error localizedDescription])) : String(); 307 owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed") + errorDetails); }); 308 pendingReceiptRefreshRequests.remove (i); 309 return; 310 } 311 } 312 } 313 } 314 updatedTransactionsjuce::InAppPurchases::Pimpl315 void updatedTransactions (SKPaymentQueue*, NSArray<SKPaymentTransaction*>* transactions) override 316 { 317 for (SKPaymentTransaction* transaction in transactions) 318 { 319 switch (transaction.transactionState) 320 { 321 case SKPaymentTransactionStatePurchasing: break; 322 case SKPaymentTransactionStateDeferred: break; 323 case SKPaymentTransactionStateFailed: processTransactionFinish (transaction, false); break; 324 case SKPaymentTransactionStatePurchased: processTransactionFinish (transaction, true); break; 325 case SKPaymentTransactionStateRestored: processTransactionFinish (transaction, true); break; 326 default: jassertfalse; break; // Unexpected transaction state 327 } 328 } 329 } 330 restoreCompletedTransactionsFailedWithErrorjuce::InAppPurchases::Pimpl331 void restoreCompletedTransactionsFailedWithError (SKPaymentQueue*, NSError* error) override 332 { 333 owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, nsStringToJuce (error.localizedDescription)); }); 334 } 335 restoreCompletedTransactionsFinishedjuce::InAppPurchases::Pimpl336 void restoreCompletedTransactionsFinished (SKPaymentQueue*) override 337 { 338 owner.listeners.call ([this] (Listener& l) { l.purchasesListRestored (restoredPurchases, true, NEEDS_TRANS ("Success")); }); 339 restoredPurchases.clear(); 340 } 341 updatedDownloadsjuce::InAppPurchases::Pimpl342 void updatedDownloads (SKPaymentQueue*, NSArray<SKDownload*>* downloads) override 343 { 344 for (SKDownload* download in downloads) 345 { 346 if (auto* pendingDownload = getPendingDownloadFor (download)) 347 { 348 #if JUCE_IOS 349 switch (download.downloadState) 350 #else 351 switch (download.state) 352 #endif 353 { 354 case SKDownloadStateWaiting: break; 355 case SKDownloadStatePaused: owner.listeners.call ([&] (Listener& l) { l.productDownloadPaused (*pendingDownload); }); break; 356 case SKDownloadStateActive: owner.listeners.call ([&] (Listener& l) { l.productDownloadProgressUpdate (*pendingDownload, 357 download.progress, 358 RelativeTime (download.timeRemaining)); }); break; 359 case SKDownloadStateFinished: 360 case SKDownloadStateFailed: 361 case SKDownloadStateCancelled: processDownloadFinish (pendingDownload, download); break; 362 363 default: jassertfalse; break; // Unexpected download state 364 } 365 } 366 } 367 } 368 369 //============================================================================== notifyProductsInfoReceivedjuce::InAppPurchases::Pimpl370 void notifyProductsInfoReceived (NSArray<SKProduct*>* products) 371 { 372 Array<Product> productsToReturn; 373 374 for (SKProduct* skProduct in products) 375 productsToReturn.add (SKProductToIAPProduct (skProduct)); 376 377 owner.listeners.call ([&] (Listener& l) { l.productsInfoReturned (productsToReturn); }); 378 } 379 startPurchasejuce::InAppPurchases::Pimpl380 void startPurchase (NSArray<SKProduct*>* products) 381 { 382 if ([products count] > 0) 383 { 384 // Only one product can be bought at once! 385 jassert ([products count] == 1); 386 387 auto* product = products[0]; 388 auto payment = [SKPayment paymentWithProduct: product]; 389 [[SKPaymentQueue defaultQueue] addPayment: payment]; 390 } 391 else 392 { 393 owner.listeners.call ([] (Listener& l) { l.productPurchaseFinished ({}, false, NEEDS_TRANS ("Your app is not setup for payments")); }); 394 } 395 } 396 397 //============================================================================== removeInvalidDownloadsjuce::InAppPurchases::Pimpl398 Array<Download*> removeInvalidDownloads (const Array<Download*>& downloadsToUse) 399 { 400 Array<Download*> downloads (downloadsToUse); 401 402 for (int i = downloads.size(); --i >= 0;) 403 { 404 auto hasPendingDownload = hasDownloadInPendingDownloadsTransaction (*downloads[i]); 405 406 // Invalid download passed, it does not exist in pending downloads list 407 jassert (hasPendingDownload); 408 409 if (! hasPendingDownload) 410 downloads.remove (i); 411 } 412 413 return downloads; 414 } 415 hasDownloadInPendingDownloadsTransactionjuce::InAppPurchases::Pimpl416 bool hasDownloadInPendingDownloadsTransaction (const Download& download) 417 { 418 for (auto* pdt : pendingDownloadsTransactions) 419 for (auto* pendingDownload : pdt->downloads) 420 if (pendingDownload == &download) 421 return true; 422 423 return false; 424 } 425 426 //============================================================================== processTransactionFinishjuce::InAppPurchases::Pimpl427 void processTransactionFinish (SKPaymentTransaction* transaction, bool success) 428 { 429 auto orderId = nsStringToJuce (transaction.transactionIdentifier); 430 auto packageName = nsStringToJuce ([[NSBundle mainBundle] bundleIdentifier]); 431 auto productId = nsStringToJuce (transaction.payment.productIdentifier); 432 auto purchaseTime = Time (1000 * (int64) transaction.transactionDate.timeIntervalSince1970) 433 .toString (true, true, true, true); 434 435 Purchase purchase { orderId, productId, packageName, purchaseTime, {} }; 436 437 Array<Download*> downloads; 438 439 // If transaction failed or there are no downloads, finish the transaction immediately, otherwise 440 // finish the transaction only after all downloads are finished. 441 if (transaction.transactionState == SKPaymentTransactionStateFailed 442 || transaction.downloads == nil 443 || [transaction.downloads count] == 0) 444 { 445 [[SKPaymentQueue defaultQueue] finishTransaction: transaction]; 446 } 447 else 448 { 449 // On application startup or when the app is resumed we may receive multiple 450 // "purchased" callbacks with the same underlying transaction. Sadly, only 451 // the last set of downloads will be valid. 452 auto* pdt = getPendingDownloadsTransactionForSKTransaction (transaction); 453 454 if (pdt == nullptr) 455 { 456 pdt = pendingDownloadsTransactions.add (new PendingDownloadsTransaction (transaction)); 457 } 458 else 459 { 460 pdt->downloads.clear(); 461 pdt->addDownloadsFromSKTransaction (transaction); 462 } 463 464 for (auto* download : pdt->downloads) 465 downloads.add (download); 466 } 467 468 if (transaction.transactionState == SKPaymentTransactionStateRestored) 469 restoredPurchases.add ({ purchase, downloads }); 470 else 471 owner.listeners.call ([&] (Listener& l) { l.productPurchaseFinished ({ purchase, downloads }, success, 472 SKPaymentTransactionStateToString (transaction.transactionState)); }); 473 } 474 getPendingDownloadsTransactionForSKTransactionjuce::InAppPurchases::Pimpl475 PendingDownloadsTransaction* getPendingDownloadsTransactionForSKTransaction (SKPaymentTransaction* transaction) 476 { 477 for (auto* pdt : pendingDownloadsTransactions) 478 if (pdt->transaction == transaction) 479 return pdt; 480 481 return nullptr; 482 } 483 484 //============================================================================== getPendingDownloadsTransactionSKDownloadForjuce::InAppPurchases::Pimpl485 PendingDownloadsTransaction* getPendingDownloadsTransactionSKDownloadFor (SKDownload* download) 486 { 487 for (auto* pdt : pendingDownloadsTransactions) 488 for (auto* pendingDownload : pdt->downloads) 489 if (pendingDownload->download == download) 490 return pdt; 491 492 jassertfalse; 493 return nullptr; 494 } 495 getPendingDownloadForjuce::InAppPurchases::Pimpl496 Download* getPendingDownloadFor (SKDownload* download) 497 { 498 if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download)) 499 for (auto* pendingDownload : pdt->downloads) 500 if (pendingDownload->download == download) 501 return pendingDownload; 502 503 jassertfalse; 504 return nullptr; 505 } 506 processDownloadFinishjuce::InAppPurchases::Pimpl507 void processDownloadFinish (Download* pendingDownload, SKDownload* download) 508 { 509 if (auto* pdt = getPendingDownloadsTransactionSKDownloadFor (download)) 510 { 511 #if JUCE_IOS 512 SKDownloadState state = download.downloadState; 513 #else 514 SKDownloadState state = download.state; 515 #endif 516 517 auto contentURL = state == SKDownloadStateFinished 518 ? URL (nsStringToJuce (download.contentURL.absoluteString)) 519 : URL(); 520 521 owner.listeners.call ([&] (Listener& l) { l.productDownloadFinished (*pendingDownload, contentURL); }); 522 523 if (pdt->canBeMarkedAsFinished()) 524 { 525 // All downloads finished, mark transaction as finished too. 526 [[SKPaymentQueue defaultQueue] finishTransaction: pdt->transaction]; 527 528 pendingDownloadsTransactions.removeObject (pdt); 529 } 530 } 531 } 532 533 //============================================================================== processReceiptRefreshResponseWithSubscriptionsSharedSecretjuce::InAppPurchases::Pimpl534 void processReceiptRefreshResponseWithSubscriptionsSharedSecret (const String& secret) 535 { 536 auto receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; 537 538 if (auto receiptData = [NSData dataWithContentsOfURL: receiptURL]) 539 fetchReceiptDetailsFromAppStore (receiptData, secret); 540 else 541 owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed")); }); 542 } 543 fetchReceiptDetailsFromAppStorejuce::InAppPurchases::Pimpl544 void fetchReceiptDetailsFromAppStore (NSData* receiptData, const String& secret) 545 { 546 auto requestContents = [NSMutableDictionary dictionaryWithCapacity: (NSUInteger) (secret.isNotEmpty() ? 2 : 1)]; 547 [requestContents setObject: [receiptData base64EncodedStringWithOptions:0] forKey: nsStringLiteral ("receipt-data")]; 548 549 if (secret.isNotEmpty()) 550 [requestContents setObject: juceStringToNS (secret) forKey: nsStringLiteral ("password")]; 551 552 NSError* error; 553 auto requestData = [NSJSONSerialization dataWithJSONObject: requestContents 554 options: 0 555 error: &error]; 556 if (requestData == nil) 557 { 558 sendReceiptFetchFail(); 559 return; 560 } 561 562 #if JUCE_IN_APP_PURCHASES_USE_SANDBOX_ENVIRONMENT 563 auto storeURL = "https://sandbox.itunes.apple.com/verifyReceipt"; 564 #else 565 auto storeURL = "https://buy.itunes.apple.com/verifyReceipt"; 566 #endif 567 568 // TODO: use juce URL here 569 auto* urlPtr = [NSURL URLWithString: nsStringLiteral (storeURL)]; 570 auto storeRequest = [NSMutableURLRequest requestWithURL: urlPtr]; 571 [storeRequest setHTTPMethod: nsStringLiteral ("POST")]; 572 [storeRequest setHTTPBody: requestData]; 573 574 auto task = [[NSURLSession sharedSession] dataTaskWithRequest: storeRequest 575 completionHandler: 576 ^(NSData* data, NSURLResponse*, NSError* connectionError) 577 { 578 if (connectionError != nil) 579 { 580 sendReceiptFetchFail(); 581 } 582 else 583 { 584 NSError* err; 585 586 if (NSDictionary* receiptDetails = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &err]) 587 processReceiptDetails (receiptDetails); 588 else 589 sendReceiptFetchFail(); 590 } 591 }]; 592 593 [task resume]; 594 } 595 processReceiptDetailsjuce::InAppPurchases::Pimpl596 void processReceiptDetails (NSDictionary* receiptDetails) 597 { 598 if (auto receipt = getAs<NSDictionary> (receiptDetails[nsStringLiteral ("receipt")])) 599 { 600 if (auto bundleId = getAs<NSString> (receipt[nsStringLiteral ("bundle_id")])) 601 { 602 if (auto inAppPurchases = getAs<NSArray> (receipt[nsStringLiteral ("in_app")])) 603 { 604 Array<Listener::PurchaseInfo> purchases; 605 606 for (id inAppPurchaseData in inAppPurchases) 607 { 608 if (auto* purchaseData = getAs<NSDictionary> (inAppPurchaseData)) 609 { 610 // Ignore products that were cancelled. 611 if (purchaseData[nsStringLiteral ("cancellation_date")] != nil) 612 continue; 613 614 if (auto transactionId = getAs<NSString> (purchaseData[nsStringLiteral ("original_transaction_id")])) 615 { 616 if (auto productId = getAs<NSString> (purchaseData[nsStringLiteral ("product_id")])) 617 { 618 auto purchaseTime = getPurchaseDateMs (purchaseData[nsStringLiteral ("purchase_date_ms")]); 619 620 if (purchaseTime > 0) 621 { 622 purchases.add ({ { nsStringToJuce (transactionId), 623 nsStringToJuce (productId), 624 nsStringToJuce (bundleId), 625 Time (purchaseTime).toString (true, true, true, true), 626 {} }, {} }); 627 } 628 else 629 { 630 return sendReceiptFetchFailAsync(); 631 } 632 } 633 } 634 } 635 else 636 { 637 return sendReceiptFetchFailAsync(); 638 } 639 } 640 641 MessageManager::callAsync ([this, purchases] { owner.listeners.call ([&] (Listener& l) { l.purchasesListRestored (purchases, true, NEEDS_TRANS ("Success")); }); }); 642 return; 643 } 644 } 645 } 646 647 sendReceiptFetchFailAsync(); 648 } 649 sendReceiptFetchFailjuce::InAppPurchases::Pimpl650 void sendReceiptFetchFail() 651 { 652 owner.listeners.call ([] (Listener& l) { l.purchasesListRestored ({}, false, NEEDS_TRANS ("Receipt fetch failed")); }); 653 } 654 sendReceiptFetchFailAsyncjuce::InAppPurchases::Pimpl655 void sendReceiptFetchFailAsync() 656 { 657 MessageManager::callAsync ([this] { sendReceiptFetchFail(); }); 658 } 659 getPurchaseDateMsjuce::InAppPurchases::Pimpl660 static int64 getPurchaseDateMs (id date) 661 { 662 if (auto dateAsNumber = getAs<NSNumber> (date)) 663 { 664 return [dateAsNumber longLongValue]; 665 } 666 else if (auto dateAsString = getAs<NSString> (date)) 667 { 668 auto formatter = [[NSNumberFormatter alloc] init]; 669 [formatter setNumberStyle: NSNumberFormatterDecimalStyle]; 670 dateAsNumber = [formatter numberFromString: dateAsString]; 671 [formatter release]; 672 return [dateAsNumber longLongValue]; 673 } 674 675 return -1; 676 } 677 678 //============================================================================== SKProductToIAPProductjuce::InAppPurchases::Pimpl679 static Product SKProductToIAPProduct (SKProduct* skProduct) 680 { 681 NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; 682 [numberFormatter setFormatterBehavior: NSNumberFormatterBehavior10_4]; 683 [numberFormatter setNumberStyle: NSNumberFormatterCurrencyStyle]; 684 [numberFormatter setLocale: skProduct.priceLocale]; 685 686 auto identifier = nsStringToJuce (skProduct.productIdentifier); 687 auto title = nsStringToJuce (skProduct.localizedTitle); 688 auto description = nsStringToJuce (skProduct.localizedDescription); 689 auto priceLocale = nsStringToJuce ([skProduct.priceLocale objectForKey: NSLocaleLanguageCode]); 690 auto price = nsStringToJuce ([numberFormatter stringFromNumber: skProduct.price]); 691 692 [numberFormatter release]; 693 694 return { identifier, title, description, price, priceLocale }; 695 } 696 SKPaymentTransactionStateToStringjuce::InAppPurchases::Pimpl697 static String SKPaymentTransactionStateToString (SKPaymentTransactionState state) 698 { 699 switch (state) 700 { 701 case SKPaymentTransactionStatePurchasing: return NEEDS_TRANS ("Purchasing"); 702 case SKPaymentTransactionStatePurchased: return NEEDS_TRANS ("Success"); 703 case SKPaymentTransactionStateFailed: return NEEDS_TRANS ("Failure"); 704 case SKPaymentTransactionStateRestored: return NEEDS_TRANS ("Restored"); 705 case SKPaymentTransactionStateDeferred: return NEEDS_TRANS ("Deferred"); 706 default: jassertfalse; return NEEDS_TRANS ("Unknown status"); 707 } 708 709 } 710 SKDownloadStateToDownloadStatusjuce::InAppPurchases::Pimpl711 static Download::Status SKDownloadStateToDownloadStatus (SKDownloadState state) 712 { 713 switch (state) 714 { 715 case SKDownloadStateWaiting: return Download::Status::waiting; 716 case SKDownloadStateActive: return Download::Status::active; 717 case SKDownloadStatePaused: return Download::Status::paused; 718 case SKDownloadStateFinished: return Download::Status::finished; 719 case SKDownloadStateFailed: return Download::Status::failed; 720 case SKDownloadStateCancelled: return Download::Status::cancelled; 721 default: jassertfalse; return Download::Status::waiting; 722 } 723 } 724 downloadsToSKDownloadsjuce::InAppPurchases::Pimpl725 static NSArray<SKDownload*>* downloadsToSKDownloads (const Array<Download*>& downloads) 726 { 727 NSMutableArray<SKDownload*>* skDownloads = [NSMutableArray arrayWithCapacity: (NSUInteger) downloads.size()]; 728 729 for (const auto& d : downloads) 730 if (auto impl = dynamic_cast<DownloadImpl*>(d)) 731 [skDownloads addObject: impl->download]; 732 733 return skDownloads; 734 } 735 736 template <typename ObjCType> getAsjuce::InAppPurchases::Pimpl737 static ObjCType* getAs (id o) 738 { 739 if (o == nil || ! [o isKindOfClass: [ObjCType class]]) 740 return nil; 741 742 return (ObjCType*) o; 743 } 744 745 //============================================================================== 746 InAppPurchases& owner; 747 748 OwnedArray<PendingProductInfoRequest> pendingProductInfoRequests; 749 OwnedArray<PendingReceiptRefreshRequest> pendingReceiptRefreshRequests; 750 751 OwnedArray<PendingDownloadsTransaction> pendingDownloadsTransactions; 752 Array<Listener::PurchaseInfo> restoredPurchases; 753 }; 754 755 } // namespace juce 756