Skip to content

Commit 50139f4

Browse files
adalparidcalhoun
andauthored
CMM-942 auto generate application password with cookies nonce authentication (#22352)
* Updating RS library * Providing the client with the new authentication method * Handling password creation * Handling authentication * Minor fix * Error handling * Refactor to get site info * updating rs version * Calling createForCurrentUser * Fixing AIBot api changes * Making a real call * Using null appId * Minor apiRootUrl fix * Using WPuuid * Minor refactor * Using custom okhttp client with cookie jat * Extracting into SiteFragment * Using a dialog for AP creation * String change * detekt and style * Claude PR suggestions * Adding and fixing tests * Update libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpapi/rs/WpApiClientProvider.kt Co-authored-by: David Calhoun <[email protected]> --------- Co-authored-by: David Calhoun <[email protected]>
1 parent f42f392 commit 50139f4

File tree

14 files changed

+574
-18
lines changed

14 files changed

+574
-18
lines changed

WordPress/src/main/AndroidManifest.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,10 @@
151151
android:name=".ui.accounts.login.applicationpassword.ApplicationPasswordRequiredDialogActivity"
152152
android:theme="@style/WordPress.TransparentDialog"
153153
android:exported="false" />
154+
<activity
155+
android:name=".ui.accounts.login.applicationpassword.ApplicationPasswordAutoAuthDialogActivity"
156+
android:theme="@style/WordPress.TransparentDialog"
157+
android:exported="false" />
154158

155159
<activity
156160
android:name=".ui.accounts.LoginMagicLinkInterceptActivity"

WordPress/src/main/java/org/wordpress/android/support/aibot/repository/AIBotSupportRepository.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import uniffi.wp_api.AddMessageToBotConversationParams
1515
import uniffi.wp_api.BotConversationSummary
1616
import uniffi.wp_api.CreateBotConversationParams
1717
import uniffi.wp_api.GetBotConversationParams
18+
import uniffi.wp_api.ListBotConversationsParams
19+
import uniffi.wp_api.ListBotConversationsSummaryMethod
1820
import java.util.Date
1921
import javax.inject.Inject
2022
import javax.inject.Named
@@ -54,7 +56,12 @@ class AIBotSupportRepository @Inject constructor(
5456

5557
suspend fun loadConversations(): List<BotConversation> = withContext(ioDispatcher) {
5658
val response = wpComApiClient.request { requestBuilder ->
57-
requestBuilder.supportBots().getBotConverationList(BOT_ID)
59+
requestBuilder.supportBots().getBotConversationList(
60+
botId = BOT_ID,
61+
params = ListBotConversationsParams(
62+
summaryMethod = ListBotConversationsSummaryMethod.LAST_MESSAGE
63+
)
64+
)
5865
}
5966
when (response) {
6067
is WpRequestResult.Success -> {
@@ -154,8 +161,8 @@ class AIBotSupportRepository @Inject constructor(
154161
BotConversation (
155162
id = chatId.toLong(),
156163
createdAt = createdAt,
157-
mostRecentMessageDate = lastMessage.createdAt,
158-
lastMessage = lastMessage.content,
164+
mostRecentMessageDate = summaryMessage.createdAt,
165+
lastMessage = summaryMessage.content,
159166
messages = listOf()
160167
)
161168

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ class ApplicationPasswordLoginHelper @Inject constructor(
204204
}
205205

206206
companion object {
207-
private const val ANDROID_JETPACK_CLIENT = "android-jetpack-client"
208-
private const val ANDROID_WORDPRESS_CLIENT = "android-wordpress-client"
207+
const val ANDROID_JETPACK_CLIENT = "android-jetpack-client"
208+
const val ANDROID_WORDPRESS_CLIENT = "android-wordpress-client"
209209
private const val JETPACK_SUCCESS_URL = "jetpack://app-pass-authorize"
210210
private const val WORDPRESS_SUCCESS_URL = "wordpress://app-pass-authorize"
211211
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package org.wordpress.android.ui.accounts.login.applicationpassword
2+
3+
import android.content.Context
4+
import android.content.Intent
5+
import android.os.Bundle
6+
import androidx.activity.ComponentActivity
7+
import androidx.activity.compose.setContent
8+
import androidx.activity.viewModels
9+
import androidx.compose.foundation.clickable
10+
import androidx.compose.foundation.layout.Column
11+
import androidx.compose.foundation.layout.Spacer
12+
import androidx.compose.foundation.layout.height
13+
import androidx.compose.foundation.layout.padding
14+
import androidx.compose.foundation.layout.size
15+
import androidx.compose.foundation.rememberScrollState
16+
import androidx.compose.foundation.verticalScroll
17+
import androidx.compose.material.icons.Icons
18+
import androidx.compose.material.icons.outlined.Info
19+
import androidx.compose.material3.AlertDialog
20+
import androidx.compose.material3.Button
21+
import androidx.compose.material3.CircularProgressIndicator
22+
import androidx.compose.material3.Icon
23+
import androidx.compose.material3.MaterialTheme
24+
import androidx.compose.material3.Text
25+
import androidx.compose.material3.TextButton
26+
import androidx.compose.runtime.Composable
27+
import androidx.compose.runtime.collectAsState
28+
import androidx.compose.runtime.getValue
29+
import androidx.compose.runtime.mutableStateOf
30+
import androidx.compose.runtime.saveable.rememberSaveable
31+
import androidx.compose.runtime.setValue
32+
import androidx.compose.ui.Modifier
33+
import androidx.compose.ui.res.stringResource
34+
import androidx.compose.ui.text.style.TextDecoration
35+
import androidx.compose.ui.unit.dp
36+
import androidx.lifecycle.lifecycleScope
37+
import dagger.hilt.android.AndroidEntryPoint
38+
import kotlinx.coroutines.launch
39+
import org.wordpress.android.R
40+
import org.wordpress.android.fluxc.model.SiteModel
41+
import org.wordpress.android.ui.compose.theme.AppThemeM3
42+
import org.wordpress.android.ui.compose.unit.Margin
43+
44+
@AndroidEntryPoint
45+
class ApplicationPasswordAutoAuthDialogActivity : ComponentActivity() {
46+
private val viewModel: ApplicationPasswordAutoAuthDialogViewModel by viewModels()
47+
48+
override fun onCreate(savedInstanceState: Bundle?) {
49+
super.onCreate(savedInstanceState)
50+
51+
// Get the site from intent extras
52+
val site: SiteModel? = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
53+
intent.getParcelableExtra(EXTRA_SITE, SiteModel::class.java)
54+
} else {
55+
@Suppress("DEPRECATION")
56+
intent.getParcelableExtra(EXTRA_SITE)
57+
}
58+
59+
if (site == null) {
60+
finish()
61+
return
62+
}
63+
64+
// Observe navigation events
65+
lifecycleScope.launch {
66+
viewModel.navigationEvent.collect { event ->
67+
when (event) {
68+
is ApplicationPasswordAutoAuthDialogViewModel.NavigationEvent.Success -> {
69+
setResult(RESULT_SUCCESS)
70+
finish()
71+
}
72+
is ApplicationPasswordAutoAuthDialogViewModel.NavigationEvent.Error -> {
73+
setResult(RESULT_ERROR)
74+
finish()
75+
}
76+
}
77+
}
78+
}
79+
80+
setContent {
81+
AppThemeM3 {
82+
val isLoading = viewModel.isLoading.collectAsState()
83+
ApplicationPasswordAutoAuthDialog(
84+
isLoading = isLoading.value,
85+
onDismiss = {
86+
setResult(RESULT_DISMISSED)
87+
finish()
88+
},
89+
onConfirm = { viewModel.createApplicationPassword(site) }
90+
)
91+
}
92+
}
93+
}
94+
95+
companion object {
96+
private const val EXTRA_SITE = "extra_site"
97+
const val RESULT_SUCCESS = -1
98+
const val RESULT_ERROR = -0
99+
const val RESULT_DISMISSED = 1
100+
101+
fun createIntent(context: Context, site: SiteModel): Intent {
102+
return Intent(context, ApplicationPasswordAutoAuthDialogActivity::class.java).apply {
103+
putExtra(EXTRA_SITE, site)
104+
}
105+
}
106+
}
107+
}
108+
109+
@Composable
110+
fun ApplicationPasswordAutoAuthDialog(
111+
isLoading: Boolean,
112+
onDismiss: () -> Unit,
113+
onConfirm: () -> Unit,
114+
) {
115+
var showMore by rememberSaveable { mutableStateOf(false) }
116+
117+
AlertDialog(
118+
onDismissRequest = { if (!isLoading) onDismiss() },
119+
icon = {
120+
Icon(
121+
imageVector = Icons.Outlined.Info,
122+
contentDescription = null,
123+
tint = MaterialTheme.colorScheme.primary,
124+
modifier = Modifier.size(Margin.ExtraLarge.value)
125+
)
126+
},
127+
title = { Text(text = stringResource(R.string.application_password_info_title)) },
128+
text = {
129+
Column(
130+
modifier = Modifier
131+
.verticalScroll(rememberScrollState())
132+
.padding(vertical = Margin.Small.value)
133+
) {
134+
Text(text = stringResource(R.string.application_password_info_description_1))
135+
136+
if (!showMore) {
137+
Spacer(modifier = Modifier.height(Margin.Medium.value))
138+
Text(
139+
text = stringResource(R.string.learn_more),
140+
style = MaterialTheme.typography.bodyMedium,
141+
color = MaterialTheme.colorScheme.primary,
142+
textDecoration = TextDecoration.Underline,
143+
modifier = Modifier
144+
.clickable { showMore = true }
145+
.padding(vertical = Margin.Small.value)
146+
)
147+
} else {
148+
Spacer(modifier = Modifier.height(Margin.Medium.value))
149+
Text(text = stringResource(R.string.application_password_info_description_2))
150+
Spacer(modifier = Modifier.height(Margin.Medium.value))
151+
Text(text = stringResource(R.string.application_password_info_description_3))
152+
Spacer(modifier = Modifier.height(Margin.Medium.value))
153+
Text(text = stringResource(R.string.application_password_info_description_4))
154+
}
155+
}
156+
},
157+
confirmButton = {
158+
Button(
159+
onClick = onConfirm,
160+
enabled = !isLoading
161+
) {
162+
if (isLoading) {
163+
CircularProgressIndicator(
164+
modifier = Modifier.size(16.dp),
165+
strokeWidth = 2.dp
166+
)
167+
} else {
168+
Text(text = stringResource(R.string.create))
169+
}
170+
}
171+
},
172+
dismissButton = {
173+
TextButton(
174+
onClick = onDismiss
175+
) {
176+
Text(text = stringResource(R.string.cancel))
177+
}
178+
}
179+
)
180+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package org.wordpress.android.ui.accounts.login.applicationpassword
2+
3+
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
5+
import dagger.hilt.android.lifecycle.HiltViewModel
6+
import kotlinx.coroutines.flow.MutableSharedFlow
7+
import kotlinx.coroutines.flow.MutableStateFlow
8+
import kotlinx.coroutines.flow.SharedFlow
9+
import kotlinx.coroutines.flow.StateFlow
10+
import kotlinx.coroutines.flow.asSharedFlow
11+
import kotlinx.coroutines.flow.asStateFlow
12+
import kotlinx.coroutines.launch
13+
import org.wordpress.android.fluxc.model.SiteModel
14+
import org.wordpress.android.fluxc.network.rest.wpapi.rs.WpApiClientProvider
15+
import org.wordpress.android.fluxc.utils.AppLogWrapper
16+
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper
17+
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.Companion.ANDROID_JETPACK_CLIENT
18+
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.Companion.ANDROID_WORDPRESS_CLIENT
19+
import org.wordpress.android.ui.accounts.login.ApplicationPasswordLoginHelper.UriLogin
20+
import org.wordpress.android.util.AppLog
21+
import org.wordpress.android.util.BuildConfigWrapper
22+
import rs.wordpress.api.kotlin.WpRequestResult
23+
import uniffi.wp_api.ApplicationPasswordCreateParams
24+
import uniffi.wp_api.WpUuid
25+
import java.text.SimpleDateFormat
26+
import java.util.Date
27+
import java.util.Locale
28+
import javax.inject.Inject
29+
30+
@HiltViewModel
31+
class ApplicationPasswordAutoAuthDialogViewModel @Inject constructor(
32+
private val wpApiClientProvider: WpApiClientProvider,
33+
private val applicationPasswordLoginHelper: ApplicationPasswordLoginHelper,
34+
private val buildConfigWrapper: BuildConfigWrapper,
35+
private val appLogWrapper: AppLogWrapper,
36+
) : ViewModel() {
37+
private val _navigationEvent = MutableSharedFlow<NavigationEvent>()
38+
val navigationEvent: SharedFlow<NavigationEvent> = _navigationEvent.asSharedFlow()
39+
40+
private val _isLoading = MutableStateFlow(false)
41+
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
42+
43+
@Suppress("TooGenericExceptionCaught")
44+
fun createApplicationPassword(site: SiteModel) {
45+
viewModelScope.launch {
46+
try {
47+
require(site.username.isNotBlank()) { "Site username is required for cookie authentication" }
48+
require(site.password.isNotBlank()) { "Site password is required for cookie authentication" }
49+
50+
_isLoading.value = true
51+
val client = wpApiClientProvider.getWpApiClientCookiesNonceAuthentication(
52+
site = site,
53+
)
54+
val appName = if (buildConfigWrapper.isJetpackApp) {
55+
ANDROID_JETPACK_CLIENT
56+
} else {
57+
ANDROID_WORDPRESS_CLIENT
58+
}
59+
val appId = WpUuid()
60+
val response = client.request { requestBuilder ->
61+
requestBuilder.applicationPasswords().createForCurrentUser(
62+
params = ApplicationPasswordCreateParams(
63+
appId = appId.uuidString(),
64+
name =
65+
"$appName-${SimpleDateFormat("yyyy-MM-dd_HH:mm", Locale.getDefault()).format(Date())}"
66+
)
67+
)
68+
}
69+
when (response) {
70+
is WpRequestResult.Success -> {
71+
val name = site.username
72+
val password = response.response.data.password
73+
val apiRootUrl = wpApiClientProvider.getApiRootUrlFrom(site)
74+
applicationPasswordLoginHelper.storeApplicationPasswordCredentialsFrom(
75+
UriLogin(
76+
siteUrl = site.url,
77+
user = name,
78+
password = password,
79+
apiRootUrl = apiRootUrl
80+
)
81+
)
82+
_navigationEvent.emit(NavigationEvent.Success)
83+
}
84+
85+
else -> {
86+
appLogWrapper.e(AppLog.T.API, "Error creating application password")
87+
_navigationEvent.emit(NavigationEvent.Error)
88+
}
89+
}
90+
} catch (e: Exception) {
91+
appLogWrapper.e(AppLog.T.API, "Exception creating application password: ${e.message}")
92+
_navigationEvent.emit(NavigationEvent.Error)
93+
} finally {
94+
_isLoading.value = false
95+
}
96+
}
97+
}
98+
99+
sealed class NavigationEvent {
100+
object Success : NavigationEvent()
101+
object Error : NavigationEvent()
102+
}
103+
}

0 commit comments

Comments
 (0)