Skip to content
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
edb4b59
Updating RS library
adalpari Nov 10, 2025
971444d
Providing the client with the new authentication method
adalpari Nov 10, 2025
f74bcc6
Handling password creation
adalpari Nov 10, 2025
6ad7752
Handling authentication
adalpari Nov 10, 2025
52ffc2d
Minor fix
adalpari Nov 10, 2025
ec0ac06
Error handling
adalpari Nov 10, 2025
8f5698f
Refactor to get site info
adalpari Nov 10, 2025
8f9129c
updating rs version
adalpari Nov 12, 2025
638d802
Calling createForCurrentUser
adalpari Nov 12, 2025
88dea9c
Fixing AIBot api changes
adalpari Nov 12, 2025
f14246a
Making a real call
adalpari Nov 12, 2025
909a486
Using null appId
adalpari Nov 12, 2025
b336cfd
Minor apiRootUrl fix
adalpari Nov 13, 2025
143e3be
Using WPuuid
adalpari Nov 13, 2025
fd176a8
Minor refactor
adalpari Nov 13, 2025
3b3bb27
Using custom okhttp client with cookie jat
adalpari Nov 17, 2025
170fda7
Extracting into SiteFragment
adalpari Nov 17, 2025
32cf370
Using a dialog for AP creation
adalpari Nov 17, 2025
5bb0792
String change
adalpari Nov 17, 2025
b210131
detekt and style
adalpari Nov 17, 2025
84ad1d0
Merge branch 'trunk' into feat/CMM-942-auto-generate-Application-Pass…
adalpari Nov 17, 2025
77e533b
Claude PR suggestions
adalpari Nov 17, 2025
a691f38
Merge branch 'feat/CMM-942-auto-generate-Application-Password-with-Co…
adalpari Nov 17, 2025
69dcf6e
Adding and fixing tests
adalpari Nov 17, 2025
f948a83
Update libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/r…
adalpari Nov 20, 2025
3a47ab1
Merge branch 'trunk' into feat/CMM-942-auto-generate-Application-Pass…
adalpari Nov 20, 2025
658258c
Merge branch 'trunk' into feat/CMM-942-auto-generate-Application-Pass…
adalpari Nov 20, 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
4 changes: 4 additions & 0 deletions WordPress/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@
android:name=".ui.accounts.login.applicationpassword.ApplicationPasswordRequiredDialogActivity"
android:theme="@style/WordPress.TransparentDialog"
android:exported="false" />
<activity
android:name=".ui.accounts.login.applicationpassword.ApplicationPasswordAutoAuthDialogActivity"
android:theme="@style/WordPress.TransparentDialog"
android:exported="false" />

<activity
android:name=".ui.accounts.LoginMagicLinkInterceptActivity"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import uniffi.wp_api.AddMessageToBotConversationParams
import uniffi.wp_api.BotConversationSummary
import uniffi.wp_api.CreateBotConversationParams
import uniffi.wp_api.GetBotConversationParams
import uniffi.wp_api.ListBotConversationsParams
import uniffi.wp_api.ListBotConversationsSummaryMethod
import java.util.Date
import javax.inject.Inject
import javax.inject.Named
Expand Down Expand Up @@ -54,7 +56,12 @@ class AIBotSupportRepository @Inject constructor(

suspend fun loadConversations(): List<BotConversation> = withContext(ioDispatcher) {
val response = wpComApiClient.request { requestBuilder ->
requestBuilder.supportBots().getBotConverationList(BOT_ID)
requestBuilder.supportBots().getBotConversationList(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These changes have been done because after updating the RS library, the method signature changed. So, I needed to change the call to keeo the project working. No, need to open a new PR for this small change.

botId = BOT_ID,
params = ListBotConversationsParams(
summaryMethod = ListBotConversationsSummaryMethod.LAST_MESSAGE
)
)
}
when (response) {
is WpRequestResult.Success -> {
Expand Down Expand Up @@ -154,8 +161,8 @@ class AIBotSupportRepository @Inject constructor(
BotConversation (
id = chatId.toLong(),
createdAt = createdAt,
mostRecentMessageDate = lastMessage.createdAt,
lastMessage = lastMessage.content,
mostRecentMessageDate = summaryMessage.createdAt,
lastMessage = summaryMessage.content,
messages = listOf()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ class ApplicationPasswordLoginHelper @Inject constructor(
}

companion object {
private const val ANDROID_JETPACK_CLIENT = "android-jetpack-client"
private const val ANDROID_WORDPRESS_CLIENT = "android-wordpress-client"
const val ANDROID_JETPACK_CLIENT = "android-jetpack-client"
const val ANDROID_WORDPRESS_CLIENT = "android-wordpress-client"
private const val JETPACK_SUCCESS_URL = "jetpack://app-pass-authorize"
private const val WORDPRESS_SUCCESS_URL = "wordpress://app-pass-authorize"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package org.wordpress.android.ui.accounts.login.applicationpassword

import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.wordpress.android.R
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.compose.theme.AppThemeM3
import org.wordpress.android.ui.compose.unit.Margin

@AndroidEntryPoint
class ApplicationPasswordAutoAuthDialogActivity : ComponentActivity() {
private val viewModel: ApplicationPasswordAutoAuthDialogViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Get the site from intent extras
val site: SiteModel? = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(EXTRA_SITE, SiteModel::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(EXTRA_SITE)
}

if (site == null) {
finish()
return
}

// Observe navigation events
lifecycleScope.launch {
viewModel.navigationEvent.collect { event ->
when (event) {
is ApplicationPasswordAutoAuthDialogViewModel.NavigationEvent.Success -> {
setResult(RESULT_SUCCESS)
finish()
}
is ApplicationPasswordAutoAuthDialogViewModel.NavigationEvent.Error -> {
setResult(RESULT_ERROR)
finish()
}
}
}
}

setContent {
AppThemeM3 {
val isLoading = viewModel.isLoading.collectAsState()
ApplicationPasswordAutoAuthDialog(
isLoading = isLoading.value,
onDismiss = {
setResult(RESULT_DISMISSED)
finish()
},
onConfirm = { viewModel.createApplicationPassword(site) }
)
}
}
}

companion object {
private const val EXTRA_SITE = "extra_site"
const val RESULT_SUCCESS = -1
const val RESULT_ERROR = -0
const val RESULT_DISMISSED = 1

fun createIntent(context: Context, site: SiteModel): Intent {
return Intent(context, ApplicationPasswordAutoAuthDialogActivity::class.java).apply {
putExtra(EXTRA_SITE, site)
}
}
}
}

@Composable
fun ApplicationPasswordAutoAuthDialog(
isLoading: Boolean,
onDismiss: () -> Unit,
onConfirm: () -> Unit,
) {
var showMore by rememberSaveable { mutableStateOf(false) }

AlertDialog(
onDismissRequest = { if (!isLoading) onDismiss() },
icon = {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(Margin.ExtraLarge.value)
)
},
title = { Text(text = stringResource(R.string.application_password_info_title)) },
text = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(vertical = Margin.Small.value)
) {
Text(text = stringResource(R.string.application_password_info_description_1))

if (!showMore) {
Spacer(modifier = Modifier.height(Margin.Medium.value))
Text(
text = stringResource(R.string.learn_more),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
modifier = Modifier
.clickable { showMore = true }
.padding(vertical = Margin.Small.value)
)
} else {
Spacer(modifier = Modifier.height(Margin.Medium.value))
Text(text = stringResource(R.string.application_password_info_description_2))
Spacer(modifier = Modifier.height(Margin.Medium.value))
Text(text = stringResource(R.string.application_password_info_description_3))
Spacer(modifier = Modifier.height(Margin.Medium.value))
Text(text = stringResource(R.string.application_password_info_description_4))
}
}
},
confirmButton = {
Button(
onClick = onConfirm,
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp
)
} else {
Text(text = stringResource(R.string.create))
}
}
},
dismissButton = {
TextButton(
onClick = onDismiss
) {
Text(text = stringResource(R.string.cancel))
}
}
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.wordpress.android.ui.accounts.login.applicationpassword

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider
import org.wordpress.android.fluxc.utils.AppLogWrapper
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.Companion.ANDROID_JETPACK_CLIENT
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.Companion.ANDROID_WORDPRESS_CLIENT
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.UriLogin
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.BuildConfigWrapper
import rs.wordpress.api.kotlin.WpRequestResult
import uniffi.wp_api.ApplicationPasswordCreateParams
import uniffi.wp_api.WpUuid
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject

@HiltViewModel
class ApplicationPasswordAutoAuthDialogViewModel @Inject constructor(
private val wpApiClientProvider: WpApiClientProvider,
private val applicationPasswordLoginHelper: ApplicationPasswordLoginHelper,
private val buildConfigWrapper: BuildConfigWrapper,
private val appLogWrapper: AppLogWrapper,
) : ViewModel() {
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()

private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()

@Suppress("TooGenericExceptionCaught")
fun createApplicationPassword(site: SiteModel) {
viewModelScope.launch {
try {
require(site.username.isNotBlank()) { "Site username is required for cookie authentication" }
require(site.password.isNotBlank()) { "Site password is required for cookie authentication" }

_isLoading.value = true
val client = wpApiClientProvider.getWpApiClientCookiesNonceAuthentication(
site = site,
)
val appName = if (buildConfigWrapper.isJetpackApp) {
ANDROID_JETPACK_CLIENT
} else {
ANDROID_WORDPRESS_CLIENT
}
val appId = WpUuid()
val response = client.request { requestBuilder ->
requestBuilder.applicationPasswords().createForCurrentUser(
params = ApplicationPasswordCreateParams(
appId = appId.uuidString(),
name =
"$appName-${SimpleDateFormat("yyyy-MM-dd_HH:mm", Locale.getDefault()).format(Date())}"
)
)
}
when (response) {
is WpRequestResult.Success -> {
val name = site.username
val password = response.response.data.password
val apiRootUrl = wpApiClientProvider.getApiRootUrlFrom(site)
applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(
UriLogin(
siteUrl = site.url,
user = name,
password = password,
apiRootUrl = apiRootUrl
)
)
_navigationEvent.emit(NavigationEvent.Success)
}

else -> {
appLogWrapper.e(AppLog.T.API, "Error creating application password")
_navigationEvent.emit(NavigationEvent.Error)
}
}
} catch (e: Exception) {
appLogWrapper.e(AppLog.T.API, "Exception creating application password: ${e.message}")
_navigationEvent.emit(NavigationEvent.Error)
Comment on lines +90 to +92
Copy link

Choose a reason for hiding this comment

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

Error Handling: Catching all exceptions with a generic error message makes debugging difficult. Consider:

  1. Catching specific exception types (NetworkException, AuthException, etc.)
  2. Providing user-friendly error messages based on the exception type
  3. Logging the full stack trace: appLogWrapper.e(AppLog.T.API, "Exception creating application password", e)

This will help both users understand what went wrong and developers debug production issues.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Users cannot take any action if the creation fails. We will redirect them to the webview flow

} finally {
_isLoading.value = false
}
}
}

sealed class NavigationEvent {
object Success : NavigationEvent()
object Error : NavigationEvent()
}
}
Loading