mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 08:22:16 +03:00
add localization support to tap_request_dialog
This commit is contained in:
parent
2aeffce0a4
commit
6d69ed37f1
@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
@ -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),
|
||||
|
@ -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": {}
|
||||
}
|
Loading…
Reference in New Issue
Block a user