This commit is contained in:
Adam Velebil 2022-08-09 16:27:49 +02:00
commit 6080d2985e
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
18 changed files with 837 additions and 21 deletions

View File

@ -30,7 +30,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
compileSdkVersion 32
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -46,7 +46,7 @@ android {
}
defaultConfig {
applicationId "com.yubico.yubioath"
applicationId "com.yubico.yubioath.preview"
minSdkVersion project.minSdkVersion
targetSdkVersion project.targetSdkVersion
versionCode flutterVersionCode.toInteger()
@ -83,10 +83,11 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
// Lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.fragment:fragment-ktx:1.5.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
// testing dependencies
testImplementation "junit:junit:$project.junitVersion"

View File

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yubico.authenticator">
@ -15,15 +16,18 @@
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_label">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:launchMode="singleTask"
android:resizeableActivity="false"
android:screenOrientation="portrait"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
@ -41,10 +45,27 @@
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
android:resource="@xml/device_filter" />
</activity>
<activity
android:name=".NdefActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/NdefActivityTheme">
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="my.yubico.com"
android:scheme="https" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
</manifest>

View File

@ -0,0 +1,7 @@
package com.yubico.authenticator
class Constants {
companion object {
const val EXTRA_OPENED_THROUGH_NFC = "openedThroughNfcTap"
}
}

View File

@ -4,10 +4,12 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.nfc.NfcAdapter
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.yubico.authenticator.Constants.Companion.EXTRA_OPENED_THROUGH_NFC
import com.yubico.authenticator.logging.FlutterLog
import com.yubico.authenticator.logging.Log
import com.yubico.authenticator.oath.OathManager
@ -45,6 +47,16 @@ class MainActivity : FlutterFragmentActivity() {
yubikit = YubiKitManager(this)
setupYubiKeyDiscovery()
setupYubiKitLogger()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
}
private fun setupYubiKeyDiscovery() {
viewModel.handleYubiKey.observe(this) {
if (it) {
Log.d(TAG, "Starting usb discovery")
@ -59,8 +71,6 @@ class MainActivity : FlutterFragmentActivity() {
Log.d(TAG, "Stopped usb discovery")
}
}
setupYubiKitLogger()
}
fun startNfcDiscovery(): Boolean =
@ -118,6 +128,17 @@ class MainActivity : FlutterFragmentActivity() {
override fun onResume() {
super.onResume()
try {
if (intent.getBooleanExtra(EXTRA_OPENED_THROUGH_NFC, false)) {
// make nfc available to yubikit
NfcAdapter.getDefaultAdapter(this).disableReaderMode(this)
setupYubiKeyDiscovery()
}
} catch (e: Exception) {
Log.e(TAG, "Failure when resuming YubiKey discovery", e.stackTraceToString())
}
startNfcDiscovery()
}

View File

@ -0,0 +1,129 @@
package com.yubico.authenticator
import android.app.Activity
import android.content.*
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Build
import android.os.Bundle
import android.widget.Toast
import com.yubico.authenticator.Constants.Companion.EXTRA_OPENED_THROUGH_NFC
import com.yubico.authenticator.logging.Log
import com.yubico.authenticator.yubiclip.scancode.KeyboardLayout
import com.yubico.yubikit.core.util.NdefUtils
import java.nio.charset.StandardCharsets
typealias ResourceId = Int
class NdefActivity : Activity() {
private var openAppOnNfcTap: Boolean = false
private var copyOtpOnNfcTap: Boolean = false
private lateinit var clipKbdLayout: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val prefs: SharedPreferences = getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)
openAppOnNfcTap = prefs.getBoolean(PREF_NFC_OPEN_APP, false)
copyOtpOnNfcTap = prefs.getBoolean(PREF_NFC_COPY_OTP, false)
clipKbdLayout = prefs.getString(PREF_CLIP_KBD_LAYOUT, DEFAULT_CLIP_KBD_LAYOUT)!!
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent(intent)
}
override fun onPause() {
super.onPause()
overridePendingTransition(0, 0)
}
private fun handleIntent(intent: Intent) {
intent.data?.let {
if (copyOtpOnNfcTap) {
try {
val otpSlotContent = parseOtpFromIntent()
setPrimaryClip(otpSlotContent.content)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
showToast(
when (otpSlotContent.type) {
OtpType.Otp -> R.string.otp_success_set_otp_to_clipboard
OtpType.Password -> R.string.otp_success_set_password_to_clipboard
}, Toast.LENGTH_SHORT
)
}
} catch (illegalArgumentException: IllegalArgumentException) {
Log.e(
TAG,
illegalArgumentException.message ?: "Failure when handling YubiKey OTP",
illegalArgumentException.stackTraceToString()
)
showToast(R.string.otp_parse_failure, Toast.LENGTH_LONG)
} catch (_: UnsupportedOperationException) {
showToast(R.string.otp_set_clip_failure, Toast.LENGTH_LONG)
}
}
if (openAppOnNfcTap) {
val mainAppIntent = Intent(this, MainActivity::class.java).apply {
putExtra(EXTRA_OPENED_THROUGH_NFC, true)
}
startActivity(mainAppIntent)
}
finishAndRemoveTask()
}
}
private fun showToast(value: ResourceId, length: Int) {
Toast.makeText(this, value, length).show()
}
private fun parseOtpFromIntent(): OtpSlotValue {
val parcelable = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
if (parcelable != null && parcelable.isNotEmpty()) {
val ndefPayloadBytes =
NdefUtils.getNdefPayloadBytes((parcelable[0] as NdefMessage).toByteArray())
return if (ndefPayloadBytes.all { it in 32..126 }) {
OtpSlotValue(OtpType.Otp, String(ndefPayloadBytes, StandardCharsets.US_ASCII))
} else {
val kbd: KeyboardLayout = KeyboardLayout.forName(clipKbdLayout)
OtpSlotValue(OtpType.Password, kbd.fromScanCodes(ndefPayloadBytes))
}
}
throw IllegalArgumentException("Failed to parse OTP from the intent")
}
private fun setPrimaryClip(otp: String) {
try {
val clipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboardManager.setPrimaryClip(ClipData.newPlainText(otp, otp))
} catch (e: Exception) {
Log.e(TAG, "Failed to copy otp string to clipboard", e.stackTraceToString())
throw UnsupportedOperationException()
}
}
companion object {
const val TAG = "YubicoAuthenticatorOTPActivity"
const val PREFS_FILE = "FlutterSharedPreferences"
const val PREF_NFC_OPEN_APP = "flutter.prefNfcOpenApp"
const val PREF_NFC_COPY_OTP = "flutter.prefNfcCopyOtp"
const val PREF_CLIP_KBD_LAYOUT = "flutter.prefClipKbdLayout"
const val DEFAULT_CLIP_KBD_LAYOUT = "US"
}
enum class OtpType {
Otp, Password
}
data class OtpSlotValue(val type: OtpType, val content: String)
}

View File

@ -0,0 +1,141 @@
package com.yubico.authenticator.yubiclip.scancode;
/**
* Created by mjenny on 7/12/15.
*/
public class DECHKeyboardLayout extends KeyboardLayout {
private static final String[] usb2key1 = new String[]{
"",
"",
"",
"",
"a",
"b",
"c",
"d",
"e",
"f",
"g", /* 0xa */
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q", /* 0x14 */
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"z",
"y",
"1", /* 0x1e */
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"\n", /* 0x28 */
"",
"",
"\t",
" ",
"'",
"^",
"ü",
"¨",
"",
"$",
"ö",
"ä",
"§",
",",
".",
"-", /* 0x38 */
};
private static final String[] usb2key2 = new String[]{
"",
"",
"",
"",
"A",
"B",
"C",
"D",
"E",
"F",
"G", /* 0x8a */
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q", /* 0x94 */
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Z",
"Y",
"+",
"\"",
"*",
"ç",
"%",
"&",
"/",
"(",
")",
"=",
"",
"",
"",
"",
"",
"?",
"`",
"è",
"!",
"",
"£",
"Ö",
"Ä",
"°",
";",
":",
"_",
};
@Override
protected String fromScanCode(int code) {
if (code < SHIFT) {
if (code < usb2key1.length) {
return usb2key1[code];
}
} else {
code = code ^ SHIFT;
if (code < usb2key2.length) {
return usb2key2[code];
}
}
return "";
}
}

View File

@ -0,0 +1,141 @@
package com.yubico.authenticator.yubiclip.scancode;
/**
* Created by kemiren on 6/18/14.
*/
public class DEKeyboardLayout extends KeyboardLayout {
private static final String[] usb2key1 = new String[]{
"",
"",
"",
"",
"a",
"b",
"c",
"d",
"e",
"f",
"g", /* 0xa */
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q", /* 0x14 */
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"z",
"y",
"1", /* 0x1e */
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"\n", /* 0x28 */
"",
"",
"\t",
" ",
"ß",
"´",
"ü",
"+",
"",
"#",
"ö",
"'",
"^",
",",
".",
"-", /* 0x38 */
};
private static final String[] usb2key2 = new String[]{
"",
"",
"",
"",
"A",
"B",
"C",
"D",
"E",
"F",
"G", /* 0x8a */
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q", /* 0x94 */
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Z",
"Y",
"!",
"\"",
"§",
"$",
"%",
"&",
"/",
"(",
")",
"=",
"",
"",
"",
"",
"",
"?",
"`",
"Ü",
"*",
"",
">",
"Ö",
"Ä",
"'",
";",
":",
"_",
};
@Override
protected String fromScanCode(int code) {
if (code < SHIFT) {
if (code < usb2key1.length) {
return usb2key1[code];
}
} else {
code = code ^ SHIFT;
if (code < usb2key2.length) {
return usb2key2[code];
}
}
return "";
}
}

View File

@ -0,0 +1,40 @@
package com.yubico.authenticator.yubiclip.scancode;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
/**
* Created by dain on 2/17/14.
*/
public abstract class KeyboardLayout {
private static final Map<String, KeyboardLayout> layouts = new HashMap<String, KeyboardLayout>();
static {
layouts.put("US", new USKeyboardLayout());
layouts.put("DE", new DEKeyboardLayout());
layouts.put("DE-CH", new DECHKeyboardLayout());
}
public static KeyboardLayout forName(String name) {
return layouts.get(name.toUpperCase());
}
public static Set<String> availableLayouts() {
return new TreeSet<>(layouts.keySet());
}
protected static final int SHIFT = 0x80;
protected abstract String fromScanCode(int code);
public final String fromScanCodes(byte[] bytes) {
StringBuilder buf = new StringBuilder();
for (byte b : bytes) {
buf.append(fromScanCode(b & 0xff));
}
return buf.toString();
}
}

View File

@ -0,0 +1,141 @@
package com.yubico.authenticator.yubiclip.scancode;
/**
* Created by dain on 2/17/14.
*/
public class USKeyboardLayout extends KeyboardLayout {
private static final String[] usb2key1 = new String[]{
"",
"",
"",
"",
"a",
"b",
"c",
"d",
"e",
"f",
"g", /* 0xa */
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q", /* 0x14 */
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"1", /* 0x1e */
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"0",
"\n", /* 0x28 */
"",
"",
"\t",
" ",
"-",
"=",
"[",
"]",
"",
"\\",
";",
"'",
"`",
",",
".",
"/", /* 0x38 */
};
private static final String[] usb2key2 = new String[]{
"",
"",
"",
"",
"A",
"B",
"C",
"D",
"E",
"F",
"G", /* 0x8a */
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q", /* 0x94 */
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"!",
"@",
"#",
"$",
"%",
"^",
"&",
"*",
"(",
")",
"",
"",
"",
"",
"",
"_",
"+",
"{",
"}",
"",
"|",
":",
"\"",
"~",
"<",
">",
"?",
};
@Override
protected String fromScanCode(int code) {
if (code < SHIFT) {
if (code < usb2key1.length) {
return usb2key1[code];
}
} else {
code = code ^ SHIFT;
if (code < usb2key2.length) {
return usb2key2[code];
}
}
return "";
}
}

View File

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label">Yubico Authenticator</string>
<string name="app_label">Yubico Authenticator Preview</string>
<string name="otp_success_set_otp_to_clipboard">Successfully copied OTP code from YubiKey to clipboard.</string>
<string name="otp_success_set_password_to_clipboard">Successfully copied password from YubiKey to clipboard.</string>
<string name="otp_parse_failure">Failed to parse OTP code from YubiKey.</string>
<string name="otp_set_clip_failure">Failed to access clipboard when trying to copy OTP code from YubiKey.</string>
</resources>

View File

@ -15,4 +15,15 @@
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
<style name="NdefActivityTheme" parent="NormalTheme">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowAnimationStyle">@null</item>
<item name="android:windowDisablePreview">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
</resources>

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.7.0'
repositories {
google()
mavenCentral()

View File

@ -7,12 +7,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher.dart';
import 'app/state.dart';
import 'version.dart';
import 'app/logging.dart';
import 'app/message.dart';
import 'app/state.dart';
import 'core/state.dart';
import 'desktop/state.dart';
import 'version.dart';
import 'widgets/responsive_dialog.dart';
final _log = Logger('about');
@ -31,7 +31,9 @@ class AboutPage extends ConsumerWidget {
Padding(
padding: const EdgeInsets.only(top: 24.0),
child: Text(
'Yubico Authenticator',
Platform.isAndroid
? 'Yubico Authenticator Preview'
: 'Yubico Authenticator',
style: Theme.of(context).textTheme.titleMedium,
),
),

View File

@ -13,8 +13,8 @@ import 'package:yubico_authenticator/app/state.dart';
import 'package:yubico_authenticator/core/models.dart';
import 'package:yubico_authenticator/oath/state.dart';
import '../../oath/models.dart';
import '../../cancellation_exception.dart';
import '../../oath/models.dart';
import 'command_providers.dart';
final _log = Logger('android.oath.state');
@ -186,7 +186,9 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
var result = jsonDecode(resultString);
var addedCredential = OathCredential.fromJson(result['credential']);
var addedCredCode = OathCode.fromJson(result['code']);
var addedCredCode =
result['code'] != null ? OathCode.fromJson(result['code']) : null;
if (mounted) {
final newState = state!.toList();

View File

@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/core/state.dart';
import '../../app/state.dart';
import '../../widgets/list_title.dart';
import '../../widgets/responsive_dialog.dart';
class AndroidSettingsPage extends ConsumerWidget {
const AndroidSettingsPage({super.key});
static const String prefNfcOpenApp = 'prefNfcOpenApp';
static const String prefNfcCopyOtp = 'prefNfcCopyOtp';
static const String prefClipKbdLayout = 'prefClipKbdLayout';
static const String defaultClipKbdLayout = 'US';
@override
Widget build(BuildContext context, WidgetRef ref) {
final nfcOpenApp = ref.watch(prefProvider).getBool(prefNfcOpenApp) ?? false;
final nfcCopyOtp = ref.watch(prefProvider).getBool(prefNfcCopyOtp) ?? false;
final clipKbdLayout =
ref.watch(prefProvider).getString(prefClipKbdLayout) ??
defaultClipKbdLayout;
final themeMode = ref.watch(themeModeProvider);
return ResponsiveDialog(
title: const Text('Settings'),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const ListTitle('General'),
SwitchListTile(
title: const Text('Open authenticator on NFC tap'),
value: nfcOpenApp,
onChanged: (value) {
ref.read(prefProvider).setBool(prefNfcOpenApp, value);
ref.refresh(prefProvider);
}),
const ListTitle('Yubiclip'),
SwitchListTile(
title: const Text('Copy OTP to clipboard'),
value: nfcCopyOtp,
onChanged: (value) {
ref.read(prefProvider).setBool(prefNfcCopyOtp, value);
ref.refresh(prefProvider);
}),
ListTile(
title: const Text('Static password keyboard layout'),
subtitle: Text('Current: $clipKbdLayout'),
onTap: () async {
var newValue = await _selectKbdLayout(context, clipKbdLayout);
if (newValue != clipKbdLayout) {
await ref
.read(prefProvider)
.setString(prefClipKbdLayout, newValue);
ref.refresh(prefProvider);
}
},
),
const ListTitle('Appearance'),
ListTile(
title: const Text('App theme'),
subtitle: Text(ref.read(themeModeProvider).name),
onTap: () async {
var newMode = await _selectAppearance(context, themeMode);
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
},
),
],
),
);
}
Future<String> _selectKbdLayout(
BuildContext context, String currentKbdLayout) async =>
await showDialog<String>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('Choose keyboard layout'),
children: <Widget>[
RadioListTile<String>(
title: const Text('US'),
value: 'US',
groupValue: currentKbdLayout,
onChanged: (mode) {
Navigator.pop(context, 'US');
}),
RadioListTile<String>(
title: const Text('DE'),
value: 'DE',
groupValue: currentKbdLayout,
onChanged: (mode) {
Navigator.pop(context, 'DE');
}),
RadioListTile<String>(
title: const Text('DE-CH'),
value: 'DE-CH',
groupValue: currentKbdLayout,
onChanged: (mode) {
Navigator.pop(context, 'DE-CH');
}),
],
);
}) ??
defaultClipKbdLayout;
Future<ThemeMode> _selectAppearance(
BuildContext context, ThemeMode themeMode) async =>
await showDialog<ThemeMode>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: const Text('Choose app theme'),
children: <Widget>[
RadioListTile<ThemeMode>(
title: const Text('System default'),
value: ThemeMode.system,
groupValue: themeMode,
onChanged: (mode) {
Navigator.pop(context, ThemeMode.system);
}),
RadioListTile<ThemeMode>(
title: const Text('Light mode'),
value: ThemeMode.light,
groupValue: themeMode,
onChanged: (mode) {
Navigator.pop(context, ThemeMode.light);
}),
RadioListTile<ThemeMode>(
title: const Text('Dark mode'),
value: ThemeMode.dark,
groupValue: themeMode,
onChanged: (mode) {
Navigator.pop(context, ThemeMode.dark);
}),
],
);
}) ??
ThemeMode.system;
}

View File

@ -44,6 +44,7 @@ enum Application {
}
Availability getAvailability(YubiKeyData data) {
if (this == Application.management) {
final version = data.info.version;
final available = (version.major > 4 || // YK5 and up

View File

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../management/views/management_screen.dart';
import '../../about_page.dart';
import '../../android/views/android_settings_page.dart';
import '../../management/views/management_screen.dart';
import '../../settings_page.dart';
import '../message.dart';
import '../models.dart';
@ -98,7 +101,9 @@ class MainPageDrawer extends ConsumerWidget {
if (shouldPop) nav.pop();
showBlurDialog(
context: context,
builder: (context) => const SettingsPage(),
builder: (context) => Platform.isAndroid
? const AndroidSettingsPage()
: const SettingsPage(),
routeSettings: const RouteSettings(name: 'settings'),
);
},

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -34,15 +36,20 @@ class MainPage extends ConsumerWidget {
final deviceNode = ref.watch(currentDeviceProvider);
if (deviceNode == null) {
return const MessagePage(message: 'Insert your YubiKey');
return MessagePage(message: Platform.isAndroid ? 'Insert or tap your YubiKey' : 'Insert your YubiKey');
} else {
return ref.watch(currentDeviceDataProvider).when(
data: (data) {
final app = ref.watch(currentAppProvider);
if (app.getAvailability(data) != Availability.enabled) {
return const MessagePage(
if (app.getAvailability(data) == Availability.unsupported) {
return MessagePage(
header: 'Application not supported',
message: 'The used YubiKey does not support \'${app.name}\' application',
);
} else if (app.getAvailability(data) != Availability.enabled) {
return MessagePage(
header: 'Application disabled',
message: 'Enable the application on your YubiKey to access',
message: 'Enable the \'${app.name}\' application on your YubiKey to access',
);
}