diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml
index 3fe82298701c..f6bdfdc55b21 100644
--- a/WordPress/src/main/AndroidManifest.xml
+++ b/WordPress/src/main/AndroidManifest.xml
@@ -151,6 +151,10 @@
android:name=".ui.accounts.login.applicationpassword.ApplicationPasswordRequiredDialogActivity"
android:theme="@style/WordPress.TransparentDialog"
android:exported="false" />
+
= withContext(ioDispatcher) {
val response = wpComApiClient.request { requestBuilder ->
- requestBuilder.supportBots().getBotConverationList(BOT_ID)
+ requestBuilder.supportBots().getBotConversationList(
+ botId = BOT_ID,
+ params = ListBotConversationsParams(
+ summaryMethod = ListBotConversationsSummaryMethod.LAST_MESSAGE
+ )
+ )
}
when (response) {
is WpRequestResult.Success -> {
@@ -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()
)
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt
index 9619b459f977..c34c6c188fe5 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/ApplicationPasswordLoginHelper.kt
@@ -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"
}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogActivity.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogActivity.kt
new file mode 100644
index 000000000000..a7c6f89ee474
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogActivity.kt
@@ -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))
+ }
+ }
+ )
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogViewModel.kt
new file mode 100644
index 000000000000..30b04d7a7250
--- /dev/null
+++ b/WordPress/src/main/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogViewModel.kt
@@ -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()
+ val navigationEvent: SharedFlow = _navigationEvent.asSharedFlow()
+
+ private val _isLoading = MutableStateFlow(false)
+ val isLoading: StateFlow = _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)
+ } finally {
+ _isLoading.value = false
+ }
+ }
+ }
+
+ sealed class NavigationEvent {
+ object Success : NavigationEvent()
+ object Error : NavigationEvent()
+ }
+}
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
index 091f121e0978..e8d3f0ce136d 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteFragment.kt
@@ -8,6 +8,7 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.View
import android.view.WindowManager
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.StringRes
import androidx.core.text.HtmlCompat
import androidx.core.view.isVisible
@@ -21,6 +22,7 @@ import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.analytics.AnalyticsTracker
import org.wordpress.android.databinding.MySiteFragmentBinding
+import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.QuickStartStore
import org.wordpress.android.ui.ActivityLauncher
@@ -31,6 +33,7 @@ import org.wordpress.android.ui.RequestCodes
import org.wordpress.android.ui.TextInputDialogFragment
import org.wordpress.android.ui.WPWebViewActivity
import org.wordpress.android.ui.accounts.LoginEpilogueActivity
+import org.wordpress.android.ui.accounts.login.applicationpassword.ApplicationPasswordAutoAuthDialogActivity
import org.wordpress.android.ui.bloganuary.learnmore.BloganuaryNudgeLearnMoreOverlayFragment
import org.wordpress.android.ui.deeplinks.DeepLinkingIntentReceiverActivity
import org.wordpress.android.ui.domains.DomainRegistrationActivity
@@ -147,6 +150,26 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
private var binding: MySiteFragmentBinding? = null
private var siteTitle: String? = null
+ private var pendingApplicationPasswordSite: SiteModel? = null
+
+ private val applicationPasswordAutoAuthLauncher = registerForActivityResult(
+ ActivityResultContracts.StartActivityForResult()
+ ) { result ->
+ pendingApplicationPasswordSite?.let { site ->
+ when (result.resultCode) {
+ ApplicationPasswordAutoAuthDialogActivity.RESULT_SUCCESS -> {
+ viewModel.onApplicationPasswordCreated(site)
+ }
+ ApplicationPasswordAutoAuthDialogActivity.RESULT_ERROR -> {
+ viewModel.onApplicationPasswordCreationError(site)
+ }
+ ApplicationPasswordAutoAuthDialogActivity.RESULT_DISMISSED -> {
+ // User dismissed the dialog, no action needed
+ }
+ }
+ pendingApplicationPasswordSite = null
+ }
+ }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -741,6 +764,14 @@ class MySiteFragment : Fragment(R.layout.my_site_fragment),
is SiteNavigationAction.OpenApplicationPasswordAuthentication -> {
activityNavigator.openApplicationPasswordLogin(requireActivity(), action.url)
}
+ is SiteNavigationAction.OpenApplicationPasswordAutoAuthentication -> {
+ pendingApplicationPasswordSite = action.site
+ val intent = ApplicationPasswordAutoAuthDialogActivity.createIntent(
+ requireContext(),
+ action.site
+ )
+ applicationPasswordAutoAuthLauncher.launch(intent)
+ }
}
private fun openBloganuaryNudgeOverlay(isPromptsEnabled: Boolean) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
index 833c166cb6dd..351e4a238f99 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/MySiteViewModel.kt
@@ -53,6 +53,7 @@ import javax.inject.Inject
import javax.inject.Named
import org.wordpress.android.ui.mysite.cards.applicationpassword.ApplicationPasswordViewModelSlice
import org.wordpress.android.ui.posts.GutenbergKitWarmupHelper
+import org.wordpress.android.ui.utils.UiString
@Suppress("LargeClass", "LongMethod", "LongParameterList")
class MySiteViewModel @Inject constructor(
@@ -438,6 +439,34 @@ class MySiteViewModel @Inject constructor(
)
}
+ fun onApplicationPasswordCreated(site: SiteModel) {
+ // Hide the Application Password creation card
+ applicationPasswordViewModelSlice.uiModelMutable.postValue(null)
+ _onSnackbarMessage.postValue(
+ Event(
+ SnackbarMessageHolder(
+ UiString.UiStringResWithParams(
+ R.string.application_password_credentials_stored,
+ UiString.UiStringText(site.url)
+ )
+ )
+ )
+ )
+ }
+
+ fun onApplicationPasswordCreationError(site: SiteModel) {
+ _onSnackbarMessage.postValue(
+ Event(
+ SnackbarMessageHolder(
+ UiString.UiStringResWithParams(
+ R.string.application_password_credentials_storing_error,
+ UiString.UiStringText(site.url)
+ )
+ )
+ )
+ )
+ }
+
// FluxC events
@Subscribe(threadMode = MAIN)
fun onPostUploaded(event: OnPostUploaded) {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt
index 444a2828fb41..7da21cf6d829 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/SiteNavigationAction.kt
@@ -96,6 +96,7 @@ sealed class SiteNavigationAction {
object OpenApplicationPasswordsList : SiteNavigationAction()
data class OpenApplicationPasswordAuthentication(val url: String) : SiteNavigationAction()
+ data class OpenApplicationPasswordAutoAuthentication(val site: SiteModel) : SiteNavigationAction()
}
sealed class BloggingPromptCardNavigationAction: SiteNavigationAction() {
diff --git a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSlice.kt b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSlice.kt
index dec0bf78fb79..e1f6821f371f 100644
--- a/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSlice.kt
+++ b/WordPress/src/main/java/org/wordpress/android/ui/mysite/cards/applicationpassword/ApplicationPasswordViewModelSlice.kt
@@ -15,7 +15,7 @@ import org.wordpress.android.ui.pages.SnackbarMessageHolder
import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures
import org.wordpress.android.ui.prefs.experimentalfeatures.ExperimentalFeatures.Feature
import org.wordpress.android.ui.utils.ListItemInteraction
-import org.wordpress.android.ui.utils.UiString
+import org.wordpress.android.ui.utils.UiString.UiStringRes
import org.wordpress.android.viewmodel.Event
import javax.inject.Inject
@@ -64,19 +64,19 @@ class ApplicationPasswordViewModelSlice @Inject constructor(
if (authorizationUrlComplete.isEmpty()) {
uiModelMutable.postValue(null)
} else {
- postAuthenticationUrl(authorizationUrlComplete)
+ showApplicationPasswordCreateCard(site)
}
}
}
- private fun postAuthenticationUrl(authorizationUrlComplete: String) {
+ private fun showApplicationPasswordCreateCard(site: SiteModel) {
uiModelMutable.postValue(
MySiteCardAndItem.Card.QuickLinksItem(
listOf(
QuickLinkItem(
- label = UiString.UiStringRes(R.string.application_password_title),
+ label = UiStringRes(R.string.application_password_title),
icon = R.drawable.ic_lock_white_24dp,
- onClick = ListItemInteraction.create { onClick(authorizationUrlComplete) }
+ onClick = ListItemInteraction.create { onClick(site) }
)
)
)
@@ -84,10 +84,10 @@ class ApplicationPasswordViewModelSlice @Inject constructor(
}
- private fun onClick(authorizationUrlComplete: String) {
+ private fun onClick(site: SiteModel) {
_onNavigation.postValue(
Event(
- SiteNavigationAction.OpenApplicationPasswordAuthentication(authorizationUrlComplete)
+ SiteNavigationAction.OpenApplicationPasswordAutoAuthentication(site)
)
)
}
diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml
index 76136f8ccb3f..bda818efb71a 100644
--- a/WordPress/src/main/res/values/strings.xml
+++ b/WordPress/src/main/res/values/strings.xml
@@ -5093,6 +5093,7 @@ translators: %s: Select control option value e.g: "Auto, 25%". -->
You can view and manage these passwords in your user profile within your WordPress site\'s admin panel.
Please note that revoking application passwords used by the app will terminate the app\'s access to your site. Be cautious when revoking application passwords created by the app.
Enable
+ Create
Never used
Name:
Created:
diff --git a/WordPress/src/test/java/org/wordpress/android/support/aibot/repository/AIBotSupportRepositoryTest.kt b/WordPress/src/test/java/org/wordpress/android/support/aibot/repository/AIBotSupportRepositoryTest.kt
index e4a5f401f93b..0aff080b9dae 100644
--- a/WordPress/src/test/java/org/wordpress/android/support/aibot/repository/AIBotSupportRepositoryTest.kt
+++ b/WordPress/src/test/java/org/wordpress/android/support/aibot/repository/AIBotSupportRepositoryTest.kt
@@ -22,7 +22,7 @@ import uniffi.wp_api.MessageContext
import uniffi.wp_api.SupportBotsRequestAddMessageToBotConversationResponse
import uniffi.wp_api.SupportBotsRequestCreateBotConversationResponse
import uniffi.wp_api.SupportBotsRequestGetBotConversationResponse
-import uniffi.wp_api.SupportBotsRequestGetBotConverationListResponse
+import uniffi.wp_api.SupportBotsRequestGetBotConversationListResponse
import uniffi.wp_api.UserMessageContext
import uniffi.wp_api.UserPaidSupportEligibility
import uniffi.wp_api.WpNetworkHeaderMap
@@ -64,7 +64,7 @@ class AIBotSupportRepositoryTest : BaseUnitTest() {
)
// Create the actual response type
- val response = SupportBotsRequestGetBotConverationListResponse(
+ val response = SupportBotsRequestGetBotConversationListResponse(
data = testConversations,
headerMap = mock()
)
@@ -72,7 +72,7 @@ class AIBotSupportRepositoryTest : BaseUnitTest() {
val successResponse = WpRequestResult.Success(response = response)
repository.init(testAccessToken, testUserId)
- whenever(wpComApiClient.request(any()))
+ whenever(wpComApiClient.request(any()))
.thenReturn(successResponse)
val result = repository.loadConversations()
@@ -268,7 +268,7 @@ class AIBotSupportRepositoryTest : BaseUnitTest() {
return BotConversationSummary(
chatId = chatId.toULong(),
createdAt = Date(),
- lastMessage = BotMessageSummary(
+ summaryMessage = BotMessageSummary(
content = message,
createdAt = Date(),
role = "user"
diff --git a/WordPress/src/test/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogViewModelTest.kt
new file mode 100644
index 000000000000..095c077046b6
--- /dev/null
+++ b/WordPress/src/test/java/org/wordpress/android/ui/accounts/login/applicationpassword/ApplicationPasswordAutoAuthDialogViewModelTest.kt
@@ -0,0 +1,151 @@
+package org.wordpress.android.ui.accounts.login.applicationpassword
+
+import app.cash.turbine.test
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+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.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.wordpress.android.BaseUnitTest
+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.util.BuildConfigWrapper
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@ExperimentalCoroutinesApi
+class ApplicationPasswordAutoAuthDialogViewModelTest : BaseUnitTest() {
+ @Mock
+ lateinit var wpApiClientProvider: WpApiClientProvider
+
+ @Mock
+ lateinit var applicationPasswordLoginHelper: ApplicationPasswordLoginHelper
+
+ @Mock
+ lateinit var buildConfigWrapper: BuildConfigWrapper
+
+ @Mock
+ lateinit var appLogWrapper: AppLogWrapper
+
+ private lateinit var viewModel: ApplicationPasswordAutoAuthDialogViewModel
+
+ private val testSite = SiteModel().apply {
+ url = "https://example.com"
+ username = "testuser"
+ password = "testpass123"
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.openMocks(this)
+ viewModel = ApplicationPasswordAutoAuthDialogViewModel(
+ wpApiClientProvider,
+ applicationPasswordLoginHelper,
+ buildConfigWrapper,
+ appLogWrapper
+ )
+ }
+
+ @Test
+ fun `createApplicationPassword with exception during API call emits Error`() = runTest {
+ // Given
+ val testException = RuntimeException("API client creation failed")
+ whenever(wpApiClientProvider.getWpApiClientCookiesNonceAuthentication(eq(testSite)))
+ .doThrow(testException)
+
+ // When & Then
+ viewModel.navigationEvent.test {
+ viewModel.isLoading.test {
+ // Initially not loading
+ assertFalse(awaitItem())
+
+ viewModel.createApplicationPassword(testSite)
+
+ // Should become loading
+ assertTrue(awaitItem())
+
+ // Should stop loading even when exception occurs
+ assertFalse(awaitItem())
+
+ cancelAndIgnoreRemainingEvents()
+ }
+
+ // Should emit error event
+ val navigationEvent = awaitItem()
+ assertEquals(
+ ApplicationPasswordAutoAuthDialogViewModel.NavigationEvent.Error,
+ navigationEvent
+ )
+
+ // Should log error with exception message
+ verify(appLogWrapper, times(1)).e(any(), any())
+
+ // Should NOT store credentials
+ verify(applicationPasswordLoginHelper, times(0)).storeApplicationPasswordCredentialsFrom(any())
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `createApplicationPassword with blank username emits Error`() = runTest {
+ // Given
+ val invalidSite = testSite.apply { username = "" }
+
+ // When & Then
+ viewModel.navigationEvent.test {
+ viewModel.createApplicationPassword(invalidSite)
+
+ // Should emit error event
+ val navigationEvent = awaitItem()
+ assertEquals(
+ ApplicationPasswordAutoAuthDialogViewModel.NavigationEvent.Error,
+ navigationEvent
+ )
+
+ // Should log error
+ verify(appLogWrapper, times(1)).e(any(), any())
+
+ // Should NOT make API call
+ verify(wpApiClientProvider, times(0)).getWpApiClientCookiesNonceAuthentication(any())
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+
+ @Test
+ fun `createApplicationPassword with blank password emits Error`() = runTest {
+ // Given
+ val invalidSite = testSite.apply { password = "" }
+
+ // When & Then
+ viewModel.navigationEvent.test {
+ viewModel.createApplicationPassword(invalidSite)
+
+ // Should emit error event
+ val navigationEvent = awaitItem()
+ assertEquals(
+ ApplicationPasswordAutoAuthDialogViewModel.NavigationEvent.Error,
+ navigationEvent
+ )
+
+ // Should log error
+ verify(appLogWrapper, times(1)).e(any(), any())
+
+ // Should NOT make API call
+ verify(wpApiClientProvider, times(0)).getWpApiClientCookiesNonceAuthentication(any())
+
+ cancelAndIgnoreRemainingEvents()
+ }
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0042e92c586d..83e333c7b6b1 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -101,7 +101,7 @@ wellsql = '2.0.0'
wordpress-aztec = 'v2.1.4'
wordpress-lint = '2.2.0'
wordpress-persistent-edittext = '1.0.2'
-wordpress-rs = 'trunk-fb107b497caaf2b1f4ffcf9f487784792561a645'
+wordpress-rs = 'trunk-3d2dafdc1f8b058b4ed9101673fdf690671da73c'
wordpress-utils = '3.14.0'
automattic-ucrop = '2.2.11'
zendesk = '5.5.1'
diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/rs/WpApiClientProvider.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/rs/WpApiClientProvider.kt
index 8fe5542ff4be..b4b351a890fa 100644
--- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/rs/WpApiClientProvider.kt
+++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/rs/WpApiClientProvider.kt
@@ -1,9 +1,15 @@
package org.wordpress.android.fluxc.network.rest.wpapi.rs
+import okhttp3.Cookie
+import okhttp3.CookieJar
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.fluxc.network.rest.wpapi.applicationpasswords.WpAppNotifierHandler
import rs.wordpress.api.kotlin.WpApiClient
+import rs.wordpress.api.kotlin.WpHttpClient
import rs.wordpress.api.kotlin.WpRequestExecutor
+import uniffi.wp_api.CookiesNonceAuthenticationProvider
import uniffi.wp_api.WpAppNotifier
import uniffi.wp_api.WpAuthenticationProvider
import java.net.URL
@@ -33,5 +39,48 @@ class WpApiClientProvider @Inject constructor(
return client
}
- private fun SiteModel.buildUrl(): String = wpApiRestUrl ?: "${url}/wp-json"
+ fun getWpApiClientCookiesNonceAuthentication(site: SiteModel): WpApiClient {
+ // Create OkHttpClient with cookie jar for cookies/nonce authentication
+ val okHttpClient = OkHttpClient.Builder()
+ .cookieJar(object : CookieJar {
+ // We are storing the cookie in memory as this is a one-time call and there is no need to persist it
+ private val cookieStore = mutableMapOf>()
+
+ override fun saveFromResponse(url: HttpUrl, cookies: List) {
+ cookieStore[url.host] = cookies
+ }
+
+ override fun loadForRequest(url: HttpUrl): List {
+ return cookieStore[url.host] ?: emptyList()
+ }
+ })
+ .build()
+
+ val httpClient = WpHttpClient.CustomOkHttpClient(okHttpClient)
+ val requestExecutor = WpRequestExecutor(httpClient)
+
+ val cookiesNonceProvider = CookiesNonceAuthenticationProvider.withSiteUrl(
+ url = site.url,
+ username = site.username,
+ password = site.password,
+ requestExecutor = requestExecutor
+ )
+ val authProvider = WpAuthenticationProvider.dynamic(cookiesNonceProvider)
+ val client = WpApiClient(
+ wpOrgSiteApiRootUrl = URL(site.buildUrl()),
+ authProvider = authProvider,
+ requestExecutor = requestExecutor,
+ appNotifier = object : WpAppNotifier {
+ override suspend fun requestedWithInvalidAuthentication(requestUrl: String) {
+ wpAppNotifierHandler.notifyRequestedWithInvalidAuthentication(site)
+ }
+ }
+ )
+ return client
+ }
+
+ fun getApiRootUrlFrom(site: SiteModel): String = site.buildUrl()
+
+ private fun SiteModel.buildUrl(): String =
+ wpApiRestUrl?.takeIf { it.isNotEmpty() } ?: "${url}/wp-json"
}