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
1 change: 1 addition & 0 deletions libs/SalesforceSDK/res/values/sf__strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<string name="sf__clear_cache">Clear cache</string>
<string name="sf__reload">Reload</string>
<string name="sf__launch_idp">Log In with IDP App</string>
<string name="sf__login_for_admins">Login for Admins</string>
<string name="sf__login_with_biometric">Log In with Biometric</string>
<string name="sf__setup_biometric_unlock">Setup Biometric Unlock</string>
<string name="sf__back_button_content_description">Back</string>
Expand Down
117 changes: 59 additions & 58 deletions libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Observer
import com.salesforce.androidsdk.R.color.sf__background
import com.salesforce.androidsdk.R.color.sf__background_dark
import com.salesforce.androidsdk.R.color.sf__primary_color
import com.salesforce.androidsdk.R.drawable.sf__action_back
import com.salesforce.androidsdk.R.string.cannot_use_another_apps_login_qr_code
import com.salesforce.androidsdk.R.string.sf__biometric_opt_in_title
Expand Down Expand Up @@ -175,9 +174,27 @@ import java.security.cert.X509Certificate
*/
open class LoginActivity : FragmentActivity() {

/** The activity result launcher used when browser-based authentication loads the OAuth authorization URL in the external browser custom tab activity */
/**
* The activity result launcher used when browser-based authentication loads
* the OAuth authorization URL in the external browser custom tab activity
* */
@VisibleForTesting
internal val customTabLauncher = registerForActivityResult(
/* contract = */ StartActivityForResult(),
/* callback = */ CustomTabActivityResult(),
)

/**
* The activity result launcher used by the "Login for Admin" flow.
*
* Unlike [customTabLauncher], cancelling here leaves the existing WebView
* login page intact and does not reveal the server picker.
*/
@VisibleForTesting
internal val customTabLauncher = registerForActivityResult(StartActivityForResult(), CustomTabActivityResult())
internal val adminLoginCustomTabLauncher = registerForActivityResult(
/* contract = */ StartActivityForResult(),
/* callback = */ AdminCustomTabActivityResult(),
)

// View Model
@VisibleForTesting(otherwise = PROTECTED)
Expand Down Expand Up @@ -276,16 +293,27 @@ open class LoginActivity : FragmentActivity() {
certAuthOrLogin()
}

// Observe front door bridge URL to load in the WebView when present.
viewModel.frontDoorBridgeUrl.observe(this) { url ->
if (url != null) {
viewModel.loading.value = true
}
}

// Take control of the back logic if the device is locked.
// TODO: Remove SDK_INT check when min API > 33
if (SDK_INT >= TIRAMISU && biometricAuthenticationManager?.locked == true) {
onBackPressedDispatcher.addCallback { handleBackBehavior() }
}

// Add view model observers.
viewModel.browserCustomTabUrl.observe(this, BrowserCustomTabUrlObserver())
viewModel.pendingServer.observe(this, PendingServerObserver())

// Set callback for when browser custom tab URL is ready to launch.
viewModel.onBrowserCustomTabReady = { url ->
loadLoginPageInCustomTab(url, customTabLauncher)
}

// Support magic links
if (viewModel.jwt != null) {
swapJWTForAccessToken()
Expand Down Expand Up @@ -751,7 +779,19 @@ open class LoginActivity : FragmentActivity() {
)
}

private fun loadLoginPageInCustomTab(loginUrl: String, customTabLauncher: ActivityResultLauncher<Intent>) {
/**
* Launches the current login server in a Custom Tab. This allows Admins
* to authenticate with a Passkey or by other methods that require
* Advanced/Browser Authentication.
*/
@Deprecated(message = "This function will be replaced by a permanent solution in 14.0.")
fun launchLoginForAdminsAction() {
val loginUrl = viewModel.browserCustomTabUrl.value ?: return
loadLoginPageInCustomTab(loginUrl, adminLoginCustomTabLauncher)
}

@VisibleForTesting
internal fun loadLoginPageInCustomTab(loginUrl: String, customTabLauncher: ActivityResultLauncher<Intent>) {
val customTabsIntent = CustomTabsIntent.Builder().apply {
/*
* Set a custom animation to slide in and out for Chrome custom tab
Expand All @@ -763,8 +803,11 @@ open class LoginActivity : FragmentActivity() {
// Replace the default 'Close Tab' button with a custom back arrow instead of 'x'
setCloseButtonIcon(decodeResource(resources, sf__action_back))
setShareState(CustomTabsIntent.SHARE_STATE_OFF)

// Use app provided color if set. Fallback to dynamic color.
val background: Color = viewModel.topBarColor ?: viewModel.dynamicBackgroundColor.value
setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder().setToolbarColor(getColor(sf__primary_color)).build()
CustomTabColorSchemeParams.Builder().setToolbarColor(background.toArgb()).build()
)
}.build()

Expand Down Expand Up @@ -874,7 +917,7 @@ open class LoginActivity : FragmentActivity() {

// Choose front door bridge use by verifying intent data and such that only front door bridge URLs with matching consumer keys are used.
val uiBridgeApiParametersFrontDoorBridgeUrlMismatchedConsumerKey = uiBridgeApiParametersConsumerKey != null && uiBridgeApiParametersConsumerKey != viewModel.bootConfig.remoteAccessConsumerKey
viewModel.isUsingFrontDoorBridge = (isFrontdoorBridgeUrlIntent(intent) || isQrCodeLoginUrlIntent(intent)) && !uiBridgeApiParametersFrontDoorBridgeUrlMismatchedConsumerKey
val shouldUseFrontDoorBridge = (isFrontdoorBridgeUrlIntent(intent) || isQrCodeLoginUrlIntent(intent)) && !uiBridgeApiParametersFrontDoorBridgeUrlMismatchedConsumerKey

// Alert the user if the front door bridge URL is not for this app and was discarded.
if (uiBridgeApiParametersFrontDoorBridgeUrlMismatchedConsumerKey) {
Expand All @@ -888,7 +931,7 @@ open class LoginActivity : FragmentActivity() {
}

// Use the front door URL as the login page if applicable.
if (viewModel.isUsingFrontDoorBridge && uiBridgeApiParameters?.frontdoorBridgeUrl != null) {
if (shouldUseFrontDoorBridge && uiBridgeApiParameters?.frontdoorBridgeUrl != null) {
loginWithFrontdoorBridgeUrl(
uiBridgeApiParameters.frontdoorBridgeUrl,
uiBridgeApiParameters.pkceCodeVerifier
Expand Down Expand Up @@ -1079,35 +1122,6 @@ open class LoginActivity : FragmentActivity() {
applyUiBridgeApiFrontDoorUrl(intent)
}

/**
* Starts a browser custom tab for the OAuth authorization URL according to
* the authentication configuration. The activity only takes action when
* browser-based authentication requires a browser custom tab to be started.
* UI front-door bridge use bypasses the need for browser custom tab.
* @param authorizationUrl The selected login server's OAuth authorization
* URL
* @param activityResultLauncher The activity result launcher to use when
* browser-based authentication requires a browser custom tab
* @param isBrowserLoginEnabled Indicates if browser-based authentication is
* enabled
* @param isUsingFrontDoorBridge Indicates if a UI bridge API front door
* bridge URL is in use
* @param singleServerCustomTabActivity Indicates single server custom
* browser tab authentication is active
*/
@VisibleForTesting
internal open fun startBrowserCustomTabAuthorization(
authorizationUrl: String,
activityResultLauncher: ActivityResultLauncher<Intent>,
isBrowserLoginEnabled: Boolean = SalesforceSDKManager.getInstance().isBrowserLoginEnabled,
isUsingFrontDoorBridge: Boolean = viewModel.isUsingFrontDoorBridge,
singleServerCustomTabActivity: Boolean = viewModel.singleServerCustomTabActivity,
) {
if ((singleServerCustomTabActivity.or(isBrowserLoginEnabled)).and(!isUsingFrontDoorBridge)) {
loadLoginPageInCustomTab(authorizationUrl, activityResultLauncher)
}
}

/**
* A web view client which intercepts the redirect to the OAuth callback URL. That redirect marks the end of
* the user facing portion of the authentication flow.
Expand Down Expand Up @@ -1591,32 +1605,19 @@ open class LoginActivity : FragmentActivity() {
}
}

// endregion
// region Observer Classes

/**
* An observer for browser custom tab URL that continues the authentication
* flow by loading the login URL in a web browser custom tab when browser-
* based authentication is required.
* @param activity The login activity. This parameter is intended for
* testing purposes only. Defaults to this inner class receiver
* Activity result callback for the "Login for Admin" custom tab.
*/
internal inner class BrowserCustomTabUrlObserver(
private val activity: LoginActivity = this@LoginActivity
) : Observer<String> {
override fun onChanged(value: String) {
if (value == "about:blank") {
return
}

activity.startBrowserCustomTabAuthorization(
authorizationUrl = value,
activityResultLauncher = activity.customTabLauncher,
isBrowserLoginEnabled = SalesforceSDKManager.getInstance().isBrowserLoginEnabled,
)
@VisibleForTesting
internal inner class AdminCustomTabActivityResult : ActivityResultCallback<ActivityResult> {
override fun onActivityResult(result: ActivityResult) {
// Intentional no-op: keep the existing WebView visible on cancel.
}
}

// endregion
// region Observer Classes

/**
* An observer for pending login server that continues the authentication
* flow by determining the switch between default login and Salesforce
Expand Down
Loading
Loading