Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,19 @@ open class LoginViewModel(

/** Value representing if the back button should be shown on the login view. */
open val shouldShowBackButton = with(SalesforceSDKManager.getInstance()) {
!(userAccountManager.authenticatedUsers.isNullOrEmpty() || biometricAuthenticationManager?.locked ?: false)
when {
// Never show back while biometric-locked; user must authenticate.
biometricAuthenticationManager?.locked == true -> false
/*
* Native Login uses this WebView LoginActivity as a dismissible
* fallback: handleBackBehavior() finishes and returns to the native
* login activity, so show the back affordance to match — even with
* no authenticated users yet.
*/
nativeLoginActivity != null -> true

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.

This doesn't seem right. The screen should not show a back button unconditionally for native login. What if there are 0 users logged in?

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 question — I checked the 0-users case specifically, and it's not a dead-end.

The reason it's safe to show unconditionally for native login is that the back handler is already unconditional in that mode. LoginActivity.handleBackBehavior() finishes and returns to the native login activity whenever nativeLoginActivity != null — it never consults the user count:

// LoginActivity.handleBackBehavior()
if (nativeLoginActivity != null) {
    setResult(RESULT_CANCELED)
    finish()
    return
}

So with native login enabled, hardware/gesture back already dismisses this WebView fallback back to the native screen — including with 0 users. Before this change the visible ← button was gated on authenticated users, so it was hidden even though the navigation behind it worked, leaving no visible affordance to escape the fallback. This change just makes the button match the behavior that already ships.

I verified the 0-users path end-to-end on-device (native login enabled, no accounts): the fallback shows ←, and both the button and the gesture return to the native login screen — no dead-end. Happy to attach screenshots if useful.

Note this is scoped to LoginViewModel.shouldShowBackButton (the WebView fallback); NativeLoginManager.shouldShowBackButton — the app's own native screen — is untouched and still gates on users.

This response was generated by an AI agent on behalf of @JohnsonEricAtSalesforce.

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.

I forgot we have a separate shouldShowBack for native login.

// Otherwise show back only when an authenticated user exists.
else -> !userAccountManager.authenticatedUsers.isNullOrEmpty()
}
}

// The default, locally generated code verifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.salesforce.androidsdk.config.BootConfig
import com.salesforce.androidsdk.config.LoginServerManager.LoginServer
import com.salesforce.androidsdk.config.LoginServerManager.WELCOME_LOGIN_URL
import com.salesforce.androidsdk.config.OAuthConfig
import com.salesforce.androidsdk.security.BiometricAuthenticationManager
import com.salesforce.androidsdk.security.SalesforceKeyGenerator.getSHA256Hash
import com.salesforce.androidsdk.ui.LoginActivity
import com.salesforce.androidsdk.ui.LoginActivity.Companion.ABOUT_BLANK
Expand All @@ -46,7 +47,10 @@ import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.slot
import io.mockk.spyk
import io.mockk.unmockkObject
import io.mockk.verify
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -1474,6 +1478,83 @@ class LoginViewModelTest {
}
}

/**
* Builds a [LoginViewModel] whose [SalesforceSDKManager] is a spy that
* reports the app is using Native Login, so the WebView LoginActivity is
* the dismissible fallback, then runs [block] against it. The spy also
* reports no authenticated users, so the fallback scenario is deterministic
* regardless of any account state other suite tests leave on the shared
* SalesforceSDKManager singleton. [configureSpy] may further stub the spy
* (e.g. a locked biometric manager) before the view model is constructed.
* The object mock is always torn down.
*/
private fun withNativeLoginFallbackViewModel(
configureSpy: (SalesforceSDKManager) -> Unit = {},
block: (LoginViewModel) -> Unit,
) {
val spySdkManager = spyk(SalesforceSDKManager.getInstance())
// Any non-null Activity class works; only nativeLoginActivity's
// nullness gates the fallback, and its concrete type is never read.
every { spySdkManager.nativeLoginActivity } returns LoginActivity::class.java
// Pin the no-authenticated-users precondition on the spy rather than
// asserting on the shared singleton, which sibling tests can pollute.
every { spySdkManager.userAccountManager.authenticatedUsers } returns emptyList()
configureSpy(spySdkManager)
mockkObject(SalesforceSDKManager)
try {
every { SalesforceSDKManager.getInstance() } returns spySdkManager
block(
LoginViewModel(
bootConfig = bootConfig,
backgroundContext = testDispatcher,
)
)
} finally {
unmockkObject(SalesforceSDKManager)
}
}

/**
* When the app uses Native Login, the WebView LoginActivity is a
* dismissible fallback whose hardware-back already finishes and returns to
* the native login activity. The visible back affordance must be shown to
* match — even with no authenticated users — so the user is never stranded
* on the fallback WebView.
*/
@Test
fun test_shouldShowBackButton_isTrueForNativeLoginFallbackWithNoUsers() {
withNativeLoginFallbackViewModel { nativeLoginViewModel ->
assertTrue(
"Back button must be shown for the Native Login WebView fallback.",
nativeLoginViewModel.shouldShowBackButton,
)
}
}

/**
* The Native Login fallback back affordance must still yield to the
* biometric lock — a locked user must authenticate rather than navigate
* back. The helper pins the no-authenticated-users precondition, so the
* only branch that could show the button is the Native Login one, proving
* the biometric lock takes precedence over it.
*/
@Test
fun test_shouldShowBackButton_isFalseForNativeLoginFallbackWhenBiometricLocked() {
val lockedBioAuthManager = mockk<BiometricAuthenticationManager> {
every { locked } returns true
}
withNativeLoginFallbackViewModel(
configureSpy = { spy ->
every { spy.biometricAuthenticationManager } returns lockedBioAuthManager
},
) { nativeLoginViewModel ->
assertFalse(
"Back button must remain hidden while biometric-locked, even for Native Login.",
nativeLoginViewModel.shouldShowBackButton,
)
}
}

// endregion

@Test
Expand Down
Loading