diff --git a/Purchases/Misc/RCCrossPlatformSupport.h b/Purchases/Misc/RCCrossPlatformSupport.h index b8bd7594e5..89482cc062 100644 --- a/Purchases/Misc/RCCrossPlatformSupport.h +++ b/Purchases/Misc/RCCrossPlatformSupport.h @@ -61,11 +61,3 @@ #else #define PURCHASES_INITIATED_FROM_APP_STORE_AVAILABLE 0 #endif - -#if TARGET_OS_IOS || TARGET_OS_TV -#define IS_APPLICATION_BACKGROUNDED UIApplication.sharedApplication.applicationState == UIApplicationStateBackground -#elif TARGET_OS_OSX -#define IS_APPLICATION_BACKGROUNDED NO -#elif TARGET_OS_WATCH -#define IS_APPLICATION_BACKGROUNDED WKExtension.sharedExtension.applicationState == WKApplicationStateBackground -#endif diff --git a/Purchases/Misc/RCSystemInfo.h b/Purchases/Misc/RCSystemInfo.h index c6bdf96623..4bc56704ec 100644 --- a/Purchases/Misc/RCSystemInfo.h +++ b/Purchases/Misc/RCSystemInfo.h @@ -20,7 +20,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, copy, readonly) NSString *platformFlavorVersion; -- (BOOL)isApplicationBackgrounded; +- (void)isApplicationBackgroundedWithCompletion:(void(^)(BOOL))completion; // calls completion on the main thread + (BOOL)isSandbox; + (NSString *)frameworkVersion; diff --git a/Purchases/Misc/RCSystemInfo.m b/Purchases/Misc/RCSystemInfo.m index 00062f862c..bf32f817cf 100644 --- a/Purchases/Misc/RCSystemInfo.m +++ b/Purchases/Misc/RCSystemInfo.m @@ -83,10 +83,45 @@ + (void)setProxyURL:(nullable NSURL *)newProxyURL { } } +- (void)isApplicationBackgroundedWithCompletion:(void(^)(BOOL))completion { + dispatch_async(dispatch_get_main_queue(), ^{ + BOOL isApplicationBackgrounded = self.isApplicationBackgrounded; + completion(isApplicationBackgrounded); + }); +} + - (BOOL)isApplicationBackgrounded { - return IS_APPLICATION_BACKGROUNDED; +#if TARGET_OS_IOS + return self.isApplicationBackgroundedIOS; +#elif TARGET_OS_TV + return UIApplication.sharedApplication.applicationState == UIApplicationStateBackground; +#elif TARGET_OS_OSX + return NO; +#elif TARGET_OS_WATCH + return WKExtension.sharedExtension.applicationState == WKApplicationStateBackground; +#endif } +#if TARGET_OS_IOS +// iOS App extensions can't access UIApplication.sharedApplication, and will fail to compile if any calls to +// it are made. There are no pre-processor macros available to check if the code is running in an app extension, +// so we check if we're running in an app extension at runtime, and if not, we use KVC to call sharedApplication. +- (BOOL)isApplicationBackgroundedIOS { + if (self.isAppExtension) { + return YES; + } + NSString *sharedApplicationPropertyName = @"sharedApplication"; + + UIApplication *sharedApplication = [UIApplication valueForKey:sharedApplicationPropertyName]; + return sharedApplication.applicationState == UIApplicationStateBackground; +} + +- (BOOL)isAppExtension { + return [NSBundle.mainBundle.bundlePath hasSuffix:@".appex"]; +} + +#endif + @end diff --git a/Purchases/Public/RCPurchases.m b/Purchases/Public/RCPurchases.m index 386a50d3c3..44dc46194a 100644 --- a/Purchases/Public/RCPurchases.m +++ b/Purchases/Public/RCPurchases.m @@ -295,12 +295,18 @@ - (instancetype)initWithAppUserID:(nullable NSString *)appUserID }; [self.identityManager configureWithAppUserID:appUserID]; - if (!self.systemInfo.isApplicationBackgrounded) { - [self updateAllCachesWithCompletionBlock:callDelegate]; - } else { - [self sendCachedPurchaserInfoIfAvailable]; - } - + + [self.systemInfo isApplicationBackgroundedWithCompletion:^(BOOL isBackgrounded) { + if (!isBackgrounded) { + dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(backgroundQueue, ^{ + [self updateAllCachesWithCompletionBlock:callDelegate]; + }); + } else { + [self sendCachedPurchaserInfoIfAvailable]; + } + }]; + [self configureSubscriberAttributesManager]; self.storeKitWrapper.delegate = self; diff --git a/PurchasesTests/Mocks/MockSystemInfo.swift b/PurchasesTests/Mocks/MockSystemInfo.swift index c3c97236e2..e79aa906e1 100644 --- a/PurchasesTests/Mocks/MockSystemInfo.swift +++ b/PurchasesTests/Mocks/MockSystemInfo.swift @@ -11,7 +11,7 @@ import Foundation class MockSystemInfo: RCSystemInfo { var stubbedIsApplicationBackgrounded: Bool? - override func isApplicationBackgrounded() -> Bool { - return stubbedIsApplicationBackgrounded ?? super.isApplicationBackgrounded() + override func isApplicationBackgrounded(completion: @escaping (Bool) -> Void) { + completion(stubbedIsApplicationBackgrounded ?? false) } } diff --git a/PurchasesTests/Networking/HTTPClientTests.swift b/PurchasesTests/Networking/HTTPClientTests.swift index 96b98ffab2..37a62daee5 100644 --- a/PurchasesTests/Networking/HTTPClientTests.swift +++ b/PurchasesTests/Networking/HTTPClientTests.swift @@ -206,10 +206,17 @@ class HTTPClientTests: XCTestCase { } self.client.performRequest("GET", path: path, body: nil, headers: nil) { (status, data, responseError) in - successFailed = (status >= 500) && (data == nil) && (error == responseError as NSError?) + if let responseNSError = responseError as? NSError { + successFailed = (status >= 500 + && data == nil + && error.domain == responseNSError.domain + && error.code == responseNSError.code) + } else { + successFailed = false + } } - expect(successFailed).toEventually(equal(true), timeout: 1.0) + expect(successFailed).toEventually(equal(true)) } func testServerSide400s() { diff --git a/PurchasesTests/Purchasing/PurchasesTests.swift b/PurchasesTests/Purchasing/PurchasesTests.swift index 38bce429e5..ad0b0cfe5b 100644 --- a/PurchasesTests/Purchasing/PurchasesTests.swift +++ b/PurchasesTests/Purchasing/PurchasesTests.swift @@ -12,6 +12,8 @@ class PurchasesTests: XCTestCase { override func setUp() { self.userDefaults = UserDefaults(suiteName: "TestDefaults") + requestFetcher = MockRequestFetcher() + systemInfo = MockSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) } override func tearDown() { @@ -169,7 +171,7 @@ class PurchasesTests: XCTestCase { let receiptFetcher = MockReceiptFetcher() - let requestFetcher = MockRequestFetcher() + var requestFetcher: MockRequestFetcher! let backend = MockBackend() let storeKitWrapper = MockStoreKitWrapper() let notificationCenter = MockNotificationCenter() @@ -179,7 +181,7 @@ class PurchasesTests: XCTestCase { let deviceCache = MockDeviceCache() let subscriberAttributesManager = MockSubscriberAttributesManager() let identityManager = MockUserManager(mockAppUserID: "app_user"); - let systemInfo = MockSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) + var systemInfo: MockSystemInfo! let purchasesDelegate = MockPurchasesDelegate() @@ -641,7 +643,8 @@ class PurchasesTests: XCTestCase { } } - func testFetchesProductInfoIfNotCachedAndAppActive() { + func testFetchesProductInfoIfNotCached() { + systemInfo.stubbedIsApplicationBackgrounded = true setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") @@ -657,7 +660,7 @@ class PurchasesTests: XCTestCase { transaction.mockState = SKPaymentTransactionState.purchased self.storeKitWrapper.delegate?.storeKitWrapper(self.storeKitWrapper, updatedTransaction: transaction) - expect(self.requestFetcher.requestedProducts! as NSSet).to(contain([product.productIdentifier])) + expect(self.requestFetcher.requestedProducts! as NSSet).toEventually(contain([product.productIdentifier])) expect(self.backend.postedProductID).toNot(beNil()) expect(self.backend.postedPrice).toNot(beNil()) @@ -1270,7 +1273,7 @@ class PurchasesTests: XCTestCase { setupPurchases() - expect(self.backend.getSubscriberCallCount).to(equal(1)) + expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) purchases!.purchaserInfo { (info, error) in } @@ -2074,7 +2077,7 @@ class PurchasesTests: XCTestCase { expect(self.backend.getSubscriberCallCount).toEventually(equal(2)) expect(self.deviceCache.cachedPurchaserInfo.count).toEventually(equal(2)) expect(self.deviceCache.cachedPurchaserInfo[newAppUserID]).toNot(beNil()) - expect(self.purchasesDelegate.purchaserInfoReceivedCount).toEventually(equal(2)) + expect(self.purchasesDelegate.purchaserInfoReceivedCount).toEventually(equal(2), timeout: 3.0) expect(self.deviceCache.setPurchaserInfoCacheTimestampToNowCount).toEventually(equal(2)) expect(self.deviceCache.setOfferingsCacheTimestampToNowCount).toEventually(equal(2)) expect(self.backend.gotOfferings).toEventually(equal(2))