mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 10:11:52 +03:00
handle features dependent on SDK version
This commit is contained in:
parent
2aa3fbb52b
commit
1d99ce3fe6
@ -31,7 +31,7 @@ apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion 32
|
||||
compileSdkVersion project.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
|
@ -0,0 +1,36 @@
|
||||
package com.yubico.authenticator
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipDescription
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.PersistableBundle
|
||||
import com.yubico.authenticator.logging.Log
|
||||
|
||||
object ClipboardUtil {
|
||||
|
||||
private const val TAG = "ClipboardUtil"
|
||||
|
||||
fun setPrimaryClip(context: Context, toClipboard: String, isSensitive: Boolean) {
|
||||
try {
|
||||
val clipboardManager =
|
||||
context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
|
||||
val clipData = ClipData.newPlainText(toClipboard, toClipboard)
|
||||
clipData.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
description.extras = PersistableBundle().apply {
|
||||
putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, isSensitive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clipboardManager.setPrimaryClip(clipData)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to set string to clipboard", e.stackTraceToString())
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -6,6 +6,7 @@ import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.nfc.NfcAdapter
|
||||
import android.nfc.Tag
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.viewModels
|
||||
@ -130,7 +131,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
// Handle existing tag when launched from NDEF
|
||||
val tag = intent.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
||||
if(tag != null) {
|
||||
if (tag != null) {
|
||||
intent.removeExtra(NfcAdapter.EXTRA_TAG)
|
||||
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
@ -190,8 +191,15 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
viewModel.appContext.observe(this) {
|
||||
contextManager?.dispose()
|
||||
contextManager = when(it) {
|
||||
OperationContext.Oath -> OathManager(this, messenger, viewModel, oathViewModel, dialogManager, appPreferences)
|
||||
contextManager = when (it) {
|
||||
OperationContext.Oath -> OathManager(
|
||||
this,
|
||||
messenger,
|
||||
viewModel,
|
||||
oathViewModel,
|
||||
dialogManager,
|
||||
appPreferences
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
viewModel.connectedYubiKey.value?.let(::processYubiKey)
|
||||
@ -234,6 +242,21 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
methodCall.arguments as Boolean,
|
||||
)
|
||||
)
|
||||
"getAndroidSdkVersion" -> result.success(
|
||||
Build.VERSION.SDK_INT
|
||||
)
|
||||
"setPrimaryClip" -> {
|
||||
val toClipboard = methodCall.argument<String>("toClipboard")
|
||||
val isSensitive = methodCall.argument<Boolean>("isSensitive")
|
||||
if (toClipboard != null && isSensitive != null) {
|
||||
ClipboardUtil.setPrimaryClip(
|
||||
this@MainActivity,
|
||||
toClipboard,
|
||||
isSensitive
|
||||
)
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
else -> Log.w(TAG, "Unknown app method: ${methodCall.method}")
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class NdefActivity : Activity() {
|
||||
if (appPreferences.copyOtpOnNfcTap) {
|
||||
try {
|
||||
val otpSlotContent = parseOtpFromIntent()
|
||||
setPrimaryClip(otpSlotContent.content)
|
||||
ClipboardUtil.setPrimaryClip(this, otpSlotContent.content, true)
|
||||
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
|
||||
showToast(
|
||||
@ -95,16 +95,6 @@ class NdefActivity : Activity() {
|
||||
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"
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ allprojects {
|
||||
minSdkVersion = 21
|
||||
targetSdkVersion = 33
|
||||
compileSdkVersion = 33
|
||||
buildToolsVersion = "30.0.3"
|
||||
buildToolsVersion = "33.0.0"
|
||||
|
||||
yubiKitVersion = "2.1.0"
|
||||
junitVersion = "4.13.2"
|
||||
|
@ -2,7 +2,6 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
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 'package:logging/logging.dart';
|
||||
@ -168,7 +167,7 @@ class AboutPage extends ConsumerWidget {
|
||||
'dart': Platform.version,
|
||||
});
|
||||
final text = const JsonEncoder.withIndent(' ').convert(data);
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
await ref.read(clipboardProvider).setText(text);
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(
|
||||
@ -236,13 +235,17 @@ class LoggingPanel extends ConsumerWidget {
|
||||
onPressed: () async {
|
||||
_log.info('Copying log to clipboard ($version)...');
|
||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||
await Clipboard.setData(ClipboardData(text: logs.join('\n')));
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.general_log_copied);
|
||||
},
|
||||
);
|
||||
var clipboard = ref.read(clipboardProvider);
|
||||
await clipboard.setText(logs.join('\n'));
|
||||
if (!clipboard.platformGivesFeedback()) {
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!.general_log_copied);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
12
lib/android/app_methods.dart
Normal file
12
lib/android/app_methods.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const appMethodsChannel = MethodChannel('app.methods');
|
||||
|
||||
Future<int> getAndroidSdkVersion() async {
|
||||
return await appMethodsChannel.invokeMethod('getAndroidSdkVersion');
|
||||
}
|
||||
|
||||
Future<void> setPrimaryClip(String toClipboard, bool isSensitive) async {
|
||||
await appMethodsChannel.invokeMethod('setPrimaryClip',
|
||||
{'toClipboard': toClipboard, 'isSensitive': isSensitive});
|
||||
}
|
@ -19,6 +19,7 @@ import '../app/views/main_page.dart';
|
||||
import '../core/state.dart';
|
||||
import '../management/state.dart';
|
||||
import '../oath/state.dart';
|
||||
import 'app_methods.dart';
|
||||
import 'management/state.dart';
|
||||
import 'oath/state.dart';
|
||||
import 'qr_scanner/qr_scanner_provider.dart';
|
||||
@ -34,6 +35,9 @@ Future<Widget> initialize() async {
|
||||
|
||||
_initLicenses();
|
||||
|
||||
//initialize sdkInt value
|
||||
int androidSdkVersion = await getAndroidSdkVersion();
|
||||
|
||||
return ProviderScope(
|
||||
overrides: [
|
||||
supportedAppsProvider.overrideWithValue([
|
||||
@ -51,7 +55,9 @@ Future<Widget> initialize() async {
|
||||
managementStateProvider.overrideWithProvider(androidManagementState),
|
||||
currentDeviceProvider.overrideWithProvider(androidCurrentDeviceProvider),
|
||||
qrScannerProvider.overrideWithProvider(androidQrScannerProvider),
|
||||
windowStateProvider.overrideWithProvider(androidWindowStateProvider)
|
||||
windowStateProvider.overrideWithProvider(androidWindowStateProvider),
|
||||
clipboardProvider.overrideWithProvider(androidClipboardProvider),
|
||||
supportedThemesProvider.overrideWithProvider(androidSupportedThemesProvider)
|
||||
],
|
||||
child: DismissKeyboard(
|
||||
child: YubicoAuthenticatorApp(page: Consumer(
|
||||
@ -65,6 +71,9 @@ Future<Widget> initialize() async {
|
||||
/// initializes global handler for dialogs
|
||||
ref.read(androidDialogProvider);
|
||||
|
||||
/// set the platform version
|
||||
ref.read(androidSdkVersionProvider).setVersion(androidSdkVersion);
|
||||
|
||||
/// if the beta dialog was not shown yet, this will show it
|
||||
requestBetaDialog(ref);
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
import 'app_methods.dart';
|
||||
import 'devices.dart';
|
||||
|
||||
const _contextChannel = MethodChannel('android.state.appContext');
|
||||
const _methodsChannel = MethodChannel('app.methods');
|
||||
|
||||
final androidAllowScreenshotsProvider =
|
||||
StateNotifierProvider<AllowScreenshotsNotifier, bool>(
|
||||
@ -18,13 +19,59 @@ class AllowScreenshotsNotifier extends StateNotifier<bool> {
|
||||
|
||||
void setAllowScreenshots(bool value) async {
|
||||
final result =
|
||||
await _methodsChannel.invokeMethod('allowScreenshots', value);
|
||||
await appMethodsChannel.invokeMethod('allowScreenshots', value);
|
||||
if (mounted) {
|
||||
state = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final androidClipboardProvider = Provider<AppClipboard>(
|
||||
(ref) => _AndroidClipboard(ref),
|
||||
);
|
||||
|
||||
class _AndroidClipboard extends AppClipboard {
|
||||
final ProviderRef<AppClipboard> _ref;
|
||||
|
||||
const _AndroidClipboard(this._ref);
|
||||
|
||||
@override
|
||||
bool platformGivesFeedback() {
|
||||
return _ref.read(androidSdkVersionProvider).getVersion() >= 33;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setText(String toClipboard, {bool isSensitive = false}) async {
|
||||
await setPrimaryClip(toClipboard, isSensitive);
|
||||
}
|
||||
}
|
||||
|
||||
final androidSdkVersionProvider = StateProvider<_AndroidSdkVersion>(
|
||||
(ref) => _AndroidSdkVersion(),
|
||||
);
|
||||
|
||||
class _AndroidSdkVersion {
|
||||
int _sdkVersion = -1;
|
||||
|
||||
int getVersion() {
|
||||
return _sdkVersion;
|
||||
}
|
||||
|
||||
void setVersion(int value) {
|
||||
_sdkVersion = value;
|
||||
}
|
||||
}
|
||||
|
||||
final androidSupportedThemesProvider = StateProvider<List<ThemeMode>>((ref) {
|
||||
if (ref.read(androidSdkVersionProvider).getVersion() < 29) {
|
||||
/// the user can select from light or dark theme of the app
|
||||
return [ThemeMode.light, ThemeMode.dark];
|
||||
} else {
|
||||
/// the user can also select system theme on newer Android versions
|
||||
return ThemeMode.values;
|
||||
}
|
||||
});
|
||||
|
||||
final androidSubPageProvider =
|
||||
StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
|
||||
return _AndroidSubPageNotifier(ref.watch(supportedAppsProvider));
|
||||
|
@ -153,7 +153,8 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
title: const Text('App theme'),
|
||||
subtitle: Text(themeMode.displayName),
|
||||
onTap: () async {
|
||||
final newMode = await _selectAppearance(context, themeMode);
|
||||
final newMode = await _selectAppearance(
|
||||
ref.read(supportedThemesProvider), context, themeMode);
|
||||
ref.read(themeModeProvider.notifier).setThemeMode(newMode);
|
||||
},
|
||||
),
|
||||
@ -211,14 +212,14 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
}) ??
|
||||
_defaultClipKbdLayout;
|
||||
|
||||
Future<ThemeMode> _selectAppearance(
|
||||
Future<ThemeMode> _selectAppearance(List<ThemeMode> supportedThemes,
|
||||
BuildContext context, ThemeMode themeMode) async =>
|
||||
await showDialog<ThemeMode>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return SimpleDialog(
|
||||
title: const Text('Choose app theme'),
|
||||
children: ThemeMode.values
|
||||
children: supportedThemes
|
||||
.map((e) => RadioListTile(
|
||||
title: Text(e.displayName),
|
||||
value: e,
|
||||
@ -231,5 +232,5 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
|
||||
.toList(),
|
||||
);
|
||||
}) ??
|
||||
ThemeMode.system;
|
||||
themeMode;
|
||||
}
|
||||
|
@ -20,13 +20,23 @@ final windowStateProvider = Provider<WindowState>(
|
||||
(ref) => WindowState(focused: true, visible: true, active: true),
|
||||
);
|
||||
|
||||
final supportedThemesProvider =
|
||||
StateProvider<List<ThemeMode>>((ref) => ThemeMode.values);
|
||||
|
||||
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
||||
(ref) => ThemeModeNotifier(ref.watch(prefProvider)));
|
||||
(ref) => ThemeModeNotifier(
|
||||
ref.watch(prefProvider),
|
||||
|
||||
/// theme on index 0 is the default
|
||||
ref.read(supportedThemesProvider)[0]),
|
||||
);
|
||||
|
||||
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
||||
static const String _key = 'APP_STATE_THEME';
|
||||
final SharedPreferences _prefs;
|
||||
ThemeModeNotifier(this._prefs) : super(_fromName(_prefs.getString(_key)));
|
||||
|
||||
ThemeModeNotifier(this._prefs, ThemeMode defaultTheme)
|
||||
: super(_fromName(_prefs.getString(_key), defaultTheme));
|
||||
|
||||
void setThemeMode(ThemeMode mode) {
|
||||
_log.debug('Set theme to $mode');
|
||||
@ -34,14 +44,14 @@ class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
||||
_prefs.setString(_key, mode.name);
|
||||
}
|
||||
|
||||
static ThemeMode _fromName(String? name) {
|
||||
static ThemeMode _fromName(String? name, ThemeMode defaultTheme) {
|
||||
switch (name) {
|
||||
case 'light':
|
||||
return ThemeMode.light;
|
||||
case 'dark':
|
||||
return ThemeMode.dark;
|
||||
default:
|
||||
return ThemeMode.system;
|
||||
return defaultTheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -71,6 +81,7 @@ final currentDeviceProvider =
|
||||
|
||||
abstract class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
|
||||
CurrentDeviceNotifier(super.state);
|
||||
|
||||
setCurrentDevice(DeviceNode? device);
|
||||
}
|
||||
|
||||
@ -85,6 +96,7 @@ final currentAppProvider =
|
||||
|
||||
class CurrentAppNotifier extends StateNotifier<Application> {
|
||||
final List<Application> _supportedApps;
|
||||
|
||||
CurrentAppNotifier(this._supportedApps) : super(_supportedApps.first);
|
||||
|
||||
void setCurrentApp(Application app) {
|
||||
@ -137,6 +149,18 @@ class ContextConsumer extends StateNotifier<Function(BuildContext)?> {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AppClipboard {
|
||||
const AppClipboard();
|
||||
|
||||
Future<void> setText(String toClipboard, {bool isSensitive = false});
|
||||
|
||||
bool platformGivesFeedback();
|
||||
}
|
||||
|
||||
final clipboardProvider = Provider<AppClipboard>(
|
||||
(ref) => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
/// A callback which will be invoked with a [BuildContext] that can be used to
|
||||
/// open dialogs, show Snackbars, etc.
|
||||
///
|
||||
|
@ -118,6 +118,7 @@ Future<Widget> initialize(List<String> argv) async {
|
||||
fidoStateProvider.overrideWithProvider(desktopFidoState),
|
||||
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
|
||||
credentialProvider.overrideWithProvider(desktopCredentialProvider),
|
||||
clipboardProvider.overrideWithProvider(desktopClipboardProvider)
|
||||
],
|
||||
child: YubicoAuthenticatorApp(
|
||||
page: Consumer(
|
||||
|
@ -1,15 +1,16 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:yubico_authenticator/app/logging.dart';
|
||||
|
||||
import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
import '../core/state.dart';
|
||||
import '../app/models.dart';
|
||||
import 'models.dart';
|
||||
import 'rpc.dart';
|
||||
|
||||
@ -26,6 +27,7 @@ final rpcStateProvider = StateNotifierProvider<_RpcStateNotifier, RpcState>(
|
||||
|
||||
class _RpcStateNotifier extends StateNotifier<RpcState> {
|
||||
final RpcSession rpc;
|
||||
|
||||
_RpcStateNotifier(this.rpc) : super(const RpcState('unknown', false)) {
|
||||
_init();
|
||||
}
|
||||
@ -49,6 +51,7 @@ final desktopWindowStateProvider = Provider<WindowState>(
|
||||
class _WindowStateNotifier extends StateNotifier<WindowState>
|
||||
with WindowListener {
|
||||
Timer? _idleTimer;
|
||||
|
||||
_WindowStateNotifier()
|
||||
: super(WindowState(focused: true, visible: true, active: true)) {
|
||||
_init();
|
||||
@ -111,6 +114,22 @@ class _WindowStateNotifier extends StateNotifier<WindowState>
|
||||
}
|
||||
}
|
||||
|
||||
final desktopClipboardProvider = Provider<AppClipboard>(
|
||||
(ref) => _DesktopClipboard(),
|
||||
);
|
||||
|
||||
class _DesktopClipboard extends AppClipboard {
|
||||
@override
|
||||
bool platformGivesFeedback() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setText(String toClipboard, {bool isSensitive = false}) async {
|
||||
await Clipboard.setData(ClipboardData(text: toClipboard));
|
||||
}
|
||||
}
|
||||
|
||||
final desktopCurrentDeviceProvider =
|
||||
StateNotifierProvider<CurrentDeviceNotifier, DeviceNode?>((ref) {
|
||||
final provider = _DesktopCurrentDeviceNotifier(ref.watch(prefProvider));
|
||||
@ -121,6 +140,7 @@ final desktopCurrentDeviceProvider =
|
||||
class _DesktopCurrentDeviceNotifier extends CurrentDeviceNotifier {
|
||||
static const String _lastDevice = 'APP_STATE_LAST_DEVICE';
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
_DesktopCurrentDeviceNotifier(this._prefs) : super(null);
|
||||
|
||||
_updateAttachedDevices(List<DeviceNode>? previous, List<DeviceNode> devices) {
|
||||
|
@ -14,6 +14,7 @@ import 'account_mixin.dart';
|
||||
class AccountDialog extends ConsumerWidget with AccountMixin {
|
||||
@override
|
||||
final OathCredential credential;
|
||||
|
||||
const AccountDialog(this.credential, {super.key});
|
||||
|
||||
@override
|
||||
@ -122,7 +123,8 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
|
||||
}
|
||||
await ref.read(withContextProvider)(
|
||||
(context) async {
|
||||
copyToClipboard(context, getCode(ref));
|
||||
copyToClipboard(
|
||||
ref.watch(clipboardProvider), context, getCode(ref));
|
||||
},
|
||||
);
|
||||
return null;
|
||||
|
@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
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';
|
||||
|
||||
@ -68,11 +67,14 @@ mixin AccountMixin {
|
||||
}
|
||||
|
||||
@protected
|
||||
void copyToClipboard(BuildContext context, OathCode? code) {
|
||||
void copyToClipboard(
|
||||
AppClipboard clipboard, BuildContext context, OathCode? code) {
|
||||
if (code != null) {
|
||||
Clipboard.setData(ClipboardData(text: code.value));
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.oath_copied_to_clipboard);
|
||||
clipboard.setText(code.value, isSensitive: true);
|
||||
if (!clipboard.platformGivesFeedback()) {
|
||||
showMessage(
|
||||
context, AppLocalizations.of(context)!.oath_copied_to_clipboard);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -117,11 +119,14 @@ mixin AccountMixin {
|
||||
action: code == null || expired
|
||||
? null
|
||||
: (context) {
|
||||
Clipboard.setData(ClipboardData(text: code.value));
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.oath_copied_to_clipboard);
|
||||
var clipboard = ref.read(clipboardProvider);
|
||||
clipboard.setText(code.value, isSensitive: true);
|
||||
if (!clipboard.platformGivesFeedback()) {
|
||||
showMessage(
|
||||
context,
|
||||
AppLocalizations.of(context)!
|
||||
.oath_copied_to_clipboard);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (manual)
|
||||
|
@ -80,8 +80,8 @@ class AccountView extends ConsumerWidget with AccountMixin {
|
||||
ref,
|
||||
)
|
||||
: getCode(ref);
|
||||
await withContext(
|
||||
(context) async => copyToClipboard(context, code));
|
||||
await withContext((context) async =>
|
||||
copyToClipboard(ref.watch(clipboardProvider), context, code));
|
||||
},
|
||||
);
|
||||
} on CancellationException catch (_) {
|
||||
|
Loading…
Reference in New Issue
Block a user