From bc0e76d02984bafc7cabc1d2204145cbad575173 Mon Sep 17 00:00:00 2001 From: Cesar de la Vega Date: Tue, 28 Jul 2020 12:55:16 -0700 Subject: [PATCH] Hotfix Release 3.5.2 (#299) * Feature/defer cache updates if woken from push notification (#288) * Added logic to skip updating cache on setup if app is not active * inverted checks from is app active to !is app backgrounded * made the app backgrounded method an instance method to make tests easier. Added tests to check that caches aren't updated when the SDK is set up with the app running in the background. * updated value of isApplicationBackgrounded for macOS, since the value for NSApplication.sharedApplication.isActive is nil for whatever reason. * Version 3.5.2 Co-authored-by: aboedo --- .jazzy.yaml | 4 +- CHANGELOG.md | 4 ++ Purchases.podspec | 2 +- Purchases.xcodeproj/project.pbxproj | 4 ++ Purchases/Info.plist | 2 +- Purchases/Misc/RCCrossPlatformSupport.h | 13 +++- Purchases/Misc/RCSystemInfo.h | 3 + Purchases/Misc/RCSystemInfo.m | 7 ++- Purchases/Public/RCPurchases.m | 25 ++++++-- PurchasesTests/Info.plist | 2 +- PurchasesTests/Mocks/MockSystemInfo.swift | 17 +++++ .../Purchasing/PurchasesTests.swift | 63 ++++++++++++++++--- 12 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 PurchasesTests/Mocks/MockSystemInfo.swift 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))