mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Merge branch 'main' into translate
This commit is contained in:
commit
65e95dbd7a
@ -40,6 +40,7 @@ class ActivityUtil(private val activity: Activity) {
|
|||||||
MAIN_ACTIVITY_ALIAS,
|
MAIN_ACTIVITY_ALIAS,
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||||
)
|
)
|
||||||
|
logger.debug("Enabled USB discovery by setting state of $MAIN_ACTIVITY_ALIAS to ENABLED")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,6 +56,7 @@ class ActivityUtil(private val activity: Activity) {
|
|||||||
MAIN_ACTIVITY_ALIAS,
|
MAIN_ACTIVITY_ALIAS,
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
||||||
)
|
)
|
||||||
|
logger.debug("Disabled USB discovery by setting state of $MAIN_ACTIVITY_ALIAS to DEFAULT")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -73,6 +75,7 @@ class ActivityUtil(private val activity: Activity) {
|
|||||||
NDEF_ACTIVITY_ALIAS,
|
NDEF_ACTIVITY_ALIAS,
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
||||||
)
|
)
|
||||||
|
logger.debug("Enabled NFC discovery by setting state of $NDEF_ACTIVITY_ALIAS to DEFAULT")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -88,6 +91,7 @@ class ActivityUtil(private val activity: Activity) {
|
|||||||
NDEF_ACTIVITY_ALIAS,
|
NDEF_ACTIVITY_ALIAS,
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
||||||
)
|
)
|
||||||
|
logger.debug("Disabled NFC discovery by setting state of $NDEF_ACTIVITY_ALIAS to DISABLED")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setState(aliasName: String, enabledState: Int) {
|
private fun setState(aliasName: String, enabledState: Int) {
|
||||||
|
@ -68,6 +68,8 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
private lateinit var yubikit: YubiKitManager
|
private lateinit var yubikit: YubiKitManager
|
||||||
|
|
||||||
|
private var preserveConnectionOnPause: Boolean = false
|
||||||
|
|
||||||
// receives broadcasts when QR Scanner camera is closed
|
// receives broadcasts when QR Scanner camera is closed
|
||||||
private val qrScannerCameraClosedBR = QRScannerCameraClosedBR()
|
private val qrScannerCameraClosedBR = QRScannerCameraClosedBR()
|
||||||
private val nfcAdapterStateChangeBR = NfcAdapterStateChangedBR()
|
private val nfcAdapterStateChangeBR = NfcAdapterStateChangedBR()
|
||||||
@ -158,8 +160,12 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
appPreferences.unregisterListener(sharedPreferencesListener)
|
appPreferences.unregisterListener(sharedPreferencesListener)
|
||||||
|
|
||||||
stopUsbDiscovery()
|
if (!preserveConnectionOnPause) {
|
||||||
stopNfcDiscovery()
|
stopUsbDiscovery()
|
||||||
|
stopNfcDiscovery()
|
||||||
|
} else {
|
||||||
|
logger.debug("Any existing connections are preserved")
|
||||||
|
}
|
||||||
|
|
||||||
if (!appPreferences.openAppOnUsb) {
|
if (!appPreferences.openAppOnUsb) {
|
||||||
activityUtil.disableSystemUsbDiscovery()
|
activityUtil.disableSystemUsbDiscovery()
|
||||||
@ -179,62 +185,68 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
|
|
||||||
activityUtil.enableSystemUsbDiscovery()
|
activityUtil.enableSystemUsbDiscovery()
|
||||||
|
|
||||||
// Handle opening through otpauth:// link
|
if (!preserveConnectionOnPause) {
|
||||||
val intentData = intent.data
|
// Handle opening through otpauth:// link
|
||||||
if (intentData != null &&
|
val intentData = intent.data
|
||||||
(intentData.scheme == "otpauth" ||
|
if (intentData != null &&
|
||||||
intentData.scheme == "otpauth-migration")
|
(intentData.scheme == "otpauth" ||
|
||||||
) {
|
intentData.scheme == "otpauth-migration")
|
||||||
intent.data = null
|
) {
|
||||||
appLinkMethodChannel.handleUri(intentData)
|
intent.data = null
|
||||||
}
|
appLinkMethodChannel.handleUri(intentData)
|
||||||
|
}
|
||||||
|
|
||||||
// Handle existing tag when launched from NDEF
|
// Handle existing tag when launched from NDEF
|
||||||
val tag = intent.parcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
val tag = intent.parcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
intent.removeExtra(NfcAdapter.EXTRA_TAG)
|
intent.removeExtra(NfcAdapter.EXTRA_TAG)
|
||||||
|
|
||||||
val executor = Executors.newSingleThreadExecutor()
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
val device = NfcYubiKeyDevice(tag, nfcConfiguration.timeout, executor)
|
val device = NfcYubiKeyDevice(tag, nfcConfiguration.timeout, executor)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
contextManager?.processYubiKey(device)
|
contextManager?.processYubiKey(device)
|
||||||
device.remove {
|
device.remove {
|
||||||
executor.shutdown()
|
executor.shutdown()
|
||||||
startNfcDiscovery()
|
startNfcDiscovery()
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logger.error("Error processing YubiKey in AppContextManager", e)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
logger.error("Error processing YubiKey in AppContextManager", e)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
startNfcDiscovery()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
startNfcDiscovery()
|
|
||||||
}
|
|
||||||
|
|
||||||
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
|
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
|
||||||
val device = intent.parcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
|
val device = intent.parcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
|
||||||
if (device != null) {
|
if (device != null) {
|
||||||
// start the USB discover only if the user approved the app to use the device
|
// start the USB discover only if the user approved the app to use the device
|
||||||
if (usbManager.hasPermission(device)) {
|
if (usbManager.hasPermission(device)) {
|
||||||
startUsbDiscovery()
|
startUsbDiscovery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if any YubiKeys are connected, use them directly
|
||||||
|
val deviceIterator = usbManager.deviceList.values.iterator()
|
||||||
|
while (deviceIterator.hasNext()) {
|
||||||
|
val device = deviceIterator.next()
|
||||||
|
if (device.vendorId == YUBICO_VENDOR_ID) {
|
||||||
|
// the device might not have a USB permission
|
||||||
|
// it will be requested during during the UsbDiscovery
|
||||||
|
startUsbDiscovery()
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if any YubiKeys are connected, use them directly
|
logger.debug("Resume with preserved connection")
|
||||||
val deviceIterator = usbManager.deviceList.values.iterator()
|
|
||||||
while (deviceIterator.hasNext()) {
|
|
||||||
val device = deviceIterator.next()
|
|
||||||
if (device.vendorId == YUBICO_VENDOR_ID) {
|
|
||||||
// the device might not have a USB permission
|
|
||||||
// it will be requested during during the UsbDiscovery
|
|
||||||
startUsbDiscovery()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
appPreferences.registerListener(sharedPreferencesListener)
|
appPreferences.registerListener(sharedPreferencesListener)
|
||||||
|
|
||||||
|
preserveConnectionOnPause = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration) {
|
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration) {
|
||||||
@ -374,6 +386,14 @@ class MainActivity : FlutterFragmentActivity() {
|
|||||||
"getAndroidSdkVersion" -> result.success(
|
"getAndroidSdkVersion" -> result.success(
|
||||||
Build.VERSION.SDK_INT
|
Build.VERSION.SDK_INT
|
||||||
)
|
)
|
||||||
|
|
||||||
|
"preserveConnectionOnPause" -> {
|
||||||
|
preserveConnectionOnPause = true
|
||||||
|
result.success(
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
"setPrimaryClip" -> {
|
"setPrimaryClip" -> {
|
||||||
val toClipboard = methodCall.argument<String>("toClipboard")
|
val toClipboard = methodCall.argument<String>("toClipboard")
|
||||||
val isSensitive = methodCall.argument<Boolean>("isSensitive")
|
val isSensitive = methodCall.argument<Boolean>("isSensitive")
|
||||||
|
@ -176,7 +176,7 @@ class OathManager(
|
|||||||
delay(delayMs)
|
delay(delayMs)
|
||||||
}
|
}
|
||||||
val currentState = lifecycleOwner.lifecycle.currentState
|
val currentState = lifecycleOwner.lifecycle.currentState
|
||||||
if (currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
if (currentState.isAtLeast(Lifecycle.State.STARTED)) {
|
||||||
requestRefresh()
|
requestRefresh()
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
@ -45,16 +45,16 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
|
targetSdk 34
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
def camerax_version = "1.2.3"
|
def camerax_version = "1.3.0"
|
||||||
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
||||||
implementation "androidx.camera:camera-view:${camerax_version}"
|
implementation "androidx.camera:camera-view:${camerax_version}"
|
||||||
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||||
|
|
||||||
//noinspection GradleDependency
|
implementation "com.google.zxing:core:3.5.2"
|
||||||
implementation "com.google.zxing:core:3.3.3"
|
|
||||||
implementation "com.google.zxing:android-core:3.3.0"
|
implementation "com.google.zxing:android-core:3.3.0"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#Fri Jun 23 08:50:38 CEST 2017
|
#Mon Oct 16 08:48:17 CEST 2023
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
|
||||||
|
@ -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.
|
||||||
@ -22,8 +22,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
@ -36,7 +34,9 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.google.zxing.*
|
import com.google.zxing.BinaryBitmap
|
||||||
|
import com.google.zxing.NotFoundException
|
||||||
|
import com.google.zxing.RGBLuminanceSource
|
||||||
import com.google.zxing.common.HybridBinarizer
|
import com.google.zxing.common.HybridBinarizer
|
||||||
import io.flutter.plugin.common.BinaryMessenger
|
import io.flutter.plugin.common.BinaryMessenger
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
@ -44,6 +44,9 @@ import io.flutter.plugin.common.PluginRegistry
|
|||||||
import io.flutter.plugin.common.StandardMessageCodec
|
import io.flutter.plugin.common.StandardMessageCodec
|
||||||
import io.flutter.plugin.platform.PlatformView
|
import io.flutter.plugin.platform.PlatformView
|
||||||
import io.flutter.plugin.platform.PlatformViewFactory
|
import io.flutter.plugin.platform.PlatformViewFactory
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
import java.util.concurrent.ExecutorService
|
import java.util.concurrent.ExecutorService
|
||||||
@ -80,7 +83,7 @@ internal class QRScannerView(
|
|||||||
) : PlatformView {
|
) : PlatformView {
|
||||||
|
|
||||||
private val stateChangeObserver = StateChangeObserver(context)
|
private val stateChangeObserver = StateChangeObserver(context)
|
||||||
private val uiThreadHandler = Handler(Looper.getMainLooper())
|
private val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "QRScannerView"
|
const val TAG = "QRScannerView"
|
||||||
@ -104,11 +107,17 @@ internal class QRScannerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun requestPermissions(activity: Activity) {
|
private fun requestPermissions(activity: Activity) {
|
||||||
ActivityCompat.requestPermissions(
|
coroutineScope.launch {
|
||||||
activity,
|
methodChannel.invokeMethod(
|
||||||
PERMISSIONS_TO_REQUEST,
|
"beforePermissionsRequest", null
|
||||||
PERMISSION_REQUEST_CODE
|
)
|
||||||
)
|
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
activity,
|
||||||
|
PERMISSIONS_TO_REQUEST,
|
||||||
|
PERMISSION_REQUEST_CODE
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val qrScannerView = View.inflate(context, R.layout.qr_scanner_view, null)
|
private val qrScannerView = View.inflate(context, R.layout.qr_scanner_view, null)
|
||||||
@ -147,7 +156,6 @@ internal class QRScannerView(
|
|||||||
return qrScannerView
|
return qrScannerView
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun dispose() {
|
override fun dispose() {
|
||||||
cameraProvider?.unbindAll()
|
cameraProvider?.unbindAll()
|
||||||
preview = null
|
preview = null
|
||||||
@ -229,7 +237,7 @@ internal class QRScannerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun reportViewInitialized(permissionsGranted: Boolean) {
|
private fun reportViewInitialized(permissionsGranted: Boolean) {
|
||||||
uiThreadHandler.post {
|
coroutineScope.launch {
|
||||||
methodChannel.invokeMethod(
|
methodChannel.invokeMethod(
|
||||||
"viewInitialized",
|
"viewInitialized",
|
||||||
JSONObject(mapOf("permissionsGranted" to permissionsGranted)).toString()
|
JSONObject(mapOf("permissionsGranted" to permissionsGranted)).toString()
|
||||||
@ -238,7 +246,7 @@ internal class QRScannerView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun reportCodeFound(code: String) {
|
private fun reportCodeFound(code: String) {
|
||||||
uiThreadHandler.post {
|
coroutineScope.launch {
|
||||||
methodChannel.invokeMethod(
|
methodChannel.invokeMethod(
|
||||||
"codeFound", JSONObject(
|
"codeFound", JSONObject(
|
||||||
mapOf("value" to code)
|
mapOf("value" to code)
|
||||||
@ -290,11 +298,6 @@ internal class QRScannerView(
|
|||||||
) : ImageAnalysis.Analyzer {
|
) : ImageAnalysis.Analyzer {
|
||||||
|
|
||||||
var analysisPaused = false
|
var analysisPaused = false
|
||||||
|
|
||||||
val multiFormatReader = MultiFormatReader().also {
|
|
||||||
it.setHints(mapOf(DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE)))
|
|
||||||
}
|
|
||||||
|
|
||||||
var analyzedImagesCount = 0
|
var analyzedImagesCount = 0
|
||||||
|
|
||||||
private fun ByteBuffer.toByteArray(lastRowPadding: Int): ByteArray {
|
private fun ByteBuffer.toByteArray(lastRowPadding: Int): ByteArray {
|
||||||
@ -392,14 +395,14 @@ internal class QRScannerView(
|
|||||||
fullSize
|
fullSize
|
||||||
}
|
}
|
||||||
|
|
||||||
val result: com.google.zxing.Result = multiFormatReader.decode(bitmapToProcess)
|
val result = QrCodeScanner.decodeFromBinaryBitmap(bitmapToProcess)
|
||||||
if (analysisPaused) {
|
if (analysisPaused) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
analysisPaused = true // pause
|
analysisPaused = true // pause
|
||||||
Log.v(TAG, "Analysis result: ${result.text}")
|
Log.v(TAG, "Analysis result: $result")
|
||||||
listener.invoke(Result.success(result.text))
|
listener.invoke(Result.success(result))
|
||||||
} catch (_: NotFoundException) {
|
} catch (_: NotFoundException) {
|
||||||
if (analyzedImagesCount == 0) {
|
if (analyzedImagesCount == 0) {
|
||||||
Log.v(TAG, " No QR code found (NotFoundException)")
|
Log.v(TAG, " No QR code found (NotFoundException)")
|
||||||
|
@ -65,10 +65,24 @@ class QRScannerZxingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: Result) {
|
override fun onMethodCall(call: MethodCall, result: Result) {
|
||||||
if (call.method == "getPlatformVersion") {
|
when (call.method) {
|
||||||
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
"getPlatformVersion" -> {
|
||||||
} else {
|
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
||||||
result.notImplemented()
|
}
|
||||||
|
|
||||||
|
"scanBitmap" -> {
|
||||||
|
val bytes = call.argument<ByteArray>("bytes")
|
||||||
|
if (bytes != null) {
|
||||||
|
val scanResult = QrCodeScanner.decodeFromBytes(bytes)
|
||||||
|
result.success(scanResult)
|
||||||
|
} else {
|
||||||
|
result.error("Failure", "Invalid image", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,80 @@
|
|||||||
|
package com.yubico.authenticator.flutter_plugins.qrscanner_zxing
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.zxing.BarcodeFormat
|
||||||
|
import com.google.zxing.BinaryBitmap
|
||||||
|
import com.google.zxing.DecodeHintType
|
||||||
|
import com.google.zxing.MultiFormatReader
|
||||||
|
import com.google.zxing.NotFoundException
|
||||||
|
import com.google.zxing.RGBLuminanceSource
|
||||||
|
import com.google.zxing.common.HybridBinarizer
|
||||||
|
|
||||||
|
object QrCodeScanner {
|
||||||
|
|
||||||
|
private val qrCodeScanner = MultiFormatReader().also {
|
||||||
|
it.setHints(
|
||||||
|
mapOf(
|
||||||
|
DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE),
|
||||||
|
DecodeHintType.ALSO_INVERTED to true,
|
||||||
|
DecodeHintType.TRY_HARDER to true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeFromBinaryBitmap(binaryBitmap: BinaryBitmap): String {
|
||||||
|
val result = qrCodeScanner.decodeWithState(binaryBitmap)
|
||||||
|
return result.text
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun decodeFromBytes(
|
||||||
|
byteArray: ByteArray,
|
||||||
|
sampleSize: Int
|
||||||
|
): String? {
|
||||||
|
var bitmap: Bitmap? = null
|
||||||
|
try {
|
||||||
|
Log.v(
|
||||||
|
TAG,
|
||||||
|
"Decoding with sampleSize $sampleSize"
|
||||||
|
)
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
options.inSampleSize = sampleSize
|
||||||
|
bitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size, options)
|
||||||
|
bitmap?.let {
|
||||||
|
val pixels = IntArray(it.allocationByteCount)
|
||||||
|
it.getPixels(pixels, 0, it.width, 0, 0, it.width, it.height)
|
||||||
|
|
||||||
|
val luminanceSource =
|
||||||
|
RGBLuminanceSource(it.width, it.height, pixels)
|
||||||
|
|
||||||
|
val binaryBitmap = BinaryBitmap(HybridBinarizer(luminanceSource))
|
||||||
|
val scanResult = decodeFromBinaryBitmap(binaryBitmap)
|
||||||
|
Log.v(TAG, "Scan result: $scanResult")
|
||||||
|
return scanResult
|
||||||
|
}
|
||||||
|
Log.e(TAG, "Could not decode image data.")
|
||||||
|
return null
|
||||||
|
} catch (_: NotFoundException) {
|
||||||
|
Log.e(TAG, "No QR code found/decoded.")
|
||||||
|
return null
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "Exception while decoding data: ", e)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
bitmap?.let {
|
||||||
|
it.recycle()
|
||||||
|
bitmap = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeFromBytes(byteArray: ByteArray): String? {
|
||||||
|
return decodeFromBytes(byteArray, 1)
|
||||||
|
?: decodeFromBytes(byteArray, 4)
|
||||||
|
?: decodeFromBytes(byteArray, 8)
|
||||||
|
?: decodeFromBytes(byteArray, 12)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TAG = "QRScanner"
|
||||||
|
}
|
@ -26,9 +26,11 @@ apply plugin: 'kotlin-android'
|
|||||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 33
|
compileSdk 34
|
||||||
ndkVersion flutter.ndkVersion
|
ndkVersion flutter.ndkVersion
|
||||||
|
|
||||||
|
namespace 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing_example'
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
@ -48,7 +50,7 @@ android {
|
|||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 34
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
}
|
}
|
||||||
@ -60,6 +62,10 @@ android {
|
|||||||
signingConfig signingConfigs.debug
|
signingConfig signingConfigs.debug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flutter {
|
flutter {
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.7.21'
|
ext.kotlin_version = '1.9.10'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:7.1.3'
|
classpath 'com.android.tools.build:gradle:8.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -26,6 +26,6 @@ subprojects {
|
|||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register('clean', Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
|
||||||
|
@ -14,7 +14,9 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
|
||||||
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
|
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
|
||||||
|
|
||||||
import 'cutout_overlay.dart';
|
import 'cutout_overlay.dart';
|
||||||
@ -64,6 +66,36 @@ class AppHomePage extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
},
|
},
|
||||||
child: const Text("Open QR Scanner")),
|
child: const Text("Open QR Scanner")),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
var channel = MethodChannelQRScannerZxing();
|
||||||
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
|
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;
|
||||||
|
if (bytes != null) {
|
||||||
|
var value = await channel.scanBitmap(bytes);
|
||||||
|
final snackBar = SnackBar(
|
||||||
|
content: Text(value == null
|
||||||
|
? 'No QR code detected'
|
||||||
|
: 'QR: $value'));
|
||||||
|
scaffoldMessenger.showSnackBar(snackBar);
|
||||||
|
} else {
|
||||||
|
// no files selected
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text("Scan from file")),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -37,10 +37,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
|
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.17.1"
|
version: "1.17.2"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -57,6 +57,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.3.1"
|
||||||
|
ffi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: ffi
|
||||||
|
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.0"
|
||||||
|
file_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: file_picker
|
||||||
|
sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.5.0"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@ -70,19 +86,24 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.1"
|
||||||
|
flutter_plugin_android_lifecycle:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_plugin_android_lifecycle
|
||||||
|
sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.16"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
js:
|
flutter_web_plugins:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description: flutter
|
||||||
name: js
|
source: sdk
|
||||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
version: "0.0.0"
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.6.7"
|
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -95,18 +116,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
|
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.15"
|
version: "0.12.16"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
|
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.0"
|
version: "0.5.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -127,10 +148,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: plugin_platform_interface
|
name: plugin_platform_interface
|
||||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
version: "2.1.6"
|
||||||
qrscanner_zxing:
|
qrscanner_zxing:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -147,10 +168,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
|
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.10.0"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -187,10 +208,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
|
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1"
|
version: "0.6.0"
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -199,6 +220,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.4"
|
||||||
|
web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: web
|
||||||
|
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.4-beta"
|
||||||
|
win32:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: win32
|
||||||
|
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.9"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.0.0-0 <4.0.0"
|
dart: ">=3.1.0-185.0.dev <4.0.0"
|
||||||
flutter: ">=2.5.0"
|
flutter: ">=3.7.0"
|
||||||
|
@ -4,7 +4,7 @@ description: Demonstrates how to use the qrscanner_zxing plugin.
|
|||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.0-266.1.beta <3.0.0"
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@ -14,6 +14,7 @@ dependencies:
|
|||||||
path: ../
|
path: ../
|
||||||
|
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
|
file_picker: ^5.3.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -14,10 +14,16 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'qrscanner_zxing_platform_interface.dart';
|
import 'qrscanner_zxing_platform_interface.dart';
|
||||||
|
|
||||||
class QRScannerZxing {
|
class QRScannerZxing {
|
||||||
Future<String?> getPlatformVersion() {
|
Future<String?> getPlatformVersion() {
|
||||||
return QRScannerZxingPlatform.instance.getPlatformVersion();
|
return QRScannerZxingPlatform.instance.getPlatformVersion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> scanBitmap(Uint8List bitmap) {
|
||||||
|
return QRScannerZxingPlatform.instance.scanBitmap(bitmap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
@ -31,4 +31,11 @@ class MethodChannelQRScannerZxing extends QRScannerZxingPlatform {
|
|||||||
await methodChannel.invokeMethod<String>('getPlatformVersion');
|
await methodChannel.invokeMethod<String>('getPlatformVersion');
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> scanBitmap(Uint8List bytes) async {
|
||||||
|
final result = await methodChannel
|
||||||
|
.invokeMethod<String>('scanBitmap', {'bytes': bytes});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
|
|
||||||
import 'qrscanner_zxing_method_channel.dart';
|
import 'qrscanner_zxing_method_channel.dart';
|
||||||
@ -42,4 +44,8 @@ abstract class QRScannerZxingPlatform extends PlatformInterface {
|
|||||||
Future<String?> getPlatformVersion() {
|
Future<String?> getPlatformVersion() {
|
||||||
throw UnimplementedError('platformVersion() has not been implemented.');
|
throw UnimplementedError('platformVersion() has not been implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> scanBitmap(Uint8List bytes) {
|
||||||
|
throw UnimplementedError('scanBitmap(Uint8List) has not been implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,13 +24,21 @@ import 'package:flutter/services.dart';
|
|||||||
|
|
||||||
class QRScannerZxingView extends StatefulWidget {
|
class QRScannerZxingView extends StatefulWidget {
|
||||||
final int marginPct;
|
final int marginPct;
|
||||||
|
/// Called when a code has been detected.
|
||||||
final Function(String rawData) onDetect;
|
final Function(String rawData) onDetect;
|
||||||
|
/// Called before the system UI with runtime permissions request is
|
||||||
|
/// displayed.
|
||||||
|
final Function()? beforePermissionsRequest;
|
||||||
|
/// Called after the view is completely initialized.
|
||||||
|
///
|
||||||
|
/// permissionsGranted is true if the user granted camera permissions.
|
||||||
final Function(bool permissionsGranted) onViewInitialized;
|
final Function(bool permissionsGranted) onViewInitialized;
|
||||||
|
|
||||||
const QRScannerZxingView(
|
const QRScannerZxingView(
|
||||||
{Key? key,
|
{Key? key,
|
||||||
required this.marginPct,
|
required this.marginPct,
|
||||||
required this.onDetect,
|
required this.onDetect,
|
||||||
|
this.beforePermissionsRequest,
|
||||||
required this.onViewInitialized})
|
required this.onViewInitialized})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@ -51,6 +59,9 @@ class QRScannerZxingViewState extends State<QRScannerZxingView> {
|
|||||||
var rawValue = arguments["value"];
|
var rawValue = arguments["value"];
|
||||||
widget.onDetect(rawValue);
|
widget.onDetect(rawValue);
|
||||||
return;
|
return;
|
||||||
|
case "beforePermissionsRequest":
|
||||||
|
widget.beforePermissionsRequest?.call();
|
||||||
|
return;
|
||||||
case "viewInitialized":
|
case "viewInitialized":
|
||||||
var arguments = jsonDecode(call.arguments);
|
var arguments = jsonDecode(call.arguments);
|
||||||
var permissionsGranted = arguments["permissionsGranted"];
|
var permissionsGranted = arguments["permissionsGranted"];
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
||||||
import 'package:qrscanner_zxing/qrscanner_zxing.dart';
|
import 'package:qrscanner_zxing/qrscanner_zxing.dart';
|
||||||
@ -25,6 +27,10 @@ class MockQRScannerZxingPlatform
|
|||||||
implements QRScannerZxingPlatform {
|
implements QRScannerZxingPlatform {
|
||||||
@override
|
@override
|
||||||
Future<String?> getPlatformVersion() => Future.value('42');
|
Future<String?> getPlatformVersion() => Future.value('42');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> scanBitmap(Uint8List bytes) =>
|
||||||
|
Future.value(bytes.length.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@ -42,4 +48,12 @@ void main() {
|
|||||||
|
|
||||||
expect(await qrscannerZxingPlugin.getPlatformVersion(), '42');
|
expect(await qrscannerZxingPlugin.getPlatformVersion(), '42');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('scanBitmap', () async {
|
||||||
|
QRScannerZxing qrscannerZxingPlugin = QRScannerZxing();
|
||||||
|
MockQRScannerZxingPlatform fakePlatform = MockQRScannerZxingPlatform();
|
||||||
|
QRScannerZxingPlatform.instance = fakePlatform;
|
||||||
|
|
||||||
|
expect(await qrscannerZxingPlugin.scanBitmap(Uint8List(10)), '10');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -165,6 +165,7 @@ class AboutPage extends ConsumerWidget {
|
|||||||
'os': Platform.operatingSystem,
|
'os': Platform.operatingSystem,
|
||||||
'os_version': Platform.operatingSystemVersion,
|
'os_version': Platform.operatingSystemVersion,
|
||||||
});
|
});
|
||||||
|
data.insert(data.length - 1, ref.read(featureFlagProvider));
|
||||||
final text = const JsonEncoder.withIndent(' ').convert(data);
|
final text = const JsonEncoder.withIndent(' ').convert(data);
|
||||||
await ref.read(clipboardProvider).setText(text);
|
await ref.read(clipboardProvider).setText(text);
|
||||||
await ref.read(withContextProvider)(
|
await ref.read(withContextProvider)(
|
||||||
|
@ -34,6 +34,16 @@ Future<bool> isNfcEnabled() async {
|
|||||||
return await appMethodsChannel.invokeMethod('isNfcEnabled');
|
return await appMethodsChannel.invokeMethod('isNfcEnabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The next onPause/onResume lifecycle event will not stop and start
|
||||||
|
/// USB/NFC discovery which will preserve the current YubiKey connection.
|
||||||
|
///
|
||||||
|
/// This function should be called before showing system dialogs, such as
|
||||||
|
/// native file picker or permission request dialogs.
|
||||||
|
/// The state automatically resets during onResume call.
|
||||||
|
Future<void> preserveConnectedDeviceWhenPaused() async {
|
||||||
|
await appMethodsChannel.invokeMethod('preserveConnectionOnPause');
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> openNfcSettings() async {
|
Future<void> openNfcSettings() async {
|
||||||
await appMethodsChannel.invokeMethod('openNfcSettings');
|
await appMethodsChannel.invokeMethod('openNfcSettings');
|
||||||
}
|
}
|
||||||
|
@ -53,39 +53,37 @@ Future<Widget> initialize() async {
|
|||||||
|
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
supportedAppsProvider.overrideWithValue([
|
supportedAppsProvider.overrideWith(implementedApps([
|
||||||
Application.oath,
|
Application.oath,
|
||||||
]),
|
])),
|
||||||
prefProvider.overrideWithValue(await SharedPreferences.getInstance()),
|
prefProvider.overrideWithValue(await SharedPreferences.getInstance()),
|
||||||
logLevelProvider.overrideWith((ref) => AndroidLogger()),
|
logLevelProvider.overrideWith((ref) => AndroidLogger()),
|
||||||
attachedDevicesProvider
|
attachedDevicesProvider.overrideWith(
|
||||||
.overrideWith(
|
() => AndroidAttachedDevicesNotifier(),
|
||||||
() => AndroidAttachedDevicesNotifier(),
|
),
|
||||||
),
|
|
||||||
currentDeviceDataProvider.overrideWith(
|
currentDeviceDataProvider.overrideWith(
|
||||||
(ref) => ref.watch(androidDeviceDataProvider),
|
(ref) => ref.watch(androidDeviceDataProvider),
|
||||||
),
|
),
|
||||||
oathStateProvider.overrideWithProvider(androidOathStateProvider),
|
oathStateProvider.overrideWithProvider(androidOathStateProvider),
|
||||||
credentialListProvider
|
credentialListProvider
|
||||||
.overrideWithProvider(androidCredentialListProvider),
|
.overrideWithProvider(androidCredentialListProvider),
|
||||||
currentAppProvider.overrideWith(
|
currentAppProvider.overrideWith(
|
||||||
(ref) => AndroidSubPageNotifier(ref.watch(supportedAppsProvider))
|
(ref) => AndroidSubPageNotifier(ref.watch(supportedAppsProvider))),
|
||||||
),
|
|
||||||
managementStateProvider.overrideWithProvider(androidManagementState),
|
managementStateProvider.overrideWithProvider(androidManagementState),
|
||||||
currentDeviceProvider.overrideWith(
|
currentDeviceProvider.overrideWith(
|
||||||
() => AndroidCurrentDeviceNotifier(),
|
() => AndroidCurrentDeviceNotifier(),
|
||||||
),
|
),
|
||||||
qrScannerProvider
|
qrScannerProvider
|
||||||
.overrideWith(androidQrScannerProvider(await getHasCamera())),
|
.overrideWith(androidQrScannerProvider(await getHasCamera())),
|
||||||
windowStateProvider.overrideWith((ref) => ref.watch(androidWindowStateProvider)),
|
windowStateProvider
|
||||||
|
.overrideWith((ref) => ref.watch(androidWindowStateProvider)),
|
||||||
clipboardProvider.overrideWith(
|
clipboardProvider.overrideWith(
|
||||||
(ref) => ref.watch(androidClipboardProvider),
|
(ref) => ref.watch(androidClipboardProvider),
|
||||||
),
|
),
|
||||||
androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()),
|
androidSdkVersionProvider.overrideWithValue(await getAndroidSdkVersion()),
|
||||||
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
androidNfcSupportProvider.overrideWithValue(await getHasNfc()),
|
||||||
supportedThemesProvider
|
supportedThemesProvider.overrideWith(
|
||||||
.overrideWith(
|
(ref) => ref.watch(androidSupportedThemesProvider),
|
||||||
(ref) => ref.watch(androidSupportedThemesProvider),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
child: DismissKeyboard(
|
child: DismissKeyboard(
|
||||||
|
@ -22,6 +22,7 @@ const _prefix = 'android.keys';
|
|||||||
|
|
||||||
const okButton = Key('$_prefix.ok');
|
const okButton = Key('$_prefix.ok');
|
||||||
const manualEntryButton = Key('$_prefix.manual_entry');
|
const manualEntryButton = Key('$_prefix.manual_entry');
|
||||||
|
const readFromImage = Key('$_prefix.read_image_file');
|
||||||
|
|
||||||
const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
|
const nfcBypassTouchSetting = Key('$_prefix.nfc_bypass_touch');
|
||||||
const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');
|
const nfcSilenceSoundsSettings = Key('$_prefix.nfc_silence_sounds');
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
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:yubico_authenticator/android/qr_scanner/qr_scanner_provider.dart';
|
||||||
|
|
||||||
import 'qr_scanner_scan_status.dart';
|
import 'qr_scanner_scan_status.dart';
|
||||||
|
|
||||||
@ -47,45 +48,69 @@ class QRScannerPermissionsUI extends StatelessWidget {
|
|||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
Row(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
Row(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
Column(
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.q_want_to_scan,
|
||||||
|
textScaleFactor: 0.7,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
onPermissionRequest();
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
l10n.s_review_permissions,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Column(children: [
|
||||||
|
Text(
|
||||||
|
l10n.q_have_account_info,
|
||||||
|
textScaleFactor: 0.7,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
|
||||||
l10n.q_have_account_info,
|
|
||||||
textScaleFactor: 0.7,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop('');
|
Navigator.of(context).pop(
|
||||||
|
AndroidQrScanner.kQrScannerRequestManualEntry);
|
||||||
},
|
},
|
||||||
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),
|
||||||
),
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l10n.q_want_to_scan,
|
|
||||||
textScaleFactor: 0.7,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
onPermissionRequest();
|
Navigator.of(context).pop(
|
||||||
|
AndroidQrScanner.kQrScannerRequestReadFromFile);
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
l10n.s_review_permissions,
|
l10n.s_read_from_file,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
)),
|
))
|
||||||
],
|
]),
|
||||||
)
|
],
|
||||||
])
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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.
|
||||||
@ -14,35 +14,110 @@
|
|||||||
* 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:qrscanner_zxing/qrscanner_zxing_method_channel.dart';
|
||||||
|
import 'package:yubico_authenticator/android/app_methods.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 '../../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);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<String?> scanQr([String? _]) async {
|
Future<String?> scanQr([String? imageData]) async {
|
||||||
var scannedCode = await _withContext(
|
if (imageData == null) {
|
||||||
(context) async => await Navigator.of(context).push(PageRouteBuilder(
|
var scannedCode = await _withContext((context) async =>
|
||||||
pageBuilder: (_, __, ___) =>
|
await Navigator.of(context).push(PageRouteBuilder(
|
||||||
Theme(data: AppTheme.darkTheme, child: const QrScannerView()),
|
pageBuilder: (_, __, ___) =>
|
||||||
settings: const RouteSettings(name: 'android_qr_scanner_view'),
|
Theme(data: AppTheme.darkTheme, child: const QrScannerView()),
|
||||||
transitionDuration: const Duration(seconds: 0),
|
settings: const RouteSettings(name: 'android_qr_scanner_view'),
|
||||||
reverseTransitionDuration: const Duration(seconds: 0),
|
transitionDuration: const Duration(seconds: 0),
|
||||||
)));
|
reverseTransitionDuration: const Duration(seconds: 0),
|
||||||
if (scannedCode == null) {
|
)));
|
||||||
// user has cancelled the scan
|
if (scannedCode == null) {
|
||||||
throw CancellationException();
|
// user has cancelled the scan
|
||||||
|
throw CancellationException();
|
||||||
|
}
|
||||||
|
if (scannedCode == '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return scannedCode;
|
||||||
|
} else {
|
||||||
|
var zxingChannel = MethodChannelQRScannerZxing();
|
||||||
|
return await zxingChannel.scanBitmap(base64Decode(imageData));
|
||||||
}
|
}
|
||||||
if (scannedCode == '') {
|
}
|
||||||
return null;
|
|
||||||
|
static Future<void> handleScannedData(
|
||||||
|
String? qrData,
|
||||||
|
WithContext withContext,
|
||||||
|
QrScanner qrScanner,
|
||||||
|
AppLocalizations l10n,
|
||||||
|
) async {
|
||||||
|
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:
|
||||||
|
await preserveConnectedDeviceWhenPaused();
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
allowedExtensions: ['png', 'jpg', 'gif', 'webp'],
|
||||||
|
type: FileType.custom,
|
||||||
|
allowMultiple: false,
|
||||||
|
lockParentWindow: true,
|
||||||
|
withData: true,
|
||||||
|
dialogTitle: l10n.l_qr_select_file);
|
||||||
|
|
||||||
|
if (result == null || !result.isSinglePick) {
|
||||||
|
// no result
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bytes = result.files.first.bytes;
|
||||||
|
if (bytes != null) {
|
||||||
|
final b64bytes = base64Encode(bytes);
|
||||||
|
final imageQrData = await qrScanner.scanQr(b64bytes);
|
||||||
|
if (imageQrData != null) {
|
||||||
|
await withContext((context) =>
|
||||||
|
handleUri(context, null, imageQrData, 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));
|
||||||
}
|
}
|
||||||
return scannedCode;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
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: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';
|
||||||
@ -73,18 +74,32 @@ class QRScannerUI extends StatelessWidget {
|
|||||||
textScaleFactor: 0.7,
|
textScaleFactor: 0.7,
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
OutlinedButton(
|
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||||
onPressed: () {
|
OutlinedButton(
|
||||||
Navigator.of(context).pop('');
|
onPressed: () {
|
||||||
},
|
Navigator.of(context).pop(
|
||||||
key: keys.manualEntryButton,
|
AndroidQrScanner.kQrScannerRequestManualEntry);
|
||||||
child: Text(
|
},
|
||||||
l10n.s_enter_manually,
|
key: keys.manualEntryButton,
|
||||||
style: const TextStyle(color: Colors.white),
|
child: Text(
|
||||||
)),
|
l10n.s_enter_manually,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
)),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(
|
||||||
|
AndroidQrScanner.kQrScannerRequestReadFromFile);
|
||||||
|
},
|
||||||
|
key: keys.readFromImage,
|
||||||
|
child: Text(
|
||||||
|
l10n.s_read_from_file,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
))
|
||||||
|
])
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8)
|
const SizedBox(height: 16)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
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:qrscanner_zxing/qrscanner_zxing_view.dart';
|
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
|
||||||
|
import 'package:yubico_authenticator/android/app_methods.dart';
|
||||||
|
|
||||||
import '../../oath/models.dart';
|
import '../../oath/models.dart';
|
||||||
import 'qr_scanner_overlay_view.dart';
|
import 'qr_scanner_overlay_view.dart';
|
||||||
@ -138,17 +139,21 @@ class _QrScannerViewState extends State<QrScannerView> {
|
|||||||
maintainSize: true,
|
maintainSize: true,
|
||||||
visible: _permissionsGranted,
|
visible: _permissionsGranted,
|
||||||
child: QRScannerZxingView(
|
child: QRScannerZxingView(
|
||||||
key: _zxingViewKey,
|
key: _zxingViewKey,
|
||||||
marginPct: 10,
|
marginPct: 10,
|
||||||
onDetect: (scannedData) => handleResult(scannedData),
|
onDetect: (scannedData) => handleResult(scannedData),
|
||||||
onViewInitialized: (bool permissionsGranted) {
|
onViewInitialized: (bool permissionsGranted) {
|
||||||
Future.delayed(const Duration(milliseconds: 50), () {
|
Future.delayed(const Duration(milliseconds: 50), () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_previewInitialized = true;
|
_previewInitialized = true;
|
||||||
_permissionsGranted = permissionsGranted;
|
_permissionsGranted = permissionsGranted;
|
||||||
});
|
|
||||||
});
|
});
|
||||||
})),
|
});
|
||||||
|
},
|
||||||
|
beforePermissionsRequest: () async {
|
||||||
|
await preserveConnectedDeviceWhenPaused();
|
||||||
|
},
|
||||||
|
)),
|
||||||
Visibility(
|
Visibility(
|
||||||
visible: _permissionsGranted,
|
visible: _permissionsGranted,
|
||||||
child: QRScannerOverlay(
|
child: QRScannerOverlay(
|
||||||
|
25
lib/app/features.dart
Normal file
25
lib/app/features.dart
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../core/state.dart';
|
||||||
|
|
||||||
|
final oath = root.feature('oath');
|
||||||
|
final fido = root.feature('fido');
|
||||||
|
final piv = root.feature('piv');
|
||||||
|
final management = root.feature('management');
|
||||||
|
final openpgp = root.feature('openpgp');
|
||||||
|
final hsmauth = root.feature('hsmauth');
|
||||||
|
final otp = root.feature('otp');
|
@ -21,6 +21,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||||||
|
|
||||||
import '../../management/models.dart';
|
import '../../management/models.dart';
|
||||||
import '../core/models.dart';
|
import '../core/models.dart';
|
||||||
|
import '../core/state.dart';
|
||||||
|
|
||||||
part 'models.freezed.dart';
|
part 'models.freezed.dart';
|
||||||
|
|
||||||
@ -129,6 +130,7 @@ class ActionItem with _$ActionItem {
|
|||||||
Intent? intent,
|
Intent? intent,
|
||||||
ActionStyle? actionStyle,
|
ActionStyle? actionStyle,
|
||||||
Key? key,
|
Key? key,
|
||||||
|
Feature? feature,
|
||||||
}) = _ActionItem;
|
}) = _ActionItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -633,6 +633,7 @@ mixin _$ActionItem {
|
|||||||
Intent? get intent => throw _privateConstructorUsedError;
|
Intent? get intent => throw _privateConstructorUsedError;
|
||||||
ActionStyle? get actionStyle => throw _privateConstructorUsedError;
|
ActionStyle? get actionStyle => throw _privateConstructorUsedError;
|
||||||
Key? get key => throw _privateConstructorUsedError;
|
Key? get key => throw _privateConstructorUsedError;
|
||||||
|
Feature? get feature => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
$ActionItemCopyWith<ActionItem> get copyWith =>
|
$ActionItemCopyWith<ActionItem> get copyWith =>
|
||||||
@ -653,7 +654,8 @@ abstract class $ActionItemCopyWith<$Res> {
|
|||||||
Widget? trailing,
|
Widget? trailing,
|
||||||
Intent? intent,
|
Intent? intent,
|
||||||
ActionStyle? actionStyle,
|
ActionStyle? actionStyle,
|
||||||
Key? key});
|
Key? key,
|
||||||
|
Feature? feature});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -677,6 +679,7 @@ class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem>
|
|||||||
Object? intent = freezed,
|
Object? intent = freezed,
|
||||||
Object? actionStyle = freezed,
|
Object? actionStyle = freezed,
|
||||||
Object? key = freezed,
|
Object? key = freezed,
|
||||||
|
Object? feature = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
icon: null == icon
|
icon: null == icon
|
||||||
@ -711,6 +714,10 @@ class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem>
|
|||||||
? _value.key
|
? _value.key
|
||||||
: key // ignore: cast_nullable_to_non_nullable
|
: key // ignore: cast_nullable_to_non_nullable
|
||||||
as Key?,
|
as Key?,
|
||||||
|
feature: freezed == feature
|
||||||
|
? _value.feature
|
||||||
|
: feature // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Feature?,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -731,7 +738,8 @@ abstract class _$$_ActionItemCopyWith<$Res>
|
|||||||
Widget? trailing,
|
Widget? trailing,
|
||||||
Intent? intent,
|
Intent? intent,
|
||||||
ActionStyle? actionStyle,
|
ActionStyle? actionStyle,
|
||||||
Key? key});
|
Key? key,
|
||||||
|
Feature? feature});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -753,6 +761,7 @@ class __$$_ActionItemCopyWithImpl<$Res>
|
|||||||
Object? intent = freezed,
|
Object? intent = freezed,
|
||||||
Object? actionStyle = freezed,
|
Object? actionStyle = freezed,
|
||||||
Object? key = freezed,
|
Object? key = freezed,
|
||||||
|
Object? feature = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$_ActionItem(
|
return _then(_$_ActionItem(
|
||||||
icon: null == icon
|
icon: null == icon
|
||||||
@ -787,6 +796,10 @@ class __$$_ActionItemCopyWithImpl<$Res>
|
|||||||
? _value.key
|
? _value.key
|
||||||
: key // ignore: cast_nullable_to_non_nullable
|
: key // ignore: cast_nullable_to_non_nullable
|
||||||
as Key?,
|
as Key?,
|
||||||
|
feature: freezed == feature
|
||||||
|
? _value.feature
|
||||||
|
: feature // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Feature?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -802,7 +815,8 @@ class _$_ActionItem implements _ActionItem {
|
|||||||
this.trailing,
|
this.trailing,
|
||||||
this.intent,
|
this.intent,
|
||||||
this.actionStyle,
|
this.actionStyle,
|
||||||
this.key});
|
this.key,
|
||||||
|
this.feature});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Widget icon;
|
final Widget icon;
|
||||||
@ -820,10 +834,12 @@ class _$_ActionItem implements _ActionItem {
|
|||||||
final ActionStyle? actionStyle;
|
final ActionStyle? actionStyle;
|
||||||
@override
|
@override
|
||||||
final Key? key;
|
final Key? key;
|
||||||
|
@override
|
||||||
|
final Feature? feature;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ActionItem(icon: $icon, title: $title, subtitle: $subtitle, shortcut: $shortcut, trailing: $trailing, intent: $intent, actionStyle: $actionStyle, key: $key)';
|
return 'ActionItem(icon: $icon, title: $title, subtitle: $subtitle, shortcut: $shortcut, trailing: $trailing, intent: $intent, actionStyle: $actionStyle, key: $key, feature: $feature)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -842,12 +858,13 @@ class _$_ActionItem implements _ActionItem {
|
|||||||
(identical(other.intent, intent) || other.intent == intent) &&
|
(identical(other.intent, intent) || other.intent == intent) &&
|
||||||
(identical(other.actionStyle, actionStyle) ||
|
(identical(other.actionStyle, actionStyle) ||
|
||||||
other.actionStyle == actionStyle) &&
|
other.actionStyle == actionStyle) &&
|
||||||
(identical(other.key, key) || other.key == key));
|
(identical(other.key, key) || other.key == key) &&
|
||||||
|
(identical(other.feature, feature) || other.feature == feature));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType, icon, title, subtitle, shortcut,
|
int get hashCode => Object.hash(runtimeType, icon, title, subtitle, shortcut,
|
||||||
trailing, intent, actionStyle, key);
|
trailing, intent, actionStyle, key, feature);
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
@ -865,7 +882,8 @@ abstract class _ActionItem implements ActionItem {
|
|||||||
final Widget? trailing,
|
final Widget? trailing,
|
||||||
final Intent? intent,
|
final Intent? intent,
|
||||||
final ActionStyle? actionStyle,
|
final ActionStyle? actionStyle,
|
||||||
final Key? key}) = _$_ActionItem;
|
final Key? key,
|
||||||
|
final Feature? feature}) = _$_ActionItem;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget get icon;
|
Widget get icon;
|
||||||
@ -884,6 +902,8 @@ abstract class _ActionItem implements ActionItem {
|
|||||||
@override
|
@override
|
||||||
Key? get key;
|
Key? get key;
|
||||||
@override
|
@override
|
||||||
|
Feature? get feature;
|
||||||
|
@override
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$_ActionItemCopyWith<_$_ActionItem> get copyWith =>
|
_$$_ActionItemCopyWith<_$_ActionItem> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
|
@ -27,6 +27,7 @@ import 'package:yubico_authenticator/app/logging.dart';
|
|||||||
|
|
||||||
import '../core/state.dart';
|
import '../core/state.dart';
|
||||||
import 'models.dart';
|
import 'models.dart';
|
||||||
|
import 'features.dart' as features;
|
||||||
|
|
||||||
final _log = Logger('app.state');
|
final _log = Logger('app.state');
|
||||||
|
|
||||||
@ -37,7 +38,25 @@ const officialLocales = [
|
|||||||
|
|
||||||
// Override this to alter the set of supported apps.
|
// Override this to alter the set of supported apps.
|
||||||
final supportedAppsProvider =
|
final supportedAppsProvider =
|
||||||
Provider<List<Application>>((ref) => Application.values);
|
Provider<List<Application>>(implementedApps(Application.values));
|
||||||
|
|
||||||
|
extension on Application {
|
||||||
|
Feature get _feature => switch (this) {
|
||||||
|
Application.oath => features.oath,
|
||||||
|
Application.fido => features.fido,
|
||||||
|
Application.otp => features.otp,
|
||||||
|
Application.piv => features.piv,
|
||||||
|
Application.management => features.management,
|
||||||
|
Application.openpgp => features.openpgp,
|
||||||
|
Application.hsmauth => features.oath,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Application> Function(Ref) implementedApps(List<Application> apps) =>
|
||||||
|
(ref) {
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
return apps.where((app) => hasFeature(app._feature)).toList();
|
||||||
|
};
|
||||||
|
|
||||||
// Default implementation is always focused, override with platform specific version.
|
// Default implementation is always focused, override with platform specific version.
|
||||||
final windowStateProvider = Provider<WindowState>(
|
final windowStateProvider = Provider<WindowState>(
|
||||||
|
@ -15,7 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../../widgets/list_title.dart';
|
import '../../widgets/list_title.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
|
||||||
@ -26,6 +28,7 @@ class ActionListItem extends StatelessWidget {
|
|||||||
final Widget? trailing;
|
final Widget? trailing;
|
||||||
final void Function(BuildContext context)? onTap;
|
final void Function(BuildContext context)? onTap;
|
||||||
final ActionStyle actionStyle;
|
final ActionStyle actionStyle;
|
||||||
|
final Feature? feature;
|
||||||
|
|
||||||
const ActionListItem({
|
const ActionListItem({
|
||||||
super.key,
|
super.key,
|
||||||
@ -35,6 +38,7 @@ class ActionListItem extends StatelessWidget {
|
|||||||
this.trailing,
|
this.trailing,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
this.actionStyle = ActionStyle.normal,
|
this.actionStyle = ActionStyle.normal,
|
||||||
|
this.feature,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -67,7 +71,7 @@ class ActionListItem extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActionListSection extends StatelessWidget {
|
class ActionListSection extends ConsumerWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<ActionListItem> children;
|
final List<ActionListItem> children;
|
||||||
|
|
||||||
@ -82,6 +86,7 @@ class ActionListSection extends StatelessWidget {
|
|||||||
final intent = action.intent;
|
final intent = action.intent;
|
||||||
return ActionListItem(
|
return ActionListItem(
|
||||||
key: action.key,
|
key: action.key,
|
||||||
|
feature: action.feature,
|
||||||
actionStyle: action.actionStyle ?? ActionStyle.normal,
|
actionStyle: action.actionStyle ?? ActionStyle.normal,
|
||||||
icon: action.icon,
|
icon: action.icon,
|
||||||
title: action.title,
|
title: action.title,
|
||||||
@ -96,14 +101,22 @@ class ActionListSection extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => SizedBox(
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
width: 360,
|
final hasFeature = ref.watch(featureProvider);
|
||||||
child: Column(children: [
|
final enabledChildren = children
|
||||||
ListTitle(
|
.where((item) => item.feature == null || hasFeature(item.feature!));
|
||||||
title,
|
if (enabledChildren.isEmpty) {
|
||||||
textStyle: Theme.of(context).textTheme.bodyLarge,
|
return const SizedBox();
|
||||||
),
|
}
|
||||||
...children,
|
return SizedBox(
|
||||||
]),
|
width: 360,
|
||||||
);
|
child: Column(children: [
|
||||||
|
ListTitle(
|
||||||
|
title,
|
||||||
|
textStyle: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
...enabledChildren,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,14 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../shortcuts.dart';
|
import '../shortcuts.dart';
|
||||||
import 'action_popup_menu.dart';
|
import 'action_popup_menu.dart';
|
||||||
|
|
||||||
class AppListItem extends StatefulWidget {
|
class AppListItem extends ConsumerStatefulWidget {
|
||||||
final Widget? leading;
|
final Widget? leading;
|
||||||
final String title;
|
final String title;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
@ -41,10 +42,10 @@ class AppListItem extends StatefulWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StatefulWidget> createState() => _AppListItemState();
|
ConsumerState<ConsumerStatefulWidget> createState() => _AppListItemState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppListItemState extends State<AppListItem> {
|
class _AppListItemState extends ConsumerState<AppListItem> {
|
||||||
final FocusNode _focusNode = FocusNode();
|
final FocusNode _focusNode = FocusNode();
|
||||||
int _lastTap = 0;
|
int _lastTap = 0;
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ class _AppListItemState extends State<AppListItem> {
|
|||||||
final buildPopupActions = widget.buildPopupActions;
|
final buildPopupActions = widget.buildPopupActions;
|
||||||
final activationIntent = widget.activationIntent;
|
final activationIntent = widget.activationIntent;
|
||||||
final trailing = widget.trailing;
|
final trailing = widget.trailing;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
|
||||||
return Shortcuts(
|
return Shortcuts(
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
@ -72,11 +74,17 @@ class _AppListItemState extends State<AppListItem> {
|
|||||||
onSecondaryTapDown: buildPopupActions == null
|
onSecondaryTapDown: buildPopupActions == null
|
||||||
? null
|
? null
|
||||||
: (details) {
|
: (details) {
|
||||||
showPopupMenu(
|
final menuItems = buildPopupActions(context)
|
||||||
context,
|
.where((action) =>
|
||||||
details.globalPosition,
|
action.feature == null || hasFeature(action.feature!))
|
||||||
buildPopupActions(context),
|
.toList();
|
||||||
);
|
if (menuItems.isNotEmpty) {
|
||||||
|
showPopupMenu(
|
||||||
|
context,
|
||||||
|
details.globalPosition,
|
||||||
|
menuItems,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
|
@ -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';
|
||||||
@ -100,32 +99,17 @@ class MainPage extends ConsumerWidget {
|
|||||||
tooltip: l10n.s_add_account,
|
tooltip: l10n.s_add_account,
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final withContext = ref.read(withContextProvider);
|
final withContext = ref.read(withContextProvider);
|
||||||
final scanner = ref.read(qrScannerProvider);
|
final qrScanner = ref.read(qrScannerProvider);
|
||||||
if (scanner != null) {
|
if (qrScanner != null) {
|
||||||
try {
|
try {
|
||||||
final qrData = await scanner.scanQr();
|
final qrData = await qrScanner.scanQr();
|
||||||
if (qrData != null) {
|
await AndroidQrScanner.handleScannedData(
|
||||||
await withContext((context) =>
|
qrData, withContext, qrScanner, l10n);
|
||||||
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,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -51,3 +51,65 @@ abstract class ApplicationStateNotifier<T>
|
|||||||
state = AsyncValue.data(value);
|
state = AsyncValue.data(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Feature flags
|
||||||
|
sealed class BaseFeature {
|
||||||
|
String get path;
|
||||||
|
String _subpath(String key);
|
||||||
|
|
||||||
|
Feature feature(String key, {bool enabled = true}) =>
|
||||||
|
Feature._(this, key, enabled: enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RootFeature extends BaseFeature {
|
||||||
|
_RootFeature._();
|
||||||
|
@override
|
||||||
|
String get path => '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String _subpath(String key) => key;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Feature extends BaseFeature {
|
||||||
|
final BaseFeature parent;
|
||||||
|
final String key;
|
||||||
|
final bool _defaultState;
|
||||||
|
|
||||||
|
Feature._(this.parent, this.key, {bool enabled = true})
|
||||||
|
: _defaultState = enabled;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get path => parent._subpath(key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String _subpath(String key) => '$path.$key';
|
||||||
|
}
|
||||||
|
|
||||||
|
final BaseFeature root = _RootFeature._();
|
||||||
|
|
||||||
|
typedef FeatureProvider = bool Function(Feature feature);
|
||||||
|
|
||||||
|
final featureFlagProvider =
|
||||||
|
StateNotifierProvider<FeatureFlagsNotifier, Map<String, bool>>(
|
||||||
|
(_) => FeatureFlagsNotifier());
|
||||||
|
|
||||||
|
class FeatureFlagsNotifier extends StateNotifier<Map<String, bool>> {
|
||||||
|
FeatureFlagsNotifier() : super({});
|
||||||
|
|
||||||
|
void loadConfig(Map<String, dynamic> config) {
|
||||||
|
const falsey = [0, false, null];
|
||||||
|
state = {for (final k in config.keys) k: !falsey.contains(config[k])};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final featureProvider = Provider<FeatureProvider>((ref) {
|
||||||
|
final featureMap = ref.watch(featureFlagProvider);
|
||||||
|
|
||||||
|
bool isEnabled(BaseFeature feature) => switch (feature) {
|
||||||
|
_RootFeature() => true,
|
||||||
|
Feature() => isEnabled(feature.parent) &&
|
||||||
|
(featureMap[feature.path] ?? feature._defaultState),
|
||||||
|
};
|
||||||
|
|
||||||
|
return isEnabled;
|
||||||
|
});
|
||||||
|
@ -157,6 +157,11 @@ Future<Widget> initialize(List<String> argv) async {
|
|||||||
.toFilePath();
|
.toFilePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Locate feature flags file
|
||||||
|
final featureFile = File(Uri.file(Platform.resolvedExecutable)
|
||||||
|
.resolve('features.json')
|
||||||
|
.toFilePath());
|
||||||
|
|
||||||
final rpcFuture = _initHelper(exe!);
|
final rpcFuture = _initHelper(exe!);
|
||||||
_initLicenses();
|
_initLicenses();
|
||||||
|
|
||||||
@ -167,12 +172,12 @@ Future<Widget> initialize(List<String> argv) async {
|
|||||||
|
|
||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
supportedAppsProvider.overrideWithValue([
|
supportedAppsProvider.overrideWith(implementedApps([
|
||||||
Application.oath,
|
Application.oath,
|
||||||
Application.fido,
|
Application.fido,
|
||||||
Application.piv,
|
Application.piv,
|
||||||
Application.management,
|
Application.management,
|
||||||
]),
|
])),
|
||||||
prefProvider.overrideWithValue(prefs),
|
prefProvider.overrideWithValue(prefs),
|
||||||
rpcProvider.overrideWith((_) => rpcFuture),
|
rpcProvider.overrideWith((_) => rpcFuture),
|
||||||
windowStateProvider.overrideWith(
|
windowStateProvider.overrideWith(
|
||||||
@ -218,15 +223,33 @@ Future<Widget> initialize(List<String> argv) async {
|
|||||||
ref.read(rpcProvider).valueOrNull?.setLogLevel(level);
|
ref.read(rpcProvider).valueOrNull?.setLogLevel(level);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load feature flags, if they exist
|
||||||
|
featureFile.exists().then(
|
||||||
|
(exists) async {
|
||||||
|
if (exists) {
|
||||||
|
try {
|
||||||
|
final featureConfig =
|
||||||
|
jsonDecode(await featureFile.readAsString());
|
||||||
|
ref
|
||||||
|
.read(featureFlagProvider.notifier)
|
||||||
|
.loadConfig(featureConfig);
|
||||||
|
} catch (error) {
|
||||||
|
_log.error('Failed to parse feature flags', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize systray
|
// Initialize systray
|
||||||
ref.watch(systrayProvider);
|
ref.watch(systrayProvider);
|
||||||
|
|
||||||
// Show a loading or error page while the Helper isn't ready
|
// Show a loading or error page while the Helper isn't ready
|
||||||
return ref.watch(rpcProvider).when(
|
return Consumer(
|
||||||
data: (data) => const MainPage(),
|
builder: (context, ref, child) => ref.watch(rpcProvider).when(
|
||||||
error: (error, stackTrace) => AppFailurePage(cause: error),
|
data: (data) => const MainPage(),
|
||||||
loading: () => _HelperWaiter(),
|
error: (error, stackTrace) => AppFailurePage(cause: error),
|
||||||
);
|
loading: () => _HelperWaiter(),
|
||||||
|
));
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
32
lib/fido/features.dart
Normal file
32
lib/fido/features.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../app/features.dart';
|
||||||
|
|
||||||
|
final actions = fido.feature('actions');
|
||||||
|
|
||||||
|
final actionsPin = actions.feature('pin');
|
||||||
|
final actionsAddFingerprint = actions.feature('addFingerprint');
|
||||||
|
final actionsReset = actions.feature('reset');
|
||||||
|
|
||||||
|
final credentials = fido.feature('credentials');
|
||||||
|
|
||||||
|
final credentialsDelete = credentials.feature('delete');
|
||||||
|
|
||||||
|
final fingerprints = fido.feature('fingerprints');
|
||||||
|
|
||||||
|
final fingerprintsEdit = fingerprints.feature('edit');
|
||||||
|
final fingerprintsDelete = fingerprints.feature('delete');
|
@ -20,11 +20,13 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
import '../../app/shortcuts.dart';
|
import '../../app/shortcuts.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
|
import '../features.dart' as features;
|
||||||
|
|
||||||
List<ActionItem> buildFingerprintActions(AppLocalizations l10n) {
|
List<ActionItem> buildFingerprintActions(AppLocalizations l10n) {
|
||||||
return [
|
return [
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.editFingerintAction,
|
key: keys.editFingerintAction,
|
||||||
|
feature: features.fingerprintsEdit,
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
title: l10n.s_rename_fp,
|
title: l10n.s_rename_fp,
|
||||||
subtitle: l10n.l_rename_fp_desc,
|
subtitle: l10n.l_rename_fp_desc,
|
||||||
@ -32,6 +34,7 @@ List<ActionItem> buildFingerprintActions(AppLocalizations l10n) {
|
|||||||
),
|
),
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.deleteFingerprintAction,
|
key: keys.deleteFingerprintAction,
|
||||||
|
feature: features.fingerprintsDelete,
|
||||||
actionStyle: ActionStyle.error,
|
actionStyle: ActionStyle.error,
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
title: l10n.s_delete_fingerprint,
|
title: l10n.s_delete_fingerprint,
|
||||||
@ -45,6 +48,7 @@ List<ActionItem> buildCredentialActions(AppLocalizations l10n) {
|
|||||||
return [
|
return [
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.deleteCredentialAction,
|
key: keys.deleteCredentialAction,
|
||||||
|
feature: features.credentialsDelete,
|
||||||
actionStyle: ActionStyle.error,
|
actionStyle: ActionStyle.error,
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
title: l10n.s_delete_passkey,
|
title: l10n.s_delete_passkey,
|
||||||
|
@ -7,6 +7,8 @@ import '../../app/shortcuts.dart';
|
|||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/views/fs_dialog.dart';
|
import '../../app/views/fs_dialog.dart';
|
||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'delete_credential_dialog.dart';
|
import 'delete_credential_dialog.dart';
|
||||||
@ -26,29 +28,32 @@ class CredentialDialog extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
|
||||||
return Actions(
|
return Actions(
|
||||||
actions: {
|
actions: {
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
if (hasFeature(features.credentialsDelete))
|
||||||
final withContext = ref.read(withContextProvider);
|
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||||
final bool? deleted =
|
final withContext = ref.read(withContextProvider);
|
||||||
await ref.read(withContextProvider)((context) async =>
|
final bool? deleted =
|
||||||
await showBlurDialog(
|
await ref.read(withContextProvider)((context) async =>
|
||||||
context: context,
|
await showBlurDialog(
|
||||||
builder: (context) => DeleteCredentialDialog(
|
context: context,
|
||||||
node.path,
|
builder: (context) => DeleteCredentialDialog(
|
||||||
credential,
|
node.path,
|
||||||
),
|
credential,
|
||||||
) ??
|
),
|
||||||
false);
|
) ??
|
||||||
|
false);
|
||||||
|
|
||||||
// Pop the account dialog if deleted
|
// Pop the account dialog if deleted
|
||||||
if (deleted == true) {
|
if (deleted == true) {
|
||||||
await withContext((context) async {
|
await withContext((context) async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return deleted;
|
return deleted;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
child: FocusScope(
|
child: FocusScope(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
@ -7,7 +7,9 @@ import '../../app/shortcuts.dart';
|
|||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/views/fs_dialog.dart';
|
import '../../app/views/fs_dialog.dart';
|
||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'delete_fingerprint_dialog.dart';
|
import 'delete_fingerprint_dialog.dart';
|
||||||
import 'rename_fingerprint_dialog.dart';
|
import 'rename_fingerprint_dialog.dart';
|
||||||
@ -27,53 +29,56 @@ class FingerprintDialog extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
return Actions(
|
return Actions(
|
||||||
actions: {
|
actions: {
|
||||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
if (hasFeature(features.fingerprintsEdit))
|
||||||
final withContext = ref.read(withContextProvider);
|
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||||
final Fingerprint? renamed =
|
final withContext = ref.read(withContextProvider);
|
||||||
await withContext((context) async => await showBlurDialog(
|
final Fingerprint? renamed =
|
||||||
context: context,
|
await withContext((context) async => await showBlurDialog(
|
||||||
builder: (context) => RenameFingerprintDialog(
|
context: context,
|
||||||
node.path,
|
builder: (context) => RenameFingerprintDialog(
|
||||||
fingerprint,
|
node.path,
|
||||||
),
|
fingerprint,
|
||||||
));
|
),
|
||||||
if (renamed != null) {
|
));
|
||||||
// Replace the dialog with the renamed credential
|
if (renamed != null) {
|
||||||
await withContext((context) async {
|
// Replace the dialog with the renamed credential
|
||||||
Navigator.of(context).pop();
|
await withContext((context) async {
|
||||||
await showBlurDialog(
|
Navigator.of(context).pop();
|
||||||
context: context,
|
await showBlurDialog(
|
||||||
builder: (context) {
|
context: context,
|
||||||
return FingerprintDialog(renamed);
|
builder: (context) {
|
||||||
},
|
return FingerprintDialog(renamed);
|
||||||
);
|
},
|
||||||
});
|
);
|
||||||
}
|
});
|
||||||
return renamed;
|
}
|
||||||
}),
|
return renamed;
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
}),
|
||||||
final withContext = ref.read(withContextProvider);
|
if (hasFeature(features.fingerprintsDelete))
|
||||||
final bool? deleted =
|
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||||
await ref.read(withContextProvider)((context) async =>
|
final withContext = ref.read(withContextProvider);
|
||||||
await showBlurDialog(
|
final bool? deleted =
|
||||||
context: context,
|
await ref.read(withContextProvider)((context) async =>
|
||||||
builder: (context) => DeleteFingerprintDialog(
|
await showBlurDialog(
|
||||||
node.path,
|
context: context,
|
||||||
fingerprint,
|
builder: (context) => DeleteFingerprintDialog(
|
||||||
),
|
node.path,
|
||||||
) ??
|
fingerprint,
|
||||||
false);
|
),
|
||||||
|
) ??
|
||||||
|
false);
|
||||||
|
|
||||||
// Pop the account dialog if deleted
|
// Pop the account dialog if deleted
|
||||||
if (deleted == true) {
|
if (deleted == true) {
|
||||||
await withContext((context) async {
|
await withContext((context) async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return deleted;
|
return deleted;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
child: FocusScope(
|
child: FocusScope(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
@ -23,6 +23,7 @@ import '../../app/views/fs_dialog.dart';
|
|||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
|
import '../features.dart' as features;
|
||||||
import 'add_fingerprint_dialog.dart';
|
import 'add_fingerprint_dialog.dart';
|
||||||
import 'pin_dialog.dart';
|
import 'pin_dialog.dart';
|
||||||
import 'reset_dialog.dart';
|
import 'reset_dialog.dart';
|
||||||
@ -48,6 +49,7 @@ Widget fidoBuildActions(
|
|||||||
children: [
|
children: [
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.addFingerprintAction,
|
key: keys.addFingerprintAction,
|
||||||
|
feature: features.actionsAddFingerprint,
|
||||||
actionStyle: ActionStyle.primary,
|
actionStyle: ActionStyle.primary,
|
||||||
icon: const Icon(Icons.fingerprint_outlined),
|
icon: const Icon(Icons.fingerprint_outlined),
|
||||||
title: l10n.s_add_fingerprint,
|
title: l10n.s_add_fingerprint,
|
||||||
@ -76,6 +78,7 @@ Widget fidoBuildActions(
|
|||||||
children: [
|
children: [
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.managePinAction,
|
key: keys.managePinAction,
|
||||||
|
feature: features.actionsPin,
|
||||||
icon: const Icon(Icons.pin_outlined),
|
icon: const Icon(Icons.pin_outlined),
|
||||||
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
|
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
|
||||||
subtitle: state.hasPin
|
subtitle: state.hasPin
|
||||||
@ -96,6 +99,7 @@ Widget fidoBuildActions(
|
|||||||
}),
|
}),
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.resetAction,
|
key: keys.resetAction,
|
||||||
|
feature: features.actionsReset,
|
||||||
actionStyle: ActionStyle.error,
|
actionStyle: ActionStyle.error,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
title: l10n.s_reset_fido,
|
title: l10n.s_reset_fido,
|
||||||
|
@ -22,8 +22,10 @@ import '../../app/models.dart';
|
|||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
import '../../app/views/graphics.dart';
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import 'key_actions.dart';
|
import 'key_actions.dart';
|
||||||
|
|
||||||
class FidoLockedPage extends ConsumerWidget {
|
class FidoLockedPage extends ConsumerWidget {
|
||||||
@ -35,6 +37,9 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
final hasActions = hasFeature(features.actions);
|
||||||
|
|
||||||
if (!state.hasPin) {
|
if (!state.hasPin) {
|
||||||
if (state.bioEnroll != null) {
|
if (state.bioEnroll != null) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
@ -42,7 +47,7 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
graphic: noFingerprints,
|
graphic: noFingerprints,
|
||||||
header: l10n.s_no_fingerprints,
|
header: l10n.s_no_fingerprints,
|
||||||
message: l10n.l_set_pin_fingerprints,
|
message: l10n.l_set_pin_fingerprints,
|
||||||
keyActionsBuilder: _buildActions,
|
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -53,7 +58,7 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
? l10n.l_no_discoverable_accounts
|
? l10n.l_no_discoverable_accounts
|
||||||
: l10n.l_ready_to_use,
|
: l10n.l_ready_to_use,
|
||||||
message: l10n.l_optionally_set_a_pin,
|
message: l10n.l_optionally_set_a_pin,
|
||||||
keyActionsBuilder: _buildActions,
|
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,7 +70,7 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
graphic: manageAccounts,
|
graphic: manageAccounts,
|
||||||
header: l10n.l_ready_to_use,
|
header: l10n.l_ready_to_use,
|
||||||
message: l10n.l_register_sk_on_websites,
|
message: l10n.l_register_sk_on_websites,
|
||||||
keyActionsBuilder: _buildActions,
|
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -75,14 +80,14 @@ class FidoLockedPage extends ConsumerWidget {
|
|||||||
title: Text(l10n.s_webauthn),
|
title: Text(l10n.s_webauthn),
|
||||||
header: l10n.s_pin_change_required,
|
header: l10n.s_pin_change_required,
|
||||||
message: l10n.l_pin_change_required_desc,
|
message: l10n.l_pin_change_required_desc,
|
||||||
keyActionsBuilder: _buildActions,
|
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: Text(l10n.s_webauthn),
|
title: Text(l10n.s_webauthn),
|
||||||
keyActionsBuilder: _buildActions,
|
keyActionsBuilder: hasActions ? _buildActions : null,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_PinEntryForm(state, node),
|
_PinEntryForm(state, node),
|
||||||
@ -177,6 +182,7 @@ class _PinEntryFormState extends ConsumerState<_PinEntryForm> {
|
|||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
|
@ -25,9 +25,11 @@ import '../../app/views/app_list_item.dart';
|
|||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
import '../../app/views/graphics.dart';
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../../widgets/list_title.dart';
|
import '../../widgets/list_title.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'credential_dialog.dart';
|
import 'credential_dialog.dart';
|
||||||
import 'delete_credential_dialog.dart';
|
import 'delete_credential_dialog.dart';
|
||||||
@ -45,7 +47,9 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
List<Widget> children = [];
|
List<Widget> children = [];
|
||||||
|
|
||||||
if (state.credMgmt) {
|
if (state.credMgmt) {
|
||||||
final data = ref.watch(credentialProvider(node.path)).asData;
|
final data = ref.watch(credentialProvider(node.path)).asData;
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
@ -62,15 +66,16 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
barrierColor: Colors.transparent,
|
barrierColor: Colors.transparent,
|
||||||
builder: (context) => CredentialDialog(cred),
|
builder: (context) => CredentialDialog(cred),
|
||||||
)),
|
)),
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(
|
if (hasFeature(features.credentialsDelete))
|
||||||
onInvoke: (_) => showBlurDialog(
|
DeleteIntent: CallbackAction<DeleteIntent>(
|
||||||
context: context,
|
onInvoke: (_) => showBlurDialog(
|
||||||
builder: (context) => DeleteCredentialDialog(
|
context: context,
|
||||||
node.path,
|
builder: (context) => DeleteCredentialDialog(
|
||||||
cred,
|
node.path,
|
||||||
|
cred,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
child: _CredentialListItem(cred),
|
child: _CredentialListItem(cred),
|
||||||
)));
|
)));
|
||||||
@ -95,33 +100,38 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
barrierColor: Colors.transparent,
|
barrierColor: Colors.transparent,
|
||||||
builder: (context) => FingerprintDialog(fp),
|
builder: (context) => FingerprintDialog(fp),
|
||||||
)),
|
)),
|
||||||
EditIntent: CallbackAction<EditIntent>(
|
if (hasFeature(features.fingerprintsEdit))
|
||||||
onInvoke: (_) => showBlurDialog(
|
EditIntent: CallbackAction<EditIntent>(
|
||||||
context: context,
|
onInvoke: (_) => showBlurDialog(
|
||||||
builder: (context) => RenameFingerprintDialog(
|
context: context,
|
||||||
node.path,
|
builder: (context) => RenameFingerprintDialog(
|
||||||
fp,
|
node.path,
|
||||||
),
|
fp,
|
||||||
)),
|
),
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(
|
)),
|
||||||
onInvoke: (_) => showBlurDialog(
|
if (hasFeature(features.fingerprintsDelete))
|
||||||
context: context,
|
DeleteIntent: CallbackAction<DeleteIntent>(
|
||||||
builder: (context) => DeleteFingerprintDialog(
|
onInvoke: (_) => showBlurDialog(
|
||||||
node.path,
|
context: context,
|
||||||
fp,
|
builder: (context) => DeleteFingerprintDialog(
|
||||||
),
|
node.path,
|
||||||
)),
|
fp,
|
||||||
|
),
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
child: _FingerprintListItem(fp),
|
child: _FingerprintListItem(fp),
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hasActions = ref.watch(featureProvider)(features.actions);
|
||||||
|
|
||||||
if (children.isNotEmpty) {
|
if (children.isNotEmpty) {
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: Text(l10n.s_webauthn),
|
title: Text(l10n.s_webauthn),
|
||||||
keyActionsBuilder: (context) =>
|
keyActionsBuilder: hasActions
|
||||||
fidoBuildActions(context, node, state, nFingerprints),
|
? (context) => fidoBuildActions(context, node, state, nFingerprints)
|
||||||
|
: null,
|
||||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start, children: children),
|
crossAxisAlignment: CrossAxisAlignment.start, children: children),
|
||||||
@ -134,8 +144,9 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
graphic: noFingerprints,
|
graphic: noFingerprints,
|
||||||
header: l10n.s_no_fingerprints,
|
header: l10n.s_no_fingerprints,
|
||||||
message: l10n.l_add_one_or_more_fps,
|
message: l10n.l_add_one_or_more_fps,
|
||||||
keyActionsBuilder: (context) =>
|
keyActionsBuilder: hasActions
|
||||||
fidoBuildActions(context, node, state, 0),
|
? (context) => fidoBuildActions(context, node, state, 0)
|
||||||
|
: null,
|
||||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -145,7 +156,9 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
graphic: manageAccounts,
|
graphic: manageAccounts,
|
||||||
header: l10n.l_no_discoverable_accounts,
|
header: l10n.l_no_discoverable_accounts,
|
||||||
message: l10n.l_register_sk_on_websites,
|
message: l10n.l_register_sk_on_websites,
|
||||||
keyActionsBuilder: (context) => fidoBuildActions(context, node, state, 0),
|
keyActionsBuilder: hasActions
|
||||||
|
? (context) => fidoBuildActions(context, node, state, 0)
|
||||||
|
: null,
|
||||||
keyActionsBadge: fidoShowActionsNotifier(state),
|
keyActionsBadge: fidoShowActionsNotifier(state),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,8 @@
|
|||||||
"s_configure_yk": "Configure YubiKey",
|
"s_configure_yk": "Configure YubiKey",
|
||||||
"s_please_wait": "Please wait\u2026",
|
"s_please_wait": "Please wait\u2026",
|
||||||
"s_secret_key": "Secret key",
|
"s_secret_key": "Secret key",
|
||||||
|
"s_show_secret_key": "Show secret key",
|
||||||
|
"s_hide_secret_key": "Hide secret key",
|
||||||
"s_private_key": "Private key",
|
"s_private_key": "Private key",
|
||||||
"s_invalid_length": "Invalid length",
|
"s_invalid_length": "Invalid length",
|
||||||
"s_require_touch": "Require touch",
|
"s_require_touch": "Require touch",
|
||||||
@ -182,6 +184,8 @@
|
|||||||
"s_set_pin": "Set PIN",
|
"s_set_pin": "Set PIN",
|
||||||
"s_change_pin": "Change PIN",
|
"s_change_pin": "Change PIN",
|
||||||
"s_change_puk": "Change PUK",
|
"s_change_puk": "Change PUK",
|
||||||
|
"s_show_pin": "Show PIN",
|
||||||
|
"s_hide_pin": "Hide PIN",
|
||||||
"s_current_pin": "Current PIN",
|
"s_current_pin": "Current PIN",
|
||||||
"s_current_puk": "Current PUK",
|
"s_current_puk": "Current PUK",
|
||||||
"s_new_pin": "New PIN",
|
"s_new_pin": "New PIN",
|
||||||
@ -256,6 +260,8 @@
|
|||||||
"s_manage_password": "Manage password",
|
"s_manage_password": "Manage password",
|
||||||
"s_set_password": "Set password",
|
"s_set_password": "Set password",
|
||||||
"s_password_set": "Password set",
|
"s_password_set": "Password set",
|
||||||
|
"s_show_password": "Show password",
|
||||||
|
"s_hide_password": "Hide password",
|
||||||
"l_optional_password_protection": "Optional password protection",
|
"l_optional_password_protection": "Optional password protection",
|
||||||
"s_new_password": "New password",
|
"s_new_password": "New password",
|
||||||
"s_current_password": "Current password",
|
"s_current_password": "Current password",
|
||||||
@ -509,6 +515,7 @@
|
|||||||
"l_qr_scanned": "Scanned QR code",
|
"l_qr_scanned": "Scanned QR code",
|
||||||
"l_invalid_qr": "Invalid QR code",
|
"l_invalid_qr": "Invalid QR code",
|
||||||
"l_qr_not_found": "No QR code found",
|
"l_qr_not_found": "No QR code found",
|
||||||
|
"l_qr_select_file": "Select file with QR code",
|
||||||
"l_qr_not_read": "Failed reading QR code: {message}",
|
"l_qr_not_read": "Failed reading QR code: {message}",
|
||||||
"@l_qr_not_read" : {
|
"@l_qr_not_read" : {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@ -519,6 +526,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_file": "Read from file",
|
||||||
|
|
||||||
"@_factory_reset": {},
|
"@_factory_reset": {},
|
||||||
"s_reset": "Reset",
|
"s_reset": "Reset",
|
||||||
|
31
lib/oath/features.dart
Normal file
31
lib/oath/features.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../app/features.dart';
|
||||||
|
|
||||||
|
final actions = oath.feature('actions');
|
||||||
|
|
||||||
|
final actionsAdd = actions.feature('add');
|
||||||
|
final actionsIcons = actions.feature('icons');
|
||||||
|
final actionsPassword = actions.feature('password');
|
||||||
|
final actionsReset = actions.feature('reset');
|
||||||
|
|
||||||
|
final accounts = actions.feature('accounts');
|
||||||
|
|
||||||
|
final accountsClipboard = accounts.feature('clipboard');
|
||||||
|
final accountsPin = accounts.feature('pin');
|
||||||
|
final accountsRename = accounts.feature('rename');
|
||||||
|
final accountsDelete = accounts.feature('delete');
|
@ -27,6 +27,7 @@ import '../../app/views/fs_dialog.dart';
|
|||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
import '../../core/models.dart';
|
import '../../core/models.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'account_helper.dart';
|
import 'account_helper.dart';
|
||||||
@ -49,6 +50,7 @@ class AccountDialog extends ConsumerWidget {
|
|||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
final helper = AccountHelper(context, ref, credential);
|
final helper = AccountHelper(context, ref, credential);
|
||||||
final subtitle = helper.subtitle;
|
final subtitle = helper.subtitle;
|
||||||
|
|
||||||
@ -56,54 +58,58 @@ class AccountDialog extends ConsumerWidget {
|
|||||||
credential,
|
credential,
|
||||||
ref: ref,
|
ref: ref,
|
||||||
actions: {
|
actions: {
|
||||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
if (hasFeature(features.accountsRename))
|
||||||
final credentials = ref.read(credentialsProvider);
|
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||||
final withContext = ref.read(withContextProvider);
|
final credentials = ref.read(credentialsProvider);
|
||||||
final renamed =
|
final withContext = ref.read(withContextProvider);
|
||||||
await withContext((context) async => await showBlurDialog(
|
final renamed =
|
||||||
|
await withContext((context) async => await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => RenameAccountDialog.forOathCredential(
|
||||||
|
ref,
|
||||||
|
node,
|
||||||
|
credential,
|
||||||
|
credentials
|
||||||
|
?.map((e) => (e.issuer, e.name))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
)));
|
||||||
|
if (renamed != null) {
|
||||||
|
// Replace the dialog with the renamed credential
|
||||||
|
await withContext((context) async {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
await showBlurDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => RenameAccountDialog.forOathCredential(
|
builder: (context) {
|
||||||
ref,
|
return AccountDialog(renamed);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return renamed;
|
||||||
|
}),
|
||||||
|
if (hasFeature(features.accountsDelete))
|
||||||
|
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
final bool? deleted =
|
||||||
|
await ref.read(withContextProvider)((context) async =>
|
||||||
|
await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => DeleteAccountDialog(
|
||||||
node,
|
node,
|
||||||
credential,
|
credential,
|
||||||
credentials?.map((e) => (e.issuer, e.name)).toList() ??
|
),
|
||||||
[],
|
) ??
|
||||||
)));
|
false);
|
||||||
if (renamed != null) {
|
|
||||||
// Replace the dialog with the renamed credential
|
|
||||||
await withContext((context) async {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
await showBlurDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AccountDialog(renamed);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return renamed;
|
|
||||||
}),
|
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
|
||||||
final withContext = ref.read(withContextProvider);
|
|
||||||
final bool? deleted =
|
|
||||||
await ref.read(withContextProvider)((context) async =>
|
|
||||||
await showBlurDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => DeleteAccountDialog(
|
|
||||||
node,
|
|
||||||
credential,
|
|
||||||
),
|
|
||||||
) ??
|
|
||||||
false);
|
|
||||||
|
|
||||||
// Pop the account dialog if deleted
|
// Pop the account dialog if deleted
|
||||||
if (deleted == true) {
|
if (deleted == true) {
|
||||||
await withContext((context) async {
|
await withContext((context) async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return deleted;
|
return deleted;
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
if (helper.code == null &&
|
if (helper.code == null &&
|
||||||
|
@ -27,6 +27,7 @@ import '../../app/state.dart';
|
|||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../../widgets/circle_timer.dart';
|
import '../../widgets/circle_timer.dart';
|
||||||
import '../../widgets/custom_icons.dart';
|
import '../../widgets/custom_icons.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
@ -68,6 +69,7 @@ class AccountHelper {
|
|||||||
return [
|
return [
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.copyAction,
|
key: keys.copyAction,
|
||||||
|
feature: features.accountsClipboard,
|
||||||
icon: const Icon(Icons.copy),
|
icon: const Icon(Icons.copy),
|
||||||
title: l10n.l_copy_to_clipboard,
|
title: l10n.l_copy_to_clipboard,
|
||||||
subtitle: l10n.l_copy_code_desc,
|
subtitle: l10n.l_copy_code_desc,
|
||||||
@ -87,6 +89,7 @@ class AccountHelper {
|
|||||||
),
|
),
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.togglePinAction,
|
key: keys.togglePinAction,
|
||||||
|
feature: features.accountsPin,
|
||||||
icon: pinned
|
icon: pinned
|
||||||
? pushPinStrokeIcon
|
? pushPinStrokeIcon
|
||||||
: const Icon(Icons.push_pin_outlined),
|
: const Icon(Icons.push_pin_outlined),
|
||||||
@ -97,6 +100,7 @@ class AccountHelper {
|
|||||||
if (data.info.version.isAtLeast(5, 3))
|
if (data.info.version.isAtLeast(5, 3))
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.editAction,
|
key: keys.editAction,
|
||||||
|
feature: features.accountsRename,
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
title: l10n.s_rename_account,
|
title: l10n.s_rename_account,
|
||||||
subtitle: l10n.l_rename_account_desc,
|
subtitle: l10n.l_rename_account_desc,
|
||||||
@ -104,6 +108,7 @@ class AccountHelper {
|
|||||||
),
|
),
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.deleteAction,
|
key: keys.deleteAction,
|
||||||
|
feature: features.accountsDelete,
|
||||||
actionStyle: ActionStyle.error,
|
actionStyle: ActionStyle.error,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
title: l10n.s_delete_account,
|
title: l10n.s_delete_account,
|
||||||
|
@ -23,7 +23,9 @@ import '../../app/message.dart';
|
|||||||
import '../../app/shortcuts.dart';
|
import '../../app/shortcuts.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/views/app_list_item.dart';
|
import '../../app/views/app_list_item.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'account_dialog.dart';
|
import 'account_dialog.dart';
|
||||||
import 'account_helper.dart';
|
import 'account_helper.dart';
|
||||||
@ -81,6 +83,7 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final darkMode = theme.brightness == Brightness.dark;
|
final darkMode = theme.brightness == Brightness.dark;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
|
||||||
return registerOathActions(
|
return registerOathActions(
|
||||||
credential,
|
credential,
|
||||||
@ -94,29 +97,31 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
|||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
if (hasFeature(features.accountsRename))
|
||||||
final node = ref.read(currentDeviceProvider)!;
|
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
|
||||||
final credentials = ref.read(credentialsProvider);
|
final node = ref.read(currentDeviceProvider)!;
|
||||||
final withContext = ref.read(withContextProvider);
|
final credentials = ref.read(credentialsProvider);
|
||||||
return await withContext((context) async => await showBlurDialog(
|
final withContext = ref.read(withContextProvider);
|
||||||
context: context,
|
return await withContext((context) async => await showBlurDialog(
|
||||||
builder: (context) => RenameAccountDialog.forOathCredential(
|
context: context,
|
||||||
ref,
|
builder: (context) => RenameAccountDialog.forOathCredential(
|
||||||
node,
|
ref,
|
||||||
credential,
|
node,
|
||||||
credentials?.map((e) => (e.issuer, e.name)).toList() ?? [],
|
credential,
|
||||||
),
|
credentials?.map((e) => (e.issuer, e.name)).toList() ?? [],
|
||||||
));
|
),
|
||||||
}),
|
));
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
}),
|
||||||
final node = ref.read(currentDeviceProvider)!;
|
if (hasFeature(features.accountsDelete))
|
||||||
return await ref.read(withContextProvider)((context) async =>
|
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||||
await showBlurDialog(
|
final node = ref.read(currentDeviceProvider)!;
|
||||||
context: context,
|
return await ref.read(withContextProvider)((context) async =>
|
||||||
builder: (context) => DeleteAccountDialog(node, credential),
|
await showBlurDialog(
|
||||||
) ??
|
context: context,
|
||||||
false);
|
builder: (context) => DeleteAccountDialog(node, credential),
|
||||||
}),
|
) ??
|
||||||
|
false);
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final helper = AccountHelper(context, ref, credential);
|
final helper = AccountHelper(context, ref, credential);
|
||||||
@ -162,7 +167,9 @@ class _AccountViewState extends ConsumerState<AccountView> {
|
|||||||
onPressed:
|
onPressed:
|
||||||
Actions.handler(context, const OpenIntent()),
|
Actions.handler(context, const OpenIntent()),
|
||||||
child: helper.buildCodeIcon()),
|
child: helper.buildCodeIcon()),
|
||||||
activationIntent: const CopyIntent(),
|
activationIntent: hasFeature(features.accountsClipboard)
|
||||||
|
? const CopyIntent()
|
||||||
|
: const OpenIntent(),
|
||||||
buildPopupActions: (_) => helper.buildActions(),
|
buildPopupActions: (_) => helper.buildActions(),
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
@ -21,8 +21,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|||||||
import '../../app/message.dart';
|
import '../../app/message.dart';
|
||||||
import '../../app/shortcuts.dart';
|
import '../../app/shortcuts.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../../exception/cancellation_exception.dart';
|
import '../../exception/cancellation_exception.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
|
||||||
class TogglePinIntent extends Intent {
|
class TogglePinIntent extends Intent {
|
||||||
@ -46,18 +48,20 @@ Widget registerOathActions(
|
|||||||
required WidgetRef ref,
|
required WidgetRef ref,
|
||||||
required Widget Function(BuildContext context) builder,
|
required Widget Function(BuildContext context) builder,
|
||||||
Map<Type, Action<Intent>> actions = const {},
|
Map<Type, Action<Intent>> actions = const {},
|
||||||
}) =>
|
}) {
|
||||||
Actions(
|
final hasFeature = ref.read(featureProvider);
|
||||||
actions: {
|
return Actions(
|
||||||
RefreshIntent: CallbackAction<RefreshIntent>(onInvoke: (_) {
|
actions: {
|
||||||
final code = ref.read(codeProvider(credential));
|
RefreshIntent: CallbackAction<RefreshIntent>(onInvoke: (_) {
|
||||||
if (!(credential.oathType == OathType.totp &&
|
final code = ref.read(codeProvider(credential));
|
||||||
code != null &&
|
if (!(credential.oathType == OathType.totp &&
|
||||||
!ref.read(expiredProvider(code.validTo)))) {
|
code != null &&
|
||||||
return _calculateCode(credential, ref);
|
!ref.read(expiredProvider(code.validTo)))) {
|
||||||
}
|
return _calculateCode(credential, ref);
|
||||||
return code;
|
}
|
||||||
}),
|
return code;
|
||||||
|
}),
|
||||||
|
if (hasFeature(features.accountsClipboard))
|
||||||
CopyIntent: CallbackAction<CopyIntent>(onInvoke: (_) async {
|
CopyIntent: CallbackAction<CopyIntent>(onInvoke: (_) async {
|
||||||
var code = ref.read(codeProvider(credential));
|
var code = ref.read(codeProvider(credential));
|
||||||
if (code == null ||
|
if (code == null ||
|
||||||
@ -77,11 +81,13 @@ Widget registerOathActions(
|
|||||||
}
|
}
|
||||||
return code;
|
return code;
|
||||||
}),
|
}),
|
||||||
|
if (hasFeature(features.accountsPin))
|
||||||
TogglePinIntent: CallbackAction<TogglePinIntent>(onInvoke: (_) {
|
TogglePinIntent: CallbackAction<TogglePinIntent>(onInvoke: (_) {
|
||||||
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
...actions,
|
...actions,
|
||||||
},
|
},
|
||||||
child: Builder(builder: builder),
|
child: Builder(builder: builder),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
@ -438,6 +438,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
tooltip: _isObscure
|
||||||
|
? l10n.s_show_secret_key
|
||||||
|
: l10n.s_hide_secret_key,
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
prefixIcon: const Icon(Icons.key_outlined),
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
|
@ -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';
|
||||||
@ -27,12 +28,10 @@ import '../../app/views/fs_dialog.dart';
|
|||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
import '../../core/state.dart';
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
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,
|
||||||
@ -49,6 +48,7 @@ Widget oathBuildActions(
|
|||||||
children: [
|
children: [
|
||||||
ActionListSection(l10n.s_setup, children: [
|
ActionListSection(l10n.s_setup, children: [
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
|
feature: features.actionsAdd,
|
||||||
title: l10n.s_add_account,
|
title: l10n.s_add_account,
|
||||||
subtitle: used == null
|
subtitle: used == null
|
||||||
? l10n.l_unlock_first
|
? l10n.l_unlock_first
|
||||||
@ -59,38 +59,16 @@ 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 withContext = ref.read(withContextProvider);
|
||||||
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, withContext, qrScanner, 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,
|
||||||
@ -104,6 +82,7 @@ Widget oathBuildActions(
|
|||||||
ActionListSection(l10n.s_manage, children: [
|
ActionListSection(l10n.s_manage, children: [
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.customIconsAction,
|
key: keys.customIconsAction,
|
||||||
|
feature: features.actionsIcons,
|
||||||
title: l10n.s_custom_icons,
|
title: l10n.s_custom_icons,
|
||||||
subtitle: l10n.l_set_icons_for_accounts,
|
subtitle: l10n.l_set_icons_for_accounts,
|
||||||
icon: const Icon(Icons.image_outlined),
|
icon: const Icon(Icons.image_outlined),
|
||||||
@ -118,6 +97,7 @@ Widget oathBuildActions(
|
|||||||
}),
|
}),
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.setOrManagePasswordAction,
|
key: keys.setOrManagePasswordAction,
|
||||||
|
feature: features.actionsPassword,
|
||||||
title: oathState.hasKey
|
title: oathState.hasKey
|
||||||
? l10n.s_manage_password
|
? l10n.s_manage_password
|
||||||
: l10n.s_set_password,
|
: l10n.s_set_password,
|
||||||
@ -133,6 +113,7 @@ Widget oathBuildActions(
|
|||||||
}),
|
}),
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.resetAction,
|
key: keys.resetAction,
|
||||||
|
feature: features.actionsReset,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
actionStyle: ActionStyle.error,
|
actionStyle: ActionStyle.error,
|
||||||
title: l10n.s_reset_oath,
|
title: l10n.s_reset_oath,
|
||||||
|
@ -25,6 +25,8 @@ import '../../app/views/app_failure_page.dart';
|
|||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
import '../../app/views/graphics.dart';
|
import '../../app/views/graphics.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
@ -65,10 +67,12 @@ class _LockedView extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final hasActions = ref.watch(featureProvider)(features.actions);
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: Text(AppLocalizations.of(context)!.s_authenticator),
|
title: Text(AppLocalizations.of(context)!.s_authenticator),
|
||||||
keyActionsBuilder: (context) =>
|
keyActionsBuilder: hasActions
|
||||||
oathBuildActions(context, devicePath, oathState, ref),
|
? (context) => oathBuildActions(context, devicePath, oathState, ref)
|
||||||
|
: null,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 18),
|
padding: const EdgeInsets.symmetric(vertical: 18),
|
||||||
child: UnlockForm(
|
child: UnlockForm(
|
||||||
@ -114,15 +118,18 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
// ONLY rebuild if the number of credentials changes.
|
// ONLY rebuild if the number of credentials changes.
|
||||||
final numCreds = ref.watch(credentialListProvider(widget.devicePath)
|
final numCreds = ref.watch(credentialListProvider(widget.devicePath)
|
||||||
.select((value) => value?.length));
|
.select((value) => value?.length));
|
||||||
|
final hasActions = ref.watch(featureProvider)(features.actions);
|
||||||
if (numCreds == 0) {
|
if (numCreds == 0) {
|
||||||
return MessagePage(
|
return MessagePage(
|
||||||
title: Text(l10n.s_authenticator),
|
title: Text(l10n.s_authenticator),
|
||||||
key: keys.noAccountsView,
|
key: keys.noAccountsView,
|
||||||
graphic: noAccounts,
|
graphic: noAccounts,
|
||||||
header: l10n.s_no_accounts,
|
header: l10n.s_no_accounts,
|
||||||
keyActionsBuilder: (context) => oathBuildActions(
|
keyActionsBuilder: hasActions
|
||||||
context, widget.devicePath, widget.oathState, ref,
|
? (context) => oathBuildActions(
|
||||||
used: 0),
|
context, widget.devicePath, widget.oathState, ref,
|
||||||
|
used: 0)
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Actions(
|
return Actions(
|
||||||
@ -184,13 +191,15 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
keyActionsBuilder: (context) => oathBuildActions(
|
keyActionsBuilder: hasActions
|
||||||
context,
|
? (context) => oathBuildActions(
|
||||||
widget.devicePath,
|
context,
|
||||||
widget.oathState,
|
widget.devicePath,
|
||||||
ref,
|
widget.oathState,
|
||||||
used: numCreds ?? 0,
|
ref,
|
||||||
),
|
used: numCreds ?? 0,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
centered: numCreds == null,
|
centered: numCreds == null,
|
||||||
delayedContent: numCreds == null,
|
delayedContent: numCreds == null,
|
||||||
child: numCreds != null
|
child: numCreds != null
|
||||||
|
@ -94,6 +94,9 @@ class _UnlockFormState extends ConsumerState<UnlockForm> {
|
|||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
tooltip: _isObscure
|
||||||
|
? l10n.s_show_password
|
||||||
|
: l10n.s_hide_password,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: (_) => setState(() {
|
onChanged: (_) => setState(() {
|
||||||
|
31
lib/piv/features.dart
Normal file
31
lib/piv/features.dart
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import '../app/features.dart';
|
||||||
|
|
||||||
|
final actions = piv.feature('actions');
|
||||||
|
|
||||||
|
final actionsPin = actions.feature('pin');
|
||||||
|
final actionsPuk = actions.feature('puk');
|
||||||
|
final actionsManagementKey = actions.feature('managementKey');
|
||||||
|
final actionsReset = actions.feature('reset');
|
||||||
|
|
||||||
|
final slots = piv.feature('slots');
|
||||||
|
|
||||||
|
final slotsGenerate = slots.feature('generate');
|
||||||
|
final slotsImport = slots.feature('import');
|
||||||
|
final slotsExport = slots.feature('export');
|
||||||
|
final slotsDelete = slots.feature('delete');
|
@ -25,9 +25,11 @@ import '../../app/message.dart';
|
|||||||
import '../../app/shortcuts.dart';
|
import '../../app/shortcuts.dart';
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/models.dart';
|
import '../../app/models.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
|
import '../features.dart' as features;
|
||||||
import 'authentication_dialog.dart';
|
import 'authentication_dialog.dart';
|
||||||
import 'delete_certificate_dialog.dart';
|
import 'delete_certificate_dialog.dart';
|
||||||
import 'generate_key_dialog.dart';
|
import 'generate_key_dialog.dart';
|
||||||
@ -75,9 +77,11 @@ Widget registerPivActions(
|
|||||||
required WidgetRef ref,
|
required WidgetRef ref,
|
||||||
required Widget Function(BuildContext context) builder,
|
required Widget Function(BuildContext context) builder,
|
||||||
Map<Type, Action<Intent>> actions = const {},
|
Map<Type, Action<Intent>> actions = const {},
|
||||||
}) =>
|
}) {
|
||||||
Actions(
|
final hasFeature = ref.watch(featureProvider);
|
||||||
actions: {
|
return Actions(
|
||||||
|
actions: {
|
||||||
|
if (hasFeature(features.slotsGenerate))
|
||||||
GenerateIntent:
|
GenerateIntent:
|
||||||
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
|
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
|
||||||
final withContext = ref.read(withContextProvider);
|
final withContext = ref.read(withContextProvider);
|
||||||
@ -129,6 +133,7 @@ Widget registerPivActions(
|
|||||||
return result != null;
|
return result != null;
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
if (hasFeature(features.slotsImport))
|
||||||
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
|
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
|
||||||
final withContext = ref.read(withContextProvider);
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
@ -164,6 +169,7 @@ Widget registerPivActions(
|
|||||||
) ??
|
) ??
|
||||||
false);
|
false);
|
||||||
}),
|
}),
|
||||||
|
if (hasFeature(features.slotsExport))
|
||||||
ExportIntent: CallbackAction<ExportIntent>(onInvoke: (intent) async {
|
ExportIntent: CallbackAction<ExportIntent>(onInvoke: (intent) async {
|
||||||
final (_, cert) = await ref
|
final (_, cert) = await ref
|
||||||
.read(pivSlotsProvider(devicePath).notifier)
|
.read(pivSlotsProvider(devicePath).notifier)
|
||||||
@ -198,6 +204,7 @@ Widget registerPivActions(
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
if (hasFeature(features.slotsDelete))
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||||
final withContext = ref.read(withContextProvider);
|
final withContext = ref.read(withContextProvider);
|
||||||
if (!await withContext(
|
if (!await withContext(
|
||||||
@ -216,15 +223,17 @@ Widget registerPivActions(
|
|||||||
false);
|
false);
|
||||||
return deleted;
|
return deleted;
|
||||||
}),
|
}),
|
||||||
...actions,
|
...actions,
|
||||||
},
|
},
|
||||||
child: Builder(builder: builder),
|
child: Builder(builder: builder),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
||||||
return [
|
return [
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.generateAction,
|
key: keys.generateAction,
|
||||||
|
feature: features.slotsGenerate,
|
||||||
icon: const Icon(Icons.add_outlined),
|
icon: const Icon(Icons.add_outlined),
|
||||||
actionStyle: ActionStyle.primary,
|
actionStyle: ActionStyle.primary,
|
||||||
title: l10n.s_generate_key,
|
title: l10n.s_generate_key,
|
||||||
@ -233,6 +242,7 @@ List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
|||||||
),
|
),
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.importAction,
|
key: keys.importAction,
|
||||||
|
feature: features.slotsImport,
|
||||||
icon: const Icon(Icons.file_download_outlined),
|
icon: const Icon(Icons.file_download_outlined),
|
||||||
title: l10n.l_import_file,
|
title: l10n.l_import_file,
|
||||||
subtitle: l10n.l_import_desc,
|
subtitle: l10n.l_import_desc,
|
||||||
@ -241,6 +251,7 @@ List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
|||||||
if (hasCert) ...[
|
if (hasCert) ...[
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.exportAction,
|
key: keys.exportAction,
|
||||||
|
feature: features.slotsExport,
|
||||||
icon: const Icon(Icons.file_upload_outlined),
|
icon: const Icon(Icons.file_upload_outlined),
|
||||||
title: l10n.l_export_certificate,
|
title: l10n.l_export_certificate,
|
||||||
subtitle: l10n.l_export_certificate_desc,
|
subtitle: l10n.l_export_certificate_desc,
|
||||||
@ -248,6 +259,7 @@ List<ActionItem> buildSlotActions(bool hasCert, AppLocalizations l10n) {
|
|||||||
),
|
),
|
||||||
ActionItem(
|
ActionItem(
|
||||||
key: keys.deleteAction,
|
key: keys.deleteAction,
|
||||||
|
feature: features.slotsDelete,
|
||||||
actionStyle: ActionStyle.error,
|
actionStyle: ActionStyle.error,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
title: l10n.l_delete_certificate,
|
title: l10n.l_delete_certificate,
|
||||||
|
@ -24,6 +24,7 @@ import '../../app/views/fs_dialog.dart';
|
|||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
|
import '../features.dart' as features;
|
||||||
import 'manage_key_dialog.dart';
|
import 'manage_key_dialog.dart';
|
||||||
import 'manage_pin_puk_dialog.dart';
|
import 'manage_pin_puk_dialog.dart';
|
||||||
import 'reset_dialog.dart';
|
import 'reset_dialog.dart';
|
||||||
@ -49,6 +50,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
|||||||
children: [
|
children: [
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.managePinAction,
|
key: keys.managePinAction,
|
||||||
|
feature: features.actionsPin,
|
||||||
title: l10n.s_pin,
|
title: l10n.s_pin,
|
||||||
subtitle: pinBlocked
|
subtitle: pinBlocked
|
||||||
? (pukAttempts != 0
|
? (pukAttempts != 0
|
||||||
@ -73,6 +75,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
|||||||
: null),
|
: null),
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.managePukAction,
|
key: keys.managePukAction,
|
||||||
|
feature: features.actionsPuk,
|
||||||
title: l10n.s_puk,
|
title: l10n.s_puk,
|
||||||
subtitle: pukAttempts != null
|
subtitle: pukAttempts != null
|
||||||
? (pukAttempts == 0
|
? (pukAttempts == 0
|
||||||
@ -93,6 +96,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
|||||||
: null),
|
: null),
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.manageManagementKeyAction,
|
key: keys.manageManagementKeyAction,
|
||||||
|
feature: features.actionsManagementKey,
|
||||||
title: l10n.s_management_key,
|
title: l10n.s_management_key,
|
||||||
subtitle: usingDefaultMgmtKey
|
subtitle: usingDefaultMgmtKey
|
||||||
? l10n.l_warning_default_key
|
? l10n.l_warning_default_key
|
||||||
@ -110,6 +114,7 @@ Widget pivBuildActions(BuildContext context, DevicePath devicePath,
|
|||||||
}),
|
}),
|
||||||
ActionListItem(
|
ActionListItem(
|
||||||
key: keys.resetAction,
|
key: keys.resetAction,
|
||||||
|
feature: features.actionsReset,
|
||||||
icon: const Icon(Icons.delete_outline),
|
icon: const Icon(Icons.delete_outline),
|
||||||
actionStyle: ActionStyle.error,
|
actionStyle: ActionStyle.error,
|
||||||
title: l10n.s_reset_piv,
|
title: l10n.s_reset_piv,
|
||||||
|
@ -110,6 +110,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
_isObscure = !_isObscure;
|
_isObscure = !_isObscure;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
|
@ -25,9 +25,11 @@ import '../../app/views/app_failure_page.dart';
|
|||||||
import '../../app/views/app_list_item.dart';
|
import '../../app/views/app_list_item.dart';
|
||||||
import '../../app/views/app_page.dart';
|
import '../../app/views/app_page.dart';
|
||||||
import '../../app/views/message_page.dart';
|
import '../../app/views/message_page.dart';
|
||||||
|
import '../../core/state.dart';
|
||||||
import '../../widgets/list_title.dart';
|
import '../../widgets/list_title.dart';
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
|
import '../features.dart' as features;
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'key_actions.dart';
|
import 'key_actions.dart';
|
||||||
import 'slot_dialog.dart';
|
import 'slot_dialog.dart';
|
||||||
@ -40,6 +42,7 @@ class PivScreen extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
return ref.watch(pivStateProvider(devicePath)).when(
|
return ref.watch(pivStateProvider(devicePath)).when(
|
||||||
loading: () => MessagePage(
|
loading: () => MessagePage(
|
||||||
title: Text(l10n.s_piv),
|
title: Text(l10n.s_piv),
|
||||||
@ -54,8 +57,10 @@ class PivScreen extends ConsumerWidget {
|
|||||||
final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData;
|
final pivSlots = ref.watch(pivSlotsProvider(devicePath)).asData;
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: Text(l10n.s_piv),
|
title: Text(l10n.s_piv),
|
||||||
keyActionsBuilder: (context) =>
|
keyActionsBuilder: hasFeature(features.actions)
|
||||||
pivBuildActions(context, devicePath, pivState, ref),
|
? (context) =>
|
||||||
|
pivBuildActions(context, devicePath, pivState, ref)
|
||||||
|
: null,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
ListTitle(l10n.s_certificates),
|
ListTitle(l10n.s_certificates),
|
||||||
@ -86,16 +91,17 @@ class PivScreen extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CertificateListItem extends StatelessWidget {
|
class _CertificateListItem extends ConsumerWidget {
|
||||||
final PivSlot pivSlot;
|
final PivSlot pivSlot;
|
||||||
const _CertificateListItem(this.pivSlot);
|
const _CertificateListItem(this.pivSlot);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final slot = pivSlot.slot;
|
final slot = pivSlot.slot;
|
||||||
final certInfo = pivSlot.certInfo;
|
final certInfo = pivSlot.certInfo;
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final hasFeature = ref.watch(featureProvider);
|
||||||
|
|
||||||
return Semantics(
|
return Semantics(
|
||||||
label: slot.getDisplayName(l10n),
|
label: slot.getDisplayName(l10n),
|
||||||
@ -116,8 +122,9 @@ class _CertificateListItem extends StatelessWidget {
|
|||||||
onPressed: Actions.handler(context, const OpenIntent()),
|
onPressed: Actions.handler(context, const OpenIntent()),
|
||||||
child: const Icon(Icons.more_horiz),
|
child: const Icon(Icons.more_horiz),
|
||||||
),
|
),
|
||||||
buildPopupActions: (context) =>
|
buildPopupActions: hasFeature(features.slots)
|
||||||
buildSlotActions(certInfo != null, l10n),
|
? (context) => buildSlotActions(certInfo != null, l10n)
|
||||||
|
: null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../core/state.dart';
|
||||||
|
|
||||||
class FileDropTarget extends StatefulWidget {
|
class FileDropTarget extends StatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final Function(List<int> filedata) onFileDropped;
|
final Function(List<int> filedata) onFileDropped;
|
||||||
@ -64,6 +66,7 @@ class _FileDropTargetState extends State<FileDropTarget> {
|
|||||||
widget.onFileDropped(await file.readAsBytes());
|
widget.onFileDropped(await file.readAsBytes());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
enable: !isAndroid,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -54,6 +54,7 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
|
|||||||
title: widget.title,
|
title: widget.title,
|
||||||
actions: widget.actions,
|
actions: widget.actions,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
|
tooltip: AppLocalizations.of(context)!.s_close,
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
onPressed: widget.allowCancel
|
onPressed: widget.allowCancel
|
||||||
? () {
|
? () {
|
||||||
|
Loading…
Reference in New Issue
Block a user