mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 16:32:01 +03:00
Read QR code from file on Android
This commit is contained in:
parent
8a9d465bb1
commit
184e7a7f2c
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 Yubico.
|
* Copyright (C) 2022-2023 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -28,14 +28,14 @@ class MethodChannelQRScannerZxing extends QRScannerZxingPlatform {
|
|||||||
@override
|
@override
|
||||||
Future<String?> getPlatformVersion() async {
|
Future<String?> getPlatformVersion() async {
|
||||||
final version =
|
final version =
|
||||||
await methodChannel.invokeMethod<String>('getPlatformVersion');
|
await methodChannel.invokeMethod<String>('getPlatformVersion');
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> scanBitmap(Uint8List bytes) async {
|
Future<String?> scanBitmap(Uint8List bytes) async {
|
||||||
final version = await methodChannel
|
final result = await methodChannel
|
||||||
.invokeMethod<String>('scanBitmap', {'bytes': bytes});
|
.invokeMethod<String>('scanBitmap', {'bytes': bytes});
|
||||||
return version;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2022 Yubico.
|
* Copyright (C) 2022-2023 Yubico.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@ -16,16 +16,25 @@
|
|||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
|
||||||
import 'package:yubico_authenticator/app/state.dart';
|
import 'package:yubico_authenticator/app/state.dart';
|
||||||
import 'package:yubico_authenticator/exception/cancellation_exception.dart';
|
import 'package:yubico_authenticator/exception/cancellation_exception.dart';
|
||||||
import 'package:yubico_authenticator/theme.dart';
|
import 'package:yubico_authenticator/theme.dart';
|
||||||
|
|
||||||
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
|
import '../../app/message.dart';
|
||||||
|
import '../../oath/views/add_account_page.dart';
|
||||||
|
import '../../oath/views/utils.dart';
|
||||||
import 'qr_scanner_view.dart';
|
import 'qr_scanner_view.dart';
|
||||||
|
|
||||||
class AndroidQrScanner implements QrScanner {
|
class AndroidQrScanner implements QrScanner {
|
||||||
|
static const String kQrScannerRequestManualEntry =
|
||||||
|
'__QR_SCANNER_ENTER_MANUALLY__';
|
||||||
|
static const String kQrScannerRequestReadFromFile =
|
||||||
|
'__QR_SCANNER_SCAN_FROM_FILE__';
|
||||||
final WithContext _withContext;
|
final WithContext _withContext;
|
||||||
|
|
||||||
AndroidQrScanner(this._withContext);
|
AndroidQrScanner(this._withContext);
|
||||||
@ -33,8 +42,7 @@ class AndroidQrScanner implements QrScanner {
|
|||||||
@override
|
@override
|
||||||
Future<String?> scanQr([String? imageData]) async {
|
Future<String?> scanQr([String? imageData]) async {
|
||||||
if (imageData == null) {
|
if (imageData == null) {
|
||||||
var scannedCode = await _withContext(
|
var scannedCode = await _withContext((context) async =>
|
||||||
(context) async =>
|
|
||||||
await Navigator.of(context).push(PageRouteBuilder(
|
await Navigator.of(context).push(PageRouteBuilder(
|
||||||
pageBuilder: (_, __, ___) =>
|
pageBuilder: (_, __, ___) =>
|
||||||
Theme(data: AppTheme.darkTheme, child: const QrScannerView()),
|
Theme(data: AppTheme.darkTheme, child: const QrScannerView()),
|
||||||
@ -55,9 +63,62 @@ class AndroidQrScanner implements QrScanner {
|
|||||||
return await zxingChannel.scanBitmap(base64Decode(imageData));
|
return await zxingChannel.scanBitmap(base64Decode(imageData));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> handleScannedData(
|
||||||
|
String? qrData, WidgetRef ref, AppLocalizations l10n) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
switch (qrData) {
|
||||||
|
case null:
|
||||||
|
break;
|
||||||
|
case kQrScannerRequestManualEntry:
|
||||||
|
await withContext((context) => showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
routeSettings: const RouteSettings(name: 'oath_add_account'),
|
||||||
|
builder: (context) {
|
||||||
|
return const OathAddAccountPage(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
credentials: null,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
case kQrScannerRequestReadFromFile:
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
allowedExtensions: ['png', 'jpg', 'gif', 'webp'],
|
||||||
|
type: FileType.custom,
|
||||||
|
allowMultiple: false,
|
||||||
|
lockParentWindow: true,
|
||||||
|
withData: true,
|
||||||
|
dialogTitle: 'Select file with QR code');
|
||||||
|
|
||||||
|
if (result == null || !result.isSinglePick) {
|
||||||
|
// no result
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = result.files.first.bytes;
|
||||||
|
final scanner = ref.read(qrScannerProvider);
|
||||||
|
if (bytes != null && scanner != null) {
|
||||||
|
final b64bytes = base64Encode(bytes);
|
||||||
|
final qrData = await scanner.scanQr(b64bytes);
|
||||||
|
if (qrData != null) {
|
||||||
|
await withContext((context) =>
|
||||||
|
handleUri(context, null, qrData, null, null, l10n));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// no QR code found
|
||||||
|
await withContext(
|
||||||
|
(context) async => showMessage(context, l10n.l_qr_not_found));
|
||||||
|
|
||||||
|
default:
|
||||||
|
await withContext(
|
||||||
|
(context) => handleUri(context, null, qrData, null, null, l10n));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QrScanner? Function(dynamic) androidQrScannerProvider(hasCamera) {
|
QrScanner? Function(dynamic) androidQrScannerProvider(hasCamera) {
|
||||||
return (ref) =>
|
return (ref) =>
|
||||||
hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null;
|
hasCamera ? AndroidQrScanner(ref.watch(withContextProvider)) : null;
|
||||||
}
|
}
|
||||||
|
@ -14,13 +14,10 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:yubico_authenticator/app/state.dart';
|
import 'package:yubico_authenticator/android/qr_scanner/qr_scanner_provider.dart';
|
||||||
|
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
import 'qr_scanner_scan_status.dart';
|
import 'qr_scanner_scan_status.dart';
|
||||||
@ -79,53 +76,34 @@ class QRScannerUI extends ConsumerWidget {
|
|||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop('');
|
Navigator.of(context).pop(
|
||||||
|
AndroidQrScanner.kQrScannerRequestManualEntry);
|
||||||
},
|
},
|
||||||
key: keys.manualEntryButton,
|
key: keys.manualEntryButton,
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.s_enter_manually,
|
l10n.s_enter_manually,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
)),
|
)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () async {
|
onPressed: () {
|
||||||
Navigator.of(context).pop('');
|
Navigator.of(context).pop(
|
||||||
final result = await FilePicker.platform.pickFiles(
|
AndroidQrScanner.kQrScannerRequestReadFromFile);
|
||||||
allowedExtensions: ['png', 'jpg'],
|
|
||||||
type: FileType.custom,
|
|
||||||
allowMultiple: false,
|
|
||||||
lockParentWindow: true,
|
|
||||||
dialogTitle: 'Select file with QR code');
|
|
||||||
if (result != null && result.files.isNotEmpty) {
|
|
||||||
final fileWithCode = result.files.first;
|
|
||||||
final bytes = fileWithCode.bytes;
|
|
||||||
if (bytes == null || bytes.isEmpty) {
|
|
||||||
//err return
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (bytes.length > 3 * 1024 * 1024) {
|
|
||||||
// too big file
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final scanner = ref.read(qrScannerProvider);
|
|
||||||
if (scanner != null) {
|
|
||||||
await scanner.scanQr(base64UrlEncode(bytes));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
key: keys.readFromImage,
|
key: keys.readFromImage,
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.s_read_from_image,
|
l10n.s_read_from_file,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
)),
|
))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8)
|
const SizedBox(height: 16)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -15,19 +15,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../android/app_methods.dart';
|
import '../../android/app_methods.dart';
|
||||||
|
import '../../android/qr_scanner/qr_scanner_provider.dart';
|
||||||
import '../../android/state.dart';
|
import '../../android/state.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../fido/views/fido_screen.dart';
|
import '../../fido/views/fido_screen.dart';
|
||||||
import '../../oath/views/add_account_page.dart';
|
|
||||||
import '../../oath/views/oath_screen.dart';
|
import '../../oath/views/oath_screen.dart';
|
||||||
import '../../oath/views/utils.dart';
|
|
||||||
import '../../piv/views/piv_screen.dart';
|
import '../../piv/views/piv_screen.dart';
|
||||||
import '../../widgets/custom_icons.dart';
|
import '../../widgets/custom_icons.dart';
|
||||||
import '../message.dart';
|
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'device_error_screen.dart';
|
import 'device_error_screen.dart';
|
||||||
@ -99,33 +98,16 @@ class MainPage extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.person_add_alt_1),
|
icon: const Icon(Icons.person_add_alt_1),
|
||||||
tooltip: l10n.s_add_account,
|
tooltip: l10n.s_add_account,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final withContext = ref.read(withContextProvider);
|
|
||||||
final scanner = ref.read(qrScannerProvider);
|
final scanner = ref.read(qrScannerProvider);
|
||||||
if (scanner != null) {
|
if (scanner != null) {
|
||||||
try {
|
try {
|
||||||
final qrData = await scanner.scanQr();
|
final qrData = await scanner.scanQr();
|
||||||
if (qrData != null) {
|
await AndroidQrScanner.handleScannedData(qrData, ref, l10n);
|
||||||
await withContext((context) =>
|
|
||||||
handleUri(context, null, qrData, null, null, l10n));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} on CancellationException catch (_) {
|
} on CancellationException catch (_) {
|
||||||
// ignored - user cancelled
|
// ignored - user cancelled
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await withContext((context) => showBlurDialog(
|
|
||||||
context: context,
|
|
||||||
routeSettings:
|
|
||||||
const RouteSettings(name: 'oath_add_account'),
|
|
||||||
builder: (context) {
|
|
||||||
return const OathAddAccountPage(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
credentials: null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -516,7 +516,7 @@
|
|||||||
"q_want_to_scan": "Would like to scan?",
|
"q_want_to_scan": "Would like to scan?",
|
||||||
"q_no_qr": "No QR code?",
|
"q_no_qr": "No QR code?",
|
||||||
"s_enter_manually": "Enter manually",
|
"s_enter_manually": "Enter manually",
|
||||||
"s_read_from_image": "Provide image file",
|
"s_read_from_file": "Read from file",
|
||||||
|
|
||||||
"@_factory_reset": {},
|
"@_factory_reset": {},
|
||||||
"s_reset": "Reset",
|
"s_reset": "Reset",
|
||||||
|
@ -30,10 +30,10 @@ import '../../app/message.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/views/user_interaction.dart';
|
import '../../app/views/user_interaction.dart';
|
||||||
import '../../exception/apdu_exception.dart';
|
|
||||||
import '../../exception/cancellation_exception.dart';
|
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../desktop/models.dart';
|
import '../../desktop/models.dart';
|
||||||
|
import '../../exception/apdu_exception.dart';
|
||||||
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
import '../../widgets/choice_filter_chip.dart';
|
import '../../widgets/choice_filter_chip.dart';
|
||||||
import '../../widgets/file_drop_target.dart';
|
import '../../widgets/file_drop_target.dart';
|
||||||
@ -56,6 +56,7 @@ class OathAddAccountPage extends ConsumerStatefulWidget {
|
|||||||
final OathState? state;
|
final OathState? state;
|
||||||
final List<OathCredential>? credentials;
|
final List<OathCredential>? credentials;
|
||||||
final CredentialData? credentialData;
|
final CredentialData? credentialData;
|
||||||
|
|
||||||
const OathAddAccountPage(
|
const OathAddAccountPage(
|
||||||
this.devicePath,
|
this.devicePath,
|
||||||
this.state, {
|
this.state, {
|
||||||
|
@ -20,6 +20,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||||||
import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart';
|
import 'package:yubico_authenticator/oath/icon_provider/icon_pack_dialog.dart';
|
||||||
import 'package:yubico_authenticator/oath/views/add_account_dialog.dart';
|
import 'package:yubico_authenticator/oath/views/add_account_dialog.dart';
|
||||||
|
|
||||||
|
import '../../android/qr_scanner/qr_scanner_provider.dart';
|
||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
@ -28,11 +29,8 @@ import '../../app/views/action_list.dart';
|
|||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
import '../state.dart';
|
|
||||||
import 'add_account_page.dart';
|
|
||||||
import 'manage_password_dialog.dart';
|
import 'manage_password_dialog.dart';
|
||||||
import 'reset_dialog.dart';
|
import 'reset_dialog.dart';
|
||||||
import 'utils.dart';
|
|
||||||
|
|
||||||
Widget oathBuildActions(
|
Widget oathBuildActions(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@ -59,38 +57,14 @@ Widget oathBuildActions(
|
|||||||
icon: const Icon(Icons.person_add_alt_1_outlined),
|
icon: const Icon(Icons.person_add_alt_1_outlined),
|
||||||
onTap: used != null && (capacity == null || capacity > used)
|
onTap: used != null && (capacity == null || capacity > used)
|
||||||
? (context) async {
|
? (context) async {
|
||||||
final credentials = ref.read(credentialsProvider);
|
|
||||||
final withContext = ref.read(withContextProvider);
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
if (isAndroid) {
|
if (isAndroid) {
|
||||||
final qrScanner = ref.read(qrScannerProvider);
|
final qrScanner = ref.read(qrScannerProvider);
|
||||||
if (qrScanner != null) {
|
if (qrScanner != null) {
|
||||||
final qrData = await qrScanner.scanQr();
|
final qrData = await qrScanner.scanQr();
|
||||||
if (qrData != null) {
|
await AndroidQrScanner.handleScannedData(
|
||||||
await withContext((context) => handleUri(
|
qrData, ref, l10n);
|
||||||
context,
|
|
||||||
credentials,
|
|
||||||
qrData,
|
|
||||||
devicePath,
|
|
||||||
oathState,
|
|
||||||
l10n,
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await withContext((context) => showBlurDialog(
|
|
||||||
context: context,
|
|
||||||
routeSettings:
|
|
||||||
const RouteSettings(name: 'oath_add_account'),
|
|
||||||
builder: (context) {
|
|
||||||
return OathAddAccountPage(
|
|
||||||
devicePath,
|
|
||||||
oathState,
|
|
||||||
credentials: credentials,
|
|
||||||
credentialData: null,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
} else {
|
} else {
|
||||||
await showBlurDialog(
|
await showBlurDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
Loading…
Reference in New Issue
Block a user