Handle re-login as the same user (#8100)

In the case where `Purchases.shared.logIn` is called with the current app user id, skip invoking the internal auth delegate, because nothing critical has changed. This means that existing caches will remain. This continues to call through to the identityManager to performLogIn, so that existing logic around syncing subscriber attributes etc is preserved.
This commit is contained in:
Dave DeLong authored and GitHub committed 2026-10-21 14:13:24 -06:00
1 parent 949a10492c
commit 20ed8107b5
3 files changed
+85 -3

No files matched your search

+2 -1
View File
@@ -136,6 +136,7 @@ public final class Authentication: NSObject {
return
}
let originalAppUserID = self.identityManager.currentAppUserID
let normalizedAppUserID = appUserID.trimmingWhitespacesAndNewLines
self.ongoingUserInitiatedRequestCount.increment()
@@ -145,7 +146,7 @@ public final class Authentication: NSObject {
self.operationDispatcher.dispatchOnMainThread {
switch result {
case .success(let values):
if let delegate = self.internalDelegate {
if let delegate = self.internalDelegate, normalizedAppUserID != originalAppUserID {
delegate.authenticatorDidChangeIdentity(reason: .identified) { _ in
completion(values.info, values.created, nil)
}
@@ -19,6 +19,7 @@ import XCTest
class AuthenticationTests: TestCase {
private static let appUserID = "test-app-user-id"
private static let newAppUserID = "new-app-user-id"
private var backend: MockBackend!
private var identityManager: MockIdentityManager!
@@ -80,7 +81,7 @@ class AuthenticationTests: TestCase {
var receivedCreated: Bool?
var receivedError: PublicError?
authentication.identifyCurrentUser(as: Self.appUserID) { info, created, error in
authentication.identifyCurrentUser(as: Self.newAppUserID) { info, created, error in
receivedInfo = info
receivedCreated = created
receivedError = error
@@ -89,11 +90,47 @@ class AuthenticationTests: TestCase {
expect(receivedInfo) == expectedInfo
expect(receivedCreated) == true
expect(receivedError).to(beNil())
expect(self.identityManager.invokedLogInParametersList) == [Self.appUserID]
expect(self.identityManager.invokedLogInParametersList) == [Self.newAppUserID]
expect(self.internalDelegate.invokedAuthenticatorDidChangeIdentity) == true
expect(self.internalDelegate.invokedAuthenticatorDidChangeIdentityParametersList.last) == .identified
}
func testIdentifyCurrentUserWithCurrentAppUserIDDoesNotNotifyInternalDelegate() throws {
let authentication = self.makeAuthentication()
let expectedInfo = try CustomerInfo(data: Self.customerInfoData(originalAppUserId: Self.appUserID))
self.identityManager.mockLogInResult = .success((expectedInfo, false))
var receivedInfo: CustomerInfo?
var receivedCreated: Bool?
var receivedError: PublicError?
authentication.identifyCurrentUser(as: Self.appUserID) { info, created, error in
receivedInfo = info
receivedCreated = created
receivedError = error
}
expect(receivedInfo) == expectedInfo
expect(receivedCreated) == false
expect(receivedError).to(beNil())
expect(self.identityManager.invokedLogInParametersList) == [Self.appUserID]
expect(self.internalDelegate.invokedAuthenticatorDidChangeIdentity) == false
}
func testIdentifyCurrentUserWithUntrimmedCurrentAppUserIDDoesNotNotifyInternalDelegate() throws {
let authentication = self.makeAuthentication()
let expectedInfo = try CustomerInfo(data: Self.customerInfoData(originalAppUserId: Self.appUserID))
self.identityManager.mockLogInResult = .success((expectedInfo, false))
var receivedInfo: CustomerInfo?
authentication.identifyCurrentUser(as: " \(Self.appUserID)\n") { info, _, _ in
receivedInfo = info
}
expect(receivedInfo) == expectedInfo
expect(self.internalDelegate.invokedAuthenticatorDidChangeIdentity) == false
}
func testIdentifyCurrentUserWithStringFailureDoesNotNotifyInternalDelegate() {
let authentication = self.makeAuthentication()
let backendError = BackendError.networkError(.offlineConnection())
@@ -62,6 +62,50 @@ class PurchasesLogInTests: BasePurchasesLogInTests {
expect(self.mockRemoteConfigManager.invokedRefreshRemoteConfigCount) == baselineRefreshCount + 1
}
func testLogInWithCurrentAppUserIDDoesNotUpdateCaches() {
expect(self.backend.getCustomerInfoCallCount).toEventually(equal(1))
// the mock now reports `appUserID` as the current user
self.identityManager.mockIsAnonymous = false
self.identityManager.mockLogInResult = .success((Self.mockLoggedInInfo, false))
let baselineOfferingsCount = self.mockOfferingsManager.invokedUpdateOfferingsCacheCount
let baselineRemoteConfigCount = self.mockRemoteConfigManager.invokedRefreshRemoteConfigCount
let result = waitUntilValue { completed in
self.purchases.logIn(Self.appUserID) { customerInfo, created, error in
completed(Self.logInResult(customerInfo, created, error))
}
}
expect(result).to(beSuccess())
expect(result?.value) == (Self.mockLoggedInInfo, false)
expect(self.identityManager.invokedLogInParametersList) == [Self.appUserID]
expect(self.backend.getCustomerInfoCallCount) == 1
expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount) == baselineOfferingsCount
expect(self.mockRemoteConfigManager.invokedRefreshRemoteConfigCount) == baselineRemoteConfigCount
}
func testLogInWithUntrimmedCurrentAppUserIDDoesNotUpdateCaches() {
expect(self.backend.getCustomerInfoCallCount).toEventually(equal(1))
// the mock now reports `appUserID` as the current user
self.identityManager.mockIsAnonymous = false
self.identityManager.mockLogInResult = .success((Self.mockLoggedInInfo, false))
let baselineOfferingsCount = self.mockOfferingsManager.invokedUpdateOfferingsCacheCount
let baselineRemoteConfigCount = self.mockRemoteConfigManager.invokedRefreshRemoteConfigCount
waitUntil { completed in
self.purchases.logIn(" \(Self.appUserID) ") { _, _, _ in
completed()
}
}
expect(self.identityManager.invokedLogInParametersList) == [Self.appUserID]
expect(self.backend.getCustomerInfoCallCount) == 1
expect(self.mockOfferingsManager.invokedUpdateOfferingsCacheCount) == baselineOfferingsCount
expect(self.mockRemoteConfigManager.invokedRefreshRemoteConfigCount) == baselineRemoteConfigCount
}
func testLogInWithFailure() {
let error: BackendError = .networkError(.offlineConnection())
self.identityManager.mockLogInResult = .failure(error)