Skip to content

Commit e3a3fc4

Browse files
@W-20161958: [MSDK 13.1][Android] Cannot login GUS using Welcome endpoint (Re-factored login observables)
1 parent 3d5e77c commit e3a3fc4

3 files changed

Lines changed: 100 additions & 61 deletions

File tree

libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginActivity.kt

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ import androidx.core.content.ContextCompat.getMainExecutor
104104
import androidx.core.net.toUri
105105
import androidx.core.view.WindowCompat
106106
import androidx.fragment.app.FragmentActivity
107-
import androidx.lifecycle.lifecycleScope
107+
import androidx.lifecycle.Observer
108108
import com.salesforce.androidsdk.R.color.sf__background
109109
import com.salesforce.androidsdk.R.color.sf__background_dark
110110
import com.salesforce.androidsdk.R.color.sf__primary_color
@@ -147,7 +147,7 @@ import com.salesforce.androidsdk.util.UriFragmentParser
147147
import kotlinx.coroutines.CoroutineScope
148148
import kotlinx.coroutines.Dispatchers.Default
149149
import kotlinx.coroutines.Dispatchers.IO
150-
import kotlinx.coroutines.Dispatchers.Main
150+
import kotlinx.coroutines.Job
151151
import kotlinx.coroutines.launch
152152
import org.json.JSONObject
153153
import java.lang.String.format
@@ -287,6 +287,10 @@ open class LoginActivity : FragmentActivity() {
287287
}
288288
}
289289

290+
// Add view model observers.
291+
viewModel.loginUrl.observe(this, loginUrlObserver)
292+
viewModel.pendingServer.observe(this, pendingServerObserver)
293+
290294
// Support magic links
291295
if (viewModel.jwt != null) {
292296
swapJWTForAccessToken()
@@ -331,6 +335,7 @@ open class LoginActivity : FragmentActivity() {
331335
// Store the new intent and apply it to the activity.
332336
setIntent(intent)
333337
applyIntent()
338+
viewModel.pendingServer.value?.let { applyPendingServer(it) }
334339
}
335340

336341
private fun clearWebView(showServerPicker: Boolean = true) {
@@ -898,6 +903,9 @@ open class LoginActivity : FragmentActivity() {
898903
// endregion
899904
// region Salesforce Welcome Login Private Implementation
900905

906+
/** The Kotlin Coroutine Job fetching the pending login server's authentication configuration */
907+
private var authenticationConfigurationFetchJob: Job? = null
908+
901909
/** The previously observed pending login server for use in switching between default and Salesforce Welcome Discovery log in */
902910
private var previousPendingLoginServer: String? = null
903911

@@ -1027,8 +1035,6 @@ open class LoginActivity : FragmentActivity() {
10271035
uri: Uri
10281036
): Boolean {
10291037
return if (isSalesforceWelcomeDiscoveryMobileCallbackUrl(uri)) {
1030-
// Stop observing the pending login server since the newly started default login will do so.
1031-
viewModel.pendingServer.removeObservers(this)
10321038
startDefaultLoginWithHintAndHost(
10331039
context = this,
10341040
loginHint = uri.getQueryParameter(SALESFORCE_WELCOME_DISCOVERY_MOBILE_CALLBACK_URL_QUERY_PARAMETER_KEY_LOGIN_HINT) ?: return false,
@@ -1063,73 +1069,96 @@ open class LoginActivity : FragmentActivity() {
10631069

10641070
// If the intent is for log in using a UI Bridge API front door URL, apply it to the activity.
10651071
applyUiBridgeApiFrontDoorUrl(intent)
1072+
}
1073+
1074+
/**
1075+
* An observer for the login URL (OAuth authorization URL) that loads the
1076+
* authorization URL in a web browser custom tab when the authentication
1077+
* configuration requires browser-based authentication.
1078+
*/
1079+
private val loginUrlObserver = Observer<String> { authorizationUrl ->
1080+
if (authorizationUrl == "about:blank") {
1081+
return@Observer
1082+
}
10661083

1067-
// Apply the pending login server, if applicable.
1068-
applyPendingServer()
1084+
startBrowserCustomTabAuthorization(
1085+
sdkManager = SalesforceSDKManager.getInstance(),
1086+
authorizationUrl = authorizationUrl,
1087+
activityResultLauncher = customTabLauncher ?: return@Observer
1088+
)
10691089
}
10701090

10711091
/**
1072-
* Applies the pending login server. The authentication configuration will
1073-
* be fetched for the pending server and any applicable tasks will be
1074-
* performed such as browser based authentication. The pending login
1075-
* server will be set as the selected login server once the authentication
1076-
* configuration has been fetched.
1092+
* An observer for the pending login server. The decision to switch between
1093+
* default login and Salesforce Welcome Discovery will be made before
1094+
* applying the new value.
10771095
*/
1078-
private fun applyPendingServer() {
1096+
private val pendingServerObserver = Observer<String> { pendingLoginServer ->
10791097

1080-
// Reset the observer to consume the current and future values.
1081-
viewModel.pendingServer.removeObservers(this)
1082-
viewModel.pendingServer.observe(this) { pendingServer ->
1098+
// Guard against observing a pending login server already provided by the intent data, such as a Salesforce Welcome Discovery mobile URL.
1099+
val pendingServerUri = pendingLoginServer.toUri()
1100+
if (intent.data?.host == pendingServerUri.host) {
1101+
previousPendingLoginServer = pendingLoginServer
1102+
return@Observer
1103+
}
10831104

1084-
// Guard against observing a pending login server already provided by the intent data, such as a Salesforce Welcome Discovery mobile URL.
1085-
val pendingServerUri = pendingServer.toUri()
1086-
if (intent.data?.host == pendingServerUri.host) {
1087-
previousPendingLoginServer = pendingServer
1088-
return@observe
1089-
}
1105+
// Use the URL to switch between default or Salesforce Welcome Discovery log in, if applicable.
1106+
if (switchDefaultOrSalesforceWelcomeDiscoveryLogin(pendingServerUri)) {
1107+
previousPendingLoginServer = pendingLoginServer
1108+
return@Observer
1109+
}
10901110

1091-
// Use the URL to switch between default or Salesforce Welcome Discovery log in, if applicable.
1092-
if (switchDefaultOrSalesforceWelcomeDiscoveryLogin(pendingServerUri)) {
1093-
previousPendingLoginServer = pendingServer
1094-
return@observe
1095-
}
1111+
applyPendingServer(pendingLoginServer)
1112+
}
1113+
1114+
/**
1115+
* Applies a new pending login server. The decision to authenticate in a
1116+
* web browser-custom tab will be made, which may require fetching the
1117+
* authentication configuration. The selected server and login URL (OAuth
1118+
* authorization URL) will set to continue the flow.
1119+
*/
1120+
private fun applyPendingServer(pendingLoginServer: String) {
1121+
val sdkManager = SalesforceSDKManager.getInstance()
10961122

1097-
// Recall this pending login server for reference by future updates.
1098-
previousPendingLoginServer = pendingServer
1123+
// Recall this pending login server for reference by future updates.
1124+
previousPendingLoginServer = pendingLoginServer
10991125

1100-
if (viewModel.singleServerCustomTabActivity) {
1101-
// Skip fetching authorization and show custom tab immediately.
1102-
viewModel.selectedServer.value = pendingServer
1103-
viewModel.reloadWebView()
1104-
viewModel.loginUrl.value?.let { url ->
1105-
customTabLauncher?.let { loadLoginPageInCustomTab(url, it) }
1106-
}
1107-
} else {
1108-
with(SalesforceSDKManager.getInstance()) {
1109-
// Fetch the authentication configuration and load the login page in a custom tab if required.
1110-
fetchAuthenticationConfiguration {
1111-
lifecycleScope.launch(Main) {
1112-
viewModel.selectedServer.value = pendingServer
1113-
/* Browser-based authentication is applicable when not authenticating with a front-door bridge URL */
1114-
if (isBrowserLoginEnabled && !viewModel.isUsingFrontDoorBridge) {
1115-
if (useWebServerAuthentication) {
1116-
viewModel.loginUrl.value?.let { url ->
1117-
customTabLauncher?.let { loadLoginPageInCustomTab(url, it) }
1118-
}
1119-
} else {
1120-
/* Reload the webview now that isBrowserLoginEnabled has been set
1121-
to true so that we generate an authorization URL with PKCE values. */
1122-
viewModel.reloadWebView()
1123-
viewModel.loginUrl.value?.let { url -> customTabLauncher?.let { loadLoginPageInCustomTab(url, it) } }
1124-
}
1125-
}
1126-
}
1127-
}
1128-
}
1126+
// When authorization via a single-server, custom tab activity is requested skip fetching the authorization configuration and immediately set the selected login server to generate the OAuth authorization URL.
1127+
if (viewModel.singleServerCustomTabActivity) {
1128+
viewModel.selectedServer.postValue(pendingLoginServer)
1129+
}
1130+
// Fetch the pending login server's authentication configuration to set the selected login server and OAuth authorization URL.
1131+
else {
1132+
authenticationConfigurationFetchJob?.cancel()
1133+
authenticationConfigurationFetchJob = sdkManager.fetchAuthenticationConfiguration {
1134+
viewModel.selectedServer.postValue(pendingLoginServer)
1135+
authenticationConfigurationFetchJob = null
11291136
}
11301137
}
11311138
}
11321139

1140+
/**
1141+
* Starts a browser custom tab for the OAuth authorization URL according to
1142+
* the authentication configuration. The activity only takes action when
1143+
* browser-based authentication requires a browser custom tab to be started.
1144+
* @param sdkManager The Salesforce SDK Manager with the selected login
1145+
* server's authentication configuration
1146+
* @param authorizationUrl The selected login server's OAuth authorization
1147+
* URL
1148+
* @param activityResultLauncher The activity result launcher to use when
1149+
* browser-based authentication requires a browser custom tab.
1150+
*/
1151+
private fun startBrowserCustomTabAuthorization(
1152+
sdkManager: SalesforceSDKManager,
1153+
authorizationUrl: String,
1154+
activityResultLauncher: ActivityResultLauncher<Intent>,
1155+
) {
1156+
// Load the authorization URL in a browser custom tab if required and do nothing otherwise as the view model will load it in the web view.
1157+
if ((viewModel.singleServerCustomTabActivity || sdkManager.isBrowserLoginEnabled) && !viewModel.isUsingFrontDoorBridge /* UI front-door bridge bypasses the need for browser custom tab */) {
1158+
loadLoginPageInCustomTab(authorizationUrl, activityResultLauncher)
1159+
}
1160+
}
1161+
11331162
/**
11341163
* A web view client which intercepts the redirect to the OAuth callback URL. That redirect marks the end of
11351164
* the user facing portion of the authentication flow.

libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/LoginViewModel.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() {
109109
// Public LiveData
110110
/** The login server that has been selected by the login server manager, has an authentication configuration and is ready for use */
111111
val selectedServer = MediatorLiveData<String>()
112+
/** The selected login server's OAuth authorization URL */
112113
val loginUrl = MediatorLiveData<String>()
114+
/** The URL to be displayed in the web view. This is the `loginUrl` value when the web view is in use */
115+
val webViewUrl = MediatorLiveData<String>()
113116
var showServerPicker = mutableStateOf(false)
114117
var loading = mutableStateOf(false)
115118

@@ -214,7 +217,7 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() {
214217
pendingServer.addSource(SalesforceSDKManager.getInstance().loginServerManager.selectedServer) { newServer ->
215218
val trimmedServer = newServer.url.run { trim { it <= ' ' } }
216219
if (pendingServer.value == trimmedServer) {
217-
reloadWebView() // TODO: Review the need for this and its use prior to fetching the authentication configuration. ECJ20251201
220+
reloadWebView()
218221
} else {
219222
pendingServer.value = trimmedServer
220223
}
@@ -223,11 +226,18 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() {
223226
// Update loginUrl when selectedServer updates so webview automatically reloads
224227
loginUrl.addSource(selectedServer) { newServer ->
225228
val isNewServer = loginUrl.value?.startsWith(newServer) != true
226-
// Note the web view does not reload when browser-based authentication or a UI Bridge API front door bridge URL are active.
227-
if (isNewServer && !SalesforceSDKManager.getInstance().isBrowserLoginEnabled && !isUsingFrontDoorBridge) {
229+
if (isNewServer) {
228230
loginUrl.value = getAuthorizationUrl(newServer)
229231
}
230232
}
233+
234+
// Update the web view URL to match the OAuth authorization URL when the web view is applicable
235+
webViewUrl.addSource(loginUrl) { newLoginUrl ->
236+
// Note the web view does not reload when browser-based authentication or a UI Bridge API front door bridge URL are active.
237+
if ((!SalesforceSDKManager.getInstance().isBrowserLoginEnabled && !isUsingFrontDoorBridge) || newLoginUrl == "about:blank" /* Blank is used during reset states such as when returning to the web view from a custom tab, so it's always eligible */) {
238+
webViewUrl.value = newLoginUrl
239+
}
240+
}
231241
}
232242

233243
/** Reloads the WebView with a newly generated authorization URL. */

libs/SalesforceSDK/src/com/salesforce/androidsdk/ui/components/LoginView.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ fun LoginView() {
197197

198198
LoginView(
199199
dynamicBackgroundColor = viewModel.dynamicBackgroundColor,
200-
loginUrlData = viewModel.loginUrl,
200+
loginUrlData = viewModel.webViewUrl,
201201
topAppBar = topAppBar,
202202
webView = activity.webView,
203203
loading = viewModel.loading.value,

0 commit comments

Comments
 (0)