-
Notifications
You must be signed in to change notification settings - Fork 1.3k
CMM-942 auto generate application password with cookies nonce authentication #22352
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
Changes from all commits
edb4b59
971444d
f74bcc6
6ad7752
52ffc2d
ec0ac06
8f5698f
8f9129c
638d802
88dea9c
f14246a
909a486
b336cfd
143e3be
fd176a8
3b3bb27
170fda7
32cf370
5bb0792
b210131
84ad1d0
77e533b
a691f38
69dcf6e
f948a83
3a47ab1
658258c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
dcalhoun marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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() }, | ||
adalpari marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
This will help both users understand what went wrong and developers debug production issues.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.