Skip to content

Show the site selector and set up my site screen after the application password login #21968

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
30 commits
Select commit Hold shift + click to select a range
4b65418
Re adding the site store calls
adalpari Jun 19, 2025
762343e
Making it work with XML-RPC
adalpari Jun 19, 2025
59be3dc
Properly saving the site
adalpari Jun 20, 2025
57e4381
Adding toasts
adalpari Jun 20, 2025
a90c3ca
Adding tests
adalpari Jun 20, 2025
c9c2bdc
detekt
adalpari Jun 20, 2025
688a30f
Removing debug code
adalpari Jun 20, 2025
74d2404
Update libs/fluxc/src/main/java/org/wordpress/android/fluxc/store/Sit…
adalpari Jun 20, 2025
8f8a096
typos
adalpari Jun 20, 2025
c7b4355
Listen for when the site is fetched
adalpari Jun 20, 2025
66d73c8
Fetching profile
adalpari Jun 20, 2025
dcd4618
Correctly handling the url
adalpari Jun 20, 2025
49ad92f
Opening login epilogue
adalpari Jun 20, 2025
329d72a
Merge remote-tracking branch 'origin/trunk' into feature/CMM-519-Show…
adalpari Jun 23, 2025
412863b
detekt
adalpari Jun 23, 2025
c00c67b
Adding tests
adalpari Jun 23, 2025
9180e5b
detekt
adalpari Jun 23, 2025
d25f8c4
Removing debug code
adalpari Jun 23, 2025
2621cb1
Some fixes
adalpari Jun 23, 2025
7581317
Fixing tests
adalpari Jun 23, 2025
522904e
Adding ProgressIndicator
adalpari Jun 23, 2025
963ef5c
Clear flags for MainActivity
adalpari Jun 23, 2025
a2d7b09
Merge branch 'trunk' into feature/CMM-519-Show-the-site-selector-and-…
adalpari Jun 23, 2025
4dab50b
Using the already existing AppPrefsWrapper
adalpari Jun 24, 2025
5b47d80
Merge branch 'feature/CMM-519-Show-the-site-selector-and-set-up-MySit…
adalpari Jun 24, 2025
0df300e
Merge branch 'trunk' into feature/CMM-519-Show-the-site-selector-and-…
adalpari Jun 24, 2025
35f7818
Merge branch 'trunk' into feature/CMM-519-Show-the-site-selector-and-…
adalpari Jun 24, 2025
fbb1250
Using AppLogWrapper for loiging
adalpari Jun 24, 2025
27be1a7
Merge branch 'feature/CMM-519-Show-the-site-selector-and-set-up-MySit…
adalpari Jun 24, 2025
495bc0c
Merge branch 'trunk' into feature/CMM-519-Show-the-site-selector-and-…
adalpari Jun 25, 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 @@ -2,15 +2,22 @@ package org.wordpress.android.ui.accounts.applicationpassword

import android.content.Intent
import android.os.Bundle
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.wordpress.android.R
import org.wordpress.android.util.ToastUtils
import org.wordpress.android.ui.ActivityLauncher
import org.wordpress.android.ui.main.BaseAppCompatActivity
import org.wordpress.android.ui.main.WPMainActivity
import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.extensions.setContent
import javax.inject.Inject

@AndroidEntryPoint
Expand All @@ -23,6 +30,14 @@ class ApplicationPasswordLoginActivity: BaseAppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViewModel()
setContent {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
CircularProgressIndicator()
}
}
}

private fun initViewModel() {
Expand All @@ -31,13 +46,13 @@ class ApplicationPasswordLoginActivity: BaseAppCompatActivity() {
viewModel!!.setupSite(intent.dataString.orEmpty())
}

private fun openMainActivity(siteUrl: String?) {
if (siteUrl != null) {
private fun openMainActivity(navigationActionData: ApplicationPasswordLoginViewModel.NavigationActionData) {
if (!navigationActionData.isError && navigationActionData.siteUrl != null) {
ToastUtils.showToast(
this,
getString(
R.string.application_password_credentials_stored,
siteUrl
navigationActionData.siteUrl
)
)
intent.setData(null)
Expand All @@ -46,17 +61,35 @@ class ApplicationPasswordLoginActivity: BaseAppCompatActivity() {
this,
getString(
R.string.application_password_credentials_storing_error,
siteUrl
navigationActionData.siteUrl
)
)
}
val mainActivityIntent =
Intent(this, WPMainActivity::class.java)
mainActivityIntent.setFlags(
(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
startActivity(mainActivityIntent)

if (navigationActionData.isError) {
ActivityLauncher.showMainActivity(this)
} else if (navigationActionData.showSiteSelector) {
ActivityLauncher.showMainActivityAndLoginEpilogue(this, navigationActionData.oldSitesIDs, false)
} else if (navigationActionData.showPostSignupInterstitial) {
ActivityLauncher.showPostSignupInterstitial(this)
} else {
val mainActivityIntent = Intent(this, WPMainActivity::class.java)
mainActivityIntent.setFlags(
(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
or Intent.FLAG_ACTIVITY_CLEAR_TASK)
)
startActivity(mainActivityIntent)
}
finish()
}

override fun onStart() {
super.onStart()
viewModel?.onStart()
}

override fun onStop() {
super.onStop()
viewModel?.onStop()
}
}
Original file line number Diff line number Diff line change
@@ -1,71 +1,132 @@
package org.wordpress.android.ui.accounts.applicationpassword

import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import org.wordpress.android.fluxc.Dispatcher
import org.wordpress.android.fluxc.generated.SiteActionBuilder
import org.wordpress.android.fluxc.network.discovery.SelfHostedEndpointFinder
import org.wordpress.android.fluxc.store.SiteStore.RefreshSitesXMLRPCPayload
import org.wordpress.android.fluxc.store.SiteStore
import org.wordpress.android.fluxc.store.SiteStore.OnProfileFetched
import org.wordpress.android.fluxc.store.SiteStore.OnSiteChanged
import org.wordpress.android.fluxc.store.SiteStore.RefreshSitesXMLRPCPayload
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.login.util.SiteUtils
import org.wordpress.android.modules.IO_THREAD
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper

import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.UriLogin
import org.wordpress.android.ui.prefs.AppPrefsWrapper
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.UrlUtils
import javax.inject.Inject
import javax.inject.Named

private const val TAG = "ApplicationPasswordLoginViewModel"

class ApplicationPasswordLoginViewModel @Inject constructor(
@Named(IO_THREAD) private val ioDispatcher: CoroutineDispatcher,
// Dispatcher is the way to dispatch actions to Flux. It will call siteStore.onAction()
private val dispatcher: Dispatcher,
private val applicationPasswordLoginHelper: ApplicationPasswordLoginHelper,
private val selfHostedEndpointFinder: SelfHostedEndpointFinder,
private val siteStore: SiteStore,
private val appPrefsWrapper: AppPrefsWrapper,
private val appLogWrapper: AppLogWrapper,
) : ViewModel() {
private val _onFinishedEvent = MutableSharedFlow<String?>()
private val _onFinishedEvent = MutableSharedFlow<NavigationActionData>()
/**
* A shared flow that emits the site URL when the setup is finished.
* It can emit null if the site could not be set up.
*/
val onFinishedEvent = _onFinishedEvent.asSharedFlow()

private var currentUrlLogin: UriLogin? = null
private var oldSitesIDs: ArrayList<Int>? = null

fun onStart() {
dispatcher.register(this)
oldSitesIDs = SiteUtils.getCurrentSiteIds(siteStore, false)
}

fun onStop() {
dispatcher.unregister(this)
}

/**
* This method is called to set up the site with the provided raw data.
*
* @param rawData The raw data containing the callback data from the application password login.
*/
fun setupSite(rawData: String) {
viewModelScope.launch {
if (rawData.isEmpty()) {
appLogWrapper.e(AppLog.T.MAIN, "Cannot store credentials: rawData is empty")
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = "",
oldSitesIDs = oldSitesIDs,
isError = true
)
)
return@launch
}
val urlLogin = applicationPasswordLoginHelper.getSiteUrlLoginFromRawData(rawData)
// Store credentials if the site already exists
val credentialsStored = storeCredentials(rawData)
// If the site already exists, we can skip fetching it again
if (credentialsStored) {
_onFinishedEvent.emit(urlLogin.siteUrl)
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = urlLogin.siteUrl,
oldSitesIDs = oldSitesIDs,
isError = false
)
)
} else {
fetchSites(urlLogin)
currentUrlLogin = urlLogin
}
}
}

@Suppress("TooGenericExceptionCaught")
private suspend fun storeCredentials(rawData: String): Boolean = withContext(ioDispatcher) {
try {
if (rawData.isEmpty()) {
appLogWrapper.e(AppLog.T.DB, "Cannot store credentials: rawData is empty")
false
} else {
val siteFetched = fetchSites(urlLogin)
_onFinishedEvent.emit(if (siteFetched) urlLogin.siteUrl else null)
val credentialsStored = applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)
credentialsStored
}
} catch (e: Exception) {
appLogWrapper.e(AppLog.T.DB, "Error storing credentials: ${e.stackTrace}")
false
}
}

@Suppress("TooGenericExceptionCaught")
private suspend fun fetchSites(
urlLogin: ApplicationPasswordLoginHelper.UriLogin
): Boolean = withContext(ioDispatcher) {
urlLogin: UriLogin
) = withContext(ioDispatcher) {
try {
if (urlLogin.user.isNullOrEmpty() ||
urlLogin.password.isNullOrEmpty() ||
urlLogin.siteUrl.isNullOrEmpty()) {
Log.e(TAG, "Cannot store credentials: rawData is empty")
false
appLogWrapper.e(AppLog.T.MAIN, "Cannot store credentials: rawData is empty")
emitErrorFetching(urlLogin)
} else {
val xmlRpcEndpoint =
selfHostedEndpointFinder.verifyOrDiscoverXMLRPCEndpoint(urlLogin.siteUrl)
siteStore.onAction(
dispatcher.dispatch(
SiteActionBuilder.newFetchSitesXmlRpcFromApplicationPasswordAction(
RefreshSitesXMLRPCPayload(
username = urlLogin.user,
Expand All @@ -74,27 +135,68 @@ class ApplicationPasswordLoginViewModel @Inject constructor(
)
)
)
true
}
} catch (e: Exception) {
Log.e(TAG, "Error storing credentials", e)
false
appLogWrapper.e(AppLog.T.API, "Error storing credentials: ${e.stackTrace}")
emitErrorFetching(urlLogin)
}
}

@Suppress("TooGenericExceptionCaught")
private suspend fun storeCredentials(rawData: String): Boolean = withContext(ioDispatcher) {
try {
if (rawData.isEmpty()) {
Log.e(TAG, "Cannot store credentials: rawData is empty")
false
} else {
val credentialsStored = applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(rawData)
credentialsStored
private suspend fun emitErrorFetching(urlLogin: UriLogin) = _onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = urlLogin.siteUrl,
oldSitesIDs = oldSitesIDs,
isError = true
)
)

@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onSiteChanged(event: OnSiteChanged) {
val currentNormalizedUrl = UrlUtils.normalizeUrl(currentUrlLogin?.siteUrl)
val site = siteStore.sites.firstOrNull { UrlUtils.normalizeUrl(it.url) == currentNormalizedUrl }
if (site == null) {
appLogWrapper.e(AppLog.T.MAIN, "Site not found for URL: ${currentUrlLogin?.siteUrl}")
viewModelScope.launch {
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = false,
showPostSignupInterstitial = false,
siteUrl = currentUrlLogin?.siteUrl,
oldSitesIDs = oldSitesIDs,
isError = true
)
)
}
} catch (e: Exception) {
Log.e(TAG, "Error storing credentials", e)
false
} else {
dispatcher.dispatch(SiteActionBuilder.newFetchProfileXmlRpcAction(site))
}
}

@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.BACKGROUND)
fun onProfileFetched(event: OnProfileFetched) {
viewModelScope.launch {
_onFinishedEvent.emit(
NavigationActionData(
showSiteSelector = siteStore.hasSite(),
showPostSignupInterstitial = !siteStore.hasSite()
&& appPrefsWrapper.shouldShowPostSignupInterstitial,
siteUrl = event.site.url,
oldSitesIDs = oldSitesIDs,
isError = false
)
)
}
}

data class NavigationActionData(
val showSiteSelector: Boolean,
val showPostSignupInterstitial: Boolean,
val siteUrl: String?,
val oldSitesIDs: ArrayList<Int>?,
val isError: Boolean
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ 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.UrlUtils
import rs.wordpress.api.kotlin.ApiDiscoveryResult
import rs.wordpress.api.kotlin.WpLoginClient
import javax.inject.Inject
Expand Down Expand Up @@ -77,7 +78,8 @@ class ApplicationPasswordLoginHelper @Inject constructor(
if (uriLogin.user.isNullOrEmpty() || uriLogin.password.isNullOrEmpty() ) {
false
} else {
val site = siteSqlUtils.getSites().firstOrNull { it.url == uriLogin.siteUrl }
val normalizedUrl = UrlUtils.normalizeUrl(uriLogin.siteUrl)
val site = siteSqlUtils.getSites().firstOrNull { UrlUtils.normalizeUrl(it.url) == normalizedUrl}
if (site != null) {
site.apply {
apiRestUsernamePlain = uriLogin.user
Expand Down
Loading