Skip to content

Commit f7c7d6d

Browse files
adalpariCopilot
andauthored
Offer the application password login inside a card in MySite screen (#21878)
* Moving event into a Flow * Removing dialog * Adding the card * Handling the site change * Handling navigation * Moving code into a slice * Some code cleaning * Adding cache * Adding tests * detekt * Hiding feature by commenting the code * Update WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSliceTest.kt Co-authored-by: Copilot <[email protected]> * Update WordPress/src/test/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSliceTest.kt Co-authored-by: Copilot <[email protected]> * PR suggestions * Fixing ytest condition * Removing unnecessary annotations * Adding filtering tag to the discovery error log * Application password login add analytic events (#21886) * Adding analytic event * Minor refactor * Fixing test * Adding event properties * Detekt * Fixing test * Adding some debug logs * Reverting debug logs --------- Co-authored-by: Copilot <[email protected]>
1 parent 4223d55 commit f7c7d6d

File tree

10 files changed

+477
-381
lines changed

10 files changed

+477
-381
lines changed

WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@ import android.util.Log
44
import androidx.core.net.toUri
55
import kotlinx.coroutines.CoroutineDispatcher
66
import kotlinx.coroutines.withContext
7+
import org.wordpress.android.analytics.AnalyticsTracker
8+
import org.wordpress.android.analytics.AnalyticsTracker.Stat
79
import org.wordpress.android.fluxc.persistence.SiteSqlUtils
810
import org.wordpress.android.modules.BG_THREAD
11+
import org.wordpress.android.util.BuildConfigWrapper
912
import javax.inject.Inject
1013
import javax.inject.Named
1114

15+
private const val URL_TAG = "url"
16+
private const val SUCCESS_TAG = "success"
17+
1218
class ApplicationPasswordLoginHelper @Inject constructor(
1319
@param:Named(BG_THREAD) private val bgDispatcher: CoroutineDispatcher,
1420
private val siteSqlUtils: SiteSqlUtils,
15-
private val uriLoginWrapper: UriLoginWrapper
21+
private val uriLoginWrapper: UriLoginWrapper,
22+
private val buildConfigWrapper: BuildConfigWrapper,
1623
) {
1724
private var processedAppPasswordData: String? = null
1825

@@ -30,11 +37,13 @@ class ApplicationPasswordLoginHelper @Inject constructor(
3037
} else {
3138
val site = siteSqlUtils.getSites().firstOrNull { it.url == uriLogin.siteUrl }
3239
if (site != null) {
33-
site.apiRestUsername = uriLogin.user
34-
site.apiRestPassword = uriLogin.password
40+
site.apply {
41+
apiRestUsername = uriLogin.user
42+
apiRestPassword = uriLogin.password
43+
}
3544
siteSqlUtils.insertOrUpdateSite(site)
36-
Log.d("WP_RS", "Saved application password credentials for: ${uriLogin.siteUrl}")
37-
processedAppPasswordData = url
45+
uriLogin.siteUrl?.let { trackSuccessful(it) }
46+
processedAppPasswordData = url // Save locally to avoid duplicated calls
3847
true
3948
} else {
4049
Log.e("WP_RS", "Cannot save application password credentials for: ${uriLogin.siteUrl}")
@@ -44,6 +53,21 @@ class ApplicationPasswordLoginHelper @Inject constructor(
4453
}
4554
}
4655

56+
private fun trackSuccessful(siteUrl: String) {
57+
val properties: MutableMap<String, String?> = HashMap()
58+
properties[URL_TAG] = siteUrl
59+
properties[SUCCESS_TAG] = "true"
60+
AnalyticsTracker.track(
61+
if (buildConfigWrapper.isJetpackApp) {
62+
Stat.JP_ANDROID_APPLICATION_PASSWORD_LOGIN
63+
} else {
64+
Stat.WP_ANDROID_APPLICATION_PASSWORD_LOGIN
65+
},
66+
properties
67+
)
68+
Log.d("WP_RS", "Saved application password credentials for: $siteUrl")
69+
}
70+
4771
fun getSiteUrlFromUrl(url: String): String {
4872
return uriLoginWrapper.parseUriLogin(url).siteUrl.orEmpty()
4973
}

WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt

Lines changed: 25 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,6 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
164164
setupContentViews(savedInstanceState)
165165
setupObservers()
166166
}
167-
168-
// This is work in progress, we are not running the flow for regular users yet
169-
// viewModel.runApplicationPasswordDiscovery()
170167
}
171168

172169
override fun onSaveInstanceState(outState: Bundle) {
@@ -450,10 +447,6 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
450447
WPJetpackIndividualPluginFragment.show(requireActivity().supportFragmentManager)
451448
}
452449

453-
viewModel.onShowApplicationPasswordLoginDialog.observeEvent(viewLifecycleOwner) {
454-
showApplicationPasswordDialog(it)
455-
}
456-
457450
viewModel.onScrollTo.observeEvent(viewLifecycleOwner) {
458451
var quickStartScrollPosition = it
459452
if (quickStartScrollPosition == -1) {
@@ -471,45 +464,27 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
471464
}
472465
}
473466

474-
private fun showApplicationPasswordDialog(url: String) {
475-
// This is in progress, so texts are not finals and we are not translating them yet.
476-
val builder = android.app.AlertDialog.Builder(requireContext())
477-
builder.setTitle("Application Password")
478-
.setMessage("Would you like to authenticate this site using Applictaion Password?")
479-
.setPositiveButton("Yes") { dialog, which ->
480-
val intent = getCustomTabsIntent()
481-
val loginUri = url.toUri()
482-
val activity = requireActivity()
483-
try {
484-
intent.launchUrl(activity, loginUri)
485-
} catch (e: SecurityException) {
486-
AppLog.e(
487-
AppLog.T.UTILS,
488-
"Error opening login uri in CustomTabsIntent, attempting external browser",
489-
e
490-
)
491-
ActivityLauncher.openUrlExternal(activity, loginUri.toString())
492-
} catch (e: ActivityNotFoundException) {
493-
AppLog.e(
494-
AppLog.T.UTILS,
495-
"Error opening login uri in CustomTabsIntent, attempting external browser",
496-
e
497-
)
498-
ActivityLauncher.openUrlExternal(activity, loginUri.toString())
499-
}
500-
dialog.dismiss()
501-
}
502-
.setNeutralButton("Later") { dialog, which ->
503-
dialog.dismiss()
504-
}
505-
.setNegativeButton("No") { dialog, which ->
506-
viewModel.onApplicationPasswordLoginDialogDismissed(url)
507-
dialog.dismiss()
508-
}
509-
.setCancelable(false)
510-
511-
val dialog = builder.create()
512-
dialog.show()
467+
private fun openApplicationPasswordLogin(url: String) {
468+
val intent = getCustomTabsIntent()
469+
val loginUri = url.toUri()
470+
val activity = requireActivity()
471+
try {
472+
intent.launchUrl(activity, loginUri)
473+
} catch (e: SecurityException) {
474+
AppLog.e(
475+
AppLog.T.UTILS,
476+
"Error opening login uri in CustomTabsIntent, attempting external browser",
477+
e
478+
)
479+
ActivityLauncher.openUrlExternal(activity, loginUri.toString())
480+
} catch (e: ActivityNotFoundException) {
481+
AppLog.e(
482+
AppLog.T.UTILS,
483+
"Error opening login uri in CustomTabsIntent, attempting external browser",
484+
e
485+
)
486+
ActivityLauncher.openUrlExternal(activity, loginUri.toString())
487+
}
513488
}
514489

515490
private fun getCustomTabsIntent(): CustomTabsIntent {
@@ -803,6 +778,10 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
803778
requireActivity(),
804779
action.site
805780
)
781+
782+
is SiteNavigationAction.OpenApplicationPasswordAuthentication -> {
783+
openApplicationPasswordLogin(action.url)
784+
}
806785
}
807786

808787
private fun openBloganuaryNudgeOverlay(isPromptsEnabled: Boolean) {

WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt

Lines changed: 12 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22

33
package org.wordpress.android.ui.mysite
44

5-
import android.content.SharedPreferences
65
import android.net.Uri
7-
import android.util.Log
86
import androidx.annotation.StringRes
97
import androidx.lifecycle.LiveData
108
import androidx.lifecycle.MutableLiveData
@@ -16,7 +14,6 @@ import kotlinx.coroutines.launch
1614
import org.greenrobot.eventbus.Subscribe
1715
import org.greenrobot.eventbus.ThreadMode.MAIN
1816
import org.wordpress.android.R
19-
import org.wordpress.android.analytics.AnalyticsTracker
2017
import org.wordpress.android.analytics.AnalyticsTracker.Stat
2118
import org.wordpress.android.fluxc.Dispatcher
2219
import org.wordpress.android.fluxc.model.SiteModel
@@ -25,7 +22,6 @@ import org.wordpress.android.fluxc.store.PostStore.OnPostUploaded
2522
import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask
2623
import org.wordpress.android.modules.BG_THREAD
2724
import org.wordpress.android.modules.UI_THREAD
28-
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper
2925
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalOverlayUtil
3026
import org.wordpress.android.ui.jetpackoverlay.JetpackFeatureRemovalPhaseHelper
3127
import org.wordpress.android.ui.jetpackoverlay.individualplugin.WPJetpackIndividualPluginHelper
@@ -53,13 +49,9 @@ import org.wordpress.android.util.merge
5349
import org.wordpress.android.viewmodel.Event
5450
import org.wordpress.android.viewmodel.ScopedViewModel
5551
import org.wordpress.android.viewmodel.SingleLiveEvent
56-
import rs.wordpress.api.kotlin.WpLoginClient
5752
import javax.inject.Inject
5853
import javax.inject.Named
59-
import androidx.core.content.edit
60-
import kotlinx.coroutines.withContext
61-
import org.wordpress.android.fluxc.persistence.SiteSqlUtils
62-
import rs.wordpress.api.kotlin.ApiDiscoveryResult
54+
import org.wordpress.android.ui.mysite.cards.applicationpassword.ApplicationPasswordViewModelSlice
6355

6456
@Suppress("LargeClass", "LongMethod", "LongParameterList")
6557
class MySiteViewModel @Inject constructor(
@@ -86,16 +78,12 @@ class MySiteViewModel @Inject constructor(
8678
private val accountDataViewModelSlice: AccountDataViewModelSlice,
8779
private val dashboardCardsViewModelSlice: DashboardCardsViewModelSlice,
8880
private val dashboardItemsViewModelSlice: DashboardItemsViewModelSlice,
89-
private val wpLoginClient: WpLoginClient,
90-
private val applicationPasswordLoginHelper: ApplicationPasswordLoginHelper,
91-
private val sharedPreferences: SharedPreferences,
92-
private val siteSqlUtils: SiteSqlUtils,
81+
private val applicationPasswordViewModelSlice: ApplicationPasswordViewModelSlice,
9382
) : ScopedViewModel(mainDispatcher) {
9483
private val _onSnackbarMessage = MutableLiveData<Event<SnackbarMessageHolder>>()
9584
private val _onNavigation = MutableLiveData<Event<SiteNavigationAction>>()
9685
private val _onOpenJetpackInstallFullPluginOnboarding = SingleLiveEvent<Event<Unit>>()
9786
private val _onShowJetpackIndividualPluginOverlay = SingleLiveEvent<Event<Unit>>()
98-
private val _onShowApplicationPasswordLoginDialog = SingleLiveEvent<Event<String>>()
9987

10088
/* Capture and track the site selected event so we can circumvent refreshing sources on resume
10189
as they're already built on site select. */
@@ -118,6 +106,7 @@ class MySiteViewModel @Inject constructor(
118106

119107
val onNavigation = merge(
120108
_onNavigation,
109+
applicationPasswordViewModelSlice.onNavigation,
121110
siteInfoHeaderCardViewModelSlice.onNavigation,
122111
dashboardCardsViewModelSlice.onNavigation,
123112
dashboardItemsViewModelSlice.onNavigation
@@ -133,8 +122,6 @@ class MySiteViewModel @Inject constructor(
133122

134123
val onShowJetpackIndividualPluginOverlay = _onShowJetpackIndividualPluginOverlay as LiveData<Event<Unit>>
135124

136-
val onShowApplicationPasswordLoginDialog = _onShowApplicationPasswordLoginDialog as LiveData<Event<String>>
137-
138125
val refresh =
139126
merge(
140127
dashboardCardsViewModelSlice.refresh
@@ -150,26 +137,30 @@ class MySiteViewModel @Inject constructor(
150137

151138
val uiModel: LiveData<State> = merge(
152139
siteInfoHeaderCardViewModelSlice.uiModel,
140+
applicationPasswordViewModelSlice.uiModel,
153141
accountDataViewModelSlice.uiModel,
154142
dashboardCardsViewModelSlice.uiModel,
155143
dashboardItemsViewModelSlice.uiModel
156144
) { siteInfoHeaderCard,
145+
applicationPAsswordModel,
157146
accountData,
158147
dashboardCards,
159148
siteItems ->
160149
val nonNullSiteInfoHeaderCard =
161150
siteInfoHeaderCard ?: return@merge buildNoSiteState(accountData?.url, accountData?.name)
151+
val headerList = listOfNotNull(nonNullSiteInfoHeaderCard, applicationPAsswordModel)
162152
return@merge if (!dashboardCards.isNullOrEmpty<MySiteCardAndItem>())
163-
SiteSelected(dashboardData = listOf(nonNullSiteInfoHeaderCard) + dashboardCards)
153+
SiteSelected(dashboardData = headerList + dashboardCards)
164154
else if (!siteItems.isNullOrEmpty<MySiteCardAndItem>())
165-
SiteSelected(dashboardData = listOf(nonNullSiteInfoHeaderCard) + siteItems)
155+
SiteSelected(dashboardData = headerList + siteItems)
166156
else
167-
SiteSelected(dashboardData = listOf(nonNullSiteInfoHeaderCard))
157+
SiteSelected(dashboardData = headerList)
168158
}.distinctUntilChanged()
169159

170160
init {
171161
dispatcher.register(this)
172162
siteInfoHeaderCardViewModelSlice.initialize(viewModelScope)
163+
applicationPasswordViewModelSlice.initialize(viewModelScope)
173164
dashboardCardsViewModelSlice.initialize(viewModelScope)
174165
dashboardItemsViewModelSlice.initialize(viewModelScope)
175166
accountDataViewModelSlice.initialize(viewModelScope)
@@ -216,85 +207,6 @@ class MySiteViewModel @Inject constructor(
216207
}
217208
}
218209

219-
fun runApplicationPasswordDiscovery() {
220-
selectedSiteRepository.updateSiteSettingsIfNecessary()
221-
val site = selectedSiteRepository.getSelectedSite() ?: return
222-
223-
val firstTimeSiteOpen = !sharedPreferences.getBoolean("$SITE_ALREADY_OPENED_PREFIX${site.url}", false)
224-
if (firstTimeSiteOpen) {
225-
setSiteAsAlreadyOpened(site.url)
226-
return
227-
}
228-
viewModelScope.launch {
229-
// If the user has dismissed the authorization dialog, no need to show it again
230-
val hasDismissedAuthorizationDialog =
231-
sharedPreferences.getBoolean("$DISMISSED_AUTHORIZATION_DIALOG_PREFIX${site.url}", false)
232-
233-
// If the site is already authorized, no need to run the discovery
234-
val storedSite = siteSqlUtils.getSiteWithLocalId(site.localId())
235-
if (storedSite != null &&
236-
!storedSite.apiRestUsername.isNullOrEmpty() && !storedSite.apiRestPassword.isNullOrEmpty()) {
237-
return@launch
238-
}
239-
240-
if (!hasDismissedAuthorizationDialog) {
241-
val authorizationUrlComplete = getAuthorizationUrlComplete(site.url)
242-
if (authorizationUrlComplete.isNotEmpty()) {
243-
_onShowApplicationPasswordLoginDialog.value = Event(authorizationUrlComplete)
244-
}
245-
}
246-
}
247-
}
248-
249-
@Suppress("TooGenericExceptionCaught")
250-
private suspend fun getAuthorizationUrlComplete(siteUrl: String): String = withContext(bgDispatcher) {
251-
try {
252-
getAuthorizationUrlCompleteInternal(siteUrl)
253-
} catch (throwable: Throwable) {
254-
handleAuthenticationDiscoveryError(siteUrl, throwable)
255-
}
256-
}
257-
258-
private fun handleAuthenticationDiscoveryError(siteUrl: String, throwable: Throwable): String {
259-
Log.e("WP_RS", "VM: Error during API discovery for $siteUrl", throwable)
260-
AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_FAILED)
261-
return ""
262-
}
263-
264-
private suspend fun getAuthorizationUrlCompleteInternal(siteUrl: String): String = withContext(bgDispatcher) {
265-
when (val urlDiscoveryResult = wpLoginClient.apiDiscovery(siteUrl)) {
266-
is ApiDiscoveryResult.Success -> {
267-
val authorizationUrl = urlDiscoveryResult.success.applicationPasswordsAuthenticationUrl.url()
268-
val authorizationUrlComplete =
269-
applicationPasswordLoginHelper.appendParamsToRestAuthorizationUrl(authorizationUrl)
270-
Log.d("WP_RS", "Found authorization for $siteUrl URL: $authorizationUrlComplete")
271-
AnalyticsTracker.track(Stat.BACKGROUND_REST_AUTODISCOVERY_SUCCESSFUL)
272-
authorizationUrlComplete
273-
}
274-
275-
is ApiDiscoveryResult.FailureFetchAndParseApiRoot ->
276-
handleAuthenticationDiscoveryError(siteUrl, Exception("FailureFetchAndParseApiRoot"))
277-
278-
is ApiDiscoveryResult.FailureFindApiRoot ->
279-
handleAuthenticationDiscoveryError(siteUrl, Exception("FailureFindApiRoot"))
280-
281-
is ApiDiscoveryResult.FailureParseSiteUrl ->
282-
handleAuthenticationDiscoveryError(siteUrl, urlDiscoveryResult.error)
283-
}
284-
}
285-
286-
private fun setSiteAsAlreadyOpened(siteUrl: String) {
287-
viewModelScope.launch {
288-
sharedPreferences.edit { putBoolean("$SITE_ALREADY_OPENED_PREFIX$siteUrl", true) }
289-
}
290-
}
291-
292-
fun onApplicationPasswordLoginDialogDismissed(siteUrl: String) {
293-
viewModelScope.launch {
294-
sharedPreferences.edit { putBoolean("$DISMISSED_AUTHORIZATION_DIALOG_PREFIX$siteUrl", true) }
295-
}
296-
}
297-
298210
private fun checkAndShowJetpackFullPluginInstallOnboarding() {
299211
selectedSiteRepository.getSelectedSite()?.let { selectedSite ->
300212
if (getShowJetpackFullPluginInstallOnboardingUseCase.execute(selectedSite)) {
@@ -468,6 +380,7 @@ class MySiteViewModel @Inject constructor(
468380

469381
private fun buildDashboardOrSiteItems(site: SiteModel) {
470382
siteInfoHeaderCardViewModelSlice.buildCard(site)
383+
applicationPasswordViewModelSlice.buildCard(site)
471384
if (shouldShowDashboard(site)) {
472385
dashboardCardsViewModelSlice.buildCards(site)
473386
dashboardItemsViewModelSlice.clearValue()
@@ -479,6 +392,7 @@ class MySiteViewModel @Inject constructor(
479392

480393
private fun onSitePicked(site: SiteModel) {
481394
siteInfoHeaderCardViewModelSlice.buildCard(site)
395+
applicationPasswordViewModelSlice.buildCard(site)
482396
dashboardItemsViewModelSlice.clearValue()
483397
dashboardCardsViewModelSlice.clearValue()
484398
dashboardCardsViewModelSlice.resetShownTracker()
@@ -553,13 +467,10 @@ class MySiteViewModel @Inject constructor(
553467
companion object {
554468
const val TAG_ADD_SITE_ICON_DIALOG = "TAG_ADD_SITE_ICON_DIALOG"
555469
const val TAG_CHANGE_SITE_ICON_DIALOG = "TAG_CHANGE_SITE_ICON_DIALOG"
556-
const val TAG_REMOVE_NEXT_STEPS_DIALOG = "TAG_REMOVE_NEXT_STEPS_DIALOG"
557470
const val SITE_NAME_CHANGE_CALLBACK_ID = 1
558471
const val ARG_QUICK_START_TASK = "ARG_QUICK_START_TASK"
559472
const val HIDE_WP_ADMIN_GMT_TIME_ZONE = "GMT"
560473
private const val DELAY_BEFORE_SHOWING_JETPACK_INDIVIDUAL_PLUGIN_OVERLAY = 500L
561474
private const val DAY_ONE_EXTERNAL_URL = "https://dayoneapp.com/?utm_source=jetpack&utm_medium=prompts"
562-
private const val DISMISSED_AUTHORIZATION_DIALOG_PREFIX = "dismissed_authorization_dialog_"
563-
private const val SITE_ALREADY_OPENED_PREFIX = "site_already_opened_"
564475
}
565476
}

WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ sealed class SiteNavigationAction {
9292

9393
data class OpenBloganuaryNudgeOverlay(val isPromptsEnabled: Boolean): SiteNavigationAction()
9494
data class OpenSiteMonitoring(val site: SiteModel) : SiteNavigationAction()
95+
96+
data class OpenApplicationPasswordAuthentication(val url: String) : SiteNavigationAction()
9597
}
9698

9799
sealed class BloggingPromptCardNavigationAction: SiteNavigationAction() {

0 commit comments

Comments
 (0)