Skip to content

Commit a97de73

Browse files
@W-17449395: [Android] Welcome login email deeplinking support in MSDK 13.1 (#2715)
1 parent 95986e0 commit a97de73

File tree

12 files changed

+546
-58
lines changed

12 files changed

+546
-58
lines changed

libs/SalesforceHybrid/src/com/salesforce/androidsdk/phonegap/ui/SalesforceDroidGapActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import com.salesforce.androidsdk.config.BootConfig.isAbsoluteUrl
4343
import com.salesforce.androidsdk.config.BootConfig.validateBootConfig
4444
import com.salesforce.androidsdk.config.LoginServerManager.PRODUCTION_LOGIN_URL
4545
import com.salesforce.androidsdk.config.LoginServerManager.SANDBOX_LOGIN_URL
46+
import com.salesforce.androidsdk.config.LoginServerManager.WELCOME_LOGIN_URL
4647
import com.salesforce.androidsdk.phonegap.app.SalesforceHybridSDKManager
4748
import com.salesforce.androidsdk.phonegap.ui.SalesforceWebViewClientHelper.getAppHomeUrl
4849
import com.salesforce.androidsdk.phonegap.ui.SalesforceWebViewClientHelper.hasCachedAppHome
@@ -573,7 +574,7 @@ open class SalesforceDroidGapActivity : CordovaActivity(), SalesforceActivityInt
573574
?.url
574575
?.trim { it <= ' ' } ?: return@withTimeout
575576

576-
if (loginServer == PRODUCTION_LOGIN_URL || loginServer == SANDBOX_LOGIN_URL || !isHttpsUrl(loginServer) || loginServer.toHttpUrlOrNull() == null) {
577+
if (loginServer == PRODUCTION_LOGIN_URL || loginServer == WELCOME_LOGIN_URL || loginServer == SANDBOX_LOGIN_URL || !isHttpsUrl(loginServer) || loginServer.toHttpUrlOrNull() == null) {
577578
return@withTimeout
578579
}
579580

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<servers>
33
<server name="Production" url="https://login.salesforce.com" />
4+
<server name="Welcome" url="https://welcome.salesforce.com" />
45
<server name="Sandbox" url="https://test.salesforce.com" />
56
</servers>

libs/SalesforceSDK/src/com/salesforce/androidsdk/app/SalesforceSDKManager.kt

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ import android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
5757
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
5858
import android.webkit.CookieManager
5959
import android.webkit.URLUtil.isHttpsUrl
60+
import androidx.annotation.VisibleForTesting
61+
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
6062
import androidx.compose.material3.ColorScheme
6163
import androidx.compose.runtime.Composable
6264
import androidx.core.content.ContextCompat.RECEIVER_EXPORTED
@@ -104,6 +106,7 @@ import com.salesforce.androidsdk.config.BootConfig.getBootConfig
104106
import com.salesforce.androidsdk.config.LoginServerManager
105107
import com.salesforce.androidsdk.config.LoginServerManager.PRODUCTION_LOGIN_URL
106108
import com.salesforce.androidsdk.config.LoginServerManager.SANDBOX_LOGIN_URL
109+
import com.salesforce.androidsdk.config.LoginServerManager.WELCOME_LOGIN_URL
107110
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.IDPAppPackageName
108111
import com.salesforce.androidsdk.config.RuntimeConfig.getRuntimeConfig
109112
import com.salesforce.androidsdk.developer.support.notifications.local.ShowDeveloperSupportNotifier.Companion.BROADCAST_INTENT_ACTION_SHOW_DEVELOPER_SUPPORT
@@ -142,7 +145,7 @@ import kotlinx.coroutines.CoroutineScope
142145
import kotlinx.coroutines.Dispatchers.Default
143146
import kotlinx.coroutines.Dispatchers.Main
144147
import kotlinx.coroutines.launch
145-
import kotlinx.coroutines.withTimeout
148+
import kotlinx.coroutines.withTimeoutOrNull
146149
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
147150
import org.json.JSONObject
148151
import java.lang.String.CASE_INSENSITIVE_ORDER
@@ -339,11 +342,13 @@ open class SalesforceSDKManager protected constructor(
339342
*/
340343
@set:Synchronized
341344
open var isBrowserLoginEnabled = false
342-
protected set
345+
@VisibleForTesting(otherwise = PROTECTED)
346+
set
343347

344348
/** Optionally enables browser session sharing */
345349
var isShareBrowserSessionEnabled = false
346-
private set
350+
@VisibleForTesting
351+
set
347352

348353
/**
349354
* The custom tab browser to use during advanced authentication.
@@ -1900,36 +1905,35 @@ open class SalesforceSDKManager protected constructor(
19001905
/**
19011906
* Fetches the authentication configuration, if required.
19021907
*
1908+
* @param httpAccess The HTTP access to use for API integration. Defaults
1909+
* to null to use the default HTTP access. This parameter is intended for
1910+
* testing purposes only and should not be used in release builds.
19031911
* @param completion An optional function to invoke at the end of the action
19041912
*/
1905-
fun fetchAuthenticationConfiguration(
1906-
completion: (() -> Unit)? = null
1913+
internal fun fetchAuthenticationConfiguration(
1914+
httpAccess: HttpAccess? = null,
1915+
completion: (() -> Unit),
19071916
) = CoroutineScope(Default).launch {
1908-
runCatching {
1909-
// If this takes more than five seconds it can cause Android's application not responding report.
1910-
withTimeout(5000L) {
1911-
val loginServer = loginServerManager.selectedLoginServer?.url?.trim { it <= ' ' } ?: return@withTimeout
1912-
1913-
if (loginServer == PRODUCTION_LOGIN_URL || loginServer == SANDBOX_LOGIN_URL || !isHttpsUrl(loginServer) || loginServer.toHttpUrlOrNull() == null) {
1914-
setBrowserLoginEnabled(
1915-
browserLoginEnabled = false,
1916-
shareBrowserSessionEnabled = false
1917-
)
1917+
// If this takes more than five seconds it can cause Android's application not responding report.
1918+
withTimeoutOrNull(5000L) {
1919+
val loginServer = loginServerManager.selectedLoginServer.url.trim()
1920+
if (loginServer == PRODUCTION_LOGIN_URL || loginServer == WELCOME_LOGIN_URL || loginServer == SANDBOX_LOGIN_URL || !isHttpsUrl(loginServer) || loginServer.toHttpUrlOrNull() == null) {
1921+
setBrowserLoginEnabled(
1922+
browserLoginEnabled = false,
1923+
shareBrowserSessionEnabled = false
1924+
)
19181925

1919-
return@withTimeout
1920-
}
1926+
return@withTimeoutOrNull
1927+
}
19211928

1922-
getMyDomainAuthConfig(loginServer).let { authConfig ->
1923-
setBrowserLoginEnabled(
1924-
browserLoginEnabled = authConfig?.isBrowserLoginEnabled ?: false,
1925-
shareBrowserSessionEnabled = authConfig?.isShareBrowserSessionEnabled ?: false
1926-
)
1927-
}
1929+
getMyDomainAuthConfig(httpAccess, loginServer).let { authConfig ->
1930+
setBrowserLoginEnabled(
1931+
browserLoginEnabled = authConfig?.isBrowserLoginEnabled ?: false,
1932+
shareBrowserSessionEnabled = authConfig?.isShareBrowserSessionEnabled ?: false
1933+
)
19281934
}
1929-
}.onFailure { e ->
1930-
e(TAG, "Exception occurred while fetching authentication configuration", e)
19311935
}
19321936

1933-
completion?.invoke()
1937+
completion.invoke()
19341938
}
19351939
}

libs/SalesforceSDK/src/com/salesforce/androidsdk/auth/OAuth2.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public class OAuth2 {
104104
private static final String BIOMETRIC_AUTHENTICATION_TIMEOUT = "BIOMETRIC_AUTHENTICATION_TIMEOUT";
105105
private static final int BIOMETRIC_AUTHENTICATION_DEFAULT_TIMEOUT = 15;
106106
private static final String HYBRID_REFRESH = "hybrid_refresh"; // Grant Type Values
107+
public static final String LOGIN_HINT = "login_hint";
107108
private static final String REFRESH_TOKEN = "refresh_token"; // Grant Type Values
108109
protected static final String RESPONSE_TYPE = "response_type";
109110
private static final String SCOPE = "scope";
@@ -235,6 +236,49 @@ public String toString() {
235236
}
236237
}
237238

239+
/**
240+
* Builds the URL to the authorization web page for this login server.
241+
* You need not provide the 'refresh_token' scope, as it is provided automatically.
242+
*
243+
* This overload defaults `loginHint` to null and does not enable Salesforce Welcome Login hint.
244+
*
245+
* @param useWebServerAuthentication True to use web server flow, False to use user agent flow
246+
* @param useHybridAuthentication True to use "hybrid" flow
247+
* @param loginServer Base protocol and server to use (e.g. https://login.salesforce.com).
248+
* @param clientId OAuth client ID.
249+
* @param callbackUrl OAuth callback URL or redirect URL.
250+
* @param scopes A list of OAuth scopes to request (e.g. {"visualforce", "api"}). If null,
251+
* the default OAuth scope is provided.
252+
* @param displayType OAuth display type. If null, the default of 'touch' is used.
253+
* @param codeChallenge Code challenge to use when using web server flow
254+
* @param addlParams Any additional parameters that may be added to the request.
255+
* @return A URL to start the OAuth flow in a web browser/view.
256+
* @see <a href="https://help.salesforce.com/apex/HTViewHelpDoc?language=en&id=remoteaccess_oauth_scopes.htm">RemoteAccess OAuth Scopes</a>
257+
*/
258+
public static URI getAuthorizationUrl(
259+
boolean useWebServerAuthentication,
260+
boolean useHybridAuthentication,
261+
URI loginServer,
262+
String clientId,
263+
String callbackUrl,
264+
String[] scopes,
265+
String displayType,
266+
String codeChallenge,
267+
Map<String, String> addlParams) {
268+
return getAuthorizationUrl(
269+
useWebServerAuthentication,
270+
useHybridAuthentication,
271+
loginServer,
272+
clientId,
273+
callbackUrl,
274+
scopes,
275+
null,
276+
displayType,
277+
codeChallenge,
278+
addlParams
279+
);
280+
}
281+
238282
/**
239283
* Builds the URL to the authorization web page for this login server.
240284
* You need not provide the 'refresh_token' scope, as it is provided automatically.
@@ -246,6 +290,7 @@ public String toString() {
246290
* @param callbackUrl OAuth callback URL or redirect URL.
247291
* @param scopes A list of OAuth scopes to request (e.g. {"visualforce", "api"}). If null,
248292
* the default OAuth scope is provided.
293+
* @param loginHint When applicable, the Salesforce Welcome Login hint
249294
* @param displayType OAuth display type. If null, the default of 'touch' is used.
250295
* @param codeChallenge Code challenge to use when using web server flow
251296
* @param addlParams Any additional parameters that may be added to the request.
@@ -259,6 +304,7 @@ public static URI getAuthorizationUrl(
259304
String clientId,
260305
String callbackUrl,
261306
String[] scopes,
307+
String loginHint,
262308
String displayType,
263309
String codeChallenge,
264310
Map<String,String> addlParams) {
@@ -273,6 +319,9 @@ public static URI getAuthorizationUrl(
273319
if (scopes != null && scopes.length > 0) {
274320
sb.append(AND).append(SCOPE).append(EQUAL).append(Uri.encode(computeScopeParameter(scopes)));
275321
}
322+
if (!TextUtils.isEmpty(loginHint)) {
323+
sb.append(AND).append(LOGIN_HINT).append(EQUAL).append(Uri.encode(loginHint));
324+
}
276325
sb.append(AND).append(REDIRECT_URI).append(EQUAL).append(callbackUrl);
277326
sb.append(AND).append(DEVICE_ID).append(EQUAL).append(SalesforceSDKManager.getInstance().getDeviceId());
278327
if (useWebServerAuthentication) {

libs/SalesforceSDK/src/com/salesforce/androidsdk/config/LoginServerManager.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public class LoginServerManager {
6161

6262
// Default login servers.
6363
public static final String PRODUCTION_LOGIN_URL = "https://login.salesforce.com";
64+
public static final String WELCOME_LOGIN_URL = "https://welcome.salesforce.com";
6465
public static final String SANDBOX_LOGIN_URL = "https://test.salesforce.com";
6566

6667
// Keys used in shared preferences.
@@ -457,7 +458,7 @@ public static class LoginServer {
457458
* @param url Server URL.
458459
* @param isCustom True - if custom URL, False - otherwise.
459460
*/
460-
public LoginServer(String name, String url, boolean isCustom) {
461+
public LoginServer(@NonNull String name, @NonNull String url, boolean isCustom) {
461462
this.name = name;
462463
this.url = url;
463464
this.isCustom = isCustom;

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

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ import androidx.activity.result.ActivityResult
7474
import androidx.activity.result.ActivityResultLauncher
7575
import androidx.activity.result.contract.ActivityResultContracts
7676
import androidx.activity.viewModels
77+
import androidx.annotation.VisibleForTesting
78+
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
7779
import androidx.biometric.BiometricManager
7880
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
7981
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK
@@ -126,6 +128,7 @@ import com.salesforce.androidsdk.auth.OAuth2.TokenEndpointResponse
126128
import com.salesforce.androidsdk.auth.OAuth2.swapJWTForTokens
127129
import com.salesforce.androidsdk.auth.idp.interfaces.SPManager.Status
128130
import com.salesforce.androidsdk.auth.idp.interfaces.SPManager.StatusUpdateCallback
131+
import com.salesforce.androidsdk.config.LoginServerManager.LoginServer
129132
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.ManagedAppCertAlias
130133
import com.salesforce.androidsdk.config.RuntimeConfig.ConfigKey.RequireCertAuth
131134
import com.salesforce.androidsdk.config.RuntimeConfig.getRuntimeConfig
@@ -157,7 +160,8 @@ import java.security.cert.X509Certificate
157160
*/
158161
open class LoginActivity : FragmentActivity() {
159162
// View Model
160-
protected open val viewModel: LoginViewModel
163+
@VisibleForTesting(otherwise = PROTECTED)
164+
open val viewModel: LoginViewModel
161165
by viewModels { SalesforceSDKManager.getInstance().loginViewModelFactory }
162166

163167
// Webview and Clients
@@ -195,6 +199,9 @@ open class LoginActivity : FragmentActivity() {
195199
SalesforceSDKManager.getInstance().setViewNavigationVisibility(this)
196200
}
197201

202+
// Set the Salesforce Welcome Login hint and host for the OAuth authorize URL, if applicable.
203+
useLoginHint(intent)
204+
198205
/*
199206
* For Salesforce Identity API UI Bridge support, the overriding
200207
* frontdoor bridge URL to use in place of the default initial login URL
@@ -435,7 +442,27 @@ open class LoginActivity : FragmentActivity() {
435442
}
436443

437444
// endregion
445+
// region Salesforce Welcome Login Private Implementation
438446

447+
/**
448+
* Uses the Salesforce Welcome login hint and host in the Intent, if
449+
* applicable.
450+
*/
451+
private fun useLoginHint(intent: Intent) {
452+
453+
viewModel.loginHint = intent.getStringExtra(EXTRA_KEY_LOGIN_HINT)
454+
intent.getStringExtra(EXTRA_KEY_LOGIN_HOST)?.let { loginHost ->
455+
SalesforceSDKManager.getInstance().loginServerManager.setSelectedLoginServer(
456+
LoginServer(
457+
loginHost,
458+
"https://$loginHost",
459+
true
460+
)
461+
)
462+
}
463+
}
464+
465+
// endregion
439466
// End of Public Functions
440467

441468
protected open fun certAuthOrLogin() {
@@ -1002,6 +1029,15 @@ open class LoginActivity : FragmentActivity() {
10021029
private const val BACKGROUND_COLOR_JAVASCRIPT =
10031030
"(function() { return window.getComputedStyle(document.body, null).getPropertyValue('background-color'); })();"
10041031

1032+
// endregion
1033+
// region Log In With Login Hint Public Implementation
1034+
1035+
/** Intent extra key for login hint value */
1036+
const val EXTRA_KEY_LOGIN_HINT = "login_hint"
1037+
1038+
/** Intent extra key for login host to use with login hint */
1039+
const val EXTRA_KEY_LOGIN_HOST = "login_host"
1040+
10051041
// endregion
10061042
// region QR Code Login Via Salesforce Identity API UI Bridge Public Implementation
10071043

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() {
163163
@VisibleForTesting
164164
internal var codeVerifier: String? = null
165165

166+
/** The Salesforce Welcome Login hint parameter value for the OAuth authorize endpoint */
167+
internal var loginHint: String? = null
168+
166169
// Auth code we receive from the JWT swap for magic links.
167170
internal var authCodeForJwtFlow: String? = null
168171

@@ -317,6 +320,7 @@ open class LoginViewModel(val bootConfig: BootConfig) : ViewModel() {
317320
clientId,
318321
bootConfig.oauthRedirectURI,
319322
bootConfig.oauthScopes,
323+
loginHint,
320324
authorizationDisplayType,
321325
codeChallenge,
322326
additionalParams

libs/SalesforceSDK/src/com/salesforce/androidsdk/util/AuthConfigUtil.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import android.content.Intent;
3030
import android.text.TextUtils;
3131

32+
import androidx.annotation.Nullable;
33+
3234
import com.salesforce.androidsdk.app.SalesforceSDKManager;
3335
import com.salesforce.androidsdk.auth.HttpAccess;
3436
import com.salesforce.androidsdk.rest.RestResponse;
@@ -63,7 +65,26 @@ public class AuthConfigUtil {
6365
* @param loginUrl Login URL.
6466
* @return Auth config.
6567
*/
66-
public static MyDomainAuthConfig getMyDomainAuthConfig(String loginUrl) {
68+
public static MyDomainAuthConfig getMyDomainAuthConfig(
69+
String loginUrl
70+
) {
71+
return getMyDomainAuthConfig(null, loginUrl);
72+
}
73+
74+
/**
75+
* Returns the auth config associated with a my domain login endpoint. This call
76+
* should be made from a background thread since it makes a network request.
77+
*
78+
* @param httpAccess The HTTP access to use for API integration. Defaults
79+
* to null to use the default HTTP access. This parameter is intended for
80+
* testing purposes only and should not be used in release builds.
81+
* @param loginUrl Login URL.
82+
* @return Auth config.
83+
*/
84+
public static MyDomainAuthConfig getMyDomainAuthConfig(
85+
@Nullable HttpAccess httpAccess,
86+
String loginUrl
87+
) {
6788
if (TextUtils.isEmpty(loginUrl)) {
6889
return null;
6990
}
@@ -74,7 +95,8 @@ public static MyDomainAuthConfig getMyDomainAuthConfig(String loginUrl) {
7495
final String authConfigUrl = loginUrl + MY_DOMAIN_AUTH_CONFIG_ENDPOINT;
7596
final Request request = new Request.Builder().url(authConfigUrl).get().build();
7697
try {
77-
final Response response = HttpAccess.DEFAULT.getOkHttpClient().newCall(request).execute();
98+
final HttpAccess httpAccessResolved = httpAccess != null ? httpAccess : HttpAccess.DEFAULT;
99+
final Response response = httpAccessResolved.getOkHttpClient().newCall(request).execute();
78100
if (response.isSuccessful()) {
79101
authConfig = new MyDomainAuthConfig((new RestResponse(response)).asJSONObject());
80102
}

0 commit comments

Comments
 (0)