Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/defer cache updates if woken from push notification #288

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions Purchases.xcodeproj/project.pbxproj
Expand Up @@ -19,6 +19,7 @@
2DD02D5B24AD129A00419CD9 /* LocalReceiptParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DD02D5A24AD129A00419CD9 /* LocalReceiptParserTests.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 @@ -184,6 +185,7 @@
2DD02D5A24AD129A00419CD9 /* LocalReceiptParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalReceiptParserTests.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>"; };
2DEC0CFB24A2A1B100B0E5BB /* Package.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = SOURCE_ROOT; };
Expand Down Expand Up @@ -519,6 +521,7 @@
37E351D48260D9DC8B1EE360 /* MockSubscriberAttributesManager.swift */,
2DEB976A247DB85400A92099 /* SKProductSubscriptionDurationExtensions.swift */,
37E35EABF6D7AFE367718784 /* MockSKDiscount.swift */,
2DD7BA4C24C63A830066B4C2 /* MockSystemInfo.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -942,6 +945,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>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

watch apps have both

#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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Much easier than in Android!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apple's API consistency at its finest 🤪

#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 @@ -290,7 +290,12 @@ - (instancetype)initWithAppUserID:(nullable NSString *)appUserID
};

[self.identityManager configureWithAppUserID:appUserID];
[self updateAllCachesWithCompletionBlock:callDelegate];
if (!self.systemInfo.isApplicationBackgrounded) {
[self updateAllCachesWithCompletionBlock:callDelegate];
} else {
[self sendCachedPurchaserInfoIfAvailable];
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this bit is so that if there's something cached we still call the delegate. I believe this is a departure from Android. @vegaro was there a specific reason not to call the delegate there? I'm worried that not calling it is a bit inconsistent with the philosophy of sending whatever is cached as fast as we can.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we should be calling in Android. I think that's what I would expect as a developer.

}

[self configureSubscriberAttributesManager];

self.storeKitWrapper.delegate = self;
Expand Down Expand Up @@ -337,11 +342,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 @@ -758,6 +760,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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just cleanup here, this line was duplicated in a couple of places



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