diff --git a/Purchases/Public/RCPurchasesErrorUtils.h b/Purchases/Public/RCPurchasesErrorUtils.h index cdfaa31078..60699a8f8c 100644 --- a/Purchases/Public/RCPurchasesErrorUtils.h +++ b/Purchases/Public/RCPurchasesErrorUtils.h @@ -71,6 +71,14 @@ NS_SWIFT_NAME(Purchases.ErrorUtils) */ + (NSError *)missingReceiptFileError; +/** + * Constructs an NSError with the [RCInvalidAppUserIdError] code. + * + * @note This error is used when the appUserID can't be found in user defaults. This can happen if user defaults + * are removed manually or if the OS deletes entries when running out of space. + */ ++ (NSError *)missingAppUserIDError; + /** * Maps an SKErrorCode code to a RCPurchasesErrorCode code. Constructs an NSError with the mapped code and adds a * [RCUnderlyingErrorKey] in the NSError.userInfo dictionary. The SKErrorCode code will be mapped using diff --git a/Purchases/Public/RCPurchasesErrorUtils.m b/Purchases/Public/RCPurchasesErrorUtils.m index bc3507aa4f..e40735ab70 100644 --- a/Purchases/Public/RCPurchasesErrorUtils.m +++ b/Purchases/Public/RCPurchasesErrorUtils.m @@ -201,36 +201,31 @@ case CODE_IF_TARGET_IPHONE(SKErrorStoreProductNotAvailable, 5): @implementation RCPurchasesErrorUtils -+ (NSError *)errorWithCode:(RCPurchasesErrorCode)code -{ ++ (NSError *)errorWithCode:(RCPurchasesErrorCode)code { return [self errorWithCode:code message:nil]; } + (NSError *)errorWithCode:(RCPurchasesErrorCode)code - message:(nullable NSString *)message -{ + message:(nullable NSString *)message { return [self errorWithCode:code message:message underlyingError:nil]; } + (NSError *)errorWithCode:(RCPurchasesErrorCode)code - underlyingError:(nullable NSError *)underlyingError -{ + underlyingError:(nullable NSError *)underlyingError { return [self errorWithCode:code message:nil underlyingError:underlyingError extraUserInfo:nil]; } + (NSError *)errorWithCode:(RCPurchasesErrorCode)code message:(nullable NSString *)message - underlyingError:(nullable NSError *)underlyingError -{ + underlyingError:(nullable NSError *)underlyingError { return [self errorWithCode:code message:message underlyingError:underlyingError extraUserInfo:nil]; } + (NSError *)errorWithCode:(RCPurchasesErrorCode)code message:(nullable NSString *)message underlyingError:(nullable NSError *)underlyingError - extraUserInfo:(nullable NSDictionary *)extraUserInfo -{ + extraUserInfo:(nullable NSDictionary *)extraUserInfo { NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:extraUserInfo]; userInfo[NSLocalizedDescriptionKey] = message ?: RCPurchasesErrorDescription(code); @@ -242,21 +237,18 @@ + (NSError *)errorWithCode:(RCPurchasesErrorCode)code } + (NSError *)errorWithCode:(RCPurchasesErrorCode)code - userInfo:(NSDictionary *)userInfo -{ + userInfo:(NSDictionary *)userInfo { RCErrorLog(@"%@", RCPurchasesErrorDescription(code)); return [NSError errorWithDomain:RCPurchasesErrorDomain code:code userInfo:userInfo]; } -+ (NSError *)networkErrorWithUnderlyingError:(NSError *)underlyingError -{ ++ (NSError *)networkErrorWithUnderlyingError:(NSError *)underlyingError { return [self errorWithCode:RCNetworkError underlyingError:underlyingError]; } + (NSError *)backendUnderlyingError:(nullable NSNumber *)backendCode - backendMessage:(nullable NSString *)backendMessage -{ + backendMessage:(nullable NSString *)backendMessage { return [NSError errorWithDomain:RCBackendErrorDomain code:[backendCode integerValue] ?: RCUnknownError @@ -266,15 +258,13 @@ + (NSError *)backendUnderlyingError:(nullable NSNumber *)backendCode } + (NSError *)backendErrorWithBackendCode:(nullable NSNumber *)backendCode - backendMessage:(nullable NSString *)backendMessage -{ + backendMessage:(nullable NSString *)backendMessage { return [self backendErrorWithBackendCode:backendCode backendMessage:backendMessage extraUserInfo:nil]; } + (NSError *)backendErrorWithBackendCode:(nullable NSNumber *)backendCode backendMessage:(nullable NSString *)backendMessage - finishable:(BOOL)finishable -{ + finishable:(BOOL)finishable { return [self backendErrorWithBackendCode:backendCode backendMessage:backendMessage extraUserInfo:@{ @@ -284,8 +274,7 @@ + (NSError *)backendErrorWithBackendCode:(nullable NSNumber *)backendCode + (NSError *)backendErrorWithBackendCode:(nullable NSNumber *)backendCode backendMessage:(nullable NSString *)backendMessage - extraUserInfo:(nullable NSDictionary *)extraUserInfo -{ + extraUserInfo:(nullable NSDictionary *)extraUserInfo { RCPurchasesErrorCode errorCode; if (backendCode != nil) { errorCode = RCPurchasesErrorCodeFromRCBackendErrorCode((RCBackendErrorCode) [backendCode integerValue]); @@ -299,18 +288,19 @@ + (NSError *)backendErrorWithBackendCode:(nullable NSNumber *)backendCode extraUserInfo:extraUserInfo]; } -+ (NSError *)unexpectedBackendResponseError -{ ++ (NSError *)unexpectedBackendResponseError { return [self errorWithCode:RCUnexpectedBackendResponseError]; } -+ (NSError *)missingReceiptFileError -{ ++ (NSError *)missingReceiptFileError { return [self errorWithCode:RCMissingReceiptFileError]; } -+ (NSError *)purchasesErrorWithSKError:(NSError *)skError -{ ++ (NSError *)missingAppUserIDError { + return [self errorWithCode:RCInvalidAppUserIdError]; +} + ++ (NSError *)purchasesErrorWithSKError:(NSError *)skError { RCPurchasesErrorCode errorCode = RCPurchasesErrorCodeFromSKError(skError); return [self errorWithCode:errorCode diff --git a/Purchases/Public/RCPurchasesErrors.h b/Purchases/Public/RCPurchasesErrors.h index 6b2f7fd02a..73bf1f0b8f 100644 --- a/Purchases/Public/RCPurchasesErrors.h +++ b/Purchases/Public/RCPurchasesErrors.h @@ -52,7 +52,7 @@ typedef NS_ERROR_ENUM(RCPurchasesErrorDomain, RCPurchasesErrorCode) { RCIneligibleError, RCInsufficientPermissionsError, RCPaymentPendingError, - RCInvalidSubscriberAttributesError + RCInvalidSubscriberAttributesError, } NS_SWIFT_NAME(Purchases.ErrorCode); /** diff --git a/Purchases/Purchasing/RCIdentityManager.m b/Purchases/Purchasing/RCIdentityManager.m index 16de1bb277..9ab528c2e6 100644 --- a/Purchases/Purchasing/RCIdentityManager.m +++ b/Purchases/Purchasing/RCIdentityManager.m @@ -6,6 +6,8 @@ #import "RCIdentityManager.h" #import "RCLogUtils.h" #import "RCBackend.h" +#import "RCPurchasesErrorUtils.h" + @interface RCIdentityManager () @@ -64,11 +66,18 @@ - (void)saveAppUserID:(NSString *)appUserID { } - (void)createAlias:(NSString *)alias withCompletionBlock:(void (^)(NSError *_Nullable error))completion { - RCDebugLog(@"Creating an alias to %@ from %@", self.currentAppUserID, alias); - [self.backend createAliasForAppUserID:self.currentAppUserID withNewAppUserID:alias completion:^(NSError *_Nullable error) { + NSString *currentAppUserID = self.currentAppUserID; + if (!currentAppUserID) { + RCDebugLog(@"Couldn't create an alias because the currentAppUserID is null. " + "This might happen if the entry in UserDefaults is missing."); + completion(RCPurchasesErrorUtils.missingAppUserIDError); + return; + } + RCDebugLog(@"Creating an alias to %@ from %@", currentAppUserID, alias); + [self.backend createAliasForAppUserID:currentAppUserID withNewAppUserID:alias completion:^(NSError *_Nullable error) { if (error == nil) { RCDebugLog(@"Alias created"); - [self.deviceCache clearCachesForAppUserID:self.currentAppUserID andSaveNewUserID:alias]; + [self.deviceCache clearCachesForAppUserID:currentAppUserID andSaveNewUserID:alias]; } completion(error); }]; @@ -89,4 +98,4 @@ - (BOOL)currentUserIsAnonymous { return currentAppUserIDLooksAnonymous || isLegacyAnonymousAppUserID; } -@end \ No newline at end of file +@end diff --git a/PurchasesTests/IdentityManagerTests.swift b/PurchasesTests/IdentityManagerTests.swift index bdb133f30d..e22a776d9a 100644 --- a/PurchasesTests/IdentityManagerTests.swift +++ b/PurchasesTests/IdentityManagerTests.swift @@ -105,13 +105,43 @@ class IdentityManagerTests: XCTestCase { func testCreateAliasCallsBackend() { self.mockBackend.aliasCalled = false + self.mockDeviceCache.stubbedAppUserID = "appUserID" + self.identityManager.createAlias("cesar") { (error: Error?) in } expect(self.mockBackend.aliasCalled).toEventually(beTrue()) } + func testCreateAliasNoOpsIfNilAppUserID() { + self.mockBackend.aliasCalled = false + self.mockDeviceCache.stubbedAppUserID = nil + self.identityManager.createAlias("cesar") { (error: Error?) in + } + + expect(self.mockBackend.aliasCalled).toEventually(beFalse()) + } + + func testCreateAliasCallsCompletionWithErrorIfNilAppUserID() { + self.mockBackend.aliasCalled = false + self.mockDeviceCache.stubbedAppUserID = nil + var completionCalled = false + var receivedNSError: NSError? + self.identityManager.createAlias("cesar") { (error: Error?) in + completionCalled = true + + guard let receivedError = error else { fatalError() } + receivedNSError = receivedError as NSError + expect(receivedNSError!.code) == Purchases.ErrorCode.invalidAppUserIdError.rawValue + } + + expect(completionCalled).toEventually(beTrue()) + expect(receivedNSError).toNotEventually(beNil()) + } + func testCreateAliasIdentifiesWhenSuccessful() { + self.mockDeviceCache.cacheAppUserID("appUserID") + self.identityManager.createAlias("cesar") { (error: Error?) in } assertCorrectlyIdentified(expectedAppUserID: "cesar") @@ -128,6 +158,8 @@ class IdentityManagerTests: XCTestCase { func testCreateAliasForwardsErrors() { self.mockBackend.aliasError = Purchases.ErrorUtils.backendError(withBackendCode: Purchases.RevenueCatBackendErrorCode.invalidAPIKey.rawValue as NSNumber, backendMessage: "Invalid credentials", finishable: false) var error: Error? = nil + self.mockDeviceCache.stubbedAppUserID = "appUserID" + self.identityManager.createAlias("cesar") { (newError: Error?) in error = newError } @@ -150,12 +182,15 @@ class IdentityManagerTests: XCTestCase { func testIdentifyingWhenUserIsAnonymousCreatesAlias() { self.identityManager.configure(withAppUserID: nil) self.mockBackend.aliasError = nil + self.mockDeviceCache.cacheAppUserID("$RCAnonymousID:5d73fc46744f4e0b99e524c6763dd7fc") + self.identityManager.identifyAppUserID("cesar") { (error: Error?) in } expect(self.mockBackend.aliasCalled).toEventually(beTrue()) } func testMigrationFromRandomIDConfiguringAnonymously() { self.mockDeviceCache.stubbedLegacyAppUserID = "an_old_random" + self.identityManager.configure(withAppUserID: nil) assertCorrectlyIdentifiedWithAnonymous(usingOldID: true) expect(self.identityManager.currentAppUserID).to(equal("an_old_random"))