From 326707c675930e4d1f9f9d5084f58fbf8d730325 Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Thu, 11 Jun 2026 12:02:21 -0600 Subject: [PATCH 1/5] @W-22917500 Add diagnostic warning when OAuth code exchange fails against Lightning URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detect when the token endpoint returns unsupported_grant_type and the login server URL contains .lightning. — log a detailed developer warning and surface a localized user-facing alert instead of the generic error. Port of Android PR forcedotcom/SalesforceMobileSDK-Android/pull/2921. --- .../project.pbxproj | 4 + .../Classes/OAuth/SFOAuthCoordinator.m | 22 ++- .../SFOAuthCoordinatorLightningURLTests.swift | 127 ++++++++++++++++++ .../SalesforceSDKCoreTests-Bridging-Header.h | 9 ++ .../en.lproj/Localizable.strings | 1 + 5 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj b/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj index dcb8e8e19f..676c84c830 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj +++ b/libs/SalesforceSDKCore/SalesforceSDKCore.xcodeproj/project.pbxproj @@ -94,6 +94,7 @@ 4F7EB4AC1BFFCF0F00768720 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7EB4A01BFFCEF600768720 /* main.m */; }; 4F7EB4AD1BFFCF2300768720 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F7EB4A41BFFCEF600768720 /* ViewController.m */; }; 4F8A3B012CEC202F00ECDC76 /* JwtAccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F8A3B002CEC202F00ECDC76 /* JwtAccessToken.swift */; }; + 1A31073F5F374B9EB1162F2E /* SFOAuthCoordinatorLightningURLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */; }; 4F9E05322DD6A08000548985 /* SFSDKOAuthTokenEndpointResponseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */; }; 4F9E05342DD7BE1500548985 /* SFOAuthCredentialsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F9E05332DD7BE0A00548985 /* SFOAuthCredentialsTests.m */; }; 4FA1B2C32F0E000000000001 /* LoginForAdminTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FA1B2C32F0E000000000002 /* LoginForAdminTests.swift */; }; @@ -691,6 +692,7 @@ 4F96FD471BFD32140022F021 /* SFSDKResourceUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFSDKResourceUtils.m; sourceTree = ""; }; 4F96FD481BFD32140022F021 /* SFSDKWebUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SFSDKWebUtils.h; sourceTree = ""; }; 4F96FD491BFD32140022F021 /* SFSDKWebUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SFSDKWebUtils.m; sourceTree = ""; }; + 399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SFOAuthCoordinatorLightningURLTests.swift; path = ../SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift; sourceTree = ""; }; 4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SFSDKOAuthTokenEndpointResponseTests.m; path = ../SalesforceSDKCoreTests/SFSDKOAuthTokenEndpointResponseTests.m; sourceTree = ""; }; 4F9E05332DD7BE0A00548985 /* SFOAuthCredentialsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SFOAuthCredentialsTests.m; path = ../SalesforceSDKCoreTests/SFOAuthCredentialsTests.m; sourceTree = ""; }; 4FA1B2C32F0E000000000002 /* LoginForAdminTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginForAdminTests.swift; sourceTree = ""; }; @@ -1107,6 +1109,7 @@ E1DDC1431CAEEB34002F51DD /* SFSDKLoginHostTests.m */, B7355248228E84AF001C7759 /* SFSDKLogoutBlocker.h */, B7A901BD228E4DFA0036D749 /* SFSDKLogoutBlocker.m */, + 399A11508BCB47F490DFB724 /* SFOAuthCoordinatorLightningURLTests.swift */, 4F9E052C2DD6A06F00548985 /* SFSDKOAuthTokenEndpointResponseTests.m */, 69848CBB2364063E00893E57 /* SFSDKPushNotificationDataProvider.h */, 69848CBC2364063E00893E57 /* SFSDKPushNotificationDataProvider.m */, @@ -2256,6 +2259,7 @@ 69848CB82364035300893E57 /* SFSDKEncryptedPushNotificationTests.m in Sources */, 4F3ECD8A2EBBD150005020A6 /* SFOAuthCoordinatorTests.m in Sources */, 4FA1B2C32F0E000000000001 /* LoginForAdminTests.swift in Sources */, + 1A31073F5F374B9EB1162F2E /* SFOAuthCoordinatorLightningURLTests.swift in Sources */, 4F9E05322DD6A08000548985 /* SFSDKOAuthTokenEndpointResponseTests.m in Sources */, 4F06AF8D1C49A18E00F70798 /* SalesforceSDKManagerTests.m in Sources */, 237C186C2E44FCAE0008015C /* EncryptStreamTests.swift in Sources */, diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m index 7847f98cbc..79fae84100 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m @@ -53,6 +53,7 @@ #import #import #import +#import "SFSDKResourceUtils.h" @interface SFOAuthCoordinator() @property (nonatomic) NSString *networkIdentifier; @@ -620,7 +621,7 @@ - (void)beginTokenEndpointFlow { request.refreshToken = self.credentials.refreshToken; request.redirectURI = self.credentials.redirectUri; request.serverURL = [self.credentials overrideDomainIfNeeded]; - + __weak typeof (self) weakSelf = self; if (self.approvalCode) { [SFSDKCoreLogger i:[self class] format:@"%@: Initiating authorization code flow.", NSStringFromSelector(_cmd)]; @@ -669,7 +670,20 @@ - (void)handleResponse:(SFSDKOAuthTokenEndpointResponse *)response { [SFSDKCoreLogger d:[self class] format:@"Refresh attempt timed out after %f seconds.", self.timeout]; [self stopAuthentication]; } - [self notifyDelegateOfFailure:response.error.error authInfo:self.authInfo]; + BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:@"unsupported_grant_type"]; + BOOL isLightningURL = [self.credentials.domain containsString:@".lightning."]; + if (isUnsupportedGrantType && isLightningURL) { + [SFSDKCoreLogger w:[self class] format:@"Code exchange failed with unsupported_grant_type against Lightning URL: %@. Lightning URLs do not support authorization_code grant type. Use a My Domain login server URL instead.", self.credentials.domain]; + NSString *localizedMessage = [SFSDKResourceUtils localizedString:@"lightningUrlCodeExchangeError"]; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:response.error.error.userInfo ?: @{}]; + userInfo[NSLocalizedDescriptionKey] = localizedMessage; + NSError *diagnosticError = [NSError errorWithDomain:response.error.error.domain + code:response.error.error.code + userInfo:userInfo]; + [self notifyDelegateOfFailure:diagnosticError authInfo:self.authInfo]; + } else { + [self notifyDelegateOfFailure:response.error.error authInfo:self.authInfo]; + } self.responseData = [NSMutableData dataWithCapacity:kSFOAuthReponseBufferLength]; } } @@ -877,10 +891,10 @@ - (void)handleCustomDomainUpdateWithLoginHint:(NSString *)loginHint myDomain:(NS #pragma mark - WKNavigationDelegate (User-Agent Token Flow) - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { - + NSURL *url = navigationAction.request.URL; NSString *requestUrl = [url absoluteString]; - + // Determine if presence of discovery domain, then handle if present. SFDomainDiscoveryResult *discoveryResult = [self.domainDiscoveryCoordinator handleWithWebAction:navigationAction]; if (discoveryResult) { diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift new file mode 100644 index 0000000000..e4b59df19f --- /dev/null +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift @@ -0,0 +1,127 @@ +/* + SFOAuthCoordinatorLightningURLTests.swift + SalesforceSDKCoreTests + + 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 + +final class SFOAuthCoordinatorLightningURLTests: XCTestCase, SFOAuthCoordinatorDelegate { + + private var coordinator: SFOAuthCoordinator! + private var lastFailureError: NSError? + private var delegateExpectation: XCTestExpectation? + + override class func setUp() { + SFSDKLogoutBlocker.block() + super.setUp() + } + + override func setUp() { + super.setUp() + lastFailureError = nil + } + + override func tearDown() { + coordinator = nil + lastFailureError = nil + delegateExpectation = nil + super.tearDown() + } + + // MARK: - Constants + + private let lightningDomain = "myorg.lightning.force.com" + private let lightningSubdomain = "myorg.lightning.pc-rnd.force.com" + private let myDomain = "myorg.my.salesforce.com" + private let unsupportedGrantType = "unsupported_grant_type" + private let invalidGrant = "invalid_grant" + private let lightningMessage = SFSDKResourceUtils.localizedString("lightningUrlCodeExchangeError") + + // MARK: - Helpers + + private func handleTokenResponse(domain: String, errorCode: String) throws { + let credentials = try XCTUnwrap(OAuthCredentials(identifier: "com.salesforce.ios.oauth.lightningtest", clientId: "TestClientId", encrypted: false)) + credentials.domain = domain + credentials.redirectUri = "testapp://callback" + coordinator = SFOAuthCoordinator(credentials: credentials) + coordinator.delegate = self + delegateExpectation = expectation(description: "Delegate called") + + let params = ["error": errorCode, "error_description": "\(errorCode): grant type not supported"] + let response = SFSDKOAuthTokenEndpointResponse(dictionary: params, parseAdditionalFields: nil) + coordinator.handle(response) + + waitForExpectations(timeout: 2.0) + XCTAssertNotNil(lastFailureError, "Delegate should have received a failure error") + } + + // MARK: - SC-1: Warning triggers when BOTH conditions met + + func test_givenLightningURLAndUnsupportedGrantType_whenHandleResponse_thenDelegateReceivesLocalizedError() throws { + try handleTokenResponse(domain: lightningDomain, errorCode: unsupportedGrantType) + XCTAssertEqual(lastFailureError?.localizedDescription, lightningMessage) + } + + func test_givenLightningSubdomainAndUnsupportedGrantType_whenHandleResponse_thenDelegateReceivesLocalizedError() throws { + try handleTokenResponse(domain: lightningSubdomain, errorCode: unsupportedGrantType) + XCTAssertEqual(lastFailureError?.localizedDescription, lightningMessage) + } + + // MARK: - SC-2: Warning does NOT appear unless both conditions met + + func test_givenNonLightningURLAndUnsupportedGrantType_whenHandleResponse_thenDelegateReceivesGenericError() throws { + try handleTokenResponse(domain: myDomain, errorCode: unsupportedGrantType) + XCTAssertNotEqual(lastFailureError?.localizedDescription, lightningMessage) + } + + func test_givenLightningURLAndDifferentError_whenHandleResponse_thenDelegateReceivesGenericError() throws { + try handleTokenResponse(domain: lightningDomain, errorCode: invalidGrant) + XCTAssertNotEqual(lastFailureError?.localizedDescription, lightningMessage) + } + + // MARK: - SC-3: User-facing alert string is localized + + func test_givenLightningURLError_whenHandleResponse_thenErrorDescriptionMatchesLocalizedString() throws { + XCTAssertNotNil(lightningMessage, "Localized string key 'lightningUrlCodeExchangeError' must be defined") + XCTAssertFalse(lightningMessage.isEmpty, "Localized string must not be empty") + try handleTokenResponse(domain: lightningDomain, errorCode: unsupportedGrantType) + XCTAssertEqual(lastFailureError?.localizedDescription, lightningMessage) + } + + // MARK: - SFOAuthCoordinatorDelegate + + func oauthCoordinator(_ coordinator: SFOAuthCoordinator, didFailWithError error: Error, authInfo: AuthInfo?) { + lastFailureError = error as NSError + delegateExpectation?.fulfill() + } + + func oauthCoordinatorDidAuthenticate(_ coordinator: SFOAuthCoordinator, authInfo: AuthInfo) {} + func oauthCoordinator(_ coordinator: SFOAuthCoordinator, willBeginAuthenticationWith view: WKWebView) {} + func oauthCoordinator(_ coordinator: SFOAuthCoordinator, didBeginAuthenticationWith view: WKWebView) {} + func oauthCoordinator(_ coordinator: SFOAuthCoordinator, didBeginAuthenticationWith session: ASWebAuthenticationSession) {} + func oauthCoordinatorDidCancelBrowserAuthentication(_ coordinator: SFOAuthCoordinator) {} + func oauthCoordinatorDidBeginNativeAuthentication(_ coordinator: SFOAuthCoordinator) {} +} diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKCoreTests-Bridging-Header.h b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKCoreTests-Bridging-Header.h index 5ec8f70c25..b4308059ef 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKCoreTests-Bridging-Header.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SalesforceSDKCoreTests-Bridging-Header.h @@ -9,3 +9,12 @@ #import "SFOAuthCoordinator+Internal.h" #import "SFUserAccountManager+Internal.h" #import "SFOAuthCredentials+Internal.h" +#import "SFSDKOAuth2.h" + +@interface SFOAuthCoordinator (LightningURLTesting) +- (void)handleResponse:(SFSDKOAuthTokenEndpointResponse *)response; +@end + +@interface SFSDKOAuthTokenEndpointResponse (Testing) +- (instancetype)initWithDictionary:(NSDictionary *)nvPairs parseAdditionalFields:(NSArray *)additionalOAuthParameterKeys; +@end diff --git a/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings b/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings index 0e29c6ccc1..2080f00483 100644 --- a/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings +++ b/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings @@ -53,6 +53,7 @@ "authAlertVersionMismatchError" = "Your app has been updated, and you will need to log in again to continue using the app."; "authAlertBrowserFlowTitle" = "Log In"; "authAlertFrontdoorLoginUrlConsumerKeyMismatch" = "Cannot use another app's login QR Code. Please log in to this app."; +"lightningUrlCodeExchangeError" = "Lighting URLs (.lightning.) are not supported for OAuth code exchange. Use your My Domain (.my.) URL instead."; // LoginViewController "TITLE_LOGIN" = "Log In"; From 2b8ced2752a61f9bee99b63d6c4988beaa6f41ad Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Thu, 11 Jun 2026 13:32:48 -0600 Subject: [PATCH 2/5] @W-22917500: iOS: Add diagnostic warning when OAuth code exchange fails against Lightning URL (Use constant for unsupported_grant_type) --- .../SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m | 2 +- .../SalesforceSDKCore/Classes/Util/SFSDKOAuthConstants.h | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m index 79fae84100..a0178da8c5 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m @@ -670,7 +670,7 @@ - (void)handleResponse:(SFSDKOAuthTokenEndpointResponse *)response { [SFSDKCoreLogger d:[self class] format:@"Refresh attempt timed out after %f seconds.", self.timeout]; [self stopAuthentication]; } - BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:@"unsupported_grant_type"]; + BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:kSFOAuthErrorTypeUnsupportedGrantType]; BOOL isLightningURL = [self.credentials.domain containsString:@".lightning."]; if (isUnsupportedGrantType && isLightningURL) { [SFSDKCoreLogger w:[self class] format:@"Code exchange failed with unsupported_grant_type against Lightning URL: %@. Lightning URLs do not support authorization_code grant type. Use a My Domain login server URL instead.", self.credentials.domain]; diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SFSDKOAuthConstants.h b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SFSDKOAuthConstants.h index 73f2f1da78..0e1eca9bfb 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SFSDKOAuthConstants.h +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/Util/SFSDKOAuthConstants.h @@ -118,6 +118,7 @@ static NSString * const kSFOAuthErrorTypeInactiveUser = @"inactive static NSString * const kSFOAuthErrorTypeInactiveOrg = @"inactive_org"; static NSString * const kSFOAuthErrorTypeRateLimitExceeded = @"rate_limit_exceeded"; static NSString * const kSFOAuthErrorTypeUnsupportedResponseType = @"unsupported_response_type"; +static NSString * const kSFOAuthErrorTypeUnsupportedGrantType = @"unsupported_grant_type"; static NSString * const kSFOAuthErrorTypeTimeout = @"auth_timeout"; static NSString * const kSFOAuthErrorTypeWrongVersion = @"wrong_version"; // credentials do not match current Connected App version in the org static NSString * const kSFOAuthErrorTypeBrowserLaunchFailed = @"browser_launch_failed"; From b0a096a831e1b16bc666e91e3551d4603269ef52 Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Thu, 11 Jun 2026 13:33:10 -0600 Subject: [PATCH 3/5] @W-22917500: iOS: Add diagnostic warning when OAuth code exchange fails against Lightning URL (Use error log level instead of warning) --- .../SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m index a0178da8c5..736bac782f 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m +++ b/libs/SalesforceSDKCore/SalesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m @@ -673,7 +673,7 @@ - (void)handleResponse:(SFSDKOAuthTokenEndpointResponse *)response { BOOL isUnsupportedGrantType = [response.error.tokenEndpointErrorCode isEqualToString:kSFOAuthErrorTypeUnsupportedGrantType]; BOOL isLightningURL = [self.credentials.domain containsString:@".lightning."]; if (isUnsupportedGrantType && isLightningURL) { - [SFSDKCoreLogger w:[self class] format:@"Code exchange failed with unsupported_grant_type against Lightning URL: %@. Lightning URLs do not support authorization_code grant type. Use a My Domain login server URL instead.", self.credentials.domain]; + [SFSDKCoreLogger e:[self class] format:@"Code exchange failed with unsupported_grant_type against Lightning URL: %@. Lightning URLs do not support authorization_code grant type. Use a My Domain login server URL instead.", self.credentials.domain]; NSString *localizedMessage = [SFSDKResourceUtils localizedString:@"lightningUrlCodeExchangeError"]; NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:response.error.error.userInfo ?: @{}]; userInfo[NSLocalizedDescriptionKey] = localizedMessage; From 3b7c14a6bd1b270fe490821f98b1edb7db22901b Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Thu, 11 Jun 2026 13:33:23 -0600 Subject: [PATCH 4/5] @W-22917500: iOS: Add diagnostic warning when OAuth code exchange fails against Lightning URL (Fix typo: Lighting -> Lightning) --- .../SalesforceSDKResources.bundle/en.lproj/Localizable.strings | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings b/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings index 2080f00483..e423b29cf8 100644 --- a/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings +++ b/shared/resources/SalesforceSDKResources.bundle/en.lproj/Localizable.strings @@ -53,7 +53,7 @@ "authAlertVersionMismatchError" = "Your app has been updated, and you will need to log in again to continue using the app."; "authAlertBrowserFlowTitle" = "Log In"; "authAlertFrontdoorLoginUrlConsumerKeyMismatch" = "Cannot use another app's login QR Code. Please log in to this app."; -"lightningUrlCodeExchangeError" = "Lighting URLs (.lightning.) are not supported for OAuth code exchange. Use your My Domain (.my.) URL instead."; +"lightningUrlCodeExchangeError" = "Lightning URLs (.lightning.) are not supported for OAuth code exchange. Use your My Domain (.my.) URL instead."; // LoginViewController "TITLE_LOGIN" = "Log In"; From cef352839284a2869aa436db6c83d7e87ac0f609 Mon Sep 17 00:00:00 2001 From: "Eric C. Johnson" Date: Thu, 11 Jun 2026 13:40:56 -0600 Subject: [PATCH 5/5] @W-22917500: iOS: Add diagnostic warning when OAuth code exchange fails against Lightning URL (Remove redundant assertions on localized string) --- .../SFOAuthCoordinatorLightningURLTests.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift index e4b59df19f..561ed8b0af 100644 --- a/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift +++ b/libs/SalesforceSDKCore/SalesforceSDKCoreTests/SFOAuthCoordinatorLightningURLTests.swift @@ -105,8 +105,6 @@ final class SFOAuthCoordinatorLightningURLTests: XCTestCase, SFOAuthCoordinatorD // MARK: - SC-3: User-facing alert string is localized func test_givenLightningURLError_whenHandleResponse_thenErrorDescriptionMatchesLocalizedString() throws { - XCTAssertNotNil(lightningMessage, "Localized string key 'lightningUrlCodeExchangeError' must be defined") - XCTAssertFalse(lightningMessage.isEmpty, "Localized string must not be empty") try handleTokenResponse(domain: lightningDomain, errorCode: unsupportedGrantType) XCTAssertEqual(lastFailureError?.localizedDescription, lightningMessage) }