diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj b/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj index 565f941f96..2e518e928d 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj +++ b/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ 23D626992DF9DF2D00B898D0 /* URLSessionWebSocketTask+WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D626982DF9DF1E00B898D0 /* URLSessionWebSocketTask+WebSocketClient.swift */; }; 23D96B6F2E145AC20004B06A /* DomainDiscoveryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D96B6E2E145AC20004B06A /* DomainDiscoveryCoordinator.swift */; }; 23D96B762E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23D96B752E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift */; }; + 00209E9DDE281C6B1FB7B75A /* WelcomeDiscoveryLoginHostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A70B3207E0F66D9367BCAB /* WelcomeDiscoveryLoginHostTests.swift */; }; 23EDDEFE2DE0F7620024AD39 /* URLRequest+RestRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EDDEF82DE0F7480024AD39 /* URLRequest+RestRequest.swift */; }; 23EDDF022DE0F9EF0024AD39 /* URLRequest+RestRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EDDF012DE0F9EF0024AD39 /* URLRequest+RestRequestTests.swift */; }; 23EED88A2E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 23EED8892E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift */; }; @@ -566,6 +567,7 @@ 23D626982DF9DF1E00B898D0 /* URLSessionWebSocketTask+WebSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionWebSocketTask+WebSocketClient.swift"; sourceTree = ""; }; 23D96B6E2E145AC20004B06A /* DomainDiscoveryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDiscoveryCoordinator.swift; sourceTree = ""; }; 23D96B752E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DomainDiscoveryCoordinatorTests.swift; path = SalesforceSDKCoreTests/DomainDiscoveryCoordinatorTests.swift; sourceTree = SOURCE_ROOT; }; + A2A70B3207E0F66D9367BCAB /* WelcomeDiscoveryLoginHostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = WelcomeDiscoveryLoginHostTests.swift; path = SalesforceSDKCoreTests/WelcomeDiscoveryLoginHostTests.swift; sourceTree = SOURCE_ROOT; }; 23EDDEF82DE0F7480024AD39 /* URLRequest+RestRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+RestRequest.swift"; sourceTree = ""; }; 23EDDF012DE0F9EF0024AD39 /* URLRequest+RestRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "URLRequest+RestRequestTests.swift"; path = "SalesforceSDKCoreTests/URLRequest+RestRequestTests.swift"; sourceTree = SOURCE_ROOT; }; 23EED8892E2ACD3300646B10 /* SFOAuthCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SFOAuthCoordinatorTests.swift; path = SalesforceSDKCoreTests/SFOAuthCoordinatorTests.swift; sourceTree = SOURCE_ROOT; }; @@ -1047,6 +1049,7 @@ 237C18722E450B710008015C /* DecryptStreamTests.swift */, 4FDEVINFO112345678901ABCD /* DevInfoViewControllerTests.swift */, 23D96B752E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift */, + A2A70B3207E0F66D9367BCAB /* WelcomeDiscoveryLoginHostTests.swift */, 697F5C4E267BE29A00F382A9 /* EncryptionTests.swift */, 237C186B2E44FCAE0008015C /* EncryptStreamTests.swift */, 23A4C7472D0CAFCF00DF55EB /* JwtAccessTokenTests.swift */, @@ -2217,6 +2220,7 @@ B7E66AE923763278005A652E /* RestClientPublisherTests.swift in Sources */, 697F5C4F267BE29A00F382A9 /* EncryptionTests.swift in Sources */, 23D96B762E145B400004B06A /* DomainDiscoveryCoordinatorTests.swift in Sources */, + 00209E9DDE281C6B1FB7B75A /* WelcomeDiscoveryLoginHostTests.swift in Sources */, B7A4AE4922E8CA780060E737 /* SFSDKAuthUtilTests.swift in Sources */, 69DFE06C2B969C25000906E4 /* PushNotificationDecryptionTests.swift in Sources */, 4F3ECD8C2EBBD182005020A6 /* SFOAuthInfoTests.m in Sources */, diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m index 437a62c6df..7847f98cbc 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m @@ -868,7 +868,10 @@ - (void)handleCustomDomainUpdateWithLoginHint:(NSString *)loginHint myDomain:(NS [self stopAuthentication]; self.loginHint = loginHint; self.credentials.domain = myDomain; - [[SFUserAccountManager sharedInstance] setLoginHost:myDomain]; + // Don't call setLoginHost: here — doing so would persist the My Domain into + // SFSDKLoginHostStorage and NSUserDefaults, polluting the server picker list + // and causing the login screen to show the My Domain after logout instead of + // the Welcome/Discovery page. credentials.domain is all the auth flow needs. [self authenticate]; } diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m index 8cb0d9b7e3..87d0a56c07 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/UserAccount/SFUserAccountManager.m @@ -1708,7 +1708,10 @@ - (void)setCurrentUserInternal:(SFUserAccount*)user { BOOL isNativeLogin = self.nativeLoginEnabled && !self.shouldFallbackToWebAuthentication; // Native Login uses a secondary Connected App tied to a specifc community url. If the // next login is web based it should not try to use that url. - if (user.credentials.domain && !isNativeLogin) + // Also skip if the app uses a Welcome/Discovery domain — persisting the My Domain + // would pollute the server picker and prevent returning to the discovery page on logout. + BOOL isDiscoveryLogin = [[[SFDomainDiscoveryCoordinator alloc] init] isDiscoveryDomain:self.loginHost]; + if (user.credentials.domain && !isNativeLogin && !isDiscoveryLogin) self.loginHost = user.credentials.domain; [self didChangeValueForKey:@"currentUser"]; userChanged = YES; diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/WelcomeDiscoveryLoginHostTests.swift b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/WelcomeDiscoveryLoginHostTests.swift new file mode 100644 index 0000000000..2595a74cb1 --- /dev/null +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/WelcomeDiscoveryLoginHostTests.swift @@ -0,0 +1,139 @@ +// +// WelcomeDiscoveryLoginHostTests.swift +// SalesforceSDKCore +// +// Created by Takashi Arai on 5/27/26. +// Copyright (c) 2026-present, salesforce.com, inc. All rights reserved. +// +// Redistribution and use of this software in source and binary forms, with or without modification, +// are permitted provided that the following conditions are met: +// * Redistributions of source code must retain the above copyright notice, this list of conditions +// and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, this list of +// conditions and the following disclaimer in the documentation and/or other materials provided +// with the distribution. +// * Neither the name of salesforce.com, inc. nor the names of its contributors may be used to +// endorse or promote products derived from this software without specific prior written +// permission of salesforce.com, inc. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +// IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +// WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import XCTest +@testable import SalesforceSDKCore + +/// Tests that Welcome/Discovery domain login hosts are not overwritten by My Domain +/// after domain discovery resolves a user's org. +final class WelcomeDiscoveryLoginHostTests: XCTestCase { + + let discoveryDomain = "welcome.salesforce.com/discovery" + let myDomain = "mycompany.my.salesforce.com" + let accountManager = UserAccountManager.shared + + override func setUp() { + super.setUp() + _ = KeychainHelper.removeAll() + removeCustomLoginHosts() + } + + override func tearDown() { + accountManager.clearAllAccountState() + _ = KeychainHelper.removeAll() + removeCustomLoginHosts() + super.tearDown() + } + + private func removeCustomLoginHosts() { + let storage = SFSDKLoginHostStorage.sharedInstance() + // Remove deletable hosts in reverse order to avoid index shifting + var indicesToRemove: [Int] = [] + for i in 0..