add localization support to tap_request_dialog

This commit is contained in:
Adam Velebil 2023-05-31 15:07:40 +02:00
parent 2aeffce0a4
commit 6d69ed37f1
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
5 changed files with 196 additions and 48 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -24,10 +24,16 @@ import kotlinx.serialization.json.Json
typealias OnDialogCancelled = suspend () -> Unit
enum class Icon(val value: String) {
NFC("nfc"),
SUCCESS("success"),
ERROR("error");
enum class DialogIcon(val value: Int) {
Nfc(0),
Success(1),
Failure(2);
}
enum class DialogTitle(val value: Int) {
TapKey(0),
OperationSuccessful(1),
OperationFailed(2)
}
class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) {
@ -45,16 +51,21 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
}
}
fun showDialog(icon: Icon, title: String, description: String, cancelled: OnDialogCancelled?) {
fun showDialog(
dialogIcon: DialogIcon,
dialogTitle: DialogTitle,
dialogDescriptionId: Int,
cancelled: OnDialogCancelled?
) {
onCancelled = cancelled
coroutineScope.launch {
channel.invoke(
"show",
Json.encodeToString(
mapOf(
"title" to title,
"description" to description,
"icon" to icon.value
"title" to dialogTitle.value,
"description" to dialogDescriptionId,
"icon" to dialogIcon.value
)
)
)
@ -62,17 +73,17 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
}
suspend fun updateDialogState(
icon: Icon? = null,
title: String? = null,
description: String? = null
dialogIcon: DialogIcon? = null,
dialogTitle: DialogTitle,
dialogDescriptionId: Int? = null,
) {
channel.invoke(
"state",
Json.encodeToString(
mapOf(
"title" to title,
"description" to description,
"icon" to icon?.value
"title" to dialogTitle.value,
"description" to dialogDescriptionId,
"icon" to dialogIcon?.value
)
)
)

View File

@ -0,0 +1,34 @@
/*
* Copyright (C) 2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yubico.authenticator.oath
const val dialogDescriptionOathIndex = 100
enum class OathActionDescription(private val value: Int) {
Reset(0),
Unlock(1),
SetPassword(2),
UnsetPassword(3),
AddAccount(4),
RenameAccount(5),
DeleteAccount(6),
CalculateCode(7),
ActionFailure(8);
val id: Int
get() = value + dialogDescriptionOathIndex
}

View File

@ -359,7 +359,7 @@ class OathManager(
val credentialData: CredentialData =
CredentialData.parseUri(URI.create(uri))
addToAny = true
return useOathSessionNfc("Add account") { session ->
return useOathSessionNfc(OathActionDescription.AddAccount) { session ->
// We need to check for duplicates here since we haven't yet read the credentials
if (session.credentials.any { it.id.contentEquals(credentialData.id) }) {
throw Exception("A credential with this ID already exists!")
@ -383,7 +383,7 @@ class OathManager(
}
private suspend fun reset(): String =
useOathSession("Reset YubiKey") {
useOathSession(OathActionDescription.Reset) {
// note, it is ok to reset locked session
it.reset()
keyManager.removeKey(it.deviceId)
@ -395,7 +395,7 @@ class OathManager(
}
private suspend fun unlock(password: String, remember: Boolean): String =
useOathSession("Unlocking") {
useOathSession(OathActionDescription.Unlock) {
val accessKey = it.deriveAccessKey(password.toCharArray())
keyManager.addKey(it.deviceId, accessKey, remember)
@ -417,7 +417,7 @@ class OathManager(
currentPassword: String?,
newPassword: String,
): String =
useOathSession("Set password", unlock = false) { session ->
useOathSession(OathActionDescription.SetPassword, unlock = false) { session ->
if (session.isAccessKeySet) {
if (currentPassword == null) {
throw Exception("Must provide current password to be able to change it")
@ -436,7 +436,7 @@ class OathManager(
}
private suspend fun unsetPassword(currentPassword: String): String =
useOathSession("Unset password", unlock = false) { session ->
useOathSession(OathActionDescription.UnsetPassword, unlock = false) { session ->
if (session.isAccessKeySet) {
// test current password sent by the user
if (session.unlock(currentPassword.toCharArray())) {
@ -468,7 +468,7 @@ class OathManager(
uri: String,
requireTouch: Boolean,
): String =
useOathSession("Add account") { session ->
useOathSession(OathActionDescription.AddAccount) { session ->
val credentialData: CredentialData =
CredentialData.parseUri(URI.create(uri))
@ -489,7 +489,7 @@ class OathManager(
}
private suspend fun renameAccount(uri: String, name: String, issuer: String?): String =
useOathSession("Rename") { session ->
useOathSession(OathActionDescription.RenameAccount) { session ->
val credential = getOathCredential(session, uri)
val renamedCredential =
Credential(session.renameCredential(credential, name, issuer), session.deviceId)
@ -502,7 +502,7 @@ class OathManager(
}
private suspend fun deleteAccount(credentialId: String): String =
useOathSession("Delete account") { session ->
useOathSession(OathActionDescription.DeleteAccount) { session ->
val credential = getOathCredential(session, credentialId)
session.deleteCredential(credential)
oathViewModel.removeCredential(Credential(credential, session.deviceId))
@ -535,7 +535,7 @@ class OathManager(
}
private suspend fun calculate(credentialId: String): String =
useOathSession("Calculate") { session ->
useOathSession(OathActionDescription.CalculateCode) { session ->
val credential = getOathCredential(session, credentialId)
val code = Code.from(calculateCode(session, credential))
@ -648,7 +648,7 @@ class OathManager(
}
private suspend fun <T> useOathSession(
title: String,
oathActionDescription: OathActionDescription,
unlock: Boolean = true,
action: (YubiKitOathSession) -> T
): T {
@ -657,7 +657,7 @@ class OathManager(
unlockOnConnect.set(unlock)
return appViewModel.connectedYubiKey.value?.let {
useOathSessionUsb(it, action)
} ?: useOathSessionNfc(title, action)
} ?: useOathSessionNfc(oathActionDescription, action)
}
private suspend fun <T> useOathSessionUsb(
@ -668,7 +668,7 @@ class OathManager(
}
private suspend fun <T> useOathSessionNfc(
title: String,
oathActionDescription: OathActionDescription,
block: (YubiKitOathSession) -> T
): T {
try {
@ -678,15 +678,15 @@ class OathManager(
block.invoke(it.value)
})
}
dialogManager.showDialog(Icon.NFC, "Tap your key", title) {
Log.d(TAG, "Cancelled Dialog $title")
dialogManager.showDialog(DialogIcon.Nfc, DialogTitle.TapKey, oathActionDescription.id) {
Log.d(TAG, "Cancelled Dialog ${oathActionDescription.name}")
pendingAction?.invoke(Result.failure(CancellationException()))
pendingAction = null
}
}
dialogManager.updateDialogState(
icon = Icon.SUCCESS,
title = "Success"
dialogIcon = DialogIcon.Success,
dialogTitle = DialogTitle.OperationSuccessful
)
// TODO: This delays the closing of the dialog, but also the return value
delay(500)
@ -695,9 +695,9 @@ class OathManager(
throw cancelled
} catch (error: Throwable) {
dialogManager.updateDialogState(
icon = Icon.ERROR,
title = "Failure",
description = "Action failed - try again"
dialogIcon = DialogIcon.Failure,
dialogTitle = DialogTitle.OperationFailed,
dialogDescriptionId = OathActionDescription.ActionFailure.id
)
// TODO: This delays the closing of the dialog, but also the return value
delay(1500)

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/state.dart';
@ -27,6 +28,69 @@ import '../widgets/custom_icons.dart';
const _channel = MethodChannel('com.yubico.authenticator.channel.dialog');
// _DIcon identifies the icon which should be displayed on the dialog
enum _DIcon {
nfcIcon,
successIcon,
failureIcon,
invalid;
static _DIcon fromId(int? id) =>
const {
0: _DIcon.nfcIcon,
1: _DIcon.successIcon,
2: _DIcon.failureIcon
}[id] ??
_DIcon.invalid;
}
// _DDesc contains id of title resource for the dialog
enum _DTitle {
tapKey,
operationSuccessful,
operationFailed,
invalid;
static _DTitle fromId(int? id) =>
const {
0: _DTitle.tapKey,
1: _DTitle.operationSuccessful,
2: _DTitle.operationFailed
}[id] ??
_DTitle.invalid;
}
// _DDesc contains action description in the dialog
enum _DDesc {
// oath descriptions
oathResetApplet,
oathUnlockSession,
oathSetPassword,
oathUnsetPassword,
oathAddAccount,
oathRenameAccount,
oathDeleteAccount,
oathCalculateCode,
oathActionFailure,
invalid;
static const int dialogDescriptionOathIndex = 100;
static _DDesc fromId(int? id) =>
const {
dialogDescriptionOathIndex + 0: _DDesc.oathResetApplet,
dialogDescriptionOathIndex + 1: _DDesc.oathUnlockSession,
dialogDescriptionOathIndex + 2: _DDesc.oathSetPassword,
dialogDescriptionOathIndex + 3: _DDesc.oathUnsetPassword,
dialogDescriptionOathIndex + 4: _DDesc.oathAddAccount,
dialogDescriptionOathIndex + 5: _DDesc.oathRenameAccount,
dialogDescriptionOathIndex + 6: _DDesc.oathDeleteAccount,
dialogDescriptionOathIndex + 7: _DDesc.oathCalculateCode,
dialogDescriptionOathIndex + 8: _DDesc.oathActionFailure
}[id] ??
_DDesc.invalid;
}
final androidDialogProvider = Provider<_DialogProvider>(
(ref) {
return _DialogProvider(ref.watch(withContextProvider));
@ -65,20 +129,46 @@ class _DialogProvider {
_controller = null;
}
Widget? _getIcon(String? icon) => switch (icon) {
'nfc' => nfcIcon,
'success' => const Icon(Icons.check_circle),
'error' => const Icon(Icons.error),
Widget? _getIcon(int? icon) => switch (_DIcon.fromId(icon)) {
_DIcon.nfcIcon => nfcIcon,
_DIcon.successIcon => const Icon(Icons.check_circle),
_DIcon.failureIcon => const Icon(Icons.error),
_ => null,
};
String _getTitle(BuildContext context, int? titleId) {
final l10n = AppLocalizations.of(context)!;
return switch (_DTitle.fromId(titleId)) {
_DTitle.tapKey => l10n.s_nfc_dialog_tap_key,
_DTitle.operationSuccessful => l10n.s_nfc_dialog_operation_success,
_DTitle.operationFailed => l10n.s_nfc_dialog_operation_failed,
_ => ''
};
}
String _getDialogDescription(BuildContext context, int? descriptionId) {
final l10n = AppLocalizations.of(context)!;
return switch (_DDesc.fromId(descriptionId)) {
_DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset,
_DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock,
_DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password,
_DDesc.oathUnsetPassword => l10n.s_nfc_dialog_oath_unset_password,
_DDesc.oathAddAccount => l10n.s_nfc_dialog_oath_add_account,
_DDesc.oathRenameAccount => l10n.s_nfc_dialog_oath_rename_account,
_DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account,
_DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code,
_DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure,
_ => ''
};
}
Future<void> _updateDialogState(
String? title, String? description, String? iconName) async {
final icon = _getIcon(iconName);
int? title, int? description, int? dialogIcon) async {
final icon = _getIcon(dialogIcon);
await _withContext((context) async {
_controller?.updateContent(
title: title,
description: description,
title: _getTitle(context, title),
description: _getDialogDescription(context, description),
icon: icon != null
? IconTheme(
data: IconTheme.of(context).copyWith(size: 64),
@ -89,13 +179,12 @@ class _DialogProvider {
});
}
Future<void> _showDialog(
String title, String description, String? iconName) async {
final icon = _getIcon(iconName);
Future<void> _showDialog(int title, int description, int? dialogIcon) async {
final icon = _getIcon(dialogIcon);
_controller = await _withContext((context) async => promptUserInteraction(
context,
title: title,
description: description,
title: _getTitle(context, title),
description: _getDialogDescription(context, description),
icon: icon != null
? IconTheme(
data: IconTheme.of(context).copyWith(size: 64),

View File

@ -441,5 +441,19 @@
"l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB",
"s_allow_screenshots": "Allow screenshots",
"s_nfc_dialog_tap_key": "Tap your key",
"s_nfc_dialog_operation_success": "Success",
"s_nfc_dialog_operation_failed": "Failed",
"s_nfc_dialog_oath_reset": "Reset OATH applet",
"s_nfc_dialog_oath_unlock": "Unlock OATH applet",
"s_nfc_dialog_oath_set_password": "Set OATH password",
"s_nfc_dialog_oath_unset_password": "Remove OATH password",
"s_nfc_dialog_oath_add_account": "Add new account",
"s_nfc_dialog_oath_rename_account": "Rename account",
"s_nfc_dialog_oath_delete_account": "Delete account",
"s_nfc_dialog_oath_calculate_code": "Calculate OATH code",
"s_nfc_dialog_oath_failure": "OATH operation failed",
"@_eof": {}
}