Merge #290a348d: feat(permissions): add option to pre-approve permissio…

feat(permissions): add option to pre-approve permissions under manual approval

nostr:nevent1qqszjz353hwg2g3pcmf8h6qmc49af4ce0mvj057r9zxk6pffjh30s8gpz3mhxue69uhhyetvv9ujumn8d96zuer9wcxmzkuh

PR-Author: greenart7c3
nostr:npub1w4uswmv6lu9yel005l3qgheysmr7tk9uvwluddznju3nuxalevvs2d0jr5

PR description:

## What

When connecting a new app while the `Manually approve each permission` policy is selected, a new `Add permission` button lets the user pick extra permissions to be approved automatically 342200224 everything else keeps asking.

## Changes

- New `AddPermissionsSheet` bottom sheet: searchable, deduped list of the full supported permission catalog with checkbox multi-select
- Custom `sign_event` kind input (numeric-only) for kinds not in the catalog, with validation against existing permissions and in-session duplicates
- Wired into all three surfaces:
 - `LoginWithPubKey` (nostrsigner:// intent connect flow)
 - `BunkerConnectRequestScreen` (NIP-46 connect flow)
 - `EditPermission` screen 342200224 persisted as Allow / Always rows, still tunable afterwards
- 6 new strings translated in all 14 locales

## Validation

- ktlintCheck 342234205
- lintFreeDebug 342234205
- testFreeDebugUnitTest 342234205
This commit is contained in:
greenart7c3
2026-08-24 10:32:47 -03:00
18 changed files with 518 additions and 60 deletions
@@ -51,6 +51,7 @@ import com.greenart7c3.nostrsigner.models.Account
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.SignerType
import com.greenart7c3.nostrsigner.ui.actions.RemoveAllPermissionsDialog
import com.greenart7c3.nostrsigner.ui.components.AddPermissionsSheet
import com.greenart7c3.nostrsigner.ui.components.AmberButton
import com.greenart7c3.nostrsigner.ui.components.AmberToggles
import com.greenart7c3.nostrsigner.ui.components.LabeledBorderBox
@@ -77,6 +78,7 @@ fun EditPermission(
}
var wantsToRemovePermissions by remember { mutableStateOf(false) }
var showAddPermissions by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var checked by remember {
@@ -194,6 +196,45 @@ fun EditPermission(
text = stringResource(R.string.edit_permissions_description),
)
if (applicationData.key.isNotEmpty()) {
if (showAddPermissions) {
AddPermissionsSheet(
existingPermissions = permissions.map { Permission(it.type, it.kind) },
onAdd = { addedPermissions ->
val newPermissions = addedPermissions.map {
ApplicationPermissionsEntity(
null,
applicationData.key,
it.type.uppercase(),
it.kind,
true,
RememberType.ALWAYS.screenCode,
Long.MAX_VALUE / 1000,
0,
)
}
permissions.addAll(newPermissions)
scope.launch(Dispatchers.IO) {
Amber.instance
.dao(account.npub)
.insertPermissions(newPermissions)
}
},
onDismiss = {
showAddPermissions = false
},
)
}
AmberButton(
modifier = Modifier.padding(bottom = 20.dp),
onClick = {
showAddPermissions = true
},
text = stringResource(R.string.add_permission),
)
}
permissions.forEach { permission ->
PermissionRow(
permission = permission,
@@ -0,0 +1,265 @@
package com.greenart7c3.nostrsigner.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.greenart7c3.nostrsigner.R
import com.greenart7c3.nostrsigner.models.Permission
import com.greenart7c3.nostrsigner.models.supportedKindNumbers
import com.greenart7c3.nostrsigner.ui.theme.AmberPreview
import com.greenart7c3.nostrsigner.ui.theme.ThemePreviews
private fun permissionKey(permission: Permission) = "${permission.type}-${permission.kind}"
/**
* Bottom sheet that lets the user pick permissions from the full supported list to
* be approved automatically, excluding the ones the application already has.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddPermissionsSheet(
existingPermissions: List<Permission>,
onAdd: (List<Permission>) -> Unit,
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
val candidates = remember(existingPermissions) {
supportedKindNumbers
.distinctBy { permissionKey(it) }
.filterNot { candidate ->
existingPermissions.any {
it.type.equals(candidate.type, ignoreCase = true) && it.kind == candidate.kind
}
}
}
val selectedPermissions = remember { mutableStateMapOf<String, Boolean>() }
var search by remember { mutableStateOf("") }
// Custom sign_event kinds typed by the user, added on top of the catalog list.
val customPermissions = remember { mutableStateListOf<Permission>() }
var customKind by remember { mutableStateOf("") }
val parsedCustomKind = customKind.toIntOrNull()
val customKindExists = parsedCustomKind != null &&
existingPermissions.any {
it.type.equals("sign_event", ignoreCase = true) && it.kind == parsedCustomKind
}
val canAddCustomKind = parsedCustomKind != null &&
!customKindExists &&
customPermissions.none { it.kind == parsedCustomKind }
val customCandidates = customPermissions.toList()
val allCandidates = customCandidates + candidates
val filteredPermissions = remember(allCandidates, search) {
if (search.isBlank()) {
allCandidates
} else {
allCandidates.filter {
it.toLocalizedString(context, true).contains(search, ignoreCase = true) ||
it.kind.toString().contains(search) ||
it.type.contains(search, ignoreCase = true)
}
}
}
val hasSelection = selectedPermissions.values.any { it }
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = onDismiss,
) {
Scaffold(
bottomBar = {
BottomAppBar {
Column {
AmberButton(
modifier = Modifier.padding(horizontal = 16.dp),
enabled = hasSelection,
onClick = {
onAdd(
allCandidates.filter { selectedPermissions[permissionKey(it)] == true },
)
onDismiss()
},
text = stringResource(R.string.add),
)
IconRow(
center = true,
title = stringResource(R.string.go_back),
icon = ImageVector.vectorResource(R.drawable.back),
onClick = onDismiss,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
},
) { padding ->
Column(
Modifier
.padding(padding)
.fillMaxWidth(),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
text = stringResource(R.string.add_permission_description),
style = MaterialTheme.typography.bodyMedium,
)
OutlinedTextField(
value = search,
onValueChange = { search = it },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
placeholder = { Text(stringResource(R.string.search_permissions)) },
leadingIcon = {
Icon(Icons.Default.Search, contentDescription = null)
},
singleLine = true,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
OutlinedTextField(
value = customKind,
onValueChange = { customKind = it.filter { char -> char.isDigit() } },
modifier = Modifier.weight(1f),
label = { Text(stringResource(R.string.custom_event_kind)) },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
isError = customKindExists,
supportingText = if (customKindExists) {
{ Text(stringResource(R.string.custom_event_kind_already_added)) }
} else {
null
},
)
TextButton(
enabled = canAddCustomKind,
onClick = {
val kind = parsedCustomKind ?: return@TextButton
val permission = Permission("sign_event", kind)
customPermissions.add(permission)
selectedPermissions[permissionKey(permission)] = true
customKind = ""
},
) {
Text(stringResource(R.string.add))
}
}
Spacer(Modifier.height(8.dp))
if (filteredPermissions.isEmpty()) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = stringResource(R.string.no_permissions_found),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
LazyColumn(
Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
contentPadding = PaddingValues(horizontal = 8.dp),
) {
items(
filteredPermissions,
key = { permissionKey(it) },
) { item ->
val itemKey = permissionKey(item)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.clickable {
selectedPermissions[itemKey] = !(selectedPermissions[itemKey] ?: false)
}
.padding(vertical = 2.dp),
) {
Checkbox(
checked = selectedPermissions[itemKey] ?: false,
onCheckedChange = {
selectedPermissions[itemKey] = it
},
)
Text(
text = item.toLocalizedString(context, true),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
}
}
}
}
@ThemePreviews
@Composable
fun AddPermissionsSheetPreview() {
AmberPreview {
AddPermissionsSheet(
existingPermissions = listOf(
Permission("get_public_key", null),
Permission("sign_event", 1),
),
onAdd = {},
onDismiss = {},
)
}
}
@@ -155,6 +155,7 @@ fun BunkerConnectRequestScreen(
var deleteAfterIndex by remember { mutableIntStateOf(DeleteAfterType.NEVER.screenCode) }
var closeApp by remember { mutableStateOf(shouldCloseApp) }
var showModal by remember { mutableStateOf(false) }
var showAddPermissions by remember { mutableStateOf(false) }
var advancedOpen by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
@@ -281,7 +282,58 @@ fun BunkerConnectRequestScreen(
},
)
if (selectedOption == 1 && localPermissions.isNotEmpty()) {
if (selectedOption == 1) {
if (localPermissions.isNotEmpty()) {
Box(
Modifier
.fillMaxWidth()
.padding(top = 4.dp),
contentAlignment = Alignment.Center,
) {
ElevatedButton(
colors = ButtonDefaults.buttonColors().copy(
contentColor = Color.Black,
),
shape = RoundedCornerShape(20),
content = {
Text(stringResource(R.string.permissions))
},
onClick = {
showModal = true
},
)
}
if (showModal) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {
showModal = false
},
) {
Scaffold(
bottomBar = {
BottomAppBar {
IconRow(
center = true,
title = stringResource(R.string.go_back),
icon = ImageVector.vectorResource(R.drawable.back),
onClick = {
showModal = false
},
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
) {
EnabledPermissions(
Modifier.padding(it),
localPermissions,
)
}
}
}
}
Box(
Modifier
.fillMaxWidth()
@@ -294,41 +346,23 @@ fun BunkerConnectRequestScreen(
),
shape = RoundedCornerShape(20),
content = {
Text(stringResource(R.string.permissions))
Text(stringResource(R.string.add_permission))
},
onClick = {
showModal = true
showAddPermissions = true
},
)
}
if (showModal) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {
showModal = false
if (showAddPermissions) {
AddPermissionsSheet(
existingPermissions = localPermissions,
onAdd = { addedPermissions ->
localPermissions.addAll(addedPermissions)
},
) {
Scaffold(
bottomBar = {
BottomAppBar {
IconRow(
center = true,
title = stringResource(R.string.go_back),
icon = ImageVector.vectorResource(R.drawable.back),
onClick = {
showModal = false
},
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
) {
EnabledPermissions(
Modifier.padding(it),
localPermissions,
)
}
}
onDismiss = {
showAddPermissions = false
},
)
}
}
@@ -306,6 +306,7 @@ fun LoginWithPubKey(
)
}
var showModal by remember { mutableStateOf(false) }
var showAddPermissions by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true,
)
@@ -365,7 +366,58 @@ fun LoginWithPubKey(
},
)
if (selectedOption == 1 && localPermissions.isNotEmpty()) {
if (selectedOption == 1) {
if (localPermissions.isNotEmpty()) {
Box(
Modifier
.fillMaxWidth()
.padding(top = 4.dp),
contentAlignment = Alignment.Center,
) {
ElevatedButton(
colors = ButtonDefaults.buttonColors().copy(
contentColor = Color.Black,
),
shape = RoundedCornerShape(20),
content = {
Text(stringResource(R.string.permissions))
},
onClick = {
showModal = true
},
)
}
if (showModal) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {
showModal = false
},
) {
Scaffold(
bottomBar = {
BottomAppBar {
IconRow(
center = true,
title = stringResource(R.string.go_back),
icon = ImageVector.vectorResource(R.drawable.back),
onClick = {
showModal = false
},
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
) {
EnabledPermissions(
Modifier.padding(it),
localPermissions,
)
}
}
}
}
Box(
Modifier
.fillMaxWidth()
@@ -378,41 +430,23 @@ fun LoginWithPubKey(
),
shape = RoundedCornerShape(20),
content = {
Text(stringResource(R.string.permissions))
Text(stringResource(R.string.add_permission))
},
onClick = {
showModal = true
showAddPermissions = true
},
)
}
if (showModal) {
ModalBottomSheet(
sheetState = sheetState,
onDismissRequest = {
showModal = false
if (showAddPermissions) {
AddPermissionsSheet(
existingPermissions = localPermissions,
onAdd = { addedPermissions ->
localPermissions.addAll(addedPermissions)
},
) {
Scaffold(
bottomBar = {
BottomAppBar {
IconRow(
center = true,
title = stringResource(R.string.go_back),
icon = ImageVector.vectorResource(R.drawable.back),
onClick = {
showModal = false
},
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
},
) {
EnabledPermissions(
Modifier.padding(it),
localPermissions,
)
}
}
onDismiss = {
showAddPermissions = false
},
)
}
}
}
+6
View File
@@ -50,6 +50,12 @@
<string name="biometric_authentication_failed_explainer_with_error">Biometrien konnten den Besitzer dieses Telefons nicht authentifizieren. Fehler: %1$s</string>
<string name="delete">Löschen</string>
<string name="permissions">Berechtigungen</string>
<string name="add_permission">Berechtigung hinzufügen</string>
<string name="add_permission_description">Wähle die Berechtigungen aus, die für diese Anwendung automatisch genehmigt werden sollen.</string>
<string name="search_permissions">Berechtigungen suchen</string>
<string name="no_permissions_found">Keine Berechtigungen gefunden</string>
<string name="custom_event_kind">Benutzerdefinierte Ereignis-Art</string>
<string name="custom_event_kind_already_added">Diese Berechtigung ist bereits in der Liste</string>
<string name="delete_application">Anwendung löschen</string>
<string name="reset_bunker">Bunker zurücksetzen</string>
<string name="copy_to_clipboard">In Zwischenablage kopieren</string>
+6
View File
@@ -52,6 +52,12 @@
<string name="biometric_authentication_failed_explainer_with_error">La biometría no ha podido autenticar al propietario de este teléfono. Error: %1$s</string>
<string name="delete">Borrar</string>
<string name="permissions">Permisos</string>
<string name="add_permission">Añadir permiso</string>
<string name="add_permission_description">Selecciona los permisos que quieres que se aprueben automáticamente para esta aplicación.</string>
<string name="search_permissions">Buscar permisos</string>
<string name="no_permissions_found">No se encontraron permisos</string>
<string name="custom_event_kind">Tipo de evento personalizado</string>
<string name="custom_event_kind_already_added">Este permiso ya está en la lista</string>
<string name="delete_application">Eliminar aplicación</string>
<string name="reset_bunker">Restablecer Bunker</string>
<string name="copy_to_clipboard">Copiar al portapapeles</string>
+6
View File
@@ -50,6 +50,12 @@
<string name="biometric_authentication_failed_explainer_with_error">La biométrie n\'a pas réussi à authentifier le propriétaire de ce téléphone. Erreur : %1$s</string>
<string name="delete">Supprimer</string>
<string name="permissions">Autorisations</string>
<string name="add_permission">Ajouter une autorisation</string>
<string name="add_permission_description">Sélectionnez les autorisations que vous souhaitez voir approuvées automatiquement pour cette application.</string>
<string name="search_permissions">Rechercher des autorisations</string>
<string name="no_permissions_found">Aucune autorisation trouvée</string>
<string name="custom_event_kind">Type d\'événement personnalisé</string>
<string name="custom_event_kind_already_added">Cette autorisation est déjà dans la liste</string>
<string name="delete_application">Supprimer l\'application</string>
<string name="reset_bunker">Réinitialiser le Bunker</string>
<string name="copy_to_clipboard">Copier dans le presse-papiers</string>
+6
View File
@@ -52,6 +52,12 @@
<string name="biometric_authentication_failed_explainer_with_error">Biometrik gagal mengautentikasi pemilik ponsel ini. Error: %1$s</string>
<string name="delete">Hapus</string>
<string name="permissions">Izin</string>
<string name="add_permission">Tambahkan izin</string>
<string name="add_permission_description">Pilih izin yang ingin disetujui otomatis untuk aplikasi ini.</string>
<string name="search_permissions">Cari izin</string>
<string name="no_permissions_found">Tidak ada izin ditemukan</string>
<string name="custom_event_kind">Jenis event kustom</string>
<string name="custom_event_kind_already_added">Izin ini sudah ada di daftar</string>
<string name="delete_application">Hapus Aplikasi</string>
<string name="reset_bunker">Atur Ulang Bunker</string>
<string name="copy_to_clipboard">Salin ke clipboard</string>
@@ -52,6 +52,12 @@
<string name="biometric_authentication_failed_explainer_with_error">La biometria non è riuscita ad autenticare il proprietario del telefono. Errore: %1$s</string>
<string name="delete">Elimina</string>
<string name="permissions">Permessi</string>
<string name="add_permission">Aggiungi permesso</string>
<string name="add_permission_description">Seleziona i permessi che vuoi approvare automaticamente per questa applicazione.</string>
<string name="search_permissions">Cerca permessi</string>
<string name="no_permissions_found">Nessun permesso trovato</string>
<string name="custom_event_kind">Tipo di evento personalizzato</string>
<string name="custom_event_kind_already_added">Questo permesso è già nell\'elenco</string>
<string name="delete_application">Elimina applicazione</string>
<string name="reset_bunker">Reimposta Bunker</string>
<string name="copy_to_clipboard">Copia negli appunti</string>
+6
View File
@@ -43,6 +43,12 @@
<string name="biometric_authentication_failed_explainer_with_error">生体認証でこの端末の所有者を確認できませんでした。エラー: %1$s</string>
<string name="delete">削除</string>
<string name="permissions">権限</string>
<string name="add_permission">権限を追加</string>
<string name="add_permission_description">このアプリケーションで自動的に承認したい権限を選択してください。</string>
<string name="search_permissions">権限を検索</string>
<string name="no_permissions_found">権限が見つかりません</string>
<string name="custom_event_kind">カスタムイベント種別</string>
<string name="custom_event_kind_already_added">この権限はすでにリストにあります</string>
<string name="delete_application">アプリケーションを削除</string>
<string name="reset_bunker">Bunker をリセット</string>
<string name="copy_to_clipboard">クリップボードにコピー</string>
@@ -52,6 +52,12 @@
<string name="biometric_authentication_failed_explainer_with_error">생체 인식으로 이 휴대폰 소유자 인증에 실패했습니다. 오류: %1$s</string>
<string name="delete">삭제</string>
<string name="permissions">권한</string>
<string name="add_permission">권한 추가</string>
<string name="add_permission_description">이 애플리케이션에 대해 자동으로 승인할 권한을 선택하세요.</string>
<string name="search_permissions">권한 검색</string>
<string name="no_permissions_found">권한을 찾을 수 없습니다</string>
<string name="custom_event_kind">사용자 지정 이벤트 종류</string>
<string name="custom_event_kind_already_added">이 권한은 이미 목록에 있습니다</string>
<string name="delete_application">애플리케이션 삭제</string>
<string name="reset_bunker">Bunker 초기화</string>
<string name="copy_to_clipboard">클립보드에 복사</string>
@@ -48,6 +48,12 @@
<string name="biometric_authentication_failed_explainer_with_error">A biometria não conseguiu autenticar o proprietário deste telefone. Erro: %1$s</string>
<string name="delete">Excluir</string>
<string name="permissions">Permissões</string>
<string name="add_permission">Adicionar permissão</string>
<string name="add_permission_description">Selecione as permissões que você deseja que sejam aprovadas automaticamente para este aplicativo.</string>
<string name="search_permissions">Pesquisar permissões</string>
<string name="no_permissions_found">Nenhuma permissão encontrada</string>
<string name="custom_event_kind">Tipo de evento personalizado</string>
<string name="custom_event_kind_already_added">Esta permissão já está na lista</string>
<string name="delete_application">Excluir aplicativo</string>
<string name="reset_bunker">Redefinir Bunker</string>
<string name="copy_to_clipboard">Copiar para área de transferência</string>
+6
View File
@@ -52,6 +52,12 @@
<string name="biometric_authentication_failed_explainer_with_error">Биометрия не смогла подтвердить владельца этого телефона. Ошибка: %1$s</string>
<string name="delete">Удалить</string>
<string name="permissions">Разрешения</string>
<string name="add_permission">Добавить разрешение</string>
<string name="add_permission_description">Выберите разрешения, которые должны автоматически одобряться для этого приложения.</string>
<string name="search_permissions">Поиск разрешений</string>
<string name="no_permissions_found">Разрешения не найдены</string>
<string name="custom_event_kind">Пользовательский тип события</string>
<string name="custom_event_kind_already_added">Это разрешение уже есть в списке</string>
<string name="delete_application">Удалить приложение</string>
<string name="reset_bunker">Сбросить Bunker</string>
<string name="copy_to_clipboard">Копировать в буфер обмена</string>
+6
View File
@@ -43,6 +43,12 @@
<string name="biometric_authentication_failed_explainer_with_error">ไบโอเมตริกไม่สามารถยืนยันตัวตนเจ้าของโทรศัพท์เครื่องนี้ได้ ข้อผิดพลาด: %1$s</string>
<string name="delete">ลบ</string>
<string name="permissions">สิทธิ์</string>
<string name="add_permission">เพิ่มสิทธิ์</string>
<string name="add_permission_description">เลือกสิทธิ์ที่คุณต้องการให้อนุมัติโดยอัตโนมัติสำหรับแอปพลิเคชันนี้</string>
<string name="search_permissions">ค้นหาสิทธิ์</string>
<string name="no_permissions_found">ไม่พบสิทธิ์</string>
<string name="custom_event_kind">ชนิดเหตุการณ์แบบกำหนดเอง</string>
<string name="custom_event_kind_already_added">สิทธิ์นี้อยู่ในรายการแล้ว</string>
<string name="delete_application">ลบแอปพลิเคชัน</string>
<string name="reset_bunker">รีเซ็ต Bunker</string>
<string name="copy_to_clipboard">คัดลอกไปยังคลิปบอร์ด</string>
+6
View File
@@ -50,6 +50,12 @@
<string name="biometric_authentication_failed_explainer_with_error">Biyometrik veriler bu telefonun sahibini doğrulayamadı. Hata: %1$s</string>
<string name="delete">Sil</string>
<string name="permissions">İzinler</string>
<string name="add_permission">İzin ekle</string>
<string name="add_permission_description">Bu uygulama için otomatik olarak onaylanmasını istediğiniz izinleri seçin.</string>
<string name="search_permissions">İzinleri ara</string>
<string name="no_permissions_found">İzin bulunamadı</string>
<string name="custom_event_kind">Özel etkinlik türü</string>
<string name="custom_event_kind_already_added">Bu izin listede zaten var</string>
<string name="delete_application">Uygulamayı Sil</string>
<string name="reset_bunker">Bunker\'ı Sıfırla</string>
<string name="copy_to_clipboard">Panoya kopyala</string>
@@ -43,6 +43,12 @@
<string name="biometric_authentication_failed_explainer_with_error">Sinh trắc học không thể xác thực chủ sở hữu điện thoại này. Lỗi: %1$s</string>
<string name="delete">Xóa</string>
<string name="permissions">Quyền</string>
<string name="add_permission">Thêm quyền</string>
<string name="add_permission_description">Chọn các quyền bạn muốn được tự động phê duyệt cho ứng dụng này.</string>
<string name="search_permissions">Tìm kiếm quyền</string>
<string name="no_permissions_found">Không tìm thấy quyền nào</string>
<string name="custom_event_kind">Loại sự kiện tùy chỉnh</string>
<string name="custom_event_kind_already_added">Quyền này đã có trong danh sách</string>
<string name="delete_application">Xóa Ứng Dụng</string>
<string name="reset_bunker">Đặt lại Bunker</string>
<string name="copy_to_clipboard">Sao chép vào khay nhớ tạm</string>
+6
View File
@@ -48,6 +48,12 @@
<string name="biometric_authentication_failed_explainer_with_error">生物识别验证设备所有者身份失败:%1$s</string>
<string name="delete">删除</string>
<string name="permissions">权限</string>
<string name="add_permission">添加权限</string>
<string name="add_permission_description">选择您希望为此应用程序自动批准的权限。</string>
<string name="search_permissions">搜索权限</string>
<string name="no_permissions_found">未找到权限</string>
<string name="custom_event_kind">自定义事件类型</string>
<string name="custom_event_kind_already_added">此权限已在列表中</string>
<string name="delete_application">删除应用</string>
<string name="reset_bunker">重置 Bunker</string>
<string name="copy_to_clipboard">复制到剪贴板</string>
+6
View File
@@ -76,6 +76,12 @@
<string name="biometric_authentication_failed_explainer_with_error">Biometrics failed to authenticate the owner of this phone. Error: %1$s</string>
<string name="delete">Delete</string>
<string name="permissions">Permissions</string>
<string name="add_permission">Add permission</string>
<string name="add_permission_description">Select the permissions you want to be approved automatically for this application.</string>
<string name="search_permissions">Search permissions</string>
<string name="no_permissions_found">No permissions found</string>
<string name="custom_event_kind">Custom event kind</string>
<string name="custom_event_kind_already_added">This permission is already in the list</string>
<string name="delete_application">Delete Application</string>
<string name="reset_bunker">Reset Bunker</string>
<string name="copy_to_clipboard">Copy to clipboard</string>