Skip to content

[iOS] Fix Login for Admin and Welcome Discovery incompatibility#4078

Open
brandonpage wants to merge 4 commits into
forcedotcom:devfrom
brandonpage:fix-login-for-admin-welcome-discovery
Open

[iOS] Fix Login for Admin and Welcome Discovery incompatibility#4078
brandonpage wants to merge 4 commits into
forcedotcom:devfrom
brandonpage:fix-login-for-admin-welcome-discovery

Conversation

@brandonpage

@brandonpage brandonpage commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

The 13.2.1 Login for Admin (LFA) settings menu action fundamentally did not work with the 2-phase Welcome Discovery login flow. Tapping LFA on the discovered My Domain (phase 2) launched ASWebAuthenticationSession against welcome.salesforce.com instead of the resolved My Domain — a non-functional UX. Tapping it on the discovery page itself (phase 1) made things worse.

This PR is a narrow fix that:

  1. Hides the LFA menu entry during phase 1 of Welcome Discovery. A UIDeferredMenuElement.elementWithUncachedProvider: re-evaluates visibility every time the menu opens, keyed off +[SFLoginViewController shouldShowLoginForAdminForSession:]. Phase-1 detection: SFDomainDiscoveryCoordinator.isDiscoveryDomain AND !coordinator.domainUpdated. Per-scene scoping via view.window.windowScene.session.persistentIdentifier.

  2. Also no-ops the public loginViewControllerDidSelectLoginForAdmin: API in phase 1, so external hosts that call it directly cannot bypass the visibility gate. Logs a warning. Documented on the public header.

  3. Routes the LFA browser session to the resolved My Domain in phase 2 with the captured login_hint by introducing two LFA-scoped overrides on SFSDKAuthRequest:

    • loginAsAdminMyDomain — the resolved My Domain
    • loginAsAdminLoginHint — the username surfaced by the discovery page

    Consulted in authenticateWithRequest:loginHint: to set credentials.domain, the coordinator's loginHint, and the appConfigForLoginHost: lookup. The request's permanent loginHost is left unchanged.

  4. Clears all three LFA fields on cancel (loginAsAdmin, loginAsAdminMyDomain, loginAsAdminLoginHint) so the post-cancel restartAuthentication: re-runs against the originally configured login host.

Why LFA-scoped overrides instead of mutating oauthRequest.loginHost

An earlier version of this fix copied coordinator.credentials.domain onto session.oauthRequest.loginHost in-memory. That looked clean but produced an asymmetry: after a cancelled LFA, Reload and Clear Cache kept the WebView pinned to the My Domain instead of returning to welcome.salesforce.com/discovery. Putting the override in dedicated LFA-scoped fields keeps loginHost reflecting "the configured login server" at all times, so every other settings action stays correct by definition.

No persistence — Welcome Discovery returns after logout

The discovered My Domain is held only in memory on SFSDKAuthRequest. It is never written to setLoginHost:, SFSDKLoginHostStorage, or NSUserDefaults. After logout, the app correctly reloads the Welcome Discovery host and the user goes through phase 1 again — preserving the existing intentional behavior.

Behavior change: phase-2 LFA cancel

After tapping Cancel in the system browser sheet, the embedded WebView returns to phase 1 of Welcome Discovery (welcome.salesforce.com/discovery) and the user re-picks the account. This is intentionally different from Android, where the equivalent flow keeps the WebView on the resolved My Domain. The iOS choice matches existing iOS cancel behavior on every other login flow (cancel routes through restartAuthentication: against the configured login host) and avoids the Reload/Clear-Cache asymmetry described above. The cost is one extra account re-pick after a cancel; the benefit is symmetric, easy-to-reason behavior across all settings actions.

Backport / cherry-pick notes

Welcome Discovery ships only to a few internal apps, so this fix is not being patched into 13.2.x. However, those apps may want to cherry-pick this change onto a pre-14.0 base before 14.0 ships, so the commit is deliberately structured to apply with minimal conflict:

  • It reuses the existing loginAsAdmin property name (no rename), so it touches none of the ~70 loginAsAdmin references that pre-14.0 branches carry.
  • It does not modify SFSDKAuthSession.m (the useBrowserAuth || loginAsAdmin line already exists on those branches), avoiding the deprecated-userAgentForAuth block that differs across releases.
  • Verified against v13.2.1: 5 of 6 files apply cleanly; the only conflict is a one-line placement nudge in the test bridging header (SalesforceSDKCoreTests-Bridging-Header.h), where adjacent unrelated test categories that exist on dev are absent on 13.2.1. Trivial to resolve by keeping the new SFLoginViewController (LoginForAdminTesting) category.

Test plan

Automated tests

  • SalesforceSDKCoreTests/LoginForAdminTests — 34/34 pass
  • SalesforceSDKCoreTests/WelcomeDiscoveryLoginHostTests — 6/6 pass
  • SalesforceSDKCoreTests/DomainDiscoveryCoordinatorTests — pass

Manual verification (AuthFlowTester)

  • MTP-01 — Phase 1 of Welcome Discovery: Login for Admin entry hidden in the gear menu.
  • MTP-02 — Phase 2 of Welcome Discovery: Login for Admin entry present after picking an account.
  • MTP-03 — Phase 2 LFA tap: ASWebAuthenticationSession opens against the resolved My Domain with login_hint=<username>. Does NOT open against welcome.salesforce.com.
  • MTP-04 — Phase 2 LFA cancel: embedded WebView returns to welcome.salesforce.com/discovery (phase 1) — intentionally different from Android (see Behavior change section above).
  • MTP-05 — Phase 2 LFA: full happy-path login through to RestClient succeeds.
  • MTP-06 — Logout after phase-2 LFA login: login screen returns to Welcome Discovery, NOT the My Domain.
  • MTP-07 — Non-discovery server (login.salesforce.com): LFA still present and works as before.
  • MTP-08 — Non-discovery server LFA cancel: pre-fix behavior preserved (no regression).
  • MTP-09 — Opaque (PKCE) consumer key, useWebServerFlow=NO, phase-2 LFA: authorize URL still uses response_type=code with PKCE.
  • MTP-10 — Multi-scene (iPad Split View): phase 1 in scene A hides LFA; phase 2 in scene B shows LFA. Per-scene scoping verified.
  • MTP-11 — Public loginViewControllerDidSelectLoginForAdmin: called externally during phase 1: no-op, warning logged, no browser opens.
  • MTP-12 — Public API called during phase 2: works, browser opens against the resolved My Domain.
  • MTP-13SFSDKLoginHostStorage / NSUserDefaults inspection after phase-2 LFA + logout: My Domain is NOT persisted.
  • MTP-14 — Reload / Clear Cache from gear menu during phase 2 (and after a cancelled LFA): menu predicate re-evaluates correctly; both Reload paths produce the same URL regardless of whether LFA was previously cancelled.

Review checklist

  • No public API change — SFSDKAuthRequest.h is internal (not in public_header_files); the two new properties are not exposed. No symbols renamed.
  • No Localizable.strings change.
  • No persistence of the discovered My Domain — verified via MTP-13.
  • No new third-party dependencies, no Podfile / .podspec / .xcodeproj / .xcworkspace change.
  • Multi-user / multi-scene: verified per-scene scoping (manual MTP-10 pending).
  • No new compiler warnings.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
1 Warning
⚠️ Static Analysis found an issue with one or more files you modified. Please fix the issue(s).

Clang Static Analysis Issues

File Type Category Description Line Col
SFOAuthCoordinator Nullability Memory error nil assigned to a pointer which is expected to have non-null value 120 19
SFOAuthCoordinator Nullability Memory error nil assigned to a pointer which is expected to have non-null value 244 15
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1618 15
SFUserAccountManager Nullability Memory error Null passed to a callee that requires a non-null 2nd parameter 1633 15
SFUserAccountManager Nullability Memory error nil passed to a callee that requires a non-null 2nd parameter 2274 13

Generated by 🚫 Danger

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
TestsPassedSkippedFailed ❌️
AuthFlowTester UI Test Results all1 ran1 ❌
TestResult
AuthFlowTester UI Test Results all
AuthFlowTesterUITests.xctest
LegacyLoginTests.testCAOpaque_DefaultScopes_WebServerFlow()❌ failure

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.67347% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.38%. Comparing base (26a5133) to head (54b99a2).
⚠️ Report is 18 commits behind head on dev.

Files with missing lines Patch % Lines
...forceSDKCore/Classes/Login/SFLoginViewController.m 75.00% 7 Missing ⚠️
...ore/Classes/OAuth/DomainDiscoveryCoordinator.swift 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #4078      +/-   ##
==========================================
- Coverage   70.83%   68.38%   -2.46%     
==========================================
  Files         246      246              
  Lines       21495    21535      +40     
==========================================
- Hits        15227    14726     -501     
- Misses       6268     6809     +541     
Components Coverage Δ
Analytics 70.78% <ø> (ø)
Common 71.25% <ø> (-0.19%) ⬇️
Core 61.91% <83.67%> (-3.78%) ⬇️
SmartStore 73.44% <ø> (ø)
MobileSync 88.88% <ø> (ø)
Files with missing lines Coverage Δ
...lesforceSDKCore/Classes/OAuth/SFOAuthCoordinator.m 53.92% <100.00%> (-10.67%) ⬇️
...SDKCore/Classes/UserAccount/SFUserAccountManager.m 55.09% <100.00%> (-8.23%) ⬇️
...ore/Classes/OAuth/DomainDiscoveryCoordinator.swift 75.71% <66.66%> (-14.00%) ⬇️
...forceSDKCore/Classes/Login/SFLoginViewController.m 71.37% <75.00%> (-4.96%) ⬇️

... and 26 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
TestsPassed ☑️SkippedFailed ❌️
SalesforceSDKCore iOS ^26 Test Results666 ran664 ✅2 ❌
TestResult
SalesforceSDKCore iOS ^26 Test Results
PushNotificationManagerTests.testSetNotificationCategories_NoFilter()❌ failure
SFOAuthCoordinatorTests.testMigrateRefreshTokenSetup❌ failure

@github-actions

github-actions Bot commented Jun 19, 2026

Copy link
Copy Markdown
TestsPassed ✅SkippedFailed
SalesforceSDKCore iOS ^18 Test Results666 ran666 ✅
TestResult
No test annotations available

// Login for Admin - forces browser-based (advanced) authentication to support phishing-resistant MFA.
[menuActions addObject:[UIAction actionWithTitle:[SFSDKResourceUtils localizedString:@"LOGIN_FOR_ADMIN"]
// Wrapped in an uncached UIDeferredMenuElement so the show/hide predicate is re-evaluated each
// time the menu opens. The entry is hidden during phase 1 of Welcome Discovery (i.e. before the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

So we are hiding the menu during phase 1, do we expect admin to look for that menu in phase 2 or do we expect that to configure the correct login server manually if they need to login as admin?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hide in phase 1 so we don't have to deal with the discovery callback and the experience can be the same per platform (easier to document).

When the customer selects the org they want either the My Domain is configured to trigger Adv Auth for everyone or it loads in the WebView and the option to "Login for Admin" is in the menu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brandonpage brandonpage force-pushed the fix-login-for-admin-welcome-discovery branch from b3dc482 to cba3d81 Compare June 22, 2026 21:15
}

NSString *loginHost = session.oauthRequest.loginHost;
SFDomainDiscoveryCoordinator *discoveryCoordinator = [[SFDomainDiscoveryCoordinator alloc] init];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A fresh SFDomainDiscoveryCoordinator is allocated here purely to call isDiscoveryDomain:, and the same pattern is repeated in SFUserAccountManager.m at the loginViewControllerDidSelectLoginForAdmin: guard. If isDiscoveryDomain: is stateless (just a string predicate, no mutable coordinator state needed), consider extracting it to a class method — e.g. +[SFDomainDiscoveryCoordinator isDiscoveryDomain:] — so callers don't need to alloc a throw-away instance. Minor, but two independent allocs for the same check at independent call sites looks slightly odd to a reader.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. This adds new public API, but I don't think that is too big of a deal.

// MARK: - SFSDKAuthRequest loginAsAdmin Property

func testGivenNewAuthRequest_whenCreated_thenLoginAsAdminIsFalse() {
func testGivenNewAuthRequest_whenCreated_thenloginAsAdminIsFalse() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: the renamed test methods have a lowercase l in the middle of the method name (e.g. testGivenloginAsAdmin_..., testGivenNologinAsAdmin_...), which reads as a typo. The intent seems to be matching the ObjC property name casing, but LoginAsAdmin with a capital L would be more legible in a test name. Up to you, but worth a pass to recapitalize if you agree.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I originally replaced loginAsAdmin to loginForAdmin in a bunch of places but later changed it back to make cherry-picking cleaner.

- Extract the stateless isDiscoveryDomain check to a class method on
  SFDomainDiscoveryCoordinator so callers no longer alloc a throwaway
  instance. The instance method delegates to it; existing callers are
  unchanged. Converts the LFA menu-visibility check, the LFA phase-1
  guard, and the pre-existing setCurrentUser discovery check.
- Add class-method + instance/class parity tests.
- Capitalize "LoginAsAdmin"/"LoginForAdmin" in test method names
  (property/variable references stay lowercase).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@@ -100,6 +100,11 @@ public class DomainDiscoveryCoordinator: NSObject {

@objc
public func isDiscoveryDomain(_ domain: String?) -> Bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like the class one! Could we deprecate this one too?

brandonpage and others added 2 commits June 23, 2026 17:18
The class method must be @objc so ObjC callers (SFUserAccountManager.m,
SFLoginViewController.m) can resolve +isDiscoveryDomain: through the
generated -Swift.h header. Without it CI failed with "no known class
method for selector 'isDiscoveryDomain:'".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The stateless discovery-host check now lives on the class method, so the
instance method is redundant. Mark it @available deprecated (removed in
15.0) and migrate the last production caller (SFOAuthCoordinator) to the
class method. Annotate the instance-method tests as exercising deprecated
API so the build stays warning-free while coverage of the deprecated path
is retained until removal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants