Restore revoke action alongside delete in license list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-02 00:44:13 +08:00
parent 399f828ca8
commit 88192e470b
6 changed files with 88 additions and 3 deletions
@@ -15,6 +15,9 @@ interface LicenseApi {
@POST("v1/admin/product-keys/delete")
suspend fun deleteProductKey(@Body body: DeleteProductKeyRequest): DeleteProductKeyResponse
@POST("v1/admin/revoke")
suspend fun revokeLicense(@Body body: RevokeLicenseRequest): RevokeLicenseResponse
@GET("v1/admin/stats/downloads")
suspend fun getDownloadStats(): DownloadStatsOverview
@@ -77,6 +77,14 @@ data class DeleteProductKeyResponse(
val deleted: String? = null,
)
data class RevokeLicenseRequest(
val key: String,
)
data class RevokeLicenseResponse(
val ok: Boolean,
)
data class DownloadStatsMonth(
val month: String,
val windows: Int,
@@ -71,6 +71,10 @@ class LicenseRepository(
createApi(settings).deleteProductKey(DeleteProductKeyRequest(key))
}
suspend fun revokeLicense(settings: AppSettings, key: String): Result<Unit> = runCatching {
createApi(settings).revokeLicense(RevokeLicenseRequest(key))
}
suspend fun getDownloadStatsOverview(settings: AppSettings): Result<DownloadStatsOverview> = runCatching {
createApi(settings).getDownloadStats()
}
@@ -42,6 +42,7 @@ data class LicenseListUiState(
val licenses: List<LicenseEntry> = emptyList(),
val isLoading: Boolean = false,
val deletingKey: String? = null,
val revokingKey: String? = null,
val error: String? = null,
)
@@ -129,6 +130,25 @@ class LicenseViewModel(application: Application) : AndroidViewModel(application)
}
}
fun revokeLicense(key: String) {
viewModelScope.launch {
_listState.update { it.copy(revokingKey = key, error = null) }
licenseRepository.revokeLicense(currentSettings(), key)
.onSuccess {
loadLicenses()
_listState.update { it.copy(revokingKey = null) }
}
.onFailure { e ->
_listState.update {
it.copy(
revokingKey = null,
error = e.message ?: "Не удалось отозвать лицензию",
)
}
}
}
}
fun updatePid(value: String) {
_generateState.update { it.copy(pid = value, error = null) }
}
@@ -47,6 +47,7 @@ import ru.mailib.ttrpg.licensemanager.ui.formatLicenseExpiry
fun LicenseListScreen(viewModel: LicenseViewModel) {
val state by viewModel.listState.collectAsStateWithLifecycle()
var confirmDelete by remember { mutableStateOf<LicenseEntry?>(null) }
var confirmRevoke by remember { mutableStateOf<LicenseEntry?>(null) }
val refreshState = rememberPullRefreshState(
refreshing = state.isLoading,
onRefresh = viewModel::loadLicenses,
@@ -81,6 +82,31 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
)
}
confirmRevoke?.let { license ->
AlertDialog(
onDismissRequest = { confirmRevoke = null },
title = { Text(stringResource(R.string.revoke_key_title)) },
text = {
Text(stringResource(R.string.revoke_key_message, license.key))
},
confirmButton = {
TextButton(
onClick = {
viewModel.revokeLicense(license.key)
confirmRevoke = null
},
) {
Text(stringResource(R.string.revoke))
}
},
dismissButton = {
TextButton(onClick = { confirmRevoke = null }) {
Text(stringResource(R.string.cancel))
}
},
)
}
Box(
modifier = Modifier
.fillMaxSize()
@@ -120,7 +146,9 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
LicenseCard(
license = license,
isDeleting = state.deletingKey == license.key,
isRevoking = state.revokingKey == license.key,
onDelete = { confirmDelete = license },
onRevoke = { confirmRevoke = license },
)
}
}
@@ -139,7 +167,9 @@ fun LicenseListScreen(viewModel: LicenseViewModel) {
private fun LicenseCard(
license: LicenseEntry,
isDeleting: Boolean,
isRevoking: Boolean,
onDelete: () -> Unit,
onRevoke: () -> Unit,
) {
Card(
modifier = Modifier.fillMaxWidth(),
@@ -172,10 +202,27 @@ private fun LicenseCard(
if (license.activatedDevices.isNotEmpty()) {
DetailRow("deviceId", license.activatedDevices.joinToString(", "))
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically,
) {
if (!license.revoked) {
OutlinedButton(
onClick = onRevoke,
enabled = !isDeleting && !isRevoking,
modifier = Modifier.padding(end = 8.dp),
) {
if (isRevoking) {
CircularProgressIndicator(strokeWidth = 2.dp)
} else {
Text(stringResource(R.string.revoke))
}
}
}
OutlinedButton(
onClick = onDelete,
enabled = !isDeleting,
enabled = !isDeleting && !isRevoking,
) {
if (isDeleting) {
CircularProgressIndicator(strokeWidth = 2.dp)
+4 -1
View File
@@ -7,9 +7,12 @@
<string name="settings">Настройки</string>
<string name="delete">Удалить</string>
<string name="revoke">Отозвать</string>
<string name="cancel">Отмена</string>
<string name="delete_key_title">Удалить ключ?</string>
<string name="delete_key_message">Это действие необратимо.</string>
<string name="delete_key_message">Ключ будет удалён с сервера безвозвратно.</string>
<string name="revoke_key_title">Отозвать лицензию?</string>
<string name="revoke_key_message">Ключ %1$s перестанет работать у всех пользователей.</string>
<string name="no_licenses">Лицензий пока нет</string>
<string name="created_at">Создан</string>
<string name="devices">Устройства</string>