Skip to content

Duplicate the regular login flow to allow application password first iteration #21914

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5a627ff
Adding new experimental feature
adalpari May 26, 2025
c1477aa
Hidding it for non debul builds
adalpari May 26, 2025
54a1699
Fixing test
adalpari May 26, 2025
389b746
Added new fragment
adalpari May 26, 2025
6e36f30
Modifying fragment
adalpari May 27, 2025
c5b713f
Some cleaning
adalpari May 27, 2025
2f2a598
Adding viewmodel
adalpari May 27, 2025
6a7aef8
Moving code out of the library
adalpari May 27, 2025
bdd84d7
Some more cleaning
adalpari May 27, 2025
35ef2b2
Refactoring duplicated discovery functions
adalpari May 27, 2025
254f2c6
Emitting events
adalpari May 27, 2025
aeacee0
OPening webview
adalpari May 27, 2025
4934096
Merge branch 'trunk' into feature/CMM-398-duplicate-the-regular-login…
adalpari May 27, 2025
6750516
detekt
adalpari May 27, 2025
661ef86
Adding the feature check
adalpari May 27, 2025
63feec0
Fixing ApplicationPasswordViewModelSliceTest
adalpari May 27, 2025
e5c6f72
Fixing tests
adalpari May 27, 2025
e4db72b
detekt
adalpari May 27, 2025
f02d233
Some refactor and erro handling
adalpari May 27, 2025
94d69f2
Merge branch 'trunk' into feature/CMM-398-duplicate-the-regular-login…
adalpari May 27, 2025
afe27c5
Using navigator do de-duplicate some code
adalpari May 27, 2025
c879538
Adding custom error
adalpari May 27, 2025
e331b2c
Merge branch 'feature/CMM-398-duplicate-the-regular-login-flow-to-all…
adalpari May 27, 2025
e9cd8e5
Adding some tests
adalpari May 27, 2025
6e6bcb1
PR suggestions
adalpari May 28, 2025
9e8e513
Merge branch 'trunk' into feature/CMM-398-duplicate-the-regular-login…
adalpari May 28, 2025
e62e43d
Merge branch 'trunk' into feature/CMM-398-duplicate-the-regular-login…
adalpari May 28, 2025
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 @@ -8,6 +8,7 @@
import org.wordpress.android.ui.ShareIntentReceiverFragment;
import org.wordpress.android.ui.WPWebViewActivity;
import org.wordpress.android.ui.about.UnifiedAboutActivity;
import org.wordpress.android.ui.accounts.login.applicationpassword.LoginSiteApplicationPasswordFragment;
import org.wordpress.android.ui.accounts.signup.SignupEpilogueFragment;
import org.wordpress.android.ui.activitylog.detail.ActivityLogDetailFragment;
import org.wordpress.android.ui.activitylog.list.ActivityLogListFragment;
Expand Down Expand Up @@ -561,4 +562,6 @@ public interface AppComponent {
void inject(WPMainNavigationView object);

void inject(PostResolutionOverlayFragment object);

void inject(LoginSiteApplicationPasswordFragment object);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;

import org.wordpress.android.ui.accounts.login.applicationpassword.LoginSiteApplicationPasswordViewModel;
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 @@ -450,6 +451,11 @@ abstract class ViewModelModule {
@ViewModelKey(LoginViewModel.class)
abstract ViewModel loginViewModel(LoginViewModel viewModel);

@Binds
@IntoMap
@ViewModelKey(LoginSiteApplicationPasswordViewModel.class)
abstract ViewModel loginSiteApplicationPasswordViewModel(LoginSiteApplicationPasswordViewModel viewModel);

@Binds
@IntoMap
@ViewModelKey(StorageUtilsViewModel.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.wordpress.android.ui

import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.app.TaskStackBuilder
import androidx.core.net.toUri
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.analytics.AnalyticsTracker
Expand All @@ -29,6 +33,7 @@ import org.wordpress.android.ui.mysite.personalization.PersonalizationActivity
import org.wordpress.android.ui.quickstart.QuickStartEvent
import org.wordpress.android.ui.sitemonitor.SiteMonitorParentActivity
import org.wordpress.android.ui.sitemonitor.SiteMonitorType
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.analytics.AnalyticsUtils
import javax.inject.Inject
Expand Down Expand Up @@ -208,4 +213,48 @@ class ActivityNavigator @Inject constructor() {
) {
WPWebViewActivity.openUrlByUsingGlobalWPCOMCredentials(context, url)
}

@Suppress("TooGenericExceptionCaught")
fun openApplicationPasswordLogin(activity: Activity, url: String) {
val intent = getCustomTabsIntent(activity)
val loginUri = url.toUri()
try {
intent.launchUrl(activity, loginUri)
} catch (e: RuntimeException) {
when (e) {
is ActivityNotFoundException,
is SecurityException -> {
AppLog.e(
AppLog.T.UTILS,
"Error opening login URI in CustomTabsIntent, attempting external browser",
e
)
ActivityLauncher.openUrlExternal(activity, loginUri.toString())
}

else -> {
throw e
}
}
}
}

private fun getCustomTabsIntent(activity: Activity): CustomTabsIntent {
return CustomTabsIntent.Builder()
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
.setStartAnimations(
activity,
R.anim.activity_slide_in_from_right,
R.anim.activity_slide_out_to_left
)
.setExitAnimations(
activity,
R.anim.activity_slide_in_from_left,
R.anim.activity_slide_out_to_right
)
.setUrlBarHidingEnabled(true)
.setInstantAppsEnabled(false)
.setShowTitle(false)
.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.wordpress.android.login.LoginMagicLinkSentFragment;
import org.wordpress.android.login.LoginMode;
import org.wordpress.android.login.LoginSiteAddressFragment;
import org.wordpress.android.ui.accounts.login.applicationpassword.LoginSiteApplicationPasswordFragment;
import org.wordpress.android.login.LoginUsernamePasswordFragment;
import org.wordpress.android.login.SignupConfirmationFragment;
import org.wordpress.android.login.SignupGoogleFragment;
Expand Down Expand Up @@ -70,6 +71,8 @@
import org.wordpress.android.ui.posts.BasicFragmentDialog;
import org.wordpress.android.ui.posts.BasicFragmentDialog.BasicDialogPositiveClickInterface;
import org.wordpress.android.ui.prefs.AppPrefs;
import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures;
import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures.Feature;
import org.wordpress.android.ui.reader.services.update.ReaderUpdateLogic;
import org.wordpress.android.ui.reader.services.update.ReaderUpdateServiceStarter;
import org.wordpress.android.util.AppLog;
Expand Down Expand Up @@ -144,6 +147,8 @@ private enum SmartLockHelperState {
@Inject BuildConfigWrapper mBuildConfigWrapper;
@Inject ContactSupportFeatureConfig mContactSupportFeatureConfig;

@Inject ExperimentalFeatures mExperimentalFeatures;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Expand Down Expand Up @@ -196,7 +201,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
case JETPACK_SELFHOSTED:
case SELFHOSTED_ONLY:
mUnifiedLoginTracker.setSource(Source.SELF_HOSTED);
showFragment(new LoginSiteAddressFragment(), LoginSiteAddressFragment.TAG);
if (mExperimentalFeatures.isEnabled(Feature.EXPERIMENTAL_APPLICATION_PASSWORD_FEATURE)) {
showFragment(new LoginSiteApplicationPasswordFragment(), LoginSiteAddressFragment.TAG);
} else {
showFragment(new LoginSiteAddressFragment(), LoginSiteAddressFragment.TAG);
}
break;
case JETPACK_STATS:
mUnifiedLoginTracker.setSource(Source.JETPACK);
Expand Down Expand Up @@ -563,7 +572,12 @@ public void gotUnregisteredSocialAccount(String email, String displayName, Strin

@Override
public void loginViaSiteAddress() {
LoginSiteAddressFragment loginSiteAddressFragment = new LoginSiteAddressFragment();
final Fragment loginSiteAddressFragment;
if (mExperimentalFeatures.isEnabled(Feature.EXPERIMENTAL_APPLICATION_PASSWORD_FEATURE)) {
loginSiteAddressFragment = new LoginSiteApplicationPasswordFragment();
} else {
loginSiteAddressFragment = new LoginSiteAddressFragment();
}
slideInFragment(loginSiteAddressFragment, true, LoginSiteAddressFragment.TAG);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import kotlinx.coroutines.withContext
import org.wordpress.android.analytics.AnalyticsTracker
import org.wordpress.android.analytics.AnalyticsTracker.Stat
import org.wordpress.android.fluxc.persistence.SiteSqlUtils
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.modules.BG_THREAD
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.BuildConfigWrapper
import org.wordpress.android.util.encryption.EncryptionUtils
import rs.wordpress.api.kotlin.ApiDiscoveryResult
import rs.wordpress.api.kotlin.WpLoginClient
import javax.inject.Inject
import javax.inject.Named

Expand All @@ -21,10 +25,48 @@ class ApplicationPasswordLoginHelper @Inject constructor(
private val siteSqlUtils: SiteSqlUtils,
private val uriLoginWrapper: UriLoginWrapper,
private val buildConfigWrapper: BuildConfigWrapper,
private val encryptionUtils: EncryptionUtils
private val encryptionUtils: EncryptionUtils,
private val wpLoginClient: WpLoginClient,
private val appLogWrapper: AppLogWrapper,
) {
private var processedAppPasswordData: String? = null

@Suppress("TooGenericExceptionCaught")
suspend fun getAuthorizationUrlComplete(siteUrl: String): String =
try {
getAuthorizationUrlCompleteInternal(siteUrl)
} catch (throwable: Throwable) {
handleAuthenticationDiscoveryError(siteUrl, throwable)
}

private suspend fun getAuthorizationUrlCompleteInternal(siteUrl: String): String = withContext(bgDispatcher) {
when (val urlDiscoveryResult = wpLoginClient.apiDiscovery(siteUrl)) {
is ApiDiscoveryResult.Success -> {
val authorizationUrl = urlDiscoveryResult.success.applicationPasswordsAuthenticationUrl.url()
val authorizationUrlComplete =
uriLoginWrapper.appendParamsToRestAuthorizationUrl(authorizationUrl)
Log.d("WP_RS", "Found authorization for $siteUrl URL: $authorizationUrlComplete")
AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_SUCCESSFUL)
authorizationUrlComplete
}

is ApiDiscoveryResult.FailureFetchAndParseApiRoot ->
handleAuthenticationDiscoveryError(siteUrl, Exception("FailureFetchAndParseApiRoot"))

is ApiDiscoveryResult.FailureFindApiRoot ->
handleAuthenticationDiscoveryError(siteUrl, Exception("FailureFindApiRoot"))

is ApiDiscoveryResult.FailureParseSiteUrl ->
handleAuthenticationDiscoveryError(siteUrl, urlDiscoveryResult.error)
}
}

private fun handleAuthenticationDiscoveryError(siteUrl: String, throwable: Throwable): String {
appLogWrapper.e(AppLog.T.API, "WP_RS: Error during API discovery for $siteUrl - ${throwable.message}")
AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
return ""
}

@Suppress("ReturnCount")
suspend fun storeApplicationPasswordCredentialsFrom(url: String): Boolean {
if (url.isEmpty() || url == processedAppPasswordData) {
Expand Down Expand Up @@ -52,7 +94,10 @@ class ApplicationPasswordLoginHelper @Inject constructor(
processedAppPasswordData = url // Save locally to avoid duplicated calls
true
} else {
Log.e("WP_RS", "Cannot save application password credentials for: ${uriLogin.siteUrl}")
appLogWrapper.e(
AppLog.T.DB,
"WP_RS: Cannot save application password credentials for: ${uriLogin.siteUrl}"
)
false
}
}
Expand All @@ -71,24 +116,13 @@ class ApplicationPasswordLoginHelper @Inject constructor(
},
properties
)
Log.d("WP_RS", "Saved application password credentials for: $siteUrl")
appLogWrapper.e(AppLog.T.DB, "WP_RS: Saved application password credentials for: $siteUrl")
}

fun getSiteUrlFromUrl(url: String): String {
return uriLoginWrapper.parseUriLogin(url).siteUrl.orEmpty()
}

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", "jetpack://app-pass-authorize")
}.build().toString()
}
}

/**
* This class is created to wrap the Uri calls and let us unit test the login helper
*/
Expand All @@ -101,6 +135,17 @@ class ApplicationPasswordLoginHelper @Inject constructor(
uri.getQueryParameter("password")
)
}

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", "jetpack://app-pass-authorize")
}.build().toString()
}
}
}

data class UriLogin(
Expand Down
Loading