diff --git a/libs/SalesforceSDK/res/values/sf__strings.xml b/libs/SalesforceSDK/res/values/sf__strings.xml
index 47906df90e..61377c18ae 100644
--- a/libs/SalesforceSDK/res/values/sf__strings.xml
+++ b/libs/SalesforceSDK/res/values/sf__strings.xml
@@ -124,6 +124,7 @@
Toggle Web Server
Toggle Hybrid Token
Toggle Force Advanced Authentication
+ Toggle Ephemeral Advanced Authentication Session
Toggle DPoP
Toggle Dynamic Config
Consumer Key Field
diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt
index 5bfec6cba7..e16ca85d6a 100644
--- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt
+++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt
@@ -534,6 +534,15 @@ open class SalesforceSDKManager protected constructor(
@set:Synchronized
var customTabBrowser: String? = "com.android.chrome"
+ /**
+ * Whether advanced authentication requests an ephemeral Custom Tab. A browser that does not
+ * support ephemeral browsing may use a regular Custom Tab instead. Defaults to true.
+ */
+ @get:JvmName("shouldUseEphemeralSessionForAdvancedAuth")
+ @set:Synchronized
+ @Volatile
+ var useEphemeralSessionForAdvancedAuth = true
+
// Backing field for [useWebServerAuthentication]. The SDK reads this directly so its own
// internal use of the flag doesn't trigger the deprecation warning on the public property.
@Volatile
@@ -1980,6 +1989,7 @@ open class SalesforceSDKManager protected constructor(
"Use Web Server Authentication" to "$_useWebServerAuthentication",
"Use Hybrid Authentication Token" to "$useHybridAuthentication",
"Force Advanced Authentication" to "$_forceAdvancedAuthentication",
+ "Use Ephemeral Session for Advanced Authentication" to "$useEphemeralSessionForAdvancedAuth",
"My Domain Browser Login Enabled" to "$isBrowserLoginEnabled",
"IDP Enabled" to "$isIDPLoginFlowEnabled",
"Identity Provider" to "$isIdentityProvider",
diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt
index a6f026eef9..7f695b2917 100644
--- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt
+++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt
@@ -37,7 +37,6 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP
import android.app.AlertDialog
import android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE
-import android.content.pm.PackageManager
import android.content.pm.PackageManager.FEATURE_FACE
import android.content.pm.PackageManager.FEATURE_IRIS
import android.content.pm.PackageManager.MATCH_DEFAULT_ONLY
@@ -97,6 +96,7 @@ import androidx.biometric.BiometricPrompt.AuthenticationCallback
import androidx.biometric.BiometricPrompt.AuthenticationResult
import androidx.biometric.BiometricPrompt.PromptInfo
import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsIntent
import androidx.browser.customtabs.CustomTabsIntent.OPEN_IN_BROWSER_STATE_OFF
import androidx.browser.customtabs.ExperimentalInitialNavigationCanLeaveBrowser
@@ -1139,10 +1139,18 @@ open class LoginActivity : FragmentActivity() {
@OptIn(ExperimentalInitialNavigationCanLeaveBrowser::class)
@VisibleForTesting
- internal fun loadLoginPageInCustomTab(loginUrl: String, customTabLauncher: ActivityResultLauncher) {
+ internal fun loadLoginPageInCustomTab(
+ loginUrl: String,
+ customTabLauncher: ActivityResultLauncher,
+ callbackSchemeRegistered: (String) -> Boolean = ::isCallbackSchemeRegistered,
+ ) {
completedViaBrowserTab = true
registerAuthTypeFeatureGlobal()
- SalesforceSDKManager.getInstance().registerUsedAppFeature(FEATURE_BROWSER_LOGIN)
+ val sdkManager = SalesforceSDKManager.getInstance()
+ sdkManager.registerUsedAppFeature(FEATURE_BROWSER_LOGIN)
+
+ val customTabBrowser = sdkManager.customTabBrowser
+ val customTabBrowserExists = doesBrowserExist(customTabBrowser)
val customTabsIntent = CustomTabsIntent.Builder().apply {
/*
* Set a custom animation to slide in and out for Chrome custom tab
@@ -1169,6 +1177,10 @@ open class LoginActivity : FragmentActivity() {
setOpenInBrowserButtonState(OPEN_IN_BROWSER_STATE_OFF)
setInstantAppsEnabled(false)
setBackgroundInteractionEnabled(false)
+ configureEphemeralBrowsing(
+ enabled = sdkManager.useEphemeralSessionForAdvancedAuth,
+ requestedBrowser = customTabBrowser.takeIf { customTabBrowserExists },
+ )
}.build()
/*
@@ -1178,14 +1190,13 @@ open class LoginActivity : FragmentActivity() {
* - If getCustomTabBrowser() returns null
* - Or if the specified browser is not installed
*/
- val customTabBrowser = SalesforceSDKManager.getInstance().customTabBrowser
- if (doesBrowserExist(customTabBrowser)) {
+ if (customTabBrowserExists) {
customTabsIntent.intent.setPackage(customTabBrowser)
}
val urlString = buildCustomTabAuthorizeUrl(loginUrl, completedViaAdminCustomTab)
- if (!isCallbackSchemeRegistered(viewModel.oAuthConfig.redirectUri)) {
+ if (!callbackSchemeRegistered(viewModel.oAuthConfig.redirectUri)) {
val scheme = viewModel.oAuthConfig.redirectUri.toUri().scheme
?: viewModel.oAuthConfig.redirectUri
e(TAG, "Advanced auth misconfiguration: redirect URI scheme '$scheme' has no " +
@@ -1213,6 +1224,25 @@ open class LoginActivity : FragmentActivity() {
}
}
+ @VisibleForTesting
+ internal fun CustomTabsIntent.Builder.configureEphemeralBrowsing(
+ enabled: Boolean,
+ requestedBrowser: String?,
+ isSupported: (Context, String) -> Boolean = CustomTabsClient::isEphemeralBrowsingSupported,
+ ) {
+ if (!enabled) return
+
+ val provider = requestedBrowser ?: CustomTabsClient.getPackageName(this@LoginActivity, null)
+ if (provider != null && !isSupported(this@LoginActivity, provider)) {
+ w(
+ TAG,
+ "Ephemeral browsing was requested, but Custom Tabs provider '$provider' does not " +
+ "advertise support. The browser may use a regular session.",
+ )
+ }
+ setEphemeralBrowsingEnabled(true)
+ }
+
/**
* Appends `sdkInfo` and `auth_trigger` to the authorize URL for the native browser (Custom
* Tab) path only — the WebView path's real `User-Agent` header already carries this
diff --git a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt
index 48d0093e26..e81408e102 100644
--- a/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt
+++ b/libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginOptionsActivity.kt
@@ -103,6 +103,9 @@ class LoginOptionsActivity: ComponentActivity() {
// suppress the deprecation nudge here (it fires on the public property from outside the SDK).
@Suppress("DEPRECATION")
val forceAdvancedAuth = MutableLiveData(SalesforceSDKManager.getInstance().forceAdvancedAuthentication)
+ val useEphemeralSessionForAdvancedAuth = MutableLiveData(
+ SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth,
+ )
val useDPoP = MutableLiveData(SalesforceSDKManager.getInstance().useDPoP)
@OptIn(ExperimentalMaterial3Api::class)
@@ -133,6 +136,12 @@ class LoginOptionsActivity: ComponentActivity() {
value -> SalesforceSDKManager.getInstance().forceAdvancedAuthentication = value
},
)
+ useEphemeralSessionForAdvancedAuth.observe(
+ /* owner = */ this,
+ Observer {
+ value -> SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = value
+ },
+ )
useDPoP.observe(
/* owner = */ this,
Observer {
@@ -158,6 +167,7 @@ class LoginOptionsActivity: ComponentActivity() {
useWebServer,
useHybridToken,
forceAdvancedAuth,
+ useEphemeralSessionForAdvancedAuth,
useDPoP,
SalesforceSDKManager.getInstance().debugOverrideAppConfig,
)
@@ -337,6 +347,7 @@ fun LoginOptionsScreen(
useWebServer: MutableLiveData,
useHybridToken: MutableLiveData,
forceAdvancedAuth: MutableLiveData,
+ useEphemeralSessionForAdvancedAuth: MutableLiveData,
useDPoP: MutableLiveData,
overrideConfig: OAuthConfig?,
bootConfig: BootConfig = BootConfig.getBootConfig(LocalContext.current),
@@ -365,6 +376,11 @@ fun LoginOptionsScreen(
stringResource(R.string.sf__login_options_force_advanced_auth_toggle_content_description),
forceAdvancedAuth,
)
+ OptionToggle(
+ "Use Ephemeral Session",
+ stringResource(R.string.sf__login_options_ephemeral_session_toggle_content_description),
+ useEphemeralSessionForAdvancedAuth,
+ )
OptionToggle(
"Use DPoP",
stringResource(R.string.sf__login_options_dpop_toggle_content_description),
@@ -587,6 +603,7 @@ fun LoginOptionsScreenPreview() {
useWebServer = MutableLiveData(true),
useHybridToken = MutableLiveData(false),
forceAdvancedAuth = MutableLiveData(true),
+ useEphemeralSessionForAdvancedAuth = MutableLiveData(true),
useDPoP = MutableLiveData(false),
overrideConfig = null,
bootConfig = object : BootConfig() {
@@ -595,4 +612,4 @@ fun LoginOptionsScreenPreview() {
},
sdkManager = null,
)
-}
\ No newline at end of file
+}
diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java
index 71ec4fb79e..46dcdc411c 100644
--- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java
+++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/app/SalesforceSDKManagerTest.java
@@ -125,6 +125,26 @@ public void testDefaultTheme() {
SalesforceSDKTestManager.getInstance().isDarkTheme());
}
+ /**
+ * Test the default and Java API for ephemeral advanced-auth sessions.
+ */
+ @Test
+ public void testUseEphemeralSessionForAdvancedAuthDefaultsToTrueAndCanBeUpdated() {
+ SalesforceSDKTestManager.resetInstance();
+ SalesforceSDKTestManager.init(getInstrumentation().getTargetContext(), MainActivity.class);
+ final SalesforceSDKManager sdkManager = SalesforceSDKTestManager.getInstance();
+
+ try {
+ Assert.assertTrue(sdkManager.shouldUseEphemeralSessionForAdvancedAuth());
+
+ sdkManager.setUseEphemeralSessionForAdvancedAuth(false);
+
+ Assert.assertFalse(sdkManager.shouldUseEphemeralSessionForAdvancedAuth());
+ } finally {
+ SalesforceSDKTestManager.resetInstance();
+ }
+ }
+
/**
* Test setting dark theme.
*/
diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt
index cd5ba54f43..0fdc78dd48 100644
--- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt
+++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityScenarioTest.kt
@@ -28,6 +28,11 @@ package com.salesforce.androidsdk.ui
import android.content.Intent
import android.webkit.WebView
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.core.app.ActivityOptionsCompat
import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.lifecycle.Lifecycle.State.STARTED
@@ -38,6 +43,7 @@ import com.salesforce.androidsdk.app.Features
import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.config.LoginServerManager.PRODUCTION_LOGIN_URL
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.ui.LoginActivity.Companion.EXTRA_KEY_LOGIN_HINT
import com.salesforce.androidsdk.ui.LoginActivity.Companion.EXTRA_KEY_LOGIN_HOST
@@ -126,6 +132,91 @@ class LoginActivityScenarioTest {
}
}
+ @Test
+ @Suppress("DEPRECATION")
+ fun loadLoginPageInCustomTab_usesCurrentEphemeralSettingAndConfiguredBrowser() {
+ val sdkManager = SalesforceSDKManager.getInstance()
+ val originalForceAdvancedAuth = sdkManager.forceAdvancedAuthentication
+ val originalCustomTabBrowser = sdkManager.customTabBrowser
+ val originalUseEphemeralSession = sdkManager.useEphemeralSessionForAdvancedAuth
+ val affectedFeatures = listOf(
+ Features.FEATURE_BROWSER_LOGIN,
+ Features.FEATURE_AUTH_TYPE_WEB_SERVER_NON_HYBRID,
+ Features.FEATURE_AUTH_TYPE_WEB_SERVER_HYBRID,
+ Features.FEATURE_AUTH_TYPE_USER_AGENT_NON_HYBRID,
+ Features.FEATURE_AUTH_TYPE_USER_AGENT_HYBRID,
+ Features.FEATURE_AUTH_TYPE_NATIVE,
+ )
+ val originalFeatureState = affectedFeatures.associateWith(sdkManager::isGlobalFeatureRegistered)
+ val configuredBrowser = getApplicationContext().packageName
+ val launchedIntents = mutableListOf()
+ val launcher = object : ActivityResultLauncher() {
+ override fun launch(input: Intent, options: ActivityOptionsCompat?) {
+ launchedIntents += input
+ }
+
+ override fun unregister() = Unit
+
+ override val contract: ActivityResultContract = StartActivityForResult()
+ }
+
+ try {
+ sdkManager.forceAdvancedAuthentication = false
+ sdkManager.customTabBrowser = configuredBrowser
+
+ launch(
+ Intent(getApplicationContext(), LoginActivity::class.java)
+ ).use { activityScenario ->
+ activityScenario.onActivity { activity ->
+ activity.viewModel.oAuthConfig = OAuthConfig(
+ consumerKey = "test-consumer-key",
+ redirectUri = "testsfdc://success/done",
+ )
+ sdkManager.useEphemeralSessionForAdvancedAuth = true
+ activity.loadLoginPageInCustomTab(
+ "https://example.com/services/oauth2/authorize?client_id=abc",
+ launcher,
+ callbackSchemeRegistered = { true },
+ )
+
+ sdkManager.useEphemeralSessionForAdvancedAuth = false
+ activity.loadLoginPageInCustomTab(
+ "https://example.com/services/oauth2/authorize?client_id=abc",
+ launcher,
+ callbackSchemeRegistered = { true },
+ )
+ }
+ }
+
+ assertEquals(2, launchedIntents.size)
+ assertEquals(configuredBrowser, launchedIntents[0].`package`)
+ assertTrue(
+ launchedIntents[0].getBooleanExtra(
+ CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING,
+ false,
+ )
+ )
+ assertEquals(configuredBrowser, launchedIntents[1].`package`)
+ assertFalse(
+ launchedIntents[1].getBooleanExtra(
+ CustomTabsIntent.EXTRA_ENABLE_EPHEMERAL_BROWSING,
+ false,
+ )
+ )
+ } finally {
+ sdkManager.forceAdvancedAuthentication = originalForceAdvancedAuth
+ sdkManager.customTabBrowser = originalCustomTabBrowser
+ sdkManager.useEphemeralSessionForAdvancedAuth = originalUseEphemeralSession
+ originalFeatureState.forEach { (feature, wasRegistered) ->
+ if (wasRegistered) {
+ sdkManager.registerUsedAppFeature(feature)
+ } else {
+ sdkManager.unregisterUsedAppFeature(feature)
+ }
+ }
+ }
+ }
+
@Test
fun viewModelFrontDoorBridgeCodeVerifier_UpdatesOn_onCreateWithQrCodeLoginIntent() {
val uri = "app://android/login/qr/?bridgeJson=%7B%22pkce_code_verifier%22%3A%22__CODE_VERIFIER__%22%2C%22frontdoor_bridge_url%22%3A%22https%3A%2F%2Fmobilesdk.my.salesforce.com%2Fsecur%2Ffrontdoor.jsp%3Fotp%3D__OTP__%26startURL%3D%252Fservices%252Foauth2%252Fauthorize%253Fresponse_type%253Dcode%2526client_id%253D__CONSUMER_KEY__%2526redirect_uri%253Dtestsfdc%25253A%25252F%25252F%25252Fmobilesdk%25252Fdetect%25252Foauth%25252Fdone%2526code_challenge%253D__CODE_CHALLENGE__%26cshc%3D__CSHC__%22%7D".toUri()
diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt
index 36c57daf0c..c0a1a5d585 100644
--- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt
+++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginActivityTest.kt
@@ -41,6 +41,8 @@ import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE
import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_SECURITY_UPDATE_REQUIRED
import androidx.biometric.BiometricManager.BIOMETRIC_ERROR_UNSUPPORTED
import androidx.biometric.BiometricManager.BIOMETRIC_STATUS_UNKNOWN
+import androidx.browser.customtabs.CustomTabsClient
+import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.ui.graphics.Color
import androidx.core.net.toUri
import androidx.lifecycle.MediatorLiveData
@@ -50,6 +52,7 @@ import com.salesforce.androidsdk.app.SalesforceSDKManager
import com.salesforce.androidsdk.rest.ClientManager
import com.salesforce.androidsdk.rest.RestClient
import com.salesforce.androidsdk.rest.RestClient.OAuthRefreshInterceptor
+import com.salesforce.androidsdk.util.SalesforceSDKLogger
import com.salesforce.androidsdk.ui.LoginActivity.Companion.ABOUT_BLANK
import com.salesforce.androidsdk.ui.LoginActivity.Companion.AUTH_TRIGGER_FORCE_ADVANCED_AUTH
import com.salesforce.androidsdk.ui.LoginActivity.Companion.AUTH_TRIGGER_LOGIN_FOR_ADMIN
@@ -70,6 +73,7 @@ import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkObject
+import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.unmockkObject
import io.mockk.verify
@@ -371,6 +375,120 @@ class LoginActivityTest {
// endregion
+ // region Ephemeral Custom Tabs
+
+ @Test
+ fun configureEphemeralBrowsing_whenEnabledAndSupported_setsIntentExtra() {
+ val activity = mockk(relaxed = true)
+ every {
+ with(activity) {
+ any().configureEphemeralBrowsing(any(), any(), any())
+ }
+ } answers { callOriginal() }
+ val builder = CustomTabsIntent.Builder()
+
+ with(activity) {
+ builder.configureEphemeralBrowsing(
+ enabled = true,
+ requestedBrowser = "com.example.browser",
+ isSupported = { _, _ -> true },
+ )
+ }
+
+ assertTrue(builder.build().isEphemeralBrowsingEnabled)
+ }
+
+ @Test
+ fun configureEphemeralBrowsing_whenDisabled_doesNotSetIntentExtra() {
+ val activity = mockk(relaxed = true)
+ every {
+ with(activity) {
+ any().configureEphemeralBrowsing(any(), any(), any())
+ }
+ } answers { callOriginal() }
+ val builder = CustomTabsIntent.Builder()
+
+ with(activity) {
+ builder.configureEphemeralBrowsing(
+ enabled = false,
+ requestedBrowser = "com.example.browser",
+ isSupported = { _, _ -> error("Capability must not be checked when disabled") },
+ )
+ }
+
+ assertFalse(builder.build().isEphemeralBrowsingEnabled)
+ }
+
+ @Test
+ fun configureEphemeralBrowsing_whenSupportIsNotAdvertised_warnsAndStillSetsIntentExtra() {
+ mockkStatic(SalesforceSDKLogger::class)
+ every { SalesforceSDKLogger.w(any(), any()) } just Runs
+ val activity = mockk(relaxed = true)
+ every {
+ with(activity) {
+ any().configureEphemeralBrowsing(any(), any(), any())
+ }
+ } answers { callOriginal() }
+ val builder = CustomTabsIntent.Builder()
+ val provider = "com.example.browser"
+
+ with(activity) {
+ builder.configureEphemeralBrowsing(
+ enabled = true,
+ requestedBrowser = provider,
+ isSupported = { _, _ -> false },
+ )
+ }
+
+ assertTrue(builder.build().isEphemeralBrowsingEnabled)
+ verify(exactly = 1) {
+ SalesforceSDKLogger.w(
+ any(),
+ match { it.contains(provider) && it.contains("does not advertise support") },
+ )
+ }
+ }
+
+ @Test
+ fun configureEphemeralBrowsing_withoutRequestedBrowser_checksDefaultProvider() {
+ mockkStatic(CustomTabsClient::class)
+ mockkStatic(SalesforceSDKLogger::class)
+ every { SalesforceSDKLogger.w(any(), any()) } just Runs
+ val activity = mockk(relaxed = true)
+ every {
+ with(activity) {
+ any().configureEphemeralBrowsing(any(), any(), any())
+ }
+ } answers { callOriginal() }
+ val builder = CustomTabsIntent.Builder()
+ val provider = "com.example.defaultbrowser"
+ every { CustomTabsClient.getPackageName(activity, null) } returns provider
+ var checkedProvider: String? = null
+
+ with(activity) {
+ builder.configureEphemeralBrowsing(
+ enabled = true,
+ requestedBrowser = null,
+ isSupported = { _, packageName ->
+ checkedProvider = packageName
+ false
+ },
+ )
+ }
+
+ assertEquals(provider, checkedProvider)
+ assertTrue(builder.build().isEphemeralBrowsingEnabled)
+ verify(exactly = 1) { CustomTabsClient.getPackageName(activity, null) }
+ verify(exactly = 1) {
+ SalesforceSDKLogger.w(
+ any(),
+ match { it.contains(provider) && it.contains("does not advertise support") },
+ )
+ }
+ }
+
+ // endregion
+
// region Custom Tab toolbar color
@Test
@@ -458,21 +576,35 @@ class LoginActivityTest {
@Test
@Suppress("DEPRECATION")
- fun buildCustomTabAuthorizeUrl_withSharedBrowserSession_omitsPromptLogin() {
+ fun buildCustomTabAuthorizeUrl_promptLoginDependsOnlyOnSharedBrowserSession() {
val loginUrl = "https://example.com/services/oauth2/authorize?client_id=abc"
val sdkManager = mockk(relaxed = true)
every { sdkManager.forceAdvancedAuthentication } returns false
every { sdkManager.getUserAgent("") } returns "SalesforceMobileSDK/test"
val activity = mockk(relaxed = true)
- every { activity.sharedBrowserSession } returns true
every {
activity.buildCustomTabAuthorizeUrl(any(), any(), any())
} answers { callOriginal() }
- val result = activity.buildCustomTabAuthorizeUrl(loginUrl, false, sdkManager)
+ listOf(false, true).forEach { useEphemeralSession ->
+ every {
+ sdkManager.useEphemeralSessionForAdvancedAuth
+ } returns useEphemeralSession
- assertFalse("prompt=login should be omitted for a shared browser session",
- result.contains("prompt=login"))
+ every { activity.sharedBrowserSession } returns false
+ val isolatedResult = activity.buildCustomTabAuthorizeUrl(loginUrl, false, sdkManager)
+ assertTrue(
+ "prompt=login should be appended regardless of the ephemeral setting",
+ isolatedResult.contains("prompt=login"),
+ )
+
+ every { activity.sharedBrowserSession } returns true
+ val sharedResult = activity.buildCustomTabAuthorizeUrl(loginUrl, false, sdkManager)
+ assertFalse(
+ "prompt=login should be omitted regardless of the ephemeral setting",
+ sharedResult.contains("prompt=login"),
+ )
+ }
}
// endregion
diff --git a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt
index 0ea375c71c..c77383e8db 100644
--- a/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt
+++ b/libs/test/SalesforceSDKTest/src/com/salesforce/androidsdk/ui/LoginOptionsActivityTest.kt
@@ -78,6 +78,7 @@ class LoginOptionsActivityTest {
private var originalUseWebServer: Boolean = false
private var originalUseHybridToken: Boolean = false
private var originalForceAdvancedAuth: Boolean = true
+ private var originalUseEphemeralSessionForAdvancedAuth: Boolean = true
private lateinit var dynamicToggle: SemanticsNodeInteraction
private lateinit var consumerKeyField: SemanticsNodeInteraction
private lateinit var redirectUriField: SemanticsNodeInteraction
@@ -85,6 +86,7 @@ class LoginOptionsActivityTest {
private lateinit var webserverToggle: SemanticsNodeInteraction
private lateinit var hybridToggle: SemanticsNodeInteraction
private lateinit var forceAdvancedAuthToggle: SemanticsNodeInteraction
+ private lateinit var ephemeralSessionToggle: SemanticsNodeInteraction
private lateinit var saveButton: SemanticsNodeInteraction
@Before
@@ -94,6 +96,8 @@ class LoginOptionsActivityTest {
originalUseWebServer = SalesforceSDKManager.getInstance().useWebServerAuthentication
originalUseHybridToken = SalesforceSDKManager.getInstance().useHybridAuthentication
originalForceAdvancedAuth = SalesforceSDKManager.getInstance().forceAdvancedAuthentication
+ originalUseEphemeralSessionForAdvancedAuth =
+ SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth
SalesforceSDKManager.getInstance().loginDevMenuReload = false
dynamicToggle = composeTestRule.onNodeWithContentDescription(
@@ -117,6 +121,9 @@ class LoginOptionsActivityTest {
forceAdvancedAuthToggle = composeTestRule.onNodeWithContentDescription(
composeTestRule.activity.getString(R.string.sf__login_options_force_advanced_auth_toggle_content_description),
)
+ ephemeralSessionToggle = composeTestRule.onNodeWithContentDescription(
+ composeTestRule.activity.getString(R.string.sf__login_options_ephemeral_session_toggle_content_description),
+ )
saveButton = composeTestRule.onNodeWithText(
composeTestRule.activity.getString(R.string.sf__login_options_save_and_login),
)
@@ -129,6 +136,8 @@ class LoginOptionsActivityTest {
SalesforceSDKManager.getInstance().useWebServerAuthentication = originalUseWebServer
SalesforceSDKManager.getInstance().useHybridAuthentication = originalUseHybridToken
SalesforceSDKManager.getInstance().forceAdvancedAuthentication = originalForceAdvancedAuth
+ SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth =
+ originalUseEphemeralSessionForAdvancedAuth
SalesforceSDKManager.getInstance().debugOverrideAppConfig = null
SalesforceSDKManager.getInstance().loginDevMenuReload = false
}
@@ -286,6 +295,46 @@ class LoginOptionsActivityTest {
)
}
+ @Test
+ fun loginOptionsActivity_EphemeralSessionToggle_ReflectsAndUpdatesSdkManager() {
+ composeTestRule.activity.runOnUiThread {
+ SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = false
+ composeTestRule.activity.useEphemeralSessionForAdvancedAuth.value = false
+ }
+ composeTestRule.waitForIdle()
+
+ ephemeralSessionToggle.performScrollTo()
+ ephemeralSessionToggle.assertIsDisplayed()
+ ephemeralSessionToggle.assertIsOff()
+
+ ephemeralSessionToggle.performClick()
+ composeTestRule.waitForIdle()
+
+ ephemeralSessionToggle.assertIsOn()
+ assertTrue(SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth)
+ }
+
+ @Test
+ fun devSupportInfo_IncludesEphemeralAdvancedAuthentication_WithCurrentValue() {
+ SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = true
+ val authConfigOn = SalesforceSDKManager.getInstance().devSupportInfo.authConfigSection?.second
+ assertEquals(
+ "true",
+ authConfigOn?.find {
+ it.first == "Use Ephemeral Session for Advanced Authentication"
+ }?.second,
+ )
+
+ SalesforceSDKManager.getInstance().useEphemeralSessionForAdvancedAuth = false
+ val authConfigOff = SalesforceSDKManager.getInstance().devSupportInfo.authConfigSection?.second
+ assertEquals(
+ "false",
+ authConfigOff?.find {
+ it.first == "Use Ephemeral Session for Advanced Authentication"
+ }?.second,
+ )
+ }
+
@Test
@Suppress("DEPRECATION") // Deliberately exercises the deprecated forceAdvancedAuthentication flag.
fun devSupportInfo_IncludesForceAdvancedAuthentication_WithCurrentValue() {
@@ -560,4 +609,4 @@ class LoginOptionsActivityTest {
}
// endregion
-}
\ No newline at end of file
+}