diff --git a/.jazzy.yaml b/.jazzy.yaml index dbb50e9ae4..e786161a49 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -5,9 +5,9 @@ objc: true sdk: iphonesimulator module: Purchases umbrella_header: Purchases/Public/Purchases.h -module_version: 3.5.1 +module_version: 3.5.2 github_url: https://github.com/revenuecat/purchases-ios -github_file_prefix: https://github.com/revenuecat/purchases-ios/tree/3.5.1 +github_file_prefix: https://github.com/revenuecat/purchases-ios/tree/3.5.2 output: docs # Leaving this commented out. We used to specify this before, but now it's working without it # xcodebuild_arguments: [--objc,Purchases/Public/Purchases.h,--,-x,objective-c,-isysroot,$(xcrun --show-sdk-path),-I,$(pwd)] diff --git a/CHANGELOG.md b/CHANGELOG.md index 890d3a3ade..c41db68ae1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.5.2 +- Feature/defer cache updates if woken from push notification +https://github.com/RevenueCat/purchases-ios/pull/288 + ## 3.5.1 - Removes all references to ASIdentifierManager and advertisingIdentifier. This should help with some Kids apps being rejected https://github.com/RevenueCat/purchases-ios/pull/286 diff --git a/Purchases.podspec b/Purchases.podspec index 21ef86a46c..28ca8e7a48 100644 --- a/Purchases.podspec +++ b/Purchases.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Purchases" - s.version = "3.5.1" + s.version = "3.5.2" s.summary = "Subscription and in-app-purchase backend service." s.description = <<-DESC diff --git a/Purchases.xcodeproj/project.pbxproj b/Purchases.xcodeproj/project.pbxproj index c4f253e753..4de6c5bc0a 100644 --- a/Purchases.xcodeproj/project.pbxproj +++ b/Purchases.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 2D8DB34B24072AAE00BE3D31 /* SubscriberAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D8DB34A24072AAE00BE3D31 /* SubscriberAttributeTests.swift */; }; 2DD448FF24088473002F5694 /* RCPurchases+SubscriberAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DD448FD24088473002F5694 /* RCPurchases+SubscriberAttributes.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2DD4490024088473002F5694 /* RCPurchases+SubscriberAttributes.m in Sources */ = {isa = PBXBuildFile; fileRef = 2DD448FE24088473002F5694 /* RCPurchases+SubscriberAttributes.m */; }; + 2DD7BA4D24C63A830066B4C2 /* MockSystemInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD7BA4C24C63A830066B4C2 /* MockSystemInfo.swift */; }; 2DEB9767247DB46900A92099 /* RCISOPeriodFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = 2DEB9766247DB46900A92099 /* RCISOPeriodFormatter.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2DEB976B247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DEB976A247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift */; }; 350FBDE91F7EEF070065833D /* RCPurchases.h in Headers */ = {isa = PBXBuildFile; fileRef = 350FBDE71F7EEF070065833D /* RCPurchases.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -178,6 +179,7 @@ 2D8DB34A24072AAE00BE3D31 /* SubscriberAttributeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriberAttributeTests.swift; sourceTree = ""; }; 2DD448FD24088473002F5694 /* RCPurchases+SubscriberAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "RCPurchases+SubscriberAttributes.h"; path = "Purchases/SubscriberAttributes/RCPurchases+SubscriberAttributes.h"; sourceTree = SOURCE_ROOT; }; 2DD448FE24088473002F5694 /* RCPurchases+SubscriberAttributes.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "RCPurchases+SubscriberAttributes.m"; path = "Purchases/SubscriberAttributes/RCPurchases+SubscriberAttributes.m"; sourceTree = SOURCE_ROOT; }; + 2DD7BA4C24C63A830066B4C2 /* MockSystemInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSystemInfo.swift; sourceTree = ""; }; 2DEB9766247DB46900A92099 /* RCISOPeriodFormatter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCISOPeriodFormatter.h; sourceTree = ""; }; 2DEB976A247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKProductSubscriptionDurationExtensions.swift; sourceTree = ""; }; 350A1B84226E3E8700CCA10F /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; @@ -480,6 +482,7 @@ 37E351D48260D9DC8B1EE360 /* MockSubscriberAttributesManager.swift */, 2DEB976A247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift */, 37E35EABF6D7AFE367718784 /* MockSKDiscount.swift */, + 2DD7BA4C24C63A830066B4C2 /* MockSystemInfo.swift */, ); path = Mocks; sourceTree = ""; @@ -902,6 +905,7 @@ 37E35EBDFC5CD3068E1792A3 /* MockNotificationCenter.swift in Sources */, 37E354E0A9A371481540B2B0 /* MockAttributionFetcher.swift in Sources */, 37E35EDC57C486AC2D66B4B8 /* MockOfferingsFactory.swift in Sources */, + 2DD7BA4D24C63A830066B4C2 /* MockSystemInfo.swift in Sources */, 37E35EB7B35C86140B96C58B /* MockUserManager.swift in Sources */, 37E357E33F0E20D92EE6372E /* MockSKProduct.swift in Sources */, 37E3524CB70618E6C5F3DB49 /* MockPurchasesDelegate.swift in Sources */, diff --git a/Purchases/Info.plist b/Purchases/Info.plist index 20193b9f1f..ddcb8cf708 100644 --- a/Purchases/Info.plist +++ b/Purchases/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 3.5.1 + 3.5.2 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSHumanReadableCopyright diff --git a/Purchases/Misc/RCCrossPlatformSupport.h b/Purchases/Misc/RCCrossPlatformSupport.h index 677caa5c76..b8bd7594e5 100644 --- a/Purchases/Misc/RCCrossPlatformSupport.h +++ b/Purchases/Misc/RCCrossPlatformSupport.h @@ -17,10 +17,13 @@ #define APP_WILL_RESIGN_ACTIVE_NOTIFICATION_NAME NSExtensionHostWillResignActiveNotification #endif -#if TARGET_OS_IPHONE +#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_MACCATALYST #import #elif TARGET_OS_OSX #import +#elif TARGET_OS_WATCH +#import +#import #endif #if TARGET_OS_MACCATALYST @@ -58,3 +61,11 @@ #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 6df90b391d..c6bdf96623 100644 --- a/Purchases/Misc/RCSystemInfo.h +++ b/Purchases/Misc/RCSystemInfo.h @@ -19,6 +19,9 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, copy, readonly) NSString *platformFlavor; @property(nonatomic, copy, readonly) NSString *platformFlavorVersion; + +- (BOOL)isApplicationBackgrounded; + + (BOOL)isSandbox; + (NSString *)frameworkVersion; + (NSString *)systemVersion; diff --git a/Purchases/Misc/RCSystemInfo.m b/Purchases/Misc/RCSystemInfo.m index 2fba14bae3..22ba0eda7f 100644 --- a/Purchases/Misc/RCSystemInfo.m +++ b/Purchases/Misc/RCSystemInfo.m @@ -47,7 +47,7 @@ + (BOOL)isSandbox { } + (NSString *)frameworkVersion { - return @"3.5.1"; + return @"3.5.2"; } + (NSString *)systemVersion { @@ -75,6 +75,7 @@ + (NSURL *)serverHostURL { + (nullable NSURL *)proxyURL { return proxyURL; } + + (void)setProxyURL:(nullable NSURL *)newProxyURL { proxyURL = newProxyURL; if (newProxyURL) { @@ -82,6 +83,10 @@ + (void)setProxyURL:(nullable NSURL *)newProxyURL { } } +- (BOOL)isApplicationBackgrounded { + return IS_APPLICATION_BACKGROUNDED; +} + @end diff --git a/Purchases/Public/RCPurchases.m b/Purchases/Public/RCPurchases.m index cad316ad9c..bb2ab0a304 100644 --- a/Purchases/Public/RCPurchases.m +++ b/Purchases/Public/RCPurchases.m @@ -288,7 +288,12 @@ - (instancetype)initWithAppUserID:(nullable NSString *)appUserID }; [self.identityManager configureWithAppUserID:appUserID]; - [self updateAllCachesWithCompletionBlock:callDelegate]; + if (!self.systemInfo.isApplicationBackgrounded) { + [self updateAllCachesWithCompletionBlock:callDelegate]; + } else { + [self sendCachedPurchaserInfoIfAvailable]; + } + [self configureSubscriberAttributesManager]; self.storeKitWrapper.delegate = self; @@ -335,11 +340,8 @@ - (void)setDelegate:(id)delegate { _delegate = delegate; RCDebugLog(@"Delegate set"); - - RCPurchaserInfo *infoFromCache = [self readPurchaserInfoFromCache]; - if (infoFromCache) { - [self sendUpdatedPurchaserInfoToDelegateIfChanged:infoFromCache]; - } + + [self sendCachedPurchaserInfoIfAvailable]; } #pragma mark - Public Methods @@ -738,6 +740,17 @@ - (void)setPushToken:(nullable NSData *)pushToken { - (void)applicationDidBecomeActive:(__unused NSNotification *)notif { + [self updateAllCachesIfNeeded]; +} + +- (void)sendCachedPurchaserInfoIfAvailable { + RCPurchaserInfo *infoFromCache = [self readPurchaserInfoFromCache]; + if (infoFromCache) { + [self sendUpdatedPurchaserInfoToDelegateIfChanged:infoFromCache]; + } +} + +- (void)updateAllCachesIfNeeded { RCDebugLog(@"applicationDidBecomeActive"); if ([self.deviceCache isPurchaserInfoCacheStale]) { RCDebugLog(@"PurchaserInfo cache is stale, updating caches"); diff --git a/PurchasesTests/Info.plist b/PurchasesTests/Info.plist index a2ddb1416f..6633e584bf 100644 --- a/PurchasesTests/Info.plist +++ b/PurchasesTests/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 3.5.1 + 3.5.2 CFBundleVersion 1 diff --git a/PurchasesTests/Mocks/MockSystemInfo.swift b/PurchasesTests/Mocks/MockSystemInfo.swift new file mode 100644 index 0000000000..c3c97236e2 --- /dev/null +++ b/PurchasesTests/Mocks/MockSystemInfo.swift @@ -0,0 +1,17 @@ +// +// MockSystemInfo.swift +// PurchasesTests +// +// Created by Andrés Boedo on 7/20/20. +// Copyright © 2020 Purchases. All rights reserved. +// + +import Foundation + +class MockSystemInfo: RCSystemInfo { + var stubbedIsApplicationBackgrounded: Bool? + + override func isApplicationBackgrounded() -> Bool { + return stubbedIsApplicationBackgrounded ?? super.isApplicationBackgrounded() + } +} diff --git a/PurchasesTests/Purchasing/PurchasesTests.swift b/PurchasesTests/Purchasing/PurchasesTests.swift index 69ddb41789..8740019055 100644 --- a/PurchasesTests/Purchasing/PurchasesTests.swift +++ b/PurchasesTests/Purchasing/PurchasesTests.swift @@ -179,7 +179,8 @@ class PurchasesTests: XCTestCase { let deviceCache = MockDeviceCache() let subscriberAttributesManager = MockSubscriberAttributesManager() let identityManager = MockUserManager(mockAppUserID: "app_user"); - + let systemInfo = MockSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) + let purchasesDelegate = MockPurchasesDelegate() var purchases: Purchases! @@ -187,8 +188,7 @@ class PurchasesTests: XCTestCase { func setupPurchases(automaticCollection: Bool = false) { Purchases.automaticAppleSearchAdsAttributionCollection = automaticCollection self.identityManager.mockIsAnonymous = false - let systemInfo = RCSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) - + purchases = Purchases(appUserID: identityManager.currentAppUserID, requestFetcher: requestFetcher, receiptFetcher: receiptFetcher, @@ -209,8 +209,7 @@ class PurchasesTests: XCTestCase { func setupAnonPurchases() { Purchases.automaticAppleSearchAdsAttributionCollection = false self.identityManager.mockIsAnonymous = true - let systemInfo = RCSystemInfo(platformFlavor: nil, platformFlavorVersion: nil, finishTransactions: true) - + purchases = Purchases(appUserID: nil, requestFetcher: requestFetcher, receiptFetcher: receiptFetcher, @@ -259,10 +258,46 @@ class PurchasesTests: XCTestCase { expect(self.purchasesDelegate.purchaserInfoReceivedCount).toEventually(equal(1)) } - func testFirstInitializationCallDelegateForAnon() { - setupAnonPurchases() + func testFirstInitializationFromForegroundDelegateForAnonIfNothingCached() { + systemInfo.stubbedIsApplicationBackgrounded = false + setupPurchases() + expect(self.purchasesDelegate.purchaserInfoReceivedCount).toEventually(equal(1)) + } + + func testFirstInitializationFromBackgroundDoesntCallDelegateForAnonIfNothingCached() { + systemInfo.stubbedIsApplicationBackgrounded = true + setupPurchases() + expect(self.purchasesDelegate.purchaserInfoReceivedCount).toEventually(equal(0)) + } + + func testFirstInitializationFromBackgroundDoesntCallDelegateForAnonIfInfoCached() { + systemInfo.stubbedIsApplicationBackgrounded = true + let info = Purchases.PurchaserInfo(data: [ + "subscriber": [ + "subscriptions": [:], + "other_purchases": [:] + ]]); + + let jsonObject = info!.jsonObject() + + let object = try! JSONSerialization.data(withJSONObject: jsonObject, options: []); + self.deviceCache.cachedPurchaserInfo[identityManager.currentAppUserID] = object + + setupPurchases() expect(self.purchasesDelegate.purchaserInfoReceivedCount).toEventually(equal(1)) } + + func testFirstInitializationFromBackgroundDoesntUpdatePurchaserInfoCache() { + systemInfo.stubbedIsApplicationBackgrounded = true + setupPurchases() + expect(self.backend.getSubscriberCallCount).toEventually(equal(0)) + } + + func testFirstInitializationFromForegroundUpdatesPurchaserInfoCache() { + systemInfo.stubbedIsApplicationBackgrounded = false + setupPurchases() + expect(self.backend.getSubscriberCallCount).toEventually(equal(1)) + } func testDelegateIsCalledForRandomPurchaseSuccess() { setupPurchases() @@ -606,7 +641,7 @@ class PurchasesTests: XCTestCase { } } - func testFetchesProductInfoIfNotCached() { + func testFetchesProductInfoIfNotCachedAndAppActive() { setupPurchases() let product = MockSKProduct(mockProductIdentifier: "com.product.id1") @@ -1258,6 +1293,18 @@ class PurchasesTests: XCTestCase { expect(offerings!["base"]!.monthly?.product).toNot(beNil()) } + func testFirstInitializationGetsOfferingsIfAppActive() { + systemInfo.stubbedIsApplicationBackgrounded = false + setupPurchases() + expect(self.backend.gotOfferings).toEventually(equal(1)) + } + + func testFirstInitializationDoesntProductInfoFromOfferingsIfAppBackgrounded() { + systemInfo.stubbedIsApplicationBackgrounded = true + setupPurchases() + expect(self.backend.gotOfferings).toEventually(equal(0)) + } + func testProductInfoIsCachedForOfferings() { setupPurchases() expect(self.backend.gotOfferings).toEventually(equal(1))