Skip to content

Show password login webview and prepare internal architecture #21855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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());
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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)
""
Comment on lines +53 to +56
Copy link
Preview

Copilot AI May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider catching specific exceptions instead of Throwable to avoid masking underlying issues during API discovery.

Suggested change
} catch (throwable: Throwable) {
Log.e("WP_RS", "VM: Error during API discovery for $url", throwable)
AnalyticsTracker.track(AnalyticsTracker.Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
""
} 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)
""

Copilot uses AI. Check for mistakes.

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 0 additions & 4 deletions libs/login/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))

Expand Down

This file was deleted.