Skip to content

Commit

Permalink
Feature/defer cache updates if woken from push notification (#288)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
aboedo authored and vegaro committed Jul 28, 2020
1 parent c44d4e1 commit b1593b6
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 15 deletions.
4 changes: 4 additions & 0 deletions Purchases.xcodeproj/project.pbxproj
Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -178,6 +179,7 @@
2D8DB34A24072AAE00BE3D31 /* SubscriberAttributeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriberAttributeTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
2DEB9766247DB46900A92099 /* RCISOPeriodFormatter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCISOPeriodFormatter.h; sourceTree = "<group>"; };
2DEB976A247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKProductSubscriptionDurationExtensions.swift; sourceTree = "<group>"; };
350A1B84226E3E8700CCA10F /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
Expand Down Expand Up @@ -480,6 +482,7 @@
37E351D48260D9DC8B1EE360 /* MockSubscriberAttributesManager.swift */,
2DEB976A247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift */,
37E35EABF6D7AFE367718784 /* MockSKDiscount.swift */,
2DD7BA4C24C63A830066B4C2 /* MockSystemInfo.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down
13 changes: 12 additions & 1 deletion Purchases/Misc/RCCrossPlatformSupport.h
Expand Up @@ -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 <UIKit/UIKit.h>
#elif TARGET_OS_OSX
#import <AppKit/AppKit.h>
#elif TARGET_OS_WATCH
#import <UIKit/UIKit.h>
#import <WatchKit/WatchKit.h>
#endif

#if TARGET_OS_MACCATALYST
Expand Down Expand Up @@ -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
3 changes: 3 additions & 0 deletions Purchases/Misc/RCSystemInfo.h
Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions Purchases/Misc/RCSystemInfo.m
Expand Up @@ -75,13 +75,18 @@ + (NSURL *)serverHostURL {
+ (nullable NSURL *)proxyURL {
return proxyURL;
}

+ (void)setProxyURL:(nullable NSURL *)newProxyURL {
proxyURL = newProxyURL;
if (newProxyURL) {
RCLog(@"Purchases is being configured using a proxy for RevenueCat with URL: %@", newProxyURL);
}
}

- (BOOL)isApplicationBackgrounded {
return IS_APPLICATION_BACKGROUNDED;
}

@end


Expand Down
25 changes: 19 additions & 6 deletions Purchases/Public/RCPurchases.m
Expand Up @@ -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;
Expand Down Expand Up @@ -335,11 +340,8 @@ - (void)setDelegate:(id<RCPurchasesDelegate>)delegate
{
_delegate = delegate;
RCDebugLog(@"Delegate set");

RCPurchaserInfo *infoFromCache = [self readPurchaserInfoFromCache];
if (infoFromCache) {
[self sendUpdatedPurchaserInfoToDelegateIfChanged:infoFromCache];
}

[self sendCachedPurchaserInfoIfAvailable];
}

#pragma mark - Public Methods
Expand Down Expand Up @@ -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");
Expand Down
17 changes: 17 additions & 0 deletions 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()
}
}
63 changes: 55 additions & 8 deletions PurchasesTests/Purchasing/PurchasesTests.swift
Expand Up @@ -179,16 +179,16 @@ 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!

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,
Expand All @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -606,7 +641,7 @@ class PurchasesTests: XCTestCase {
}
}

func testFetchesProductInfoIfNotCached() {
func testFetchesProductInfoIfNotCachedAndAppActive() {
setupPurchases()
let product = MockSKProduct(mockProductIdentifier: "com.product.id1")

Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit b1593b6

Please sign in to comment.