Repository: wordpress-mobile/WordPress-Android
Pull request #21855: Show password login webview and prepare internal architecture
Author: login: adalpari
Labels: name: [Type] Task
Created: 2025-05-05T13:04:03Z | Merged: 2025-05-09T14:41:15Z

## Description

This PR is adding a new step for the Application Password login in the `LoginActivity`. See the previous discarded place for this logic: https://github.com/wordpress-mobile/WordPress-Android/pull/21852
In this new iteration we are showing to the user the authentication webview and intercepting the callback url. NOTE: the callback is yet not handled. That will be done in the next iteration: handling it and storing it.

## Testing instructions

1. Run the app and test BOTH login flow to be sure nothing is broken. They should work as always.
2. Open the project and, in `LoginActivity`, uncomment line 705
<img width="1277" alt="Screenshot 2025-05-05 at 15 03 08" src="https://github.com/user-attachments/assets/5911a04c-3f1b-4a59-ad83-26c7860a2ba6" />
3. Run the app again. And login in a self-hosted site

- [ ] Verify you see a log like: `Found authorization for https://vanilla.wpmt.co URL: https://vanilla.wpmt.co/wp-admin/authorize-application.php?app_name=android-jetpack-client&success_url=jetpack%3A%2F%2Fwpcom-authorize`

- [ ] Verify you are redirected to the authentication webview

5. Approve the connection

- [ ] Verify you are redirected to the login screen (that's enough for now)


https://github.com/user-attachments/assets/a13a1ee7-6262-4ce4-8799-19a5732509f6



<!-

-

Consider testing the following *yourself* before requesting a review:

- Compatibility with WordPress.com sites and self-hosted Jetpack sites
- Both portrait and landscape orientations
- Light and dark modes
- Dynamic font sizes: larger, smaller and bold text
- High contrast mode
- Screen reader experience (e.g., Talkback)
- Languages with large words or with letters/accents not frequently used in English
- Right-to-left languages layout
- Multi-tasking: split view and slide over

-->



Diff:
diff --git a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
index 1d4b8530b576..9989e37ba302 100644
--- a/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
+++ b/WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java
@@ -3,7 +3,6 @@
 import androidx.lifecycle.ViewModel;
 import androidx.lifecycle.ViewModelProvider;
 
-import org.wordpress.android.login.viewmodel.LoginSiteAddressViewModel;
 import org.wordpress.android.ui.accounts.LoginEpilogueViewModel;
 import org.wordpress.android.ui.accounts.LoginViewModel;
 import org.wordpress.android.ui.activitylog.list.filter.ActivityLogTypeFilterViewModel;
@@ -451,11 +450,6 @@ abstract class ViewModelModule {
     @ViewModelKey(LoginViewModel.class)
     abstract ViewModel loginViewModel(LoginViewModel viewModel);
 
-    @Binds
-    @IntoMap
-    @ViewModelKey(LoginSiteAddressViewModel.class)
-    abstract ViewModel loginSiteAddressViewModel(LoginSiteAddressViewModel viewModel);
-
     @Binds
     @IntoMap
     @ViewModelKey(StorageUtilsViewModel.class)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
index bfe074e7841e..8084553850a8 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
@@ -6,7 +6,6 @@
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
-import android.util.Log;
 import android.view.MenuItem;
 
 import androidx.annotation.NonNull;
@@ -24,7 +23,6 @@
 
 import org.wordpress.android.R;
 import org.wordpress.android.analytics.AnalyticsTracker;
-import org.wordpress.android.analytics.AnalyticsTracker.Stat;
 import org.wordpress.android.fluxc.model.SiteModel;
 import org.wordpress.android.fluxc.network.MemorizingTrustManager;
 import org.wordpress.android.fluxc.store.AccountStore.AuthEmailPayloadScheme;
@@ -481,14 +479,7 @@ public void showWPcomLoginScreen(@NonNull Context context) {
         AnalyticsTracker.track(AnalyticsTracker.Stat.LOGIN_WPCOM_WEBVIEW);
         mUnifiedLoginTracker.setFlowAndStep(Flow.WORDPRESS_COM_WEB, Step.WPCOM_WEB_START);
 
-        CustomTabsIntent intent = new CustomTabsIntent.Builder()
-                .setShareState(CustomTabsIntent.SHARE_STATE_OFF)
-                .setStartAnimations(this, R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left)
-                .setExitAnimations(this, R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right)
-                .setUrlBarHidingEnabled(true)
-                .setInstantAppsEnabled(false)
-                .setShowTitle(false)
-                .build();
+        CustomTabsIntent intent = getCustomTabsIntent();
 
         Uri loginUri = mLoginHelper.getWpcomLoginUri();
         try {
@@ -499,6 +490,17 @@ public void showWPcomLoginScreen(@NonNull Context context) {
         }
     }
 
+    @NonNull private CustomTabsIntent getCustomTabsIntent() {
+        return new CustomTabsIntent.Builder()
+                .setShareState(CustomTabsIntent.SHARE_STATE_OFF)
+                .setStartAnimations(this, R.anim.activity_slide_in_from_right, R.anim.activity_slide_out_to_left)
+                .setExitAnimations(this, R.anim.activity_slide_in_from_left, R.anim.activity_slide_out_to_right)
+                .setUrlBarHidingEnabled(true)
+                .setInstantAppsEnabled(false)
+                .setShowTitle(false)
+                .build();
+    }
+
     @Override
     public void onTermsOfServiceClicked() {
         AnalyticsTracker.track(AnalyticsTracker.Stat.SIGNUP_TERMS_OF_SERVICE_TAPPED);
@@ -699,13 +701,19 @@ public void gotXmlRpcEndpoint(String inputSiteAddress, String endpointAddress) {
         slideInFragment(loginUsernamePasswordFragment, true, LoginUsernamePasswordFragment.TAG);
 
         // In the background, run the API discovery test to see if we can add this site for the REST API
+        String authorizationUrl = mViewModel.runApiDiscovery(inputSiteAddress);
+//            launchApplicationPasswordFlow(authorizationUrl);
+    }
+
+    public void launchApplicationPasswordFlow(@NonNull String endpointAddress) {
+        CustomTabsIntent intent = getCustomTabsIntent();
+
+        Uri loginUri = Uri.parse(endpointAddress);
         try {
-            String authorizationUrl = mViewModel.runApiDiscoveryTest(inputSiteAddress);
-            Log.d("WP_RS", "Found authorization URL: " + authorizationUrl);
-            AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_SUCCESSFUL);
-        } catch (Exception ex) {
-            Log.e("WP_RS", "Unable to find authorization URL:" + ex.getMessage());
-            AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED);
+            intent.launchUrl(this, loginUri);
+        } catch (SecurityException | ActivityNotFoundException e) {
+            AppLog.e(AppLog.T.UTILS, "Error opening login uri in CustomTabsIntent, attempting external browser", e);
+            ActivityLauncher.openUrlExternal(this, loginUri.toString());
         }
     }
 
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginViewModel.kt
index 289192c3338b..8c95c38d916e 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginViewModel.kt
@@ -1,9 +1,11 @@
 package org.wordpress.android.ui.accounts
 
+import android.util.Log
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MediatorLiveData
 import androidx.lifecycle.ViewModel
 import kotlinx.coroutines.runBlocking
+import org.wordpress.android.analytics.AnalyticsTracker
 import org.wordpress.android.fluxc.store.AccountStore.AuthEmailPayloadScheme
 import org.wordpress.android.fluxc.store.SiteStore.ConnectSiteInfoPayload
 import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowNoJetpackSites
@@ -13,8 +15,13 @@ import org.wordpress.android.viewmodel.Event
 import rs.wordpress.api.kotlin.WpLoginClient
 import javax.inject.Inject
 import kotlin.text.RegexOption.IGNORE_CASE
+import org.wordpress.android.ui.accounts.login.WPcomLoginHelper
 
-class LoginViewModel @Inject constructor(private val buildConfigWrapper: BuildConfigWrapper) : ViewModel() {
+class LoginViewModel @Inject constructor(
+    private val buildConfigWrapper: BuildConfigWrapper,
+    private val wpLoginClient: WpLoginClient,
+    private val wpComLoginHelper: WPcomLoginHelper
+) : ViewModel() {
     private val _navigationEvents = MediatorLiveData<Event<LoginNavigationEvents>>()
     val navigationEvents: LiveData<Event<LoginNavigationEvents>> = _navigationEvents
 
@@ -34,8 +41,19 @@ class LoginViewModel @Inject constructor(private val buildConfigWrapper: BuildCo
         AuthEmailPayloadScheme.WORDPRESS
     }
 
-    fun runApiDiscoveryTest(input: String) = runBlocking {
-        val urlDiscovery = WpLoginClient().apiDiscovery(input)
-        urlDiscovery.apiDetails.findApplicationPasswordsAuthenticationUrl()
+    @Suppress("TooGenericExceptionCaught")
+    fun runApiDiscovery(url: String): String = runBlocking {
+        try {
+            val urlDiscovery = wpLoginClient.apiDiscovery(url)
+            val authorizationUrl = urlDiscovery.apiDetails.findApplicationPasswordsAuthenticationUrl()
+            val authorizationUrlComplete = wpComLoginHelper.appendParamsToRestAuthorizationUrl(authorizationUrl)
+            Log.d("WP_RS", "Found authorization for $url URL: $authorizationUrlComplete")
+            AnalyticsTracker.track(AnalyticsTracker.Stat.BACKGROUND_REST_AUTODISCOVERY_SUCCESSFUL)
+            authorizationUrlComplete
+        } catch (throwable: Throwable) {
+            Log.e("WP_RS", "VM: Error during API discovery for $url", throwable)
+            AnalyticsTracker.track(AnalyticsTracker.Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
+            ""
+        }
     }
 }
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/WPcomLoginHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/WPcomLoginHelper.kt
index efbd5c335380..7d38479846f1 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/WPcomLoginHelper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/WPcomLoginHelper.kt
@@ -8,6 +8,7 @@ import androidx.browser.customtabs.CustomTabsCallback
 import androidx.browser.customtabs.CustomTabsClient
 import androidx.browser.customtabs.CustomTabsServiceConnection
 import androidx.browser.customtabs.CustomTabsSession
+import androidx.core.net.toUri
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.runBlocking
@@ -20,7 +21,7 @@ import kotlin.coroutines.CoroutineContext
 class WPcomLoginHelper @Inject constructor(
     private val loginClient: WPcomLoginClient,
     private val accountStore: AccountStore,
-    appSecrets: AppSecrets
+    private val appSecrets: AppSecrets
 ) {
     private val context: CoroutineContext = Dispatchers.IO
 
@@ -61,6 +62,17 @@ class WPcomLoginHelper @Inject constructor(
     private fun codeFromAuthorizationUri(string: String): String? {
         return Uri.parse(string).getQueryParameter("code")
     }
+
+    fun appendParamsToRestAuthorizationUrl(authorizationUrl: String?): String {
+        return if (authorizationUrl.isNullOrEmpty()) {
+            authorizationUrl.orEmpty()
+        } else {
+            authorizationUrl.toUri().buildUpon().apply {
+                appendQueryParameter("app_name", "android-jetpack-client")
+                appendQueryParameter("success_url", appSecrets.redirectUri)
+            }.build().toString()
+        }
+    }
 }
 
 class ServiceConnection(
diff --git a/WordPress/src/test/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModelTest.kt
deleted file mode 100644
index 80d70bd24b60..000000000000
--- a/WordPress/src/test/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModelTest.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.wordpress.android.login.viewmodel
-
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import org.junit.Before
-import org.mockito.Mock
-import rs.wordpress.api.kotlin.WpLoginClient
-
-@ExperimentalCoroutinesApi
- class LoginSiteAddressViewModelTest {
-    @Mock
-    lateinit var wpLoginClient: WpLoginClient
-
-    private lateinit var viewModel: LoginSiteAddressViewModel
-
-    @Before
-    fun setUp() {
-        viewModel = LoginSiteAddressViewModel(wpLoginClient)
-    }
-
-    // NOTE: there's actually nothing to test yet, since the initial function is just logging the server call
- }
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginViewModelTest.kt
index 179b41207b24..4ba7c2033c3e 100644
--- a/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginViewModelTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginViewModelTest.kt
@@ -1,18 +1,35 @@
 package org.wordpress.android.ui.accounts
 
+import com.sun.jna.Pointer
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
 import org.assertj.core.api.Assertions.assertThat
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doThrow
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
 import org.wordpress.android.BaseUnitTest
 import org.wordpress.android.fluxc.store.AccountStore.AuthEmailPayloadScheme
 import org.wordpress.android.fluxc.store.SiteStore.ConnectSiteInfoPayload
 import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowNoJetpackSites
 import org.wordpress.android.ui.accounts.LoginNavigationEvents.ShowSiteAddressError
+import org.wordpress.android.ui.accounts.login.WPcomLoginHelper
 import org.wordpress.android.util.BuildConfigWrapper
 import org.wordpress.android.viewmodel.ResourceProvider
+import rs.wordpress.api.kotlin.WpLoginClient
+import uniffi.wp_api.AutoDiscoveryAttemptSuccess
+import uniffi.wp_api.ParsedUrl
+import uniffi.wp_api.WpApiDetails
+
+private const val TEST_URL = "https://www.test.com"
+private const val TEST_URL_AUTH = "https://www.test.com/auth"
+private const val TEST_URL_AUTH_SUFFIX = "?app_name=android-jetpack-client&success_url=null"
 
 @ExperimentalCoroutinesApi
 class LoginViewModelTest : BaseUnitTest() {
@@ -21,11 +38,24 @@ class LoginViewModelTest : BaseUnitTest() {
 
     @Mock
     lateinit var resourceProvider: ResourceProvider
+
+    @Mock
+    lateinit var wpLoginClient: WpLoginClient
+
+    @Mock
+    lateinit var wpComLoginHelper: WPcomLoginHelper
+
+    @Mock
+    lateinit var wpApiDetails: WpApiDetails
+
     private lateinit var viewModel: LoginViewModel
 
     @Before
     fun setUp() {
-        viewModel = LoginViewModel(buildConfigWrapper)
+        MockitoAnnotations.openMocks(this)
+        whenever(wpComLoginHelper.appendParamsToRestAuthorizationUrl(any()))
+            .thenReturn("$TEST_URL_AUTH$TEST_URL_AUTH_SUFFIX")
+        viewModel = LoginViewModel(buildConfigWrapper, wpLoginClient, wpComLoginHelper)
     }
 
     @Test
@@ -66,6 +96,34 @@ class LoginViewModelTest : BaseUnitTest() {
         assertThat(scheme).isEqualTo(AuthEmailPayloadScheme.WORDPRESS)
     }
 
+    @Test
+    fun `given login scenario, when api discovery is success, then return the authentication url`() = runTest {
+        whenever(wpLoginClient.apiDiscovery(eq(TEST_URL)))
+            .thenReturn(
+                AutoDiscoveryAttemptSuccess(
+                    ParsedUrl(Pointer.createConstant(1)),
+                    ParsedUrl(Pointer.createConstant(1)),
+                    wpApiDetails
+                )
+            )
+        whenever(wpApiDetails.findApplicationPasswordsAuthenticationUrl()).thenReturn(TEST_URL_AUTH)
+
+        val result = viewModel.runApiDiscovery(TEST_URL)
+
+        assertEquals("$TEST_URL_AUTH$TEST_URL_AUTH_SUFFIX", result)
+        verify(wpLoginClient).apiDiscovery(eq(TEST_URL))
+    }
+
+    @Test
+    fun `given login scenario, when api discovery is fails, then return empty authentication url`() = runTest {
+        whenever(wpLoginClient.apiDiscovery(eq(TEST_URL))).doThrow(RuntimeException("API discovery failed"))
+
+        val result = viewModel.runApiDiscovery(TEST_URL)
+
+        assertEquals("", result)
+        verify(wpLoginClient).apiDiscovery(eq(TEST_URL))
+    }
+
     private fun getConnectSiteInfoPayload(url: String): ConnectSiteInfoPayload =
         ConnectSiteInfoPayload(url, null)
 
diff --git a/libs/login/build.gradle b/libs/login/build.gradle
index 8de1d5f30c7a..1ff85556db38 100644
--- a/libs/login/build.gradle
+++ b/libs/login/build.gradle
@@ -56,8 +56,6 @@ dependencies {
     implementation libs.androidx.appcompat.main
 
     implementation libs.androidx.constraintlayout.main
-    implementation(libs.androidx.lifecycle.viewmodel.main)
-    implementation(libs.androidx.lifecycle.viewmodel.compose)
     implementation libs.google.material
 
     implementation libs.androidx.core.main
@@ -72,8 +70,6 @@ dependencies {
     implementation libs.androidx.credentials.main
     implementation libs.androidx.credentials.play.service.auth
 
-    implementation(libs.wordpress.rs.android)
-
     // Dagger
     implementation libs.google.dagger
     ksp libs.google.dagger.compiler
diff --git a/libs/login/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.kt b/libs/login/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.kt
index 750a22a5082e..5f594338916d 100644
--- a/libs/login/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.kt
+++ b/libs/login/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.kt
@@ -37,8 +37,6 @@ import org.wordpress.android.util.NetworkUtils
 import org.wordpress.android.util.UrlUtils
 import javax.inject.Inject
 import androidx.core.net.toUri
-import androidx.lifecycle.ViewModelProvider
-import org.wordpress.android.login.viewmodel.LoginSiteAddressViewModel
 
 class LoginSiteAddressFragment : LoginBaseDiscoveryFragment(), TextWatcher, OnEditorCommitListener,
     LoginBaseDiscoveryListener {
@@ -52,10 +50,6 @@ class LoginSiteAddressFragment : LoginBaseDiscoveryFragment(), TextWatcher, OnEd
 
     private var loginSiteAddressValidator: LoginSiteAddressValidator? = null
 
-    @Inject
-    lateinit var viewModelFactory: ViewModelProvider.Factory
-    private lateinit var viewModel: LoginSiteAddressViewModel
-
     @JvmField
     @Inject
     var accountStore: AccountStore? = null
@@ -134,8 +128,6 @@ class LoginSiteAddressFragment : LoginBaseDiscoveryFragment(), TextWatcher, OnEd
     override fun onActivityCreated(savedInstanceState: Bundle?) {
         super.onActivityCreated(savedInstanceState)
 
-        viewModel = ViewModelProvider(this, viewModelFactory)[LoginSiteAddressViewModel::class.java]
-
         if (savedInstanceState != null) {
             requestedSiteAddress = savedInstanceState.getString(KEY_REQUESTED_SITE_ADDRESS)
             connectSiteInfoUrl = savedInstanceState.getString(KEY_SITE_INFO_URL)
@@ -198,10 +190,6 @@ class LoginSiteAddressFragment : LoginBaseDiscoveryFragment(), TextWatcher, OnEd
 
         val cleanedUrl = stripKnownPaths(requestedSiteAddress.orEmpty())
 
-        // This work is in progress as right now we are just testing the API discovery through the RS library
-        // No further actions are taken
-        viewModel.runApiDiscovery(cleanedUrl)
-
         mAnalyticsListener.trackConnectedSiteInfoRequested(cleanedUrl)
         dispatcher?.dispatch(SiteActionBuilder.newFetchConnectSiteInfoAction(cleanedUrl))
 
diff --git a/libs/login/src/main/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModel.kt b/libs/login/src/main/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModel.kt
deleted file mode 100644
index dccf87bdf7c4..000000000000
--- a/libs/login/src/main/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModel.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.wordpress.android.login.viewmodel
-
-import android.util.Log
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.launch
-import rs.wordpress.api.kotlin.WpLoginClient
-import javax.inject.Inject
-
-@Suppress("TooGenericExceptionCaught")
-class LoginSiteAddressViewModel @Inject constructor(
-    private val wpLoginClient: WpLoginClient
-) : ViewModel() {
-    fun runApiDiscovery(url: String) {
-        viewModelScope.launch {
-            try {
-                val urlDiscovery = wpLoginClient.apiDiscovery(url)
-                val authorizationUrl = urlDiscovery.apiDetails.findApplicationPasswordsAuthenticationUrl()
-                Log.d("WP_RS", "VM: Found authorization URL: $authorizationUrl")
-            } catch (throwable: Throwable) {
-                Log.e("WP_RS", "VM: Error during API discovery", throwable)
-            }
-        }
-    }
-}


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T13:25:31Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new password login flow by presenting an authentication webview for API discovery and prepares the internal architecture for handling callback URLs in a subsequent iteration. Key changes include removing the obsolete LoginSiteAddressViewModel and its tests, updating LoginActivity to use a dedicated CustomTabsIntent helper for launching webviews, and enhancing LoginViewModel to perform API discovery with consolidated error handling and analytics tracking.

### Reviewed Changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| libs/login/src/main/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModel.kt | Removed obsolete view model used for API discovery |
| libs/login/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.kt | Removed dependency on LoginSiteAddressViewModel and its API discovery call |
| WordPress/src/test/java/org/wordpress/android/ui/accounts/LoginViewModelTest.kt | Added tests validating the new API discovery flow in LoginViewModel |
| WordPress/src/test/java/org/wordpress/android/login/viewmodel/LoginSiteAddressViewModelTest.kt | Removed tests for the deleted view model |
| WordPress/src/main/java/org/wordpress/android/ui/accounts/login/WPcomLoginHelper.kt | Added a helper to append query parameters to the authorization URL |
| WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginViewModel.kt | Refactored API discovery method to integrate with WPcomLoginHelper and enhanced logging/analytics |
| WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java | Simplified custom tabs handling and introduced a new method to launch the application password flow |
| WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java | Removed the dependency injection binding for the deleted view model |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **libs/login/build.gradle**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (2)</summary>

**WordPress/src/main/java/org/wordpress/android/modules/ViewModelModule.java:451**
* Ensure that removing the LoginSiteAddressViewModel binding does not affect any existing dependency injections in the app.
```
@Binds @IntoMap @ViewModelKey(LoginSiteAddressViewModel.class)
```
**libs/login/src/main/java/org/wordpress/android/login/LoginSiteAddressFragment.kt:190**
* Add a clarifying comment to explain the migration of API discovery logic, indicating where and how this functionality is now handled.
```
//            viewModel.runApiDiscovery(cleanedUrl)
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T13:54:38Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-05T14:14:53Z
user: login: nbradbury
body: Looks good - glad to see some progress on this! Feel free to merge when ready :shipit: 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T11:33:13Z

Review comments:
diff_hunk: @@ -699,13 +701,19 @@
         slideInFragment(loginUsernamePasswordFragment, true, LoginUsernamePasswordFragment.TAG);
 
         // In the background, run the API discovery test to see if we can add this site for the REST API
+        String authorizationUrl = mViewModel.runApiDiscoveryTest(inputSiteAddress);
+//            gotRestEndpoint(inputSiteAddress, authorizationUrl);
+    }
+
+    public void gotRestEndpoint(String inputSiteAddress, String endpointAddress) {
path: WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
user: login: github-advanced-security[bot]
body: ## Nullable/NonNull annotation missing on method parameter

Missing null annotation

[Show more details](https://github.com/wordpress-mobile/WordPress-Android/security/code-scanning/11578)
created_at: 2025-05-05T13:25:31Z
author_association: NONE
diff_hunk: @@ -699,13 +701,19 @@
         slideInFragment(loginUsernamePasswordFragment, true, LoginUsernamePasswordFragment.TAG);
 
         // In the background, run the API discovery test to see if we can add this site for the REST API
+        String authorizationUrl = mViewModel.runApiDiscoveryTest(inputSiteAddress);
+//            gotRestEndpoint(inputSiteAddress, authorizationUrl);
+    }
+
+    public void gotRestEndpoint(String inputSiteAddress, String endpointAddress) {
path: WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
user: login: github-advanced-security[bot]
body: ## Nullable/NonNull annotation missing on method parameter

Missing null annotation

[Show more details](https://github.com/wordpress-mobile/WordPress-Android/security/code-scanning/11579)
created_at: 2025-05-05T13:25:31Z
author_association: NONE
diff_hunk: @@ -34,8 +41,19 @@ class LoginViewModel @Inject constructor(private val buildConfigWrapper: BuildCo
         AuthEmailPayloadScheme.WORDPRESS
     }
 
-    fun runApiDiscoveryTest(input: String) = runBlocking {
-        val urlDiscovery = WpLoginClient().apiDiscovery(input)
-        urlDiscovery.apiDetails.findApplicationPasswordsAuthenticationUrl()
+    @Suppress("TooGenericExceptionCaught")
+    fun runApiDiscovery(url: String): String = runBlocking {
+        try {
+            val urlDiscovery = wpLoginClient.apiDiscovery(url)
+            val authorizationUrl = urlDiscovery.apiDetails.findApplicationPasswordsAuthenticationUrl()
+            val authorizationUrlComplete = wpComLoginHelper.appendParamsToRestAuthorizationUrl(authorizationUrl)
+            Log.d("WP_RS", "Found authorization for $url URL: $authorizationUrlComplete")
+            AnalyticsTracker.track(AnalyticsTracker.Stat.BACKGROUND_REST_AUTODISCOVERY_SUCCESSFUL)
+            authorizationUrlComplete
+        } catch (throwable: Throwable) {
+            Log.e("WP_RS", "VM: Error during API discovery for $url", throwable)
+            AnalyticsTracker.track(AnalyticsTracker.Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
+            ""
path: WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginViewModel.kt
user: login: Copilot
body: Consider catching specific exceptions instead of Throwable to avoid masking underlying issues during API discovery.
```suggestion
        } catch (e: IOException) {
            Log.e("WP_RS", "VM: Network error during API discovery for $url", e)
            AnalyticsTracker.track(AnalyticsTracker.Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
            ""
        } catch (e: IllegalArgumentException) {
            Log.e("WP_RS", "VM: Invalid argument during API discovery for $url", e)
            AnalyticsTracker.track(AnalyticsTracker.Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
            ""
```
created_at: 2025-05-05T13:54:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -699,13 +701,19 @@
         slideInFragment(loginUsernamePasswordFragment, true, LoginUsernamePasswordFragment.TAG);
 
         // In the background, run the API discovery test to see if we can add this site for the REST API
+        String authorizationUrl = mViewModel.runApiDiscovery(inputSiteAddress);
+//            launchApplicationPasswordFlow(authorizationUrl);
+    }
+
+    public void launchApplicationPasswordFlow(String endpointAddress) {
path: WordPress/src/main/java/org/wordpress/android/ui/accounts/LoginActivity.java
user: login: github-advanced-security[bot]
body: ## Nullable/NonNull annotation missing on method parameter

Missing null annotation

[Show more details](https://github.com/wordpress-mobile/WordPress-Android/security/code-scanning/11580)
created_at: 2025-05-05T14:14:53Z
author_association: NONE

PR comments:
user: login: dangermattic
created_at: 2025-05-05T13:06:04Z
body: <!--
  0 Errors
  1 Warning: PR is not assigned to a milest...
  0 Messages
  0 Markdowns
-->
<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true" data-kind="Warning">
          1 Warning
      </th>
     </tr>
  </thead>
  <tbody>
    <tr>
      <td>:warning:</td>
      <td data-sticky="false">PR is not assigned to a milestone.</td>
    </tr>
  </tbody>
</table>

<p align="right" data-meta="generated_by_pr-check">
  Generated by :no_entry_sign: <a href="https://danger.systems/">Danger</a>
</p>

author_association: COLLABORATOR
user: login: wpmobilebot
created_at: 2025-05-05T13:20:54Z
body: <!-- REUSE_ID: wordpress-prototype-build-link -->

<details><summary><img alt='WordPress' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/wordpress.png' width='20px' />📲 You can test the changes from this Pull Request in <b>WordPress</b> by scanning the QR code below to install the corresponding build.</summary>
<table>
<tr>
  <td rowspan='6' width='260px'><img src='https://api.qrserver.com/v1/create-qr-code/?size=500x500&qzone=4&data=https%3A%2F%2Fcdn.a8c-ci.services%2Fea0ffdd4a98eeacfb5a47250241399da8490ef4e%2Fwordpress-prototype-build-pr21855-30a529b.apk' width='250' height='250' /></td>
  <td><b>App Name</b></td><td><img alt='WordPress' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/wordpress.png' width='20px' /> WordPress</td>
</tr>
<tr><td><b>Flavor</b></td><td>Jalapeno</td></tr>
<tr><td><b>Build Type</b></td><td>Debug</td></tr>
<tr><td><b>Version</b></td><td>pr21855-30a529b</td></tr>
<tr><td><b>Commit</b></td><td>30a529b97f5893e9946f7c747144e5837398dbd4</td></tr>
<tr><td><b>Direct Download</b></td><td><a href='https://cdn.a8c-ci.services/ea0ffdd4a98eeacfb5a47250241399da8490ef4e/wordpress-prototype-build-pr21855-30a529b.apk'><code>wordpress-prototype-build-pr21855-30a529b.apk</code></a></td></tr>
</table>
<em>Note: Google Login is not supported on these builds.</em>
</details>

author_association: CONTRIBUTOR
user: login: wpmobilebot
created_at: 2025-05-05T13:21:40Z
body: <!-- REUSE_ID: jetpack-prototype-build-link -->

<details><summary><img alt='Jetpack' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/jetpack.png' width='20px' />📲 You can test the changes from this Pull Request in <b>Jetpack</b> by scanning the QR code below to install the corresponding build.</summary>
<table>
<tr>
  <td rowspan='6' width='260px'><img src='https://api.qrserver.com/v1/create-qr-code/?size=500x500&qzone=4&data=https%3A%2F%2Fcdn.a8c-ci.services%2F8204deb9481772038e97669f4e11f5a5e6b4f94f%2Fjetpack-prototype-build-pr21855-30a529b.apk' width='250' height='250' /></td>
  <td><b>App Name</b></td><td><img alt='Jetpack' align='top' src='https://raw.githubusercontent.com/buildkite/emojis/main/img-buildkite-64/jetpack.png' width='20px' /> Jetpack</td>
</tr>
<tr><td><b>Flavor</b></td><td>Jalapeno</td></tr>
<tr><td><b>Build Type</b></td><td>Debug</td></tr>
<tr><td><b>Version</b></td><td>pr21855-30a529b</td></tr>
<tr><td><b>Commit</b></td><td>30a529b97f5893e9946f7c747144e5837398dbd4</td></tr>
<tr><td><b>Direct Download</b></td><td><a href='https://cdn.a8c-ci.services/8204deb9481772038e97669f4e11f5a5e6b4f94f/jetpack-prototype-build-pr21855-30a529b.apk'><code>jetpack-prototype-build-pr21855-30a529b.apk</code></a></td></tr>
</table>
<em>Note: Google Login is not supported on these builds.</em>
</details>

author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-05-05T13:30:08Z
body: ## [Codecov](https://app.codecov.io/gh/wordpress-mobile/WordPress-Android/pull/21855?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile) Report
Attention: Patch coverage is `50.00000%` with `4 lines` in your changes missing coverage. Please review.
> Project coverage is 39.38%. Comparing base [(`f5f51ea`)](https://app.codecov.io/gh/wordpress-mobile/WordPress-Android/commit/f5f51eab74f586d6570a9964f618fb456815623d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile) to head [(`30a529b`)](https://app.codecov.io/gh/wordpress-mobile/WordPress-Android/commit/30a529b97f5893e9946f7c747144e5837398dbd4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile).
> Report is 2 commits behind head on trunk.

| [Files with missing lines](https://app.codecov.io/gh/wordpress-mobile/WordPress-Android/pull/21855?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile) | Patch % | Lines |
|---|---|---|
| [...ress/android/ui/accounts/login/WPcomLoginHelper.kt](https://app.codecov.io/gh/wordpress-mobile/WordPress-Android/pull/21855?src=pr&el=tree&filepath=WordPress%2Fsrc%2Fmain%2Fjava%2Forg%2Fwordpress%2Fandroid%2Fui%2Faccounts%2Flogin%2FWPcomLoginHelper.kt&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile#diff-V29yZFByZXNzL3NyYy9tYWluL2phdmEvb3JnL3dvcmRwcmVzcy9hbmRyb2lkL3VpL2FjY291bnRzL2xvZ2luL1dQY29tTG9naW5IZWxwZXIua3Q=) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/wordpress-mobile/WordPress-Android/pull/21855?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile) |

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##            trunk   #21855   +/-   ##
=======================================
  Coverage   39.38%   39.38%           
=======================================
  Files        2134     2133    -1     
  Lines      100053   100044    -9     
  Branches    15408    15408           
=======================================
  Hits        39406    39406           
+ Misses      57164    57155    -9     
  Partials     3483     3483           
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/wordpress-mobile/WordPress-Android/pull/21855?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wordpress-mobile).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T10:10:22Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=wordpress-mobile_WordPress-Android&pullRequest=21855) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-Android&pullRequest=21855&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=wordpress-mobile_WordPress-Android&pullRequest=21855&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=wordpress-mobile_WordPress-Android&pullRequest=21855&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=wordpress-mobile_WordPress-Android&pullRequest=21855&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=wordpress-mobile_WordPress-Android&pullRequest=21855&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=wordpress-mobile_WordPress-Android&pullRequest=21855)


author_association: NONE