@@ -116,6 +116,8 @@ import com.salesforce.androidsdk.util.UriFragmentParser.parse
116116import kotlinx.coroutines.CoroutineScope
117117import kotlinx.coroutines.Dispatchers.IO
118118import kotlinx.coroutines.launch
119+ import org.json.JSONObject
120+ import java.net.URLDecoder
119121import com.salesforce.androidsdk.R.layout.sf__login as sf__login_layout
120122import com.salesforce.androidsdk.R.menu.sf__login as sf__login_menu
121123
@@ -142,6 +144,9 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
142144
143145 val salesforceSDKManager = SalesforceSDKManager .getInstance()
144146
147+ // Determine if the activity was created from a deep link intent with QR code log in via UI bridge API parameters.
148+ val isDeepLinkedQrCodeLogin = isDeepLinkedQrCodeLogin(intent)
149+
145150 accountAuthenticatorResponse = intent.getParcelableExtra<AccountAuthenticatorResponse ?>(
146151 KEY_ACCOUNT_AUTHENTICATOR_RESPONSE
147152 )?.apply {
@@ -157,14 +162,19 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
157162
158163 salesforceSDKManager.setViewNavigationVisibility(this )
159164
160- // Get login options from the intent's extras
161- val loginOptions = fromBundleWithSafeLoginUrl(intent.extras)
165+ // Determine login options for QR code login or the app's usual login.
166+ val loginOptions = when {
167+ isDeepLinkedQrCodeLogin -> salesforceSDKManager.loginOptions
168+ else -> fromBundleWithSafeLoginUrl(intent.extras)
169+ }
162170
163171 // Protect against screenshots
164172 window.setFlags(FLAG_SECURE , FLAG_SECURE )
165173
166- // Fetch authentication configuration if required
167- salesforceSDKManager.fetchAuthenticationConfiguration()
174+ // Fetch authentication configuration except for QR code login.
175+ if (! isDeepLinkedQrCodeLogin) {
176+ salesforceSDKManager.fetchAuthenticationConfiguration()
177+ }
168178
169179 // Setup content view
170180 setContentView(sf__login_layout)
@@ -207,7 +217,13 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
207217 LoginActivityCreateComplete ,
208218 this
209219 )
210- certAuthOrLogin()
220+
221+ // Prompt user with log in page or log in via other configurations such as QR code.
222+ when {
223+ isDeepLinkedQrCodeLogin -> loginFromQrCode(" ?" + intent.data?.query)
224+ else -> certAuthOrLogin()
225+ }
226+
211227 if (! receiverRegistered) {
212228 authConfigReceiver = AuthConfigReceiver ().also { changeServerReceiver ->
213229 registerReceiver(
@@ -344,9 +360,9 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
344360 wasBackgrounded = false
345361 }
346362
347- public override fun onSaveInstanceState (bundle : Bundle ) {
348- super .onSaveInstanceState(bundle )
349- webviewHelper?.saveState(bundle )
363+ public override fun onSaveInstanceState (outState : Bundle ) {
364+ super .onSaveInstanceState(outState )
365+ webviewHelper?.saveState(outState )
350366 }
351367
352368 override fun onKeyDown (keyCode : Int , event : KeyEvent ) =
@@ -653,9 +669,147 @@ open class LoginActivity : AppCompatActivity(), OAuthWebviewHelperEvents {
653669
654670 open fun onBioAuthClick (view : View ? ) = presentBiometric()
655671
672+ // region QR Code Login Via UI Bridge API Public Implementation
673+
674+ /* *
675+ * Automatically log in with a UI Bridge API login QR code.
676+ * @param loginQrCodeContent The login QR code content. This should be
677+ * either a URL or URL query containing the UI Bridge API JSON parameter.
678+ * The UI Bridge API JSON parameter should contain URL-encoded JSON with two
679+ * values:
680+ * - frontdoor_bridge_url
681+ * - pkce_code_verifier
682+ *
683+ * If pkce_code_verifier is not specified then the user agent flow is used
684+ * @return Boolean true if a log in attempt is possible using the provided QR
685+ * code content, false otherwise
686+ */
687+ fun loginFromQrCode (
688+ loginQrCodeContent : String?
689+ ) = uiBridgeApiParametersFromLoginQrCodeContent(
690+ loginQrCodeContent
691+ )?.let { uiBridgeApiParameters ->
692+ loginWithFrontdoorBridgeUrl(
693+ uiBridgeApiParameters.frontdoorBridgeUrl,
694+ uiBridgeApiParameters.pkceCodeVerifier
695+ )
696+ true
697+ } ? : false
698+
699+ /* *
700+ * Automatically log in with a UI Bridge API front door bridge URL and PKCE
701+ * code verifier.
702+ * @param frontdoorBridgeUrl The UI Bridge API front door bridge URL
703+ * @param pkceCodeVerifier The PKCE code verifier
704+ */
705+ @Suppress(" MemberVisibilityCanBePrivate" )
706+ fun loginWithFrontdoorBridgeUrl (
707+ frontdoorBridgeUrl : String ,
708+ pkceCodeVerifier : String?
709+ ) = webviewHelper?.loginWithFrontdoorBridgeUrl(frontdoorBridgeUrl, pkceCodeVerifier)
710+
711+ // endregion
712+ // region QR Code Login Via UI Bridge API Private Implementation
713+
714+ /* *
715+ * Determines if QR code login is enabled for the provided intent.
716+ * @param intent The intent to determine QR code login enablement for
717+ * @return Boolean true if QR code login is enabled for the the intent or
718+ * false otherwise
719+ */
720+ private fun isDeepLinkedQrCodeLogin (
721+ intent : Intent
722+ ) = SalesforceSDKManager .getInstance().isQrCodeLoginEnabled
723+ && intent.data?.path?.contains(LOGIN_QR_PATH ) == true
724+
725+ /* *
726+ * Parses UI Bridge API parameters from the provided login QR code content.
727+ * @param loginQrCodeContent The login QR code content string
728+ * @return UiBridgeApiParameters: The UI Bridge API parameters or null if the QR code
729+ * content cannot provide them for any reason
730+ */
731+ private fun uiBridgeApiParametersFromLoginQrCodeContent (
732+ loginQrCodeContent : String?
733+ ) = loginQrCodeContent?.let { loginQrCodeContentUnwrapped ->
734+ uiBridgeApiJsonFromQrCodeContent(loginQrCodeContentUnwrapped)?.let { uiBridgeApiJson ->
735+ uiBridgeApiParametersFromUiBridgeApiJson(uiBridgeApiJson)
736+ }
737+ }
738+
739+ /* *
740+ * Parses UI Bridge API parameters JSON from the provided string, which may
741+ * be formatted to match either QR code content provided by the intent or
742+ * the app's QR code library.
743+ *
744+ * 1. From intent (external QR reader): ?bridgeJson={...}
745+ * 2. From the app's QR reader: ?bridgeJson=%7B...%7D
746+ *
747+ * @param qrCodeContent The QR code content string
748+ * @return String: The UI Bridge API parameter JSON or null if the string
749+ * cannot provide the JSON for any reason
750+ */
751+ private fun uiBridgeApiJsonFromQrCodeContent (
752+ qrCodeContent : String
753+ ) = qrCodeBridgeJsonRegexExternal.find(qrCodeContent)?.groups?.get(1 )?.value
754+ ? : qrCodeBridgeJsonRegexInternal.find(qrCodeContent)?.groups?.get(1 )?.value?.let {
755+ URLDecoder .decode(it, " UTF-8" )
756+ }
757+
758+ /* *
759+ * Creates UI Bridge API parameters from the provided JSON string.
760+ * @param uiBridgeApiParameterJsonString The UI Bridge API parameters JSON
761+ * string
762+ * @return The UI Bridge API parameters
763+ */
764+ private fun uiBridgeApiParametersFromUiBridgeApiJson (
765+ uiBridgeApiParameterJsonString : String
766+ ) = JSONObject (uiBridgeApiParameterJsonString).let { uiBridgeApiParameterJson ->
767+ UiBridgeApiParameters (
768+ uiBridgeApiParameterJson.getString(FRONTDOOR_BRIDGE_URL_KEY ),
769+ when (uiBridgeApiParameterJson.has(PKCE_CODE_VERIFIER_KEY )) {
770+ true -> uiBridgeApiParameterJson.optString(PKCE_CODE_VERIFIER_KEY )
771+ else -> null
772+ }
773+ )
774+ }
775+
776+ /* *
777+ * A data class representing UI Bridge API parameters provided by a login QR
778+ * code.
779+ */
780+ private data class UiBridgeApiParameters (
781+
782+ /* * The front door bridge URL provided by the login QR code */
783+ val frontdoorBridgeUrl : String ,
784+
785+ /* * The PKCE code verifier provided by the login QR code */
786+ val pkceCodeVerifier : String?
787+ )
788+
789+ // endregion
790+
656791 companion object {
657792 const val PICK_SERVER_REQUEST_CODE = 10
658793 private const val SETUP_REQUEST_CODE = 72
659794 private const val TAG = " LoginActivity"
795+
796+ // region QR Code Login Via UI Bridge API Constants
797+
798+ /* * The QR code login intent path */
799+ private const val LOGIN_QR_PATH = " /login/qr"
800+
801+ /* * The login QR code's UI Bridge API parameter's JSON frontdoor bridge URL key */
802+ private const val FRONTDOOR_BRIDGE_URL_KEY = " frontdoor_bridge_url"
803+
804+ /* * The login QR code's UI Bridge API parameter's JSON PKCE code verifier key */
805+ private const val PKCE_CODE_VERIFIER_KEY = " pkce_code_verifier"
806+
807+ /* * A regular expression to extract the UI Bridge API parameter JSON from the intent's login QR code content */
808+ private val qrCodeBridgeJsonRegexExternal by lazy { """ \?bridgeJson=(\{.*\})""" .toRegex() }
809+
810+ /* * A regular expression to extract the UI Bridge API parameter JSON from the app's login QR code content */
811+ private val qrCodeBridgeJsonRegexInternal by lazy { """ \?bridgeJson=(%7B.*%7D)""" .toRegex() }
812+
813+ // endregion
660814 }
661815}
0 commit comments