Merge branch 'main' into fix/android-nfc

This commit is contained in:
Adam Velebil 2022-07-21 17:24:35 +02:00
commit 902eb6718d
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
61 changed files with 1523 additions and 1070 deletions

View File

@ -48,10 +48,18 @@ jobs:
- name: Check generated files - name: Check generated files
run: git diff --exit-code run: git diff --exit-code
- name: Create dmg
run: |
brew install create-dmg
mkdir source_folder
cp -R build/macos/Build/Products/Release/"Yubico Authenticator.app" source_folder
sh create-dmg.sh
- name: Rename and archive app bundle - name: Rename and archive app bundle
run: | run: |
export REF=$(echo ${GITHUB_REF} | cut -d '/' -f 3) export REF=$(echo ${GITHUB_REF} | cut -d '/' -f 3)
mkdir deploy mkdir deploy
mv yubioath-desktop.dmg deploy
tar -czf deploy/yubioath-desktop-${REF}.app.tar.gz -C build/macos/Build/Products/Release "Yubico Authenticator.app" tar -czf deploy/yubioath-desktop-${REF}.app.tar.gz -C build/macos/Build/Products/Release "Yubico Authenticator.app"
- name: Upload artifact - name: Upload artifact

View File

@ -58,6 +58,7 @@ jobs:
run: | run: |
$env:PATH += ";$env:WIX\bin" $env:PATH += ";$env:WIX\bin"
$env:SRCDIR = "build\windows\runner\Release\" $env:SRCDIR = "build\windows\runner\Release\"
cp resources\win\license.rtf .\
heat dir .\build\windows\runner\Release\ -out fragment.wxs -gg -scom -srd -sfrag -dr INSTALLDIR -cg ApplicationFiles -var env.SRCDIR heat dir .\build\windows\runner\Release\ -out fragment.wxs -gg -scom -srd -sfrag -dr INSTALLDIR -cg ApplicationFiles -var env.SRCDIR
candle .\fragment.wxs .\resources\win\yubioath-desktop.wxs -ext WixUtilExtension -arch x64 candle .\fragment.wxs .\resources\win\yubioath-desktop.wxs -ext WixUtilExtension -arch x64
light fragment.wixobj yubioath-desktop.wixobj -ext WixUIExtension -ext WixUtilExtension -o yubioath-desktop.msi light fragment.wixobj yubioath-desktop.wixobj -ext WixUIExtension -ext WixUtilExtension -o yubioath-desktop.msi
@ -70,6 +71,7 @@ jobs:
mkdir $dest mkdir $dest
mv build\windows\runner\Release\* $dest\ mv build\windows\runner\Release\* $dest\
mv yubioath-desktop.msi deploy mv yubioath-desktop.msi deploy
mv resources deploy
- name: Upload artifact - name: Upload artifact
uses: actions/upload-artifact@v1 uses: actions/upload-artifact@v1

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v3.0.3), do not edit directly. // Autogenerated from Pigeon (v3.1.6), do not edit directly.
// See also: https://pub.dev/packages/pigeon // See also: https://pub.dev/packages/pigeon
package com.yubico.authenticator.api; package com.yubico.authenticator.api;

10
create-dmg.sh Normal file
View File

@ -0,0 +1,10 @@
create-dmg \
--volname "Yubico Authenticator" \
--background "resources/icons/yubico-msi-background.png" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "Yubico Authenticator.app" 200 190 \
--app-drop-link 600 185 \
"yubioath-desktop.dmg" \
"source_folder/"

View File

@ -280,7 +280,7 @@ class UsbDeviceNode(AbstractDeviceNode):
super().__init__(device, info) super().__init__(device, info)
def _supports_connection(self, conn_type): def _supports_connection(self, conn_type):
return self._device.supports_connection(conn_type) return self._device.pid.supports_connection(conn_type)
def _create_connection(self, conn_type): def _create_connection(self, conn_type):
connection = self._device.open_connection(conn_type) connection = self._device.open_connection(conn_type)
@ -327,7 +327,9 @@ class ReaderDeviceNode(AbstractDeviceNode):
with self._device.open_connection(SmartCardConnection) as conn: with self._device.open_connection(SmartCardConnection) as conn:
return dict(self._read_data(conn), present=True) return dict(self._read_data(conn), present=True)
except NoCardException: except NoCardException:
return dict(present=False) return dict(present=False, status="no-card")
except ValueError:
return dict(present=False, status="unknown-device")
@child @child
def ccid(self): def ccid(self):

View File

@ -171,7 +171,7 @@ class Ctap2Node(RpcNode):
self.ctap = Ctap2(connection) self.ctap = Ctap2(connection)
if target != _ctap_id(self.ctap): if target != _ctap_id(self.ctap):
raise ValueError("Re-inserted YubiKey does not match initial device") raise ValueError("Re-inserted YubiKey does not match initial device")
self.ctap.reset(event) self.ctap.reset(event=event)
self._info = self.ctap.get_info() self._info = self.ctap.get_info()
self._auth_blocked = False self._auth_blocked = False
self._token = None self._token = None
@ -343,7 +343,7 @@ class FingerprintsNode(RpcNode):
template_id = None template_id = None
while template_id is None: while template_id is None:
try: try:
template_id = enroller.capture(event) template_id = enroller.capture(event=event)
signal("capture", dict(remaining=enroller.remaining)) signal("capture", dict(remaining=enroller.remaining))
except CaptureError as e: except CaptureError as e:
signal("capture-error", dict(code=e.code)) signal("capture-error", dict(code=e.code))

View File

@ -32,7 +32,7 @@ from yubikit.core.smartcard import SmartCardConnection
from yubikit.core.otp import OtpConnection from yubikit.core.otp import OtpConnection
from yubikit.core.fido import FidoConnection from yubikit.core.fido import FidoConnection
from yubikit.management import ManagementSession, DeviceConfig, Mode from yubikit.management import ManagementSession, DeviceConfig, Mode
from ykman.device import connect_to_device from ykman.device import list_all_devices
from dataclasses import asdict from dataclasses import asdict
from time import sleep from time import sleep
import logging import logging
@ -78,12 +78,10 @@ class ManagementNode(RpcNode):
logger.debug("Waiting for device to re-appear...") logger.debug("Waiting for device to re-appear...")
for _ in range(10): for _ in range(10):
sleep(0.2) # Always sleep initially sleep(0.2) # Always sleep initially
try: for dev, info in list_all_devices(connection_types):
conn = connect_to_device(serial, connection_types)[0] if info.serial == serial:
conn.close() return
break logger.debug("Not found, sleep...")
except ValueError:
logger.debug("Not found, sleep...")
else: else:
logger.warning("Timed out waiting for device") logger.warning("Timed out waiting for device")

View File

@ -8,6 +8,7 @@ import subprocess
import tempfile import tempfile
from mss.exception import ScreenShotError from mss.exception import ScreenShotError
from PIL import Image from PIL import Image
import numpy.core.multiarray # noqa
def _capture_screen(): def _capture_screen():
@ -41,6 +42,6 @@ def scan_qr(image_data=None):
img = _capture_screen() img = _capture_screen()
result = zxingcpp.read_barcode(img) result = zxingcpp.read_barcode(img)
if result.valid: if result and result.valid:
return result.text return result.text
return None return None

350
helper/poetry.lock generated
View File

@ -30,7 +30,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]] [[package]]
name = "cffi" name = "cffi"
version = "1.15.0" version = "1.15.1"
description = "Foreign Function Interface for Python calling C code." description = "Foreign Function Interface for Python calling C code."
category = "main" category = "main"
optional = false optional = false
@ -60,7 +60,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "37.0.2" version = "37.0.4"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main" category = "main"
optional = false optional = false
@ -101,7 +101,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package]] [[package]]
name = "importlib-metadata" name = "importlib-metadata"
version = "4.11.4" version = "4.12.0"
description = "Read metadata from Python packages" description = "Read metadata from Python packages"
category = "main" category = "main"
optional = false optional = false
@ -113,7 +113,7 @@ zipp = ">=0.5"
[package.extras] [package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
perf = ["ipython"] perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
@ -174,7 +174,7 @@ python-versions = ">=3.5"
[[package]] [[package]]
name = "numpy" name = "numpy"
version = "1.22.4" version = "1.23.0"
description = "NumPy is the fundamental package for array computing with Python." description = "NumPy is the fundamental package for array computing with Python."
category = "main" category = "main"
optional = false optional = false
@ -204,14 +204,14 @@ future = "*"
[[package]] [[package]]
name = "pillow" name = "pillow"
version = "9.1.1" version = "9.2.0"
description = "Python Imaging Library (Fork)" description = "Python Imaging Library (Fork)"
category = "main" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.extras] [package.extras]
docs = ["olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinx-rtd-theme (>=1.0)", "sphinxext-opengraph"] docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]] [[package]]
@ -370,7 +370,7 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
type = "git" type = "git"
url = "https://github.com/Yubico/yubikey-manager.git" url = "https://github.com/Yubico/yubikey-manager.git"
reference = "next" reference = "next"
resolved_reference = "7e0c9e586e507f0dd021a9b7beacc621bfee7baa" resolved_reference = "32612d177db0d8dd768679ce26c4e509d10f2a97"
[[package]] [[package]]
name = "zipp" name = "zipp"
@ -386,7 +386,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-
[[package]] [[package]]
name = "zxing-cpp" name = "zxing-cpp"
version = "1.3.0" version = "1.4.0"
description = "Python bindings for the zxing-cpp barcode library" description = "Python bindings for the zxing-cpp barcode library"
category = "main" category = "main"
optional = false optional = false
@ -414,56 +414,70 @@ attrs = [
{file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
] ]
cffi = [ cffi = [
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"},
{file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"},
{file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"},
{file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"},
{file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"},
{file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"},
{file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"},
{file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"},
{file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"},
{file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"},
{file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"},
{file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"},
{file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"},
{file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"},
{file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"},
{file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"},
{file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"},
{file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"},
{file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"},
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"},
{file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"},
{file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"},
{file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"},
{file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"},
{file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"},
{file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"},
{file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"},
{file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"},
{file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"},
] ]
click = [ click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
@ -474,28 +488,28 @@ colorama = [
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
] ]
cryptography = [ cryptography = [
{file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"}, {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"},
{file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"}, {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"}, {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"},
{file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"}, {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"},
{file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"}, {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"},
{file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"}, {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"},
{file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"}, {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"},
{file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"}, {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"},
{file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"}, {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"}, {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"}, {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"},
{file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"}, {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"},
] ]
fido2 = [ fido2 = [
{file = "fido2-1.0.0-py3-none-any.whl", hash = "sha256:dce13d739b8e0df30505b33f5fd2868fad20f3b309acacce72e5f2d1b0c58761"}, {file = "fido2-1.0.0-py3-none-any.whl", hash = "sha256:dce13d739b8e0df30505b33f5fd2868fad20f3b309acacce72e5f2d1b0c58761"},
@ -505,8 +519,8 @@ future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
] ]
importlib-metadata = [ importlib-metadata = [
{file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"}, {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"},
{file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"}, {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"},
] ]
iniconfig = [ iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
@ -529,28 +543,28 @@ mss = [
{file = "mss-6.1.0.tar.gz", hash = "sha256:aebd069f3e05667fe9c7b9fa4b1771fe42a4710ce1058ce0236936ce06fa5394"}, {file = "mss-6.1.0.tar.gz", hash = "sha256:aebd069f3e05667fe9c7b9fa4b1771fe42a4710ce1058ce0236936ce06fa5394"},
] ]
numpy = [ numpy = [
{file = "numpy-1.22.4-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:ba9ead61dfb5d971d77b6c131a9dbee62294a932bf6a356e48c75ae684e635b3"}, {file = "numpy-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58bfd40eb478f54ff7a5710dd61c8097e169bc36cc68333d00a9bcd8def53b38"},
{file = "numpy-1.22.4-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1ce7ab2053e36c0a71e7a13a7475bd3b1f54750b4b433adc96313e127b870887"}, {file = "numpy-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:196cd074c3f97c4121601790955f915187736f9cf458d3ee1f1b46aff2b1ade0"},
{file = "numpy-1.22.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7228ad13744f63575b3a972d7ee4fd61815b2879998e70930d4ccf9ec721dce0"}, {file = "numpy-1.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1d88ef79e0a7fa631bb2c3dda1ea46b32b1fe614e10fedd611d3d5398447f2f"},
{file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a8ca7391b626b4c4fe20aefe79fec683279e31e7c79716863b4b25021e0e74"}, {file = "numpy-1.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d54b3b828d618a19779a84c3ad952e96e2c2311b16384e973e671aa5be1f6187"},
{file = "numpy-1.22.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a911e317e8c826ea632205e63ed8507e0dc877dcdc49744584dfc363df9ca08c"}, {file = "numpy-1.23.0-cp310-cp310-win32.whl", hash = "sha256:2b2da66582f3a69c8ce25ed7921dcd8010d05e59ac8d89d126a299be60421171"},
{file = "numpy-1.22.4-cp310-cp310-win32.whl", hash = "sha256:9ce7df0abeabe7fbd8ccbf343dc0db72f68549856b863ae3dd580255d009648e"}, {file = "numpy-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:97a76604d9b0e79f59baeca16593c711fddb44936e40310f78bfef79ee9a835f"},
{file = "numpy-1.22.4-cp310-cp310-win_amd64.whl", hash = "sha256:3e1ffa4748168e1cc8d3cde93f006fe92b5421396221a02f2274aab6ac83b077"}, {file = "numpy-1.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d8cc87bed09de55477dba9da370c1679bd534df9baa171dd01accbb09687dac3"},
{file = "numpy-1.22.4-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:59d55e634968b8f77d3fd674a3cf0b96e85147cd6556ec64ade018f27e9479e1"}, {file = "numpy-1.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f0f18804df7370571fb65db9b98bf1378172bd4e962482b857e612d1fec0f53e"},
{file = "numpy-1.22.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c1d937820db6e43bec43e8d016b9b3165dcb42892ea9f106c70fb13d430ffe72"}, {file = "numpy-1.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac86f407873b952679f5f9e6c0612687e51547af0e14ddea1eedfcb22466babd"},
{file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4c5d5eb2ec8da0b4f50c9a843393971f31f1d60be87e0fb0917a49133d257d6"}, {file = "numpy-1.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae8adff4172692ce56233db04b7ce5792186f179c415c37d539c25de7298d25d"},
{file = "numpy-1.22.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64f56fc53a2d18b1924abd15745e30d82a5782b2cab3429aceecc6875bd5add0"}, {file = "numpy-1.23.0-cp38-cp38-win32.whl", hash = "sha256:fe8b9683eb26d2c4d5db32cd29b38fdcf8381324ab48313b5b69088e0e355379"},
{file = "numpy-1.22.4-cp38-cp38-win32.whl", hash = "sha256:fb7a980c81dd932381f8228a426df8aeb70d59bbcda2af075b627bbc50207cba"}, {file = "numpy-1.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:5043bcd71fcc458dfb8a0fc5509bbc979da0131b9d08e3d5f50fb0bbb36f169a"},
{file = "numpy-1.22.4-cp38-cp38-win_amd64.whl", hash = "sha256:e96d7f3096a36c8754207ab89d4b3282ba7b49ea140e4973591852c77d09eb76"}, {file = "numpy-1.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c29b44905af288b3919803aceb6ec7fec77406d8b08aaa2e8b9e63d0fe2f160"},
{file = "numpy-1.22.4-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:4c6036521f11a731ce0648f10c18ae66d7143865f19f7299943c985cdc95afb5"}, {file = "numpy-1.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98e8e0d8d69ff4d3fa63e6c61e8cfe2d03c29b16b58dbef1f9baa175bbed7860"},
{file = "numpy-1.22.4-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:b89bf9b94b3d624e7bb480344e91f68c1c6c75f026ed6755955117de00917a7c"}, {file = "numpy-1.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a506cacf2be3a74ead5467aee97b81fca00c9c4c8b3ba16dbab488cd99ba10"},
{file = "numpy-1.22.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2d487e06ecbf1dc2f18e7efce82ded4f705f4bd0cd02677ffccfb39e5c284c7e"}, {file = "numpy-1.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:092f5e6025813e64ad6d1b52b519165d08c730d099c114a9247c9bb635a2a450"},
{file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3eb268dbd5cfaffd9448113539e44e2dd1c5ca9ce25576f7c04a5453edc26fa"}, {file = "numpy-1.23.0-cp39-cp39-win32.whl", hash = "sha256:d6ca8dabe696c2785d0c8c9b0d8a9b6e5fdbe4f922bde70d57fa1a2848134f95"},
{file = "numpy-1.22.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37431a77ceb9307c28382c9773da9f306435135fae6b80b62a11c53cfedd8802"}, {file = "numpy-1.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc431493df245f3c627c0c05c2bd134535e7929dbe2e602b80e42bf52ff760bc"},
{file = "numpy-1.22.4-cp39-cp39-win32.whl", hash = "sha256:cc7f00008eb7d3f2489fca6f334ec19ca63e31371be28fd5dad955b16ec285bd"}, {file = "numpy-1.23.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f9c3fc2adf67762c9fe1849c859942d23f8d3e0bee7b5ed3d4a9c3eeb50a2f07"},
{file = "numpy-1.22.4-cp39-cp39-win_amd64.whl", hash = "sha256:f0725df166cf4785c0bc4cbfb320203182b1ecd30fee6e541c8752a92df6aa32"}, {file = "numpy-1.23.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0d2094e8f4d760500394d77b383a1b06d3663e8892cdf5df3c592f55f3bff66"},
{file = "numpy-1.22.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0791fbd1e43bf74b3502133207e378901272f3c156c4df4954cad833b1380207"}, {file = "numpy-1.23.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:94b170b4fa0168cd6be4becf37cb5b127bd12a795123984385b8cd4aca9857e5"},
{file = "numpy-1.22.4.zip", hash = "sha256:425b390e4619f58d8526b3dcf656dde069133ae5c240229821f01b5f44ea07af"}, {file = "numpy-1.23.0.tar.gz", hash = "sha256:bd3fa4fe2e38533d5336e1272fc4e765cabbbde144309ccee8675509d5cd7b05"},
] ]
packaging = [ packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
@ -560,44 +574,64 @@ pefile = [
{file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"}, {file = "pefile-2022.5.30.tar.gz", hash = "sha256:a5488a3dd1fd021ce33f969780b88fe0f7eebb76eb20996d7318f307612a045b"},
] ]
pillow = [ pillow = [
{file = "Pillow-9.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:42dfefbef90eb67c10c45a73a9bc1599d4dac920f7dfcbf4ec6b80cb620757fe"}, {file = "Pillow-9.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb"},
{file = "Pillow-9.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ffde4c6fabb52891d81606411cbfaf77756e3b561b566efd270b3ed3791fde4e"}, {file = "Pillow-9.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c857532c719fb30fafabd2371ce9b7031812ff3889d75273827633bca0c4602"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:59789a7d06c742e9d13b883d5e3569188c16acb02eeed2510fd3bfdbc1bd1530"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c"},
{file = "Pillow-9.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d45dbe4b21a9679c3e8b3f7f4f42a45a7d3ddff8a4a16109dff0e1da30a35b2"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1"},
{file = "Pillow-9.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e9ed59d1b6ee837f4515b9584f3d26cf0388b742a11ecdae0d9237a94505d03a"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58"},
{file = "Pillow-9.1.1-cp310-cp310-win32.whl", hash = "sha256:b3fe2ff1e1715d4475d7e2c3e8dabd7c025f4410f79513b4ff2de3d51ce0fa9c"}, {file = "Pillow-9.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544"},
{file = "Pillow-9.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5b650dbbc0969a4e226d98a0b440c2f07a850896aed9266b6fedc0f7e7834108"}, {file = "Pillow-9.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e"},
{file = "Pillow-9.1.1-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:0b4d5ad2cd3a1f0d1df882d926b37dbb2ab6c823ae21d041b46910c8f8cd844b"}, {file = "Pillow-9.2.0-cp310-cp310-win32.whl", hash = "sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9370d6744d379f2de5d7fa95cdbd3a4d92f0b0ef29609b4b1687f16bc197063d"}, {file = "Pillow-9.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b761727ed7d593e49671d1827044b942dd2f4caae6e51bab144d4accf8244a84"}, {file = "Pillow-9.2.0-cp311-cp311-macosx_10_10_universal2.whl", hash = "sha256:408673ed75594933714482501fe97e055a42996087eeca7e5d06e33218d05aa8"},
{file = "Pillow-9.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a66fe50386162df2da701b3722781cbe90ce043e7d53c1fd6bd801bca6b48d4"}, {file = "Pillow-9.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:727dd1389bc5cb9827cbd1f9d40d2c2a1a0c9b32dd2261db522d22a604a6eec9"},
{file = "Pillow-9.1.1-cp37-cp37m-win32.whl", hash = "sha256:2b291cab8a888658d72b575a03e340509b6b050b62db1f5539dd5cd18fd50578"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004"},
{file = "Pillow-9.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1d4331aeb12f6b3791911a6da82de72257a99ad99726ed6b63f481c0184b6fb9"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0"},
{file = "Pillow-9.1.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8844217cdf66eabe39567118f229e275f0727e9195635a15e0e4b9227458daaf"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4"},
{file = "Pillow-9.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b6617221ff08fbd3b7a811950b5c3f9367f6e941b86259843eab77c8e3d2b56b"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20d514c989fa28e73a5adbddd7a171afa5824710d0ab06d4e1234195d2a2e546"}, {file = "Pillow-9.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:088df396b047477dd1bbc7de6e22f58400dae2f21310d9e2ec2933b2ef7dfa4f"}, {file = "Pillow-9.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1"},
{file = "Pillow-9.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53c27bd452e0f1bc4bfed07ceb235663a1df7c74df08e37fd6b03eb89454946a"}, {file = "Pillow-9.2.0-cp311-cp311-win32.whl", hash = "sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf"},
{file = "Pillow-9.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3f6c1716c473ebd1649663bf3b42702d0d53e27af8b64642be0dd3598c761fb1"}, {file = "Pillow-9.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c"},
{file = "Pillow-9.1.1-cp38-cp38-win32.whl", hash = "sha256:c67db410508b9de9c4694c57ed754b65a460e4812126e87f5052ecf23a011a54"}, {file = "Pillow-9.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069"},
{file = "Pillow-9.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:f054b020c4d7e9786ae0404278ea318768eb123403b18453e28e47cdb7a0a4bf"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f"},
{file = "Pillow-9.1.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:c17770a62a71718a74b7548098a74cd6880be16bcfff5f937f900ead90ca8e92"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8"},
{file = "Pillow-9.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3f6a6034140e9e17e9abc175fc7a266a6e63652028e157750bd98e804a8ed9a"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f372d0f08eff1475ef426344efe42493f71f377ec52237bf153c5713de987251"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09e67ef6e430f90caa093528bd758b0616f8165e57ed8d8ce014ae32df6a831d"}, {file = "Pillow-9.2.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59"},
{file = "Pillow-9.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66daa16952d5bf0c9d5389c5e9df562922a59bd16d77e2a276e575d32e38afd1"}, {file = "Pillow-9.2.0-cp37-cp37m-win32.whl", hash = "sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc"},
{file = "Pillow-9.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d78ca526a559fb84faaaf84da2dd4addef5edb109db8b81677c0bb1aad342601"}, {file = "Pillow-9.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d"},
{file = "Pillow-9.1.1-cp39-cp39-win32.whl", hash = "sha256:55e74faf8359ddda43fee01bffbc5bd99d96ea508d8a08c527099e84eb708f45"}, {file = "Pillow-9.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14"},
{file = "Pillow-9.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c150dbbb4a94ea4825d1e5f2c5501af7141ea95825fadd7829f9b11c97aaf6c"}, {file = "Pillow-9.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:769a7f131a2f43752455cc72f9f7a093c3ff3856bf976c5fb53a59d0ccc704f6"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:488f3383cf5159907d48d32957ac6f9ea85ccdcc296c14eca1a4e396ecc32098"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f"},
{file = "Pillow-9.1.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b525a356680022b0af53385944026d3486fc8c013638cf9900eb87c866afb4c"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6e760cf01259a1c0a50f3c845f9cad1af30577fd8b670339b1659c6d0e7a41dd"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4165205a13b16a29e1ac57efeee6be2dfd5b5408122d59ef2145bc3239fa340"}, {file = "Pillow-9.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937a54e5694684f74dcbf6e24cc453bfc5b33940216ddd8f4cd8f0f79167f765"}, {file = "Pillow-9.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76"},
{file = "Pillow-9.1.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:baf3be0b9446a4083cc0c5bb9f9c964034be5374b5bc09757be89f5d2fa247b8"}, {file = "Pillow-9.2.0-cp38-cp38-win32.whl", hash = "sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f"},
{file = "Pillow-9.1.1.tar.gz", hash = "sha256:7502539939b53d7565f3d11d87c78e7ec900d3c72945d4ee0e2f250d598309a0"}, {file = "Pillow-9.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8"},
{file = "Pillow-9.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc"},
{file = "Pillow-9.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20"},
{file = "Pillow-9.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60"},
{file = "Pillow-9.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4"},
{file = "Pillow-9.2.0-cp39-cp39-win32.whl", hash = "sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885"},
{file = "Pillow-9.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be"},
{file = "Pillow-9.2.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e"},
{file = "Pillow-9.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927"},
{file = "Pillow-9.2.0.tar.gz", hash = "sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04"},
] ]
pluggy = [ pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
@ -677,19 +711,19 @@ zipp = [
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
] ]
zxing-cpp = [ zxing-cpp = [
{file = "zxing-cpp-1.3.0.tar.gz", hash = "sha256:5f30545afad01a278fc8c17efae11d82e36f8c2caa87c89096aec5a8d69103b2"}, {file = "zxing-cpp-1.4.0.tar.gz", hash = "sha256:3d3ec36954ecbf9b0f633dab4b8cebcf0059d8a27f7a5969c4e41a308111af38"},
{file = "zxing_cpp-1.3.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e3a6e183b6c0aae9378f674f9e7714a39482595915cf15198d10b9ba8c33b25f"}, {file = "zxing_cpp-1.4.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25b11d77cf6b9f7405af3ed6bacf4a6e0756ea74dfda7040ff53e7c58f352b05"},
{file = "zxing_cpp-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88eadb723d20655caf81a6ba6ef64d74a266f57cbd782da82736c52a61a73fa5"}, {file = "zxing_cpp-1.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1f849205237d4bda462d0a4b745e72494f825e5b6b06581e05b58d34d9869aa"},
{file = "zxing_cpp-1.3.0-cp310-cp310-win32.whl", hash = "sha256:15fb165ada1730ab0d96b67eb2d9827870d9ae534686e27541f3b3add15b96d7"}, {file = "zxing_cpp-1.4.0-cp310-cp310-win32.whl", hash = "sha256:76e9777d943af3c51b6406b323b3f28cbf9e40cc65b53cf847fda08295f18e48"},
{file = "zxing_cpp-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:8dbb17a31ee1ac2c946a96e83b170ecefbc87a52b9c35b41809d9afff77d8879"}, {file = "zxing_cpp-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:234d672e34e607ffc8e06639e79c8e1bf2ddb7c249134a6836569e92a2f2dd64"},
{file = "zxing_cpp-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:31578db20ba0668e010cb62e4718cb86f47563ec5122e29a0746651ff1e13735"}, {file = "zxing_cpp-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1f66c61e43163740c59c58880c3a8c41ebd2109573c0494f255c9c96134e8c"},
{file = "zxing_cpp-1.3.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9253a3b6c8c143f3c22d172922226b10c8cc319d2554c73107fefce7e263daaa"}, {file = "zxing_cpp-1.4.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9418e1bd0775820a4933b60007b7f8a177e4ddd23692c1aaed2348fafc0a8e01"},
{file = "zxing_cpp-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:250afd201f08bd1be8fd349766e32ef184a463b616c13102b2f80a4422695957"}, {file = "zxing_cpp-1.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc1e48ddfd6692d183782f091fbf54e5e1d36d0070822b1eab14cfb580b1625"},
{file = "zxing_cpp-1.3.0-cp38-cp38-win32.whl", hash = "sha256:d2891dfba5c53b913867e7b01b8b430d801e15e54f53b3c05b9645dc824dfed3"}, {file = "zxing_cpp-1.4.0-cp38-cp38-win32.whl", hash = "sha256:4f340b6907780e8eb0e6473fec43ea145c4dd3275e3c21d6f887c0e28e114f29"},
{file = "zxing_cpp-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:6201e60cbefbc8de90c5f18e6e25c3cb1be19be8f369bf4dad3ab910b954f29d"}, {file = "zxing_cpp-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:71772f81c4602133b2dba6a1107339ed965725001ce9a4caaf772598110351a1"},
{file = "zxing_cpp-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:44467984c1a65a332c8656926f30af1752c1ff774c6a030b95572e0a1543b23b"}, {file = "zxing_cpp-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:331bec6b0ac8a9b339bc82956c52c022e7b2debfeb9102209483eb7538ed72d4"},
{file = "zxing_cpp-1.3.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0dbb54f8694063376d73be6f7dbddd39f3e7907ab885403d90cff7d518c54f7f"}, {file = "zxing_cpp-1.4.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b0844c6ad3c944452c980a025238ba3fbd3a414fd2c36e2bec1bc5bed03b21e"},
{file = "zxing_cpp-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3cff8a7fe960c2016bc8e217fcf02b9b1ac61b17fc5c0c5158f853088be4ad9"}, {file = "zxing_cpp-1.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a770aff618cd00dda3922de2f7085c1f84bbe02f2b6df114d19054ad41c52fb0"},
{file = "zxing_cpp-1.3.0-cp39-cp39-win32.whl", hash = "sha256:f75431cf7cddcb21c267d39a5895831a3c20abfa7676426974652d25b29ae429"}, {file = "zxing_cpp-1.4.0-cp39-cp39-win32.whl", hash = "sha256:ebe67de6a4d3c48a5ee52211ecf2003301ab39bd7d7b7dfa72ae80be429cfcf9"},
{file = "zxing_cpp-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:de9dd0a2d01969e9828c5704d709b2559a417fea562bd2f308ebc8d4a9678b5e"}, {file = "zxing_cpp-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:d0e8b54b29497ed9238f31ce522ddb0189c0d6c4597787ef2eb823ca9fb42350"},
] ]

View File

@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/android/init.dart' as android; import 'package:yubico_authenticator/android/init.dart' as android;
import 'package:yubico_authenticator/app/views/no_device_screen.dart'; import 'package:yubico_authenticator/app/views/device_error_screen.dart';
import 'package:yubico_authenticator/core/state.dart'; import 'package:yubico_authenticator/core/state.dart';
import 'package:yubico_authenticator/desktop/init.dart' as desktop; import 'package:yubico_authenticator/desktop/init.dart' as desktop;
import 'package:yubico_authenticator/oath/views/account_list.dart'; import 'package:yubico_authenticator/oath/views/account_list.dart';
@ -55,7 +55,7 @@ void main() {
await tester.pumpWidget(initializedApp); await tester.pumpWidget(initializedApp);
await tester.pump(const Duration(milliseconds: 500)); await tester.pump(const Duration(milliseconds: 500));
expect(find.byType(NoDeviceScreen), findsNothing, expect(find.byType(DeviceErrorScreen), findsNothing,
reason: 'No YubiKey connected'); reason: 'No YubiKey connected');
expect(find.byType(OathScreen), findsOneWidget); expect(find.byType(OathScreen), findsOneWidget);

View File

@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v3.0.3), do not edit directly. // Autogenerated from Pigeon (v3.1.6), do not edit directly.
// See also: https://pub.dev/packages/pigeon // See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
// @dart = 2.12 // @dart = 2.12

View File

@ -12,11 +12,11 @@ import '../management/models.dart';
final _log = Logger('yubikeyDataCommandProvider'); final _log = Logger('yubikeyDataCommandProvider');
final androidYubikeyProvider = final androidYubikeyProvider =
StateNotifierProvider<_YubikeyProvider, YubiKeyData?>((ref) { StateNotifierProvider<_YubikeyProvider, AsyncValue<YubiKeyData>>((ref) {
return _YubikeyProvider(null, ref); return _YubikeyProvider(const AsyncValue.loading(), ref);
}); });
class _YubikeyProvider extends StateNotifier<YubiKeyData?> { class _YubikeyProvider extends StateNotifier<AsyncValue<YubiKeyData>> {
final Ref _ref; final Ref _ref;
_YubikeyProvider(super.yubiKeyData, this._ref); _YubikeyProvider(super.yubiKeyData, this._ref);
@ -24,7 +24,7 @@ class _YubikeyProvider extends StateNotifier<YubiKeyData?> {
try { try {
if (input.isEmpty) { if (input.isEmpty) {
_log.debug('Yubikey was detached.'); _log.debug('Yubikey was detached.');
state = null; state = const AsyncValue.loading();
// reset other providers when YubiKey is removed // reset other providers when YubiKey is removed
_ref.refresh(androidStateProvider); _ref.refresh(androidStateProvider);
@ -49,15 +49,17 @@ class _YubikeyProvider extends StateNotifier<YubiKeyData?> {
// reset oath providers on key change // reset oath providers on key change
var yubiKeyData = YubiKeyData(deviceNode, name, deviceInfo); var yubiKeyData = YubiKeyData(deviceNode, name, deviceInfo);
if (state != yubiKeyData && state != null) { state.whenData((data) {
_ref.refresh(androidStateProvider); if (data != yubiKeyData) {
_ref.refresh(androidCredentialsProvider); _ref.refresh(androidStateProvider);
} _ref.refresh(androidCredentialsProvider);
}
});
state = yubiKeyData; state = AsyncValue.data(yubiKeyData);
} on Exception catch (e) { } on Exception catch (e) {
_log.debug('Invalid data for yubikey: $input. $e'); _log.debug('Invalid data for yubikey: $input. $e');
state = null; state = AsyncValue.error(e);
} }
} }
} }

View File

@ -35,18 +35,17 @@ class _AndroidSubPageNotifier extends CurrentAppNotifier {
final androidAttachedDevicesProvider = final androidAttachedDevicesProvider =
StateNotifierProvider<AttachedDevicesNotifier, List<DeviceNode>>((ref) { StateNotifierProvider<AttachedDevicesNotifier, List<DeviceNode>>((ref) {
var currentDeviceData = ref.watch(androidDeviceDataProvider); var currentDeviceData = ref.watch(androidDeviceDataProvider);
if (currentDeviceData != null) { List<DeviceNode> devs = currentDeviceData.maybeWhen(
return _AndroidAttachedDevicesNotifier([currentDeviceData.node]); data: (data) => [data.node], orElse: () => []);
} return _AndroidAttachedDevicesNotifier(devs);
return _AndroidAttachedDevicesNotifier([]);
}); });
class _AndroidAttachedDevicesNotifier extends AttachedDevicesNotifier { class _AndroidAttachedDevicesNotifier extends AttachedDevicesNotifier {
_AndroidAttachedDevicesNotifier(super.state); _AndroidAttachedDevicesNotifier(super.state);
} }
final androidDeviceDataProvider = final androidDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
Provider<YubiKeyData?>((ref) => ref.watch(androidYubikeyProvider)); (ref) => ref.watch(androidYubikeyProvider));
final androidCurrentDeviceProvider = final androidCurrentDeviceProvider =
StateNotifierProvider<CurrentDeviceNotifier, DeviceNode?>((ref) { StateNotifierProvider<CurrentDeviceNotifier, DeviceNode?>((ref) {

View File

@ -1,60 +1,67 @@
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../widgets/toast.dart';
import 'models.dart'; import 'models.dart';
import 'state.dart';
ScaffoldFeatureController showMessage( void Function() showMessage(
BuildContext context, BuildContext context,
String message, { String message, {
Duration duration = const Duration(seconds: 1), Duration duration = const Duration(seconds: 2),
}) { }) =>
final width = MediaQuery.of(context).size.width; showToast(context, message, duration: duration);
final narrow = width < 540;
return ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(message),
duration: duration,
behavior: narrow ? SnackBarBehavior.fixed : SnackBarBehavior.floating,
width: narrow ? null : 400,
));
}
Future<void> showBottomMenu( Future<void> showBottomMenu(
BuildContext context, List<MenuAction> actions) async { BuildContext context, List<MenuAction> actions) async {
MediaQuery? mediaQuery = context.findAncestorWidgetOfExactType<MediaQuery>(); await showBlurDialog(
var width = mediaQuery?.data.size.width ?? 0;
await showModalBottomSheet(
context: context, context: context,
constraints: width > 540 ? const BoxConstraints(maxWidth: 380) : null, builder: (context) {
builder: (context) => SafeArea(child: _BottomMenu(actions))); return AlertDialog(
title: const Text('Options'),
contentPadding: const EdgeInsets.only(bottom: 24, top: 4),
content: Column(
mainAxisSize: MainAxisSize.min,
children: actions
.map((a) => ListTile(
leading: a.icon,
title: Text(a.text),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
enabled: a.action != null,
onTap: a.action == null
? null
: () {
Navigator.pop(context);
a.action?.call(context);
},
))
.toList(),
),
);
});
} }
class _BottomMenu extends ConsumerWidget { Future<T?> showBlurDialog<T>({
final List<MenuAction> actions; required BuildContext context,
const _BottomMenu(this.actions); required Widget Function(BuildContext) builder,
RouteSettings? routeSettings,
@override }) =>
Widget build(BuildContext context, WidgetRef ref) { showGeneralDialog(
// If current device changes, we need to pop back to the main Page. context: context,
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) { barrierDismissible: true,
Navigator.of(context).pop(); barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
}); barrierColor: Colors.black12,
pageBuilder: (ctx, anim1, anim2) => builder(ctx),
return Column( transitionDuration: const Duration(milliseconds: 150),
mainAxisSize: MainAxisSize.min, transitionBuilder: (ctx, anim1, anim2, child) => BackdropFilter(
children: actions filter: ImageFilter.blur(
.map((a) => ListTile( sigmaX: 20 * anim1.value, sigmaY: 20 * anim1.value),
leading: a.icon, child: FadeTransition(
title: Text(a.text), opacity: anim1,
enabled: a.action != null, child: child,
onTap: a.action == null ),
? null ),
: () { routeSettings: routeSettings,
Navigator.pop(context);
a.action?.call(context);
},
))
.toList(),
); );
}
}

View File

@ -175,11 +175,11 @@ abstract class _YubiKeyData implements YubiKeyData {
_$_YubiKeyData; _$_YubiKeyData;
@override @override
DeviceNode get node => throw _privateConstructorUsedError; DeviceNode get node;
@override @override
String get name => throw _privateConstructorUsedError; String get name;
@override @override
DeviceInfo get info => throw _privateConstructorUsedError; DeviceInfo get info;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_YubiKeyDataCopyWith<_$_YubiKeyData> get copyWith => _$$_YubiKeyDataCopyWith<_$_YubiKeyData> get copyWith =>
@ -454,11 +454,11 @@ abstract class UsbYubiKeyNode extends DeviceNode {
UsbYubiKeyNode._() : super._(); UsbYubiKeyNode._() : super._();
@override @override
DevicePath get path => throw _privateConstructorUsedError; DevicePath get path;
@override @override
String get name => throw _privateConstructorUsedError; String get name;
UsbPid get pid => throw _privateConstructorUsedError; UsbPid get pid;
DeviceInfo? get info => throw _privateConstructorUsedError; DeviceInfo? get info;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$UsbYubiKeyNodeCopyWith<_$UsbYubiKeyNode> get copyWith => _$$UsbYubiKeyNodeCopyWith<_$UsbYubiKeyNode> get copyWith =>
@ -613,9 +613,9 @@ abstract class NfcReaderNode extends DeviceNode {
NfcReaderNode._() : super._(); NfcReaderNode._() : super._();
@override @override
DevicePath get path => throw _privateConstructorUsedError; DevicePath get path;
@override @override
String get name => throw _privateConstructorUsedError; String get name;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$NfcReaderNodeCopyWith<_$NfcReaderNode> get copyWith => _$$NfcReaderNodeCopyWith<_$NfcReaderNode> get copyWith =>
@ -762,11 +762,11 @@ abstract class _MenuAction implements MenuAction {
final void Function(BuildContext)? action}) = _$_MenuAction; final void Function(BuildContext)? action}) = _$_MenuAction;
@override @override
String get text => throw _privateConstructorUsedError; String get text;
@override @override
Widget get icon => throw _privateConstructorUsedError; Widget get icon;
@override @override
void Function(BuildContext)? get action => throw _privateConstructorUsedError; void Function(BuildContext)? get action;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_MenuActionCopyWith<_$_MenuAction> get copyWith => _$$_MenuActionCopyWith<_$_MenuAction> get copyWith =>
@ -914,11 +914,11 @@ abstract class _WindowState implements WindowState {
required final bool active}) = _$_WindowState; required final bool active}) = _$_WindowState;
@override @override
bool get focused => throw _privateConstructorUsedError; bool get focused;
@override @override
bool get visible => throw _privateConstructorUsedError; bool get visible;
@override @override
bool get active => throw _privateConstructorUsedError; bool get active;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_WindowStateCopyWith<_$_WindowState> get copyWith => _$$_WindowStateCopyWith<_$_WindowState> get copyWith =>

View File

@ -60,7 +60,7 @@ class AttachedDevicesNotifier extends StateNotifier<List<DeviceNode>> {
} }
// Override with platform implementation // Override with platform implementation
final currentDeviceDataProvider = Provider<YubiKeyData?>( final currentDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
(ref) => throw UnimplementedError(), (ref) => throw UnimplementedError(),
); );
@ -77,8 +77,8 @@ abstract class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
final currentAppProvider = final currentAppProvider =
StateNotifierProvider<CurrentAppNotifier, Application>((ref) { StateNotifierProvider<CurrentAppNotifier, Application>((ref) {
final notifier = CurrentAppNotifier(ref.watch(supportedAppsProvider)); final notifier = CurrentAppNotifier(ref.watch(supportedAppsProvider));
ref.listen<YubiKeyData?>(currentDeviceDataProvider, (_, data) { ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, data) {
notifier._notifyDeviceChanged(data); notifier._notifyDeviceChanged(data.whenOrNull(data: ((data) => data)));
}, fireImmediately: true); }, fireImmediately: true);
return notifier; return notifier;
}); });

View File

@ -49,7 +49,7 @@ class AppFailurePage extends ConsumerWidget {
icon: const Icon(Icons.lock_open), icon: const Icon(Icons.lock_open),
style: AppTheme.primaryOutlinedButtonStyle(context), style: AppTheme.primaryOutlinedButtonStyle(context),
onPressed: () async { onPressed: () async {
final controller = showMessage( final closeMessage = showMessage(
context, 'Elevating permissions...', context, 'Elevating permissions...',
duration: const Duration(seconds: 30)); duration: const Duration(seconds: 30));
try { try {
@ -59,7 +59,7 @@ class AppFailurePage extends ConsumerWidget {
showMessage(context, 'Permission denied'); showMessage(context, 'Permission denied');
} }
} finally { } finally {
controller.close(); closeMessage();
} }
}), }),
]; ];

View File

@ -9,12 +9,14 @@ class AppPage extends ConsumerWidget {
final Widget? title; final Widget? title;
final Widget child; final Widget child;
final List<Widget> actions; final List<Widget> actions;
final List<PopupMenuEntry> keyActions;
final bool centered; final bool centered;
AppPage({ AppPage({
super.key, super.key,
this.title, this.title,
required this.child, required this.child,
this.actions = const [], this.actions = const [],
this.keyActions = const [],
this.centered = false, this.centered = false,
}); });
@ -82,10 +84,10 @@ class AppPage extends ConsumerWidget {
title: title, title: title,
centerTitle: true, centerTitle: true,
titleTextStyle: Theme.of(context).textTheme.titleLarge, titleTextStyle: Theme.of(context).textTheme.titleLarge,
actions: const [ actions: [
Padding( Padding(
padding: EdgeInsets.only(right: 6), padding: const EdgeInsets.only(right: 6),
child: DeviceButton(), child: DeviceButton(actions: keyActions),
), ),
], ],
), ),

View File

@ -1,75 +1,80 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../widgets/custom_icons.dart'; import '../../widgets/custom_icons.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart';
import 'device_images.dart'; import 'device_images.dart';
class DeviceAvatar extends StatelessWidget { class DeviceAvatar extends StatelessWidget {
final bool selected;
final Widget child; final Widget child;
final Widget? badge; final Widget? badge;
final double? radius; final double? radius;
const DeviceAvatar( const DeviceAvatar({super.key, required this.child, this.badge, this.radius});
{super.key,
this.selected = false,
required this.child,
this.badge,
this.radius});
factory DeviceAvatar.yubiKeyData(YubiKeyData data, factory DeviceAvatar.yubiKeyData(YubiKeyData data, {double? radius}) =>
{bool selected = false, double? radius}) =>
DeviceAvatar( DeviceAvatar(
badge: data.node is NfcReaderNode ? nfcIcon : null, badge: data.node is NfcReaderNode ? nfcIcon : null,
selected: selected,
radius: radius, radius: radius,
child: getProductImage(data.info, data.name), child: getProductImage(data.info, data.name),
); );
factory DeviceAvatar.deviceNode(DeviceNode node, factory DeviceAvatar.deviceNode(DeviceNode node, {double? radius}) =>
{bool selected = false, double? radius}) =>
node.map( node.map(
usbYubiKey: (node) { usbYubiKey: (node) {
final info = node.info; final info = node.info;
if (info != null) { if (info != null) {
return DeviceAvatar.yubiKeyData( return DeviceAvatar.yubiKeyData(
YubiKeyData(node, node.name, info), YubiKeyData(node, node.name, info),
selected: selected,
radius: radius, radius: radius,
); );
} }
return DeviceAvatar( return DeviceAvatar(
selected: selected,
radius: radius, radius: radius,
child: const Icon(Icons.device_unknown), child: const Icon(Icons.device_unknown),
); );
}, },
nfcReader: (_) => DeviceAvatar( nfcReader: (_) => DeviceAvatar(
selected: selected,
radius: radius, radius: radius,
child: nfcIcon, child: nfcIcon,
), ),
); );
factory DeviceAvatar.currentDevice(WidgetRef ref, {double? radius}) {
final deviceNode = ref.watch(currentDeviceProvider);
if (deviceNode != null) {
return ref.watch(currentDeviceDataProvider).maybeWhen(
data: (data) => DeviceAvatar.yubiKeyData(
data,
radius: radius,
),
orElse: () => DeviceAvatar.deviceNode(
deviceNode,
radius: radius,
),
);
} else {
return DeviceAvatar(
radius: radius,
child: const Icon(Icons.usb),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final radius = this.radius ?? 24; final radius = this.radius ?? 20;
return Stack( return Stack(
alignment: AlignmentDirectional.bottomEnd, alignment: AlignmentDirectional.bottomEnd,
children: [ children: [
CircleAvatar( CircleAvatar(
radius: radius, radius: radius,
backgroundColor: selected backgroundColor: Theme.of(context).colorScheme.background,
? Theme.of(context).colorScheme.primary child: IconTheme(
: Colors.transparent, data: IconTheme.of(context).copyWith(
child: CircleAvatar( size: radius,
radius: radius - 1,
backgroundColor: Theme.of(context).colorScheme.background,
child: IconTheme(
data: IconTheme.of(context).copyWith(
size: radius,
),
child: child,
), ),
child: child,
), ),
), ),
if (badge != null) if (badge != null)

View File

@ -1,57 +1,127 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../message.dart';
import '../state.dart'; import '../state.dart';
import 'device_avatar.dart'; import 'device_avatar.dart';
import 'device_picker_dialog.dart'; import 'device_picker_dialog.dart';
import 'device_utils.dart';
class _CircledDeviceAvatar extends ConsumerWidget {
final double radius;
const _CircledDeviceAvatar(this.radius);
@override
Widget build(BuildContext context, WidgetRef ref) => CircleAvatar(
radius: radius,
backgroundColor: Theme.of(context).colorScheme.primary,
child: IconTheme(
// Force the standard icon theme
data: IconTheme.of(context),
child: DeviceAvatar.currentDevice(ref, radius: radius - 1),
),
);
}
class DeviceButton extends ConsumerWidget { class DeviceButton extends ConsumerWidget {
final double radius; final double radius;
const DeviceButton({super.key, this.radius = 16}); final List<PopupMenuEntry> actions;
const DeviceButton({super.key, this.actions = const [], this.radius = 16});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final deviceNode = ref.watch(currentDeviceProvider);
final deviceData = ref.watch(currentDeviceDataProvider);
Widget deviceWidget;
if (deviceNode != null) {
if (deviceData != null) {
deviceWidget = DeviceAvatar.yubiKeyData(
deviceData,
selected: true,
radius: radius,
);
} else {
deviceWidget = DeviceAvatar.deviceNode(
deviceNode,
selected: true,
radius: radius,
);
}
} else {
deviceWidget = DeviceAvatar(
radius: radius,
selected: true,
child: const Icon(Icons.usb),
);
}
return Padding( return Padding(
padding: const EdgeInsets.only(right: 8.0), padding: const EdgeInsets.only(right: 8.0),
child: IconButton( child: IconButton(
tooltip: 'Select YubiKey or device', tooltip: 'More actions',
icon: OverflowBox( icon: OverflowBox(
maxHeight: 44, maxHeight: 44,
maxWidth: 44, maxWidth: 44,
child: deviceWidget, child: _CircledDeviceAvatar(radius),
), ),
onPressed: () { onPressed: () {
showDialog( final withContext = ref.read(withContextProvider);
showMenu(
context: context, context: context,
builder: (context) => const DevicePickerDialog(), position: const RelativeRect.fromLTRB(100, 0, 0, 0),
routeSettings: const RouteSettings(name: 'device_picker'), items: <PopupMenuEntry>[
PopupMenuItem(
padding: const EdgeInsets.only(left: 11, right: 16),
onTap: () {
// Wait for menu to close, and use the main context to open
Timer.run(() {
withContext(
(context) async {
await showBlurDialog(
context: context,
builder: (context) => const DevicePickerDialog(),
routeSettings:
const RouteSettings(name: 'device_picker'),
);
},
);
});
},
child: _SlideInWidget(radius: radius),
),
if (actions.isNotEmpty) const PopupMenuDivider(),
...actions,
],
); );
}, },
), ),
); );
} }
} }
class _SlideInWidget extends ConsumerStatefulWidget {
final double radius;
const _SlideInWidget({required this.radius});
@override
ConsumerState<_SlideInWidget> createState() => _SlideInWidgetState();
}
class _SlideInWidgetState extends ConsumerState<_SlideInWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
)..forward();
late final Animation<Offset> _offsetAnimation = Tween<Offset>(
begin: const Offset(0.9, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final messages = getDeviceMessages(
ref.watch(currentDeviceProvider),
ref.watch(currentDeviceDataProvider),
);
return SlideTransition(
position: _offsetAnimation,
child: ListTile(
dense: true,
contentPadding: EdgeInsets.zero,
minLeadingWidth: 0,
horizontalTitleGap: 13,
leading: _CircledDeviceAvatar(widget.radius),
title: Text(messages.removeAt(0)),
subtitle: Text(messages.first),
),
);
}
}

View File

@ -12,9 +12,10 @@ import 'device_avatar.dart';
import 'graphics.dart'; import 'graphics.dart';
import 'message_page.dart'; import 'message_page.dart';
class NoDeviceScreen extends ConsumerWidget { class DeviceErrorScreen extends ConsumerWidget {
final DeviceNode? node; final DeviceNode node;
const NoDeviceScreen(this.node, {super.key}); final Object? error;
const DeviceErrorScreen(this.node, {this.error, super.key});
Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) { Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
if (pid.usbInterfaces == UsbInterface.fido.value) { if (pid.usbInterfaces == UsbInterface.fido.value) {
@ -29,7 +30,7 @@ class NoDeviceScreen extends ConsumerWidget {
label: const Text('Unlock'), label: const Text('Unlock'),
icon: const Icon(Icons.lock_open), icon: const Icon(Icons.lock_open),
onPressed: () async { onPressed: () async {
final controller = showMessage( final closeMessage = showMessage(
context, 'Elevating permissions...', context, 'Elevating permissions...',
duration: const Duration(seconds: 30)); duration: const Duration(seconds: 30));
try { try {
@ -39,7 +40,7 @@ class NoDeviceScreen extends ConsumerWidget {
showMessage(context, 'Permission denied'); showMessage(context, 'Permission denied');
} }
} finally { } finally {
controller.close(); closeMessage();
} }
}, },
), ),
@ -55,12 +56,19 @@ class NoDeviceScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return node?.map( return node.map(
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid), usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
nfcReader: (_) => const MessagePage( nfcReader: (node) {
message: 'Place your YubiKey on the NFC reader', final String message;
), switch (error) {
) ?? case 'unknown-device':
const MessagePage(message: 'Insert your YubiKey'); message = 'Unrecognized device';
break;
default:
message = 'Place your YubiKey on the NFC reader';
}
return MessagePage(message: message);
},
);
} }
} }

View File

@ -2,28 +2,84 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../core/state.dart';
import '../../management/models.dart'; import '../../management/models.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'device_avatar.dart'; import 'device_avatar.dart';
import 'device_utils.dart';
String _getSubtitle(DeviceInfo info) { final _hiddenDevicesProvider =
final serial = info.serial; StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
var subtitle = ''; (ref) => _HiddenDevicesNotifier(ref.watch(prefProvider)));
if (serial != null) {
subtitle += 'S/N: $serial '; class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
static const String _key = 'DEVICE_PICKER_HIDDEN';
final SharedPreferences _prefs;
_HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
void showAll() {
state = [];
_prefs.setStringList(_key, state);
}
void hideDevice(DevicePath devicePath) {
state = [...state, devicePath.key];
_prefs.setStringList(_key, state);
} }
subtitle += 'F/W: ${info.version}';
return subtitle;
} }
class DevicePickerDialog extends ConsumerWidget { class DevicePickerDialog extends StatefulWidget {
const DevicePickerDialog({super.key}); const DevicePickerDialog({super.key});
@override
State<StatefulWidget> createState() => _DevicePickerDialogState();
}
class _DevicePickerDialogState extends State<DevicePickerDialog> {
late FocusScopeNode _focus;
@override
void initState() {
super.initState();
_focus = FocusScopeNode();
}
@override
void dispose() {
_focus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// This keeps the focus in the dialog, even if the underlying page
// changes as it does when a new device is selected.
return FocusScope(
node: _focus,
autofocus: true,
onFocusChange: (focused) {
if (!focused) {
_focus.requestFocus();
}
},
child: const _DevicePickerContent(),
);
}
}
class _DevicePickerContent extends ConsumerWidget {
const _DevicePickerContent();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final devices = ref.watch(attachedDevicesProvider).toList(); final hidden = ref.watch(_hiddenDevicesProvider);
final devices = ref
.watch(attachedDevicesProvider)
.where((e) => !hidden.contains(e.path.key))
.toList();
final currentNode = ref.watch(currentDeviceProvider); final currentNode = ref.watch(currentDeviceProvider);
final Widget hero; final Widget hero;
@ -33,24 +89,27 @@ class DevicePickerDialog extends ConsumerWidget {
devices.removeWhere((e) => e.path == currentNode.path); devices.removeWhere((e) => e.path == currentNode.path);
hero = _CurrentDeviceRow( hero = _CurrentDeviceRow(
currentNode, currentNode,
data: ref.watch(currentDeviceDataProvider), ref.watch(currentDeviceDataProvider),
onTap: () {
Navigator.of(context).pop();
},
); );
} else { } else {
hero = ListTile( hero = Column(
leading: DeviceAvatar( children: [
selected: true, _HeroAvatar(
child: Icon(Platform.isAndroid ? Icons.no_cell : Icons.usb), child: DeviceAvatar(
), radius: 64,
title: Text(Platform.isAndroid ? 'No YubiKey' : 'USB'), child: Icon(Platform.isAndroid ? Icons.no_cell : Icons.usb),
subtitle: Text(Platform.isAndroid ),
? 'Insert or tap a YubiKey' ),
: 'Insert a YubiKey'), ListTile(
onTap: () { title:
Navigator.of(context).pop(); Center(child: Text(Platform.isAndroid ? 'No YubiKey' : 'USB')),
}, subtitle: Center(
child: Text(Platform.isAndroid
? 'Insert or tap a YubiKey'
: 'Insert a YubiKey'),
),
),
],
); );
showUsb = false; showUsb = false;
} }
@ -60,136 +119,194 @@ class DevicePickerDialog extends ConsumerWidget {
ListTile( ListTile(
leading: const Padding( leading: const Padding(
padding: EdgeInsets.symmetric(horizontal: 4), padding: EdgeInsets.symmetric(horizontal: 4),
child: DeviceAvatar( child: DeviceAvatar(child: Icon(Icons.usb)),
radius: 20,
child: Icon(Icons.usb),
),
), ),
title: const Text('USB'), title: const Text('USB'),
subtitle: const Text('No YubiKey present'), subtitle: const Text('No YubiKey present'),
onTap: () { onTap: () {
Navigator.of(context).pop();
ref.read(currentDeviceProvider.notifier).setCurrentDevice(null); ref.read(currentDeviceProvider.notifier).setCurrentDevice(null);
}, },
), ),
...devices.map( ...devices.map(
(e) => _DeviceRow( (e) => e.map(
e, usbYubiKey: (node) => _DeviceRow(node, info: node.info),
info: e.map( nfcReader: (node) => _NfcDeviceRow(node),
usbYubiKey: (node) => node.info,
nfcReader: (_) => null,
),
onTap: () {
Navigator.of(context).pop();
ref.read(currentDeviceProvider.notifier).setCurrentDevice(e);
},
), ),
), ),
]; ];
return SimpleDialog( return GestureDetector(
children: [ onSecondaryTapDown: hidden.isEmpty
hero, ? null
if (others.isNotEmpty) const Divider(), : (details) {
...others, showMenu(
], context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: const ListTile(
title: Text('Show hidden devices'),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: SimpleDialog(
children: [
hero,
if (others.isNotEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Divider(),
),
...others,
],
),
);
}
}
class _HeroAvatar extends StatelessWidget {
final Widget child;
const _HeroAvatar({required this.child});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
theme.colorScheme.background,
theme.colorScheme.background.withOpacity(0.4),
(theme.dialogTheme.backgroundColor ?? theme.dialogBackgroundColor)
.withOpacity(0),
],
),
),
padding: const EdgeInsets.all(12),
child: Theme(
// Give the avatar a transparent background
data: theme.copyWith(
colorScheme:
theme.colorScheme.copyWith(background: Colors.transparent)),
child: child,
),
); );
} }
} }
class _CurrentDeviceRow extends StatelessWidget { class _CurrentDeviceRow extends StatelessWidget {
final DeviceNode node; final DeviceNode node;
final YubiKeyData? data; final AsyncValue<YubiKeyData> data;
final Function() onTap;
const _CurrentDeviceRow( const _CurrentDeviceRow(this.node, this.data);
this.node, {
this.data,
required this.onTap,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return node.when(usbYubiKey: (path, name, pid, info) { final hero = data.maybeWhen(
if (info != null) { data: (data) => DeviceAvatar.yubiKeyData(data, radius: 64),
return ListTile( orElse: () => DeviceAvatar.deviceNode(node, radius: 64),
leading: DeviceAvatar.yubiKeyData( );
data!, final messages = getDeviceMessages(node, data);
selected: true,
), return Column(
title: Text(name), children: [
subtitle: Text(_getSubtitle(info)), _HeroAvatar(child: hero),
onTap: onTap, ListTile(
); title: Text(messages.removeAt(0), textAlign: TextAlign.center),
} else { isThreeLine: messages.length > 1,
return ListTile( subtitle: Text(messages.join('\n'), textAlign: TextAlign.center),
leading: DeviceAvatar.deviceNode( )
node, ],
selected: true, );
),
title: Text(name),
subtitle: const Text('Device inaccessible'),
onTap: onTap,
);
}
}, nfcReader: (path, name) {
final info = data?.info;
if (info != null) {
return ListTile(
leading: DeviceAvatar.yubiKeyData(
data!,
selected: true,
),
title: Text(data!.name),
isThreeLine: true,
subtitle: Text('$name\n${_getSubtitle(info)}'),
onTap: onTap,
);
} else {
return ListTile(
leading: DeviceAvatar.deviceNode(
node,
selected: true,
),
title: const Text('No YubiKey present'),
subtitle: Text(name),
onTap: onTap,
);
}
});
} }
} }
class _DeviceRow extends StatelessWidget { class _DeviceRow extends ConsumerWidget {
final DeviceNode node; final DeviceNode node;
final DeviceInfo? info; final DeviceInfo? info;
final Function() onTap;
const _DeviceRow( const _DeviceRow(this.node, {this.info});
this.node, {
required this.info,
required this.onTap,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
return ListTile( return ListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4), padding: const EdgeInsets.symmetric(horizontal: 4),
child: DeviceAvatar.deviceNode( child: DeviceAvatar.deviceNode(node),
node,
radius: 20,
),
), ),
title: Text(node.name), title: Text(node.name),
subtitle: Text( subtitle: Text(
node.when( node.when(
usbYubiKey: (_, __, ___, info) => usbYubiKey: (_, __, ___, info) =>
info == null ? 'Device inaccessible' : _getSubtitle(info), info == null ? 'Device inaccessible' : getDeviceInfoString(info),
nfcReader: (_, __) => 'Select to scan', nfcReader: (_, __) => 'Select to scan',
), ),
), ),
onTap: onTap, onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(node);
},
);
}
}
class _NfcDeviceRow extends ConsumerWidget {
final DeviceNode node;
const _NfcDeviceRow(this.node);
@override
Widget build(BuildContext context, WidgetRef ref) {
final hidden = ref.watch(_hiddenDevicesProvider);
return GestureDetector(
onSecondaryTapDown: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
enabled: hidden.isNotEmpty,
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: ListTile(
title: const Text('Show hidden devices'),
dense: true,
contentPadding: EdgeInsets.zero,
enabled: hidden.isNotEmpty,
),
),
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path);
},
child: const ListTile(
title: Text('Hide device'),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: _DeviceRow(node),
); );
} }
} }

44
lib/app/views/device_utils.dart Executable file
View File

@ -0,0 +1,44 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../management/models.dart';
import '../models.dart';
String getDeviceInfoString(DeviceInfo info) {
final serial = info.serial;
var subtitle = '';
if (serial != null) {
subtitle += 'S/N: $serial ';
}
subtitle += 'F/W: ${info.version}';
return subtitle;
}
List<String> getDeviceMessages(DeviceNode? node, AsyncValue<YubiKeyData> data) {
if (node == null) {
return ['Insert a YubiKey', 'USB'];
}
final messages = data.whenOrNull(
data: (data) => [getDeviceInfoString(data.info)],
error: (error, _) {
switch (error) {
case 'unknown-device':
return ['Unrecognized device'];
case 'device-inaccessible':
return ['Device inacessible'];
}
return null;
},
) ??
['No YubiKey present'];
final name = data.asData?.value.name;
if (name != null) {
messages.insert(0, name);
}
if (node is NfcReaderNode) {
messages.add(node.name);
}
return messages;
}

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../management/views/management_screen.dart'; import '../../management/views/management_screen.dart';
import '../../about_page.dart'; import '../../about_page.dart';
import '../../settings_page.dart'; import '../../settings_page.dart';
import '../message.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
@ -35,7 +36,8 @@ class MainPageDrawer extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final supportedApps = ref.watch(supportedAppsProvider); final supportedApps = ref.watch(supportedAppsProvider);
final data = ref.watch(currentDeviceDataProvider); final data =
ref.watch(currentDeviceDataProvider).whenOrNull(data: (data) => data);
final currentApp = ref.watch(currentAppProvider); final currentApp = ref.watch(currentAppProvider);
MediaQuery? mediaQuery = MediaQuery? mediaQuery =
@ -76,7 +78,7 @@ class MainPageDrawer extends ConsumerWidget {
icon: Icon(Application.management._icon), icon: Icon(Application.management._icon),
onTap: () { onTap: () {
if (shouldPop) Navigator.of(context).pop(); if (shouldPop) Navigator.of(context).pop();
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) => ManagementScreen(data), builder: (context) => ManagementScreen(data),
); );
@ -92,7 +94,7 @@ class MainPageDrawer extends ConsumerWidget {
onTap: () { onTap: () {
final nav = Navigator.of(context); final nav = Navigator.of(context);
if (shouldPop) nav.pop(); if (shouldPop) nav.pop();
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) => const SettingsPage(), builder: (context) => const SettingsPage(),
routeSettings: const RouteSettings(name: 'settings'), routeSettings: const RouteSettings(name: 'settings'),
@ -105,7 +107,7 @@ class MainPageDrawer extends ConsumerWidget {
onTap: () { onTap: () {
final nav = Navigator.of(context); final nav = Navigator.of(context);
if (shouldPop) nav.pop(); if (shouldPop) nav.pop();
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) => const AboutPage(), builder: (context) => const AboutPage(),
routeSettings: const RouteSettings(name: 'about'), routeSettings: const RouteSettings(name: 'about'),

View File

@ -2,12 +2,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'message_page.dart'; import 'message_page.dart';
import 'no_device_screen.dart'; import 'device_error_screen.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import '../../fido/views/fido_screen.dart'; import '../../fido/views/fido_screen.dart';
import '../../oath/views/oath_screen.dart'; import '../../oath/views/oath_screen.dart';
import '../../management/views/management_screen.dart';
class MainPage extends ConsumerWidget { class MainPage extends ConsumerWidget {
const MainPage({super.key}); const MainPage({super.key});
@ -21,7 +20,7 @@ class MainPage extends ConsumerWidget {
}, },
); );
// If the current device changes, we need to pop any open dialogs. // If the current device changes, we need to pop any open dialogs.
ref.listen<YubiKeyData?>(currentDeviceDataProvider, (_, __) { ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, __) {
Navigator.of(context).popUntil((route) { Navigator.of(context).popUntil((route) {
return route.isFirst || return route.isFirst ||
[ [
@ -32,31 +31,36 @@ class MainPage extends ConsumerWidget {
].contains(route.settings.name); ].contains(route.settings.name);
}); });
}); });
final deviceData = ref.watch(currentDeviceDataProvider);
if (deviceData == null) {
final node = ref.watch(currentDeviceProvider);
return NoDeviceScreen(node);
}
final app = ref.watch(currentAppProvider);
if (app.getAvailability(deviceData) != Availability.enabled) {
return const MessagePage(
header: 'Application disabled',
message: 'Enable the application on your YubiKey to access',
);
}
switch (app) { final deviceNode = ref.watch(currentDeviceProvider);
case Application.oath: if (deviceNode == null) {
return OathScreen(deviceData.node.path); return const MessagePage(message: 'Insert your YubiKey');
case Application.management: } else {
return ManagementScreen(deviceData); return ref.watch(currentDeviceDataProvider).when(
case Application.fido: data: (data) {
return FidoScreen(deviceData); final app = ref.watch(currentAppProvider);
default: if (app.getAvailability(data) != Availability.enabled) {
return const MessagePage( return const MessagePage(
header: 'Not implemented', header: 'Application disabled',
message: 'This section has not yet been implemented', message: 'Enable the application on your YubiKey to access',
); );
}
switch (app) {
case Application.oath:
return OathScreen(data.node.path);
case Application.fido:
return FidoScreen(data);
default:
return const MessagePage(
header: 'Not supported',
message: 'This application is not supported',
);
}
},
loading: () => DeviceErrorScreen(deviceNode),
error: (error, _) => DeviceErrorScreen(deviceNode, error: error),
);
} }
} }
} }

View File

@ -8,6 +8,7 @@ class MessagePage extends StatelessWidget {
final String? header; final String? header;
final String? message; final String? message;
final List<Widget> actions; final List<Widget> actions;
final List<PopupMenuEntry> keyActions;
const MessagePage({ const MessagePage({
super.key, super.key,
@ -16,6 +17,7 @@ class MessagePage extends StatelessWidget {
this.header, this.header,
this.message, this.message,
this.actions = const [], this.actions = const [],
this.keyActions = const [],
}); });
@override @override
@ -23,6 +25,7 @@ class MessagePage extends StatelessWidget {
title: title, title: title,
centered: true, centered: true,
actions: actions, actions: actions,
keyActions: keyActions,
child: Padding( child: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Column( child: Column(

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../message.dart';
abstract class UserInteractionController { abstract class UserInteractionController {
void updateContent({String? title, String? description, Widget? icon}); void updateContent({String? title, String? description, Widget? icon});
void close(); void close();
@ -56,15 +58,28 @@ class _UserInteractionDialogState extends State<_UserInteractionDialog> {
Widget? icon = widget.controller.icon; Widget? icon = widget.controller.icon;
return AlertDialog( return AlertDialog(
scrollable: true, scrollable: true,
title: Text(widget.controller.title),
content: SizedBox( content: SizedBox(
width: 300, height: 160,
width: 100,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
if (icon != null) icon, if (icon != null)
Padding(
padding: const EdgeInsets.all(24),
child: IconTheme(
data: IconTheme.of(context).copyWith(size: 36),
child: icon,
),
),
Text(
widget.controller.title,
style: Theme.of(context).textTheme.headline6,
),
Text( Text(
widget.controller.description, widget.controller.description,
textAlign: TextAlign.center,
softWrap: true, softWrap: true,
), ),
], ],
@ -110,7 +125,7 @@ UserInteractionController promptUserInteraction(
} }
}, },
); );
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return WillPopScope( return WillPopScope(

View File

@ -88,7 +88,7 @@ enum UsbPid {
final suffix = UsbInterface.values final suffix = UsbInterface.values
.where((e) => e.value & usbInterfaces != 0) .where((e) => e.value & usbInterfaces != 0)
.map((e) => e.name.toUpperCase()) .map((e) => e.name.toUpperCase())
.join(' '); .join('+');
return '$prefix $suffix'; return '$prefix $suffix';
} }
} }

View File

@ -151,11 +151,11 @@ abstract class _Version extends Version {
const _Version._() : super._(); const _Version._() : super._();
@override @override
int get major => throw _privateConstructorUsedError; int get major;
@override @override
int get minor => throw _privateConstructorUsedError; int get minor;
@override @override
int get patch => throw _privateConstructorUsedError; int get patch;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_VersionCopyWith<_$_Version> get copyWith => _$$_VersionCopyWith<_$_Version> get copyWith =>
@ -284,9 +284,9 @@ abstract class _Pair<T1, T2> implements Pair<T1, T2> {
factory _Pair(final T1 first, final T2 second) = _$_Pair<T1, T2>; factory _Pair(final T1 first, final T2 second) = _$_Pair<T1, T2>;
@override @override
T1 get first => throw _privateConstructorUsedError; T1 get first;
@override @override
T2 get second => throw _privateConstructorUsedError; T2 get second;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_PairCopyWith<T1, T2, _$_Pair<T1, T2>> get copyWith => _$$_PairCopyWith<T1, T2, _$_Pair<T1, T2>> get copyWith =>

View File

@ -193,7 +193,8 @@ class _DesktopDevicesNotifier extends AttachedDevicesNotifier {
} }
final _desktopDeviceDataProvider = final _desktopDeviceDataProvider =
StateNotifierProvider<CurrentDeviceDataNotifier, YubiKeyData?>((ref) { StateNotifierProvider<CurrentDeviceDataNotifier, AsyncValue<YubiKeyData>>(
(ref) {
final notifier = CurrentDeviceDataNotifier( final notifier = CurrentDeviceDataNotifier(
ref.watch(rpcProvider), ref.watch(rpcProvider),
ref.watch(currentDeviceProvider), ref.watch(currentDeviceProvider),
@ -207,23 +208,26 @@ final _desktopDeviceDataProvider =
return notifier; return notifier;
}); });
final desktopDeviceDataProvider = Provider<YubiKeyData?>( final desktopDeviceDataProvider = Provider<AsyncValue<YubiKeyData>>(
(ref) { (ref) {
return ref.watch(_desktopDeviceDataProvider); return ref.watch(_desktopDeviceDataProvider);
}, },
); );
class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> { class CurrentDeviceDataNotifier extends StateNotifier<AsyncValue<YubiKeyData>> {
final RpcSession _rpc; final RpcSession _rpc;
final DeviceNode? _deviceNode; final DeviceNode? _deviceNode;
Timer? _pollTimer; Timer? _pollTimer;
CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(null) { CurrentDeviceDataNotifier(this._rpc, this._deviceNode)
: super(const AsyncValue.loading()) {
final dev = _deviceNode; final dev = _deviceNode;
if (dev is UsbYubiKeyNode) { if (dev is UsbYubiKeyNode) {
final info = dev.info; final info = dev.info;
if (info != null) { if (info != null) {
state = YubiKeyData(dev, dev.name, info); state = AsyncValue.data(YubiKeyData(dev, dev.name, info));
} else {
state = const AsyncValue.error('device-inaccessible');
} }
} }
} }
@ -254,10 +258,10 @@ class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
var result = await _rpc.command('get', node.path.segments); var result = await _rpc.command('get', node.path.segments);
if (mounted) { if (mounted) {
if (result['data']['present']) { if (result['data']['present']) {
state = YubiKeyData(node, result['data']['name'], state = AsyncValue.data(YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info'])); DeviceInfo.fromJson(result['data']['info'])));
} else { } else {
state = null; state = AsyncValue.error(result['data']['status']);
} }
} }
} on RpcError catch (e) { } on RpcError catch (e) {
@ -265,7 +269,7 @@ class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
} }
if (mounted) { if (mounted) {
_pollTimer = Timer( _pollTimer = Timer(
state == null ? _nfcAttachPollDelay : _nfcDetachPollDelay, state is AsyncData ? _nfcDetachPollDelay : _nfcAttachPollDelay,
_pollReader); _pollReader);
} }
} }

View File

@ -272,7 +272,7 @@ abstract class Success implements RpcResponse {
factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson; factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson;
@override @override
Map<String, dynamic> get body => throw _privateConstructorUsedError; Map<String, dynamic> get body;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$SuccessCopyWith<_$Success> get copyWith => _$$SuccessCopyWith<_$Success> get copyWith =>
@ -446,9 +446,9 @@ abstract class Signal implements RpcResponse {
factory Signal.fromJson(Map<String, dynamic> json) = _$Signal.fromJson; factory Signal.fromJson(Map<String, dynamic> json) = _$Signal.fromJson;
String get status => throw _privateConstructorUsedError; String get status;
@override @override
Map<String, dynamic> get body => throw _privateConstructorUsedError; Map<String, dynamic> get body;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$SignalCopyWith<_$Signal> get copyWith => _$$SignalCopyWith<_$Signal> get copyWith =>
@ -633,10 +633,10 @@ abstract class RpcError implements RpcResponse {
factory RpcError.fromJson(Map<String, dynamic> json) = _$RpcError.fromJson; factory RpcError.fromJson(Map<String, dynamic> json) = _$RpcError.fromJson;
String get status => throw _privateConstructorUsedError; String get status;
String get message => throw _privateConstructorUsedError; String get message;
@override @override
Map<String, dynamic> get body => throw _privateConstructorUsedError; Map<String, dynamic> get body;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$RpcErrorCopyWith<_$RpcError> get copyWith => _$$RpcErrorCopyWith<_$RpcError> get copyWith =>
@ -780,9 +780,9 @@ abstract class _RpcState implements RpcState {
factory _RpcState.fromJson(Map<String, dynamic> json) = _$_RpcState.fromJson; factory _RpcState.fromJson(Map<String, dynamic> json) = _$_RpcState.fromJson;
@override @override
String get version => throw _privateConstructorUsedError; String get version;
@override @override
bool get isAdmin => throw _privateConstructorUsedError; bool get isAdmin;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_RpcStateCopyWith<_$_RpcState> get copyWith => _$$_RpcStateCopyWith<_$_RpcState> get copyWith =>

View File

@ -2,13 +2,14 @@ import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:yubico_authenticator/app/logging.dart';
import 'package:yubico_authenticator/app/views/user_interaction.dart';
import '../../app/logging.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../app/state.dart'; import '../../app/state.dart';
import '../../app/views/user_interaction.dart';
import '../../core/models.dart'; import '../../core/models.dart';
import '../../oath/models.dart'; import '../../oath/models.dart';
import '../../oath/state.dart'; import '../../oath/state.dart';
@ -254,6 +255,7 @@ class _DesktopCredentialListNotifier extends OathCredentialListNotifier {
controller = await _withContext( controller = await _withContext(
(context) async => promptUserInteraction( (context) async => promptUserInteraction(
context, context,
icon: const Icon(Icons.touch_app),
title: 'Touch Required', title: 'Touch Required',
description: 'Touch the button on your YubiKey now.', description: 'Touch the button on your YubiKey now.',
), ),

View File

@ -162,9 +162,9 @@ abstract class _FidoState extends FidoState {
_$_FidoState.fromJson; _$_FidoState.fromJson;
@override @override
Map<String, dynamic> get info => throw _privateConstructorUsedError; Map<String, dynamic> get info;
@override @override
bool get unlocked => throw _privateConstructorUsedError; bool get unlocked;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_FidoStateCopyWith<_$_FidoState> get copyWith => _$$_FidoStateCopyWith<_$_FidoState> get copyWith =>
@ -471,8 +471,8 @@ abstract class _PinFailure implements PinResult {
factory _PinFailure(final int retries, final bool authBlocked) = factory _PinFailure(final int retries, final bool authBlocked) =
_$_PinFailure; _$_PinFailure;
int get retries => throw _privateConstructorUsedError; int get retries;
bool get authBlocked => throw _privateConstructorUsedError; bool get authBlocked;
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_PinFailureCopyWith<_$_PinFailure> get copyWith => _$$_PinFailureCopyWith<_$_PinFailure> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -620,9 +620,9 @@ abstract class _Fingerprint extends Fingerprint {
_$_Fingerprint.fromJson; _$_Fingerprint.fromJson;
@override @override
String get templateId => throw _privateConstructorUsedError; String get templateId;
@override @override
String? get name => throw _privateConstructorUsedError; String? get name;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_FingerprintCopyWith<_$_Fingerprint> get copyWith => _$$_FingerprintCopyWith<_$_Fingerprint> get copyWith =>
@ -828,7 +828,7 @@ class _$_EventCapture implements _EventCapture {
abstract class _EventCapture implements FingerprintEvent { abstract class _EventCapture implements FingerprintEvent {
factory _EventCapture(final int remaining) = _$_EventCapture; factory _EventCapture(final int remaining) = _$_EventCapture;
int get remaining => throw _privateConstructorUsedError; int get remaining;
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_EventCaptureCopyWith<_$_EventCapture> get copyWith => _$$_EventCaptureCopyWith<_$_EventCapture> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -978,7 +978,7 @@ class _$_EventComplete implements _EventComplete {
abstract class _EventComplete implements FingerprintEvent { abstract class _EventComplete implements FingerprintEvent {
factory _EventComplete(final Fingerprint fingerprint) = _$_EventComplete; factory _EventComplete(final Fingerprint fingerprint) = _$_EventComplete;
Fingerprint get fingerprint => throw _privateConstructorUsedError; Fingerprint get fingerprint;
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_EventCompleteCopyWith<_$_EventComplete> get copyWith => _$$_EventCompleteCopyWith<_$_EventComplete> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -1118,7 +1118,7 @@ class _$_EventError implements _EventError {
abstract class _EventError implements FingerprintEvent { abstract class _EventError implements FingerprintEvent {
factory _EventError(final int code) = _$_EventError; factory _EventError(final int code) = _$_EventError;
int get code => throw _privateConstructorUsedError; int get code;
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_EventErrorCopyWith<_$_EventError> get copyWith => _$$_EventErrorCopyWith<_$_EventError> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -1304,13 +1304,13 @@ abstract class _FidoCredential implements FidoCredential {
_$_FidoCredential.fromJson; _$_FidoCredential.fromJson;
@override @override
String get rpId => throw _privateConstructorUsedError; String get rpId;
@override @override
String get credentialId => throw _privateConstructorUsedError; String get credentialId;
@override @override
String get userId => throw _privateConstructorUsedError; String get userId;
@override @override
String get userName => throw _privateConstructorUsedError; String get userName;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_FidoCredentialCopyWith<_$_FidoCredential> get copyWith => _$$_FidoCredentialCopyWith<_$_FidoCredential> get copyWith =>

View File

@ -10,6 +10,7 @@ import 'package:yubico_authenticator/app/logging.dart';
import '../../app/message.dart'; import '../../app/message.dart';
import '../../desktop/models.dart'; import '../../desktop/models.dart';
import '../../widgets/responsive_dialog.dart'; import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../state.dart'; import '../state.dart';
import '../../fido/models.dart'; import '../../fido/models.dart';
import '../../app/models.dart'; import '../../app/models.dart';
@ -179,6 +180,8 @@ class _AddFingerprintDialogState extends ConsumerState<AddFingerprintDialog>
TextFormField( TextFormField(
focusNode: _nameFocus, focusNode: _nameFocus,
maxLength: 15, maxLength: 15,
inputFormatters: [limitBytesLength(15)],
buildCounter: buildByteCounterFor(_label),
autofocus: true, autofocus: true,
decoration: InputDecoration( decoration: InputDecoration(
enabled: _fingerprint != null, enabled: _fingerprint != null,

View File

@ -6,7 +6,7 @@ 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 '../../theme.dart'; import '../../widgets/menu_list_tile.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'pin_dialog.dart'; import 'pin_dialog.dart';
@ -27,7 +27,7 @@ class FidoLockedPage extends ConsumerWidget {
graphic: noFingerprints, graphic: noFingerprints,
header: 'No fingerprints', header: 'No fingerprints',
message: 'Set a PIN to register fingerprints', message: 'Set a PIN to register fingerprints',
actions: _buildActions(context), keyActions: _buildActions(context),
); );
} else { } else {
return MessagePage( return MessagePage(
@ -36,7 +36,7 @@ class FidoLockedPage extends ConsumerWidget {
header: state.credMgmt ? 'No discoverable accounts' : 'Ready to use', header: state.credMgmt ? 'No discoverable accounts' : 'Ready to use',
message: message:
'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites', 'Optionally set a PIN to protect access to your YubiKey\nRegister as a Security Key on websites',
actions: _buildActions(context), keyActions: _buildActions(context),
); );
} }
} }
@ -47,13 +47,13 @@ class FidoLockedPage extends ConsumerWidget {
graphic: manageAccounts, graphic: manageAccounts,
header: 'Ready to use', header: 'Ready to use',
message: 'Register as a Security Key on websites', message: 'Register as a Security Key on websites',
actions: _buildActions(context), keyActions: _buildActions(context),
); );
} }
return AppPage( return AppPage(
title: const Text('WebAuthn'), title: const Text('WebAuthn'),
actions: _buildActions(context), keyActions: _buildActions(context),
child: Column( child: Column(
children: [ children: [
_PinEntryForm(state, node), _PinEntryForm(state, node),
@ -62,48 +62,26 @@ class FidoLockedPage extends ConsumerWidget {
); );
} }
List<Widget> _buildActions(BuildContext context) => [ List<PopupMenuEntry> _buildActions(BuildContext context) => [
if (!state.hasPin) if (!state.hasPin)
OutlinedButton.icon( buildMenuItem(
style: state.bioEnroll != null title: const Text('Set PIN'),
? AppTheme.primaryOutlinedButtonStyle(context) leading: const Icon(Icons.pin),
: null, action: () {
label: const Text('Set PIN'), showBlurDialog(
icon: const Icon(Icons.pin),
onPressed: () {
showDialog(
context: context, context: context,
builder: (context) => FidoPinDialog(node.path, state), builder: (context) => FidoPinDialog(node.path, state),
); );
}, },
), ),
OutlinedButton.icon( buildMenuItem(
label: const Text('Options'), title: const Text('Reset FIDO'),
icon: const Icon(Icons.tune), leading: const Icon(Icons.delete),
onPressed: () { action: () {
showBottomMenu(context, [ showBlurDialog(
if (state.hasPin) context: context,
MenuAction( builder: (context) => ResetDialog(node),
text: 'Change PIN', );
icon: const Icon(Icons.pin),
action: (context) {
showDialog(
context: context,
builder: (context) => FidoPinDialog(node.path, state),
);
},
),
MenuAction(
text: 'Reset FIDO',
icon: const Icon(Icons.delete),
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
]);
}, },
), ),
]; ];

View File

@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart'; import '../../app/message.dart';
import '../../desktop/models.dart'; import '../../desktop/models.dart';
import '../../widgets/responsive_dialog.dart'; import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import '../../app/models.dart'; import '../../app/models.dart';
@ -69,8 +70,9 @@ class _RenameAccountDialogState extends ConsumerState<RenameFingerprintDialog> {
const Text('This will change the label of the fingerprint.'), const Text('This will change the label of the fingerprint.'),
TextFormField( TextFormField(
initialValue: _label, initialValue: _label,
// TODO: Make this field count UTF-8 bytes instead of characters.
maxLength: 15, maxLength: 15,
inputFormatters: [limitBytesLength(15)],
buildCounter: buildByteCounterFor(_label),
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Label', labelText: 'Label',

View File

@ -6,8 +6,8 @@ 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 '../../theme.dart';
import '../../widgets/list_title.dart'; import '../../widgets/list_title.dart';
import '../../widgets/menu_list_tile.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'add_fingerprint_dialog.dart'; import 'add_fingerprint_dialog.dart';
@ -57,7 +57,7 @@ class FidoUnlockedPage extends ConsumerWidget {
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) => builder: (context) =>
DeleteCredentialDialog(node.path, cred), DeleteCredentialDialog(node.path, cred),
@ -96,7 +96,7 @@ class FidoUnlockedPage extends ConsumerWidget {
children: [ children: [
IconButton( IconButton(
onPressed: () { onPressed: () {
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) => builder: (context) =>
RenameFingerprintDialog(node.path, fp), RenameFingerprintDialog(node.path, fp),
@ -105,7 +105,7 @@ class FidoUnlockedPage extends ConsumerWidget {
icon: const Icon(Icons.edit_outlined)), icon: const Icon(Icons.edit_outlined)),
IconButton( IconButton(
onPressed: () { onPressed: () {
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) => builder: (context) =>
DeleteFingerprintDialog(node.path, fp), DeleteFingerprintDialog(node.path, fp),
@ -121,7 +121,7 @@ class FidoUnlockedPage extends ConsumerWidget {
if (children.isNotEmpty) { if (children.isNotEmpty) {
return AppPage( return AppPage(
title: const Text('WebAuthn'), title: const Text('WebAuthn'),
actions: _buildActions(context), keyActions: _buildKeyActions(context),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: children), crossAxisAlignment: CrossAxisAlignment.start, children: children),
); );
@ -133,7 +133,7 @@ class FidoUnlockedPage extends ConsumerWidget {
graphic: noFingerprints, graphic: noFingerprints,
header: 'No fingerprints', header: 'No fingerprints',
message: 'Add one or more (up to five) fingerprints', message: 'Add one or more (up to five) fingerprints',
actions: _buildActions(context, fingerprintPrimary: true), keyActions: _buildKeyActions(context),
); );
} }
@ -142,7 +142,7 @@ class FidoUnlockedPage extends ConsumerWidget {
graphic: manageAccounts, graphic: manageAccounts,
header: 'No discoverable accounts', header: 'No discoverable accounts',
message: 'Register as a Security Key on websites', message: 'Register as a Security Key on websites',
actions: _buildActions(context), keyActions: _buildKeyActions(context),
); );
} }
@ -152,49 +152,36 @@ class FidoUnlockedPage extends ConsumerWidget {
child: const CircularProgressIndicator(), child: const CircularProgressIndicator(),
); );
List<Widget> _buildActions(BuildContext context, List<PopupMenuEntry> _buildKeyActions(BuildContext context) => [
{bool fingerprintPrimary = false}) =>
[
if (state.bioEnroll != null) if (state.bioEnroll != null)
OutlinedButton.icon( buildMenuItem(
style: fingerprintPrimary title: const Text('Add fingerprint'),
? AppTheme.primaryOutlinedButtonStyle(context) leading: const Icon(Icons.fingerprint),
: null, action: () {
label: const Text('Add fingerprint'), showBlurDialog(
icon: const Icon(Icons.fingerprint),
onPressed: () {
showDialog(
context: context, context: context,
builder: (context) => AddFingerprintDialog(node.path), builder: (context) => AddFingerprintDialog(node.path),
); );
}, },
), ),
OutlinedButton.icon( buildMenuItem(
label: const Text('Options'), title: const Text('Change PIN'),
icon: const Icon(Icons.tune), leading: const Icon(Icons.pin),
onPressed: () { action: () {
showBottomMenu(context, [ showBlurDialog(
MenuAction( context: context,
text: 'Change PIN', builder: (context) => FidoPinDialog(node.path, state),
icon: const Icon(Icons.pin), );
action: (context) { },
showDialog( ),
context: context, buildMenuItem(
builder: (context) => FidoPinDialog(node.path, state), title: const Text('Reset FIDO'),
); leading: const Icon(Icons.delete),
}, action: () {
), showBlurDialog(
MenuAction( context: context,
text: 'Reset FIDO', builder: (context) => ResetDialog(node),
icon: const Icon(Icons.delete), );
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
]);
}, },
), ),
]; ];

View File

@ -208,14 +208,13 @@ abstract class _DeviceConfig implements DeviceConfig {
_$_DeviceConfig.fromJson; _$_DeviceConfig.fromJson;
@override @override
Map<Transport, int> get enabledCapabilities => Map<Transport, int> get enabledCapabilities;
throw _privateConstructorUsedError;
@override @override
int? get autoEjectTimeout => throw _privateConstructorUsedError; int? get autoEjectTimeout;
@override @override
int? get challengeResponseTimeout => throw _privateConstructorUsedError; int? get challengeResponseTimeout;
@override @override
int? get deviceFlags => throw _privateConstructorUsedError; int? get deviceFlags;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_DeviceConfigCopyWith<_$_DeviceConfig> get copyWith => _$$_DeviceConfigCopyWith<_$_DeviceConfig> get copyWith =>
@ -514,22 +513,21 @@ abstract class _DeviceInfo implements DeviceInfo {
_$_DeviceInfo.fromJson; _$_DeviceInfo.fromJson;
@override @override
DeviceConfig get config => throw _privateConstructorUsedError; DeviceConfig get config;
@override @override
int? get serial => throw _privateConstructorUsedError; int? get serial;
@override @override
Version get version => throw _privateConstructorUsedError; Version get version;
@override @override
FormFactor get formFactor => throw _privateConstructorUsedError; FormFactor get formFactor;
@override @override
Map<Transport, int> get supportedCapabilities => Map<Transport, int> get supportedCapabilities;
throw _privateConstructorUsedError;
@override @override
bool get isLocked => throw _privateConstructorUsedError; bool get isLocked;
@override @override
bool get isFips => throw _privateConstructorUsedError; bool get isFips;
@override @override
bool get isSky => throw _privateConstructorUsedError; bool get isSky;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_DeviceInfoCopyWith<_$_DeviceInfo> get copyWith => _$$_DeviceInfoCopyWith<_$_DeviceInfo> get copyWith =>

View File

@ -180,7 +180,7 @@ class _ManagementScreenState extends ConsumerState<ManagementScreen> {
context, context,
'Reconfiguring YubiKey...', 'Reconfiguring YubiKey...',
duration: const Duration(seconds: 8), duration: const Duration(seconds: 8),
).close; );
} }
await ref await ref
.read(managementStateProvider(widget.deviceData.node.path).notifier) .read(managementStateProvider(widget.deviceData.node.path).notifier)

View File

@ -125,26 +125,18 @@ class CredentialData with _$CredentialData {
); );
} }
Uri toUri() { Uri toUri() => Uri(
final path = issuer != null ? '$issuer:$name' : name; scheme: 'otpauth',
var uri = 'otpauth://${oathType.name}/$path?secret=$secret'; host: oathType.name,
switch (oathType) { path: issuer != null ? '$issuer:$name' : name,
case OathType.hotp: queryParameters: {
uri += '&counter=$counter'; 'secret': secret,
break; if (oathType == OathType.totp) 'period': period.toString(),
case OathType.totp: if (oathType == OathType.hotp) 'counter': counter.toString(),
uri += '&period=$period'; if (issuer != null) 'issuer': issuer!,
break; if (digits != 6) 'digits': digits.toString(),
} if (hashAlgorithm != HashAlgorithm.sha1)
if (issuer != null) { 'algorithm': hashAlgorithm.name,
uri += '&issuer=$issuer'; },
} );
if (digits != 6) {
uri += '&digits=$digits';
}
if (hashAlgorithm != HashAlgorithm.sha1) {
uri += '&algorithm=${hashAlgorithm.name}';
}
return Uri.parse(uri);
}
} }

View File

@ -253,19 +253,19 @@ abstract class _OathCredential implements OathCredential {
_$_OathCredential.fromJson; _$_OathCredential.fromJson;
@override @override
String get deviceId => throw _privateConstructorUsedError; String get deviceId;
@override @override
String get id => throw _privateConstructorUsedError; String get id;
@override @override
String? get issuer => throw _privateConstructorUsedError; String? get issuer;
@override @override
String get name => throw _privateConstructorUsedError; String get name;
@override @override
OathType get oathType => throw _privateConstructorUsedError; OathType get oathType;
@override @override
int get period => throw _privateConstructorUsedError; int get period;
@override @override
bool get touchRequired => throw _privateConstructorUsedError; bool get touchRequired;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_OathCredentialCopyWith<_$_OathCredential> get copyWith => _$$_OathCredentialCopyWith<_$_OathCredential> get copyWith =>
@ -424,11 +424,11 @@ abstract class _OathCode implements OathCode {
factory _OathCode.fromJson(Map<String, dynamic> json) = _$_OathCode.fromJson; factory _OathCode.fromJson(Map<String, dynamic> json) = _$_OathCode.fromJson;
@override @override
String get value => throw _privateConstructorUsedError; String get value;
@override @override
int get validFrom => throw _privateConstructorUsedError; int get validFrom;
@override @override
int get validTo => throw _privateConstructorUsedError; int get validTo;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_OathCodeCopyWith<_$_OathCode> get copyWith => _$$_OathCodeCopyWith<_$_OathCode> get copyWith =>
@ -599,9 +599,9 @@ abstract class _OathPair implements OathPair {
factory _OathPair.fromJson(Map<String, dynamic> json) = _$_OathPair.fromJson; factory _OathPair.fromJson(Map<String, dynamic> json) = _$_OathPair.fromJson;
@override @override
OathCredential get credential => throw _privateConstructorUsedError; OathCredential get credential;
@override @override
OathCode? get code => throw _privateConstructorUsedError; OathCode? get code;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_OathPairCopyWith<_$_OathPair> get copyWith => _$$_OathPairCopyWith<_$_OathPair> get copyWith =>
@ -838,17 +838,17 @@ abstract class _OathState implements OathState {
_$_OathState.fromJson; _$_OathState.fromJson;
@override @override
String get deviceId => throw _privateConstructorUsedError; String get deviceId;
@override @override
Version get version => throw _privateConstructorUsedError; Version get version;
@override @override
bool get hasKey => throw _privateConstructorUsedError; bool get hasKey;
@override @override
bool get remembered => throw _privateConstructorUsedError; bool get remembered;
@override @override
bool get locked => throw _privateConstructorUsedError; bool get locked;
@override @override
KeystoreState get keystore => throw _privateConstructorUsedError; KeystoreState get keystore;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_OathStateCopyWith<_$_OathState> get copyWith => _$$_OathStateCopyWith<_$_OathState> get copyWith =>
@ -1126,21 +1126,21 @@ abstract class _CredentialData extends CredentialData {
_$_CredentialData.fromJson; _$_CredentialData.fromJson;
@override @override
String? get issuer => throw _privateConstructorUsedError; String? get issuer;
@override @override
String get name => throw _privateConstructorUsedError; String get name;
@override @override
String get secret => throw _privateConstructorUsedError; String get secret;
@override @override
OathType get oathType => throw _privateConstructorUsedError; OathType get oathType;
@override @override
HashAlgorithm get hashAlgorithm => throw _privateConstructorUsedError; HashAlgorithm get hashAlgorithm;
@override @override
int get digits => throw _privateConstructorUsedError; int get digits;
@override @override
int get period => throw _privateConstructorUsedError; int get period;
@override @override
int get counter => throw _privateConstructorUsedError; int get counter;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)
_$$_CredentialDataCopyWith<_$_CredentialData> get copyWith => _$$_CredentialDataCopyWith<_$_CredentialData> get copyWith =>

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@ -59,15 +60,39 @@ abstract class OathCredentialListNotifier
Future<void> deleteAccount(OathCredential credential); Future<void> deleteAccount(OathCredential credential);
} }
final credentialsProvider = Provider.autoDispose<List<OathCredential>?>((ref) { final credentialsProvider = StateNotifierProvider.autoDispose<
_CredentialsProviderNotifier, List<OathCredential>?>((ref) {
final provider = _CredentialsProviderNotifier();
final node = ref.watch(currentDeviceProvider); final node = ref.watch(currentDeviceProvider);
if (node != null) { if (node != null) {
return ref.watch(credentialListProvider(node.path) ref.listen<List<OathPair>?>(credentialListProvider(node.path),
.select((pairs) => pairs?.map((e) => e.credential).toList())); (previous, next) {
provider._updatePairs(next);
});
} }
return null; return provider;
}); });
class _CredentialsProviderNotifier
extends StateNotifier<List<OathCredential>?> {
_CredentialsProviderNotifier() : super(null);
void _updatePairs(List<OathPair>? pairs) {
if (mounted) {
if (pairs == null) {
if (state != null) {
state = null;
}
} else {
final creds = pairs.map((p) => p.credential).toList();
if (!const ListEquality().equals(creds, state)) {
state = creds;
}
}
}
}
}
final codeProvider = final codeProvider =
Provider.autoDispose.family<OathCode?, OathCredential>((ref, credential) { Provider.autoDispose.family<OathCode?, OathCredential>((ref, credential) {
final node = ref.watch(currentDeviceProvider); final node = ref.watch(currentDeviceProvider);

View File

@ -3,11 +3,11 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart'; import '../../app/shortcuts.dart';
import '../../app/state.dart'; import '../../app/state.dart';
import '../../core/models.dart'; import '../../core/models.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../widgets/dialog_frame.dart';
import '../models.dart'; import '../models.dart';
import 'account_mixin.dart'; import 'account_mixin.dart';
@ -24,7 +24,7 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
// Replace this dialog with a new one, for the renamed credential. // Replace this dialog with a new one, for the renamed credential.
await ref.read(withContextProvider)((context) async { await ref.read(withContextProvider)((context) async {
Navigator.of(context).pop(); Navigator.of(context).pop();
await showDialog( await showBlurDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return AccountDialog(renamed); return AccountDialog(renamed);
@ -94,7 +94,7 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way // TODO: Solve this in a cleaner way
if (ref.watch(currentDeviceDataProvider) == null) { if (ref.watch(currentDeviceDataProvider) is! AsyncData) {
// The rest of this method assumes there is a device, and will throw an exception if not. // The rest of this method assumes there is a device, and will throw an exception if not.
// This will never be shown, as the dialog will be immediately closed // This will never be shown, as the dialog will be immediately closed
return const SizedBox(); return const SizedBox();
@ -120,70 +120,68 @@ class AccountDialog extends ConsumerWidget with AccountMixin {
return null; return null;
}), }),
}, },
child: Focus( child: FocusScope(
autofocus: true, autofocus: true,
child: DialogFrame( child: AlertDialog(
child: AlertDialog( title: Center(
title: Center( child: Text(
child: Text( title,
title, overflow: TextOverflow.fade,
overflow: TextOverflow.fade, style: Theme.of(context).textTheme.headlineSmall,
style: Theme.of(context).textTheme.headlineSmall, maxLines: 1,
maxLines: 1, softWrap: false,
softWrap: false,
),
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0), ),
content: Column( contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
crossAxisAlignment: CrossAxisAlignment.center, content: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center,
children: [ mainAxisSize: MainAxisSize.min,
if (subtitle != null) children: [
Text( if (subtitle != null)
subtitle!, Text(
overflow: TextOverflow.fade, subtitle!,
maxLines: 1, overflow: TextOverflow.fade,
softWrap: false, maxLines: 1,
// This is what ListTile uses for subtitle softWrap: false,
style: Theme.of(context).textTheme.bodyMedium!.copyWith( // This is what ListTile uses for subtitle
color: Theme.of(context).textTheme.caption!.color, style: Theme.of(context).textTheme.bodyMedium!.copyWith(
), color: Theme.of(context).textTheme.caption!.color,
), ),
const SizedBox(height: 12.0), ),
DecoratedBox( const SizedBox(height: 12.0),
decoration: BoxDecoration( DecoratedBox(
shape: BoxShape.rectangle, decoration: BoxDecoration(
color: CardTheme.of(context).color, shape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(30.0)), color: CardTheme.of(context).color,
), borderRadius: const BorderRadius.all(Radius.circular(30.0)),
child: Center( ),
child: FittedBox( child: Center(
child: DefaultTextStyle.merge( child: FittedBox(
style: const TextStyle(fontSize: 28), child: DefaultTextStyle.merge(
child: IconTheme( style: const TextStyle(fontSize: 28),
data: IconTheme.of(context).copyWith(size: 24), child: IconTheme(
child: Padding( data: IconTheme.of(context).copyWith(size: 24),
padding: const EdgeInsets.symmetric( child: Padding(
horizontal: 16.0, vertical: 8.0), padding: const EdgeInsets.symmetric(
child: buildCodeView(ref), horizontal: 16.0, vertical: 8.0),
), child: buildCodeView(ref),
), ),
), ),
), ),
), ),
), ),
], ),
),
actionsPadding: const EdgeInsets.only(top: 10.0, right: -16.0),
actions: [
Center(
child: FittedBox(
alignment: Alignment.center,
child: Row(children: _buildActions(context, ref)),
),
)
], ],
), ),
actionsPadding: const EdgeInsets.only(top: 10.0, right: -16.0),
actions: [
Center(
child: FittedBox(
alignment: Alignment.center,
child: Row(children: _buildActions(context, ref)),
),
)
],
), ),
), ),
); );

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/models.dart'; import '../../app/models.dart';
@ -8,65 +7,14 @@ import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'account_view.dart'; import 'account_view.dart';
class AccountList extends ConsumerStatefulWidget { class AccountList extends ConsumerWidget {
final DevicePath devicePath; final DevicePath devicePath;
final OathState oathState; final OathState oathState;
const AccountList(this.devicePath, this.oathState, {super.key}); const AccountList(this.devicePath, this.oathState, {super.key});
@override @override
ConsumerState<AccountList> createState() => _AccountListState(); Widget build(BuildContext context, WidgetRef ref) {
} final accounts = ref.watch(credentialListProvider(devicePath));
class _AccountListState extends ConsumerState<AccountList> {
List<OathCredential> _credentials = [];
Map<OathCredential, FocusNode> _focusNodes = {};
@override
void dispose() {
super.dispose();
for (var e in _focusNodes.values) {
e.dispose();
}
_focusNodes.clear();
}
void _updateFocusNodes() {
_focusNodes = {
for (var cred in _credentials)
cred: _focusNodes[cred] ??
FocusNode(
onKeyEvent: (node, event) {
if (event is KeyDownEvent) {
int index = -1;
ScrollPositionAlignmentPolicy policy =
ScrollPositionAlignmentPolicy.explicit;
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
index = _credentials.indexOf(cred) + 1;
policy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
index = _credentials.indexOf(cred) - 1;
policy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
}
if (index >= 0 && index < _credentials.length) {
final targetNode = _focusNodes[_credentials[index]]!;
targetNode.requestFocus();
Scrollable.ensureVisible(
targetNode.context!,
alignmentPolicy: policy,
);
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
)
};
_focusNodes.removeWhere((cred, _) => !_credentials.contains(cred));
}
@override
Widget build(BuildContext context) {
final accounts = ref.watch(credentialListProvider(widget.devicePath));
if (accounts == null) { if (accounts == null) {
return Column( return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -88,27 +36,24 @@ class _AccountListState extends ConsumerState<AccountList> {
final creds = final creds =
credentials.where((entry) => !favorites.contains(entry.credential.id)); credentials.where((entry) => !favorites.contains(entry.credential.id));
_credentials = return FocusTraversalGroup(
pinnedCreds.followedBy(creds).map((e) => e.credential).toList(); policy: WidgetOrderTraversalPolicy(),
_updateFocusNodes(); child: Column(
children: [
return Column( if (pinnedCreds.isNotEmpty) const ListTitle('Pinned'),
children: [ ...pinnedCreds.map(
if (pinnedCreds.isNotEmpty) const ListTitle('Pinned'), (entry) => AccountView(
...pinnedCreds.map( entry.credential,
(entry) => AccountView( ),
entry.credential,
focusNode: _focusNodes[entry.credential],
), ),
), if (creds.isNotEmpty) const ListTitle('Accounts'),
if (creds.isNotEmpty) const ListTitle('Accounts'), ...creds.map(
...creds.map( (entry) => AccountView(
(entry) => AccountView( entry.credential,
entry.credential, ),
focusNode: _focusNodes[entry.credential],
), ),
), ],
], ),
); );
} }
} }

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -78,7 +79,7 @@ mixin AccountMixin {
Future<OathCredential?> renameCredential( Future<OathCredential?> renameCredential(
BuildContext context, WidgetRef ref) async { BuildContext context, WidgetRef ref) async {
final node = ref.read(currentDeviceProvider)!; final node = ref.read(currentDeviceProvider)!;
return await showDialog( return await showBlurDialog(
context: context, context: context,
builder: (context) => RenameAccountDialog(node, credential), builder: (context) => RenameAccountDialog(node, credential),
); );
@ -87,7 +88,7 @@ mixin AccountMixin {
@protected @protected
Future<bool> deleteCredential(BuildContext context, WidgetRef ref) async { Future<bool> deleteCredential(BuildContext context, WidgetRef ref) async {
final node = ref.read(currentDeviceProvider)!; final node = ref.read(currentDeviceProvider)!;
return await showDialog( return await showBlurDialog(
context: context, context: context,
builder: (context) => DeleteAccountDialog(node, credential), builder: (context) => DeleteAccountDialog(node, credential),
) ?? ) ??
@ -95,67 +96,72 @@ mixin AccountMixin {
} }
@protected @protected
List<MenuAction> buildActions(BuildContext context, WidgetRef ref) { List<MenuAction> buildActions(BuildContext context, WidgetRef ref) =>
final deviceData = ref.watch(currentDeviceDataProvider); ref.watch(currentDeviceDataProvider).maybeWhen(
if (deviceData == null) { data: (data) {
return []; final code = getCode(ref);
} final expired = isExpired(code, ref);
final code = getCode(ref); final manual = credential.touchRequired ||
final expired = isExpired(code, ref); credential.oathType == OathType.hotp;
final manual = final ready = expired || credential.oathType == OathType.hotp;
credential.touchRequired || credential.oathType == OathType.hotp; final pinned = isPinned(ref);
final ready = expired || credential.oathType == OathType.hotp;
final pinned = isPinned(ref);
return [ final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C';
MenuAction( return [
text: 'Copy to clipboard', MenuAction(
icon: const Icon(Icons.copy), text: 'Copy to clipboard ($shortcut)',
action: code == null || expired icon: const Icon(Icons.copy),
? null action: code == null || expired
: (context) { ? null
Clipboard.setData(ClipboardData(text: code.value)); : (context) {
showMessage(context, 'Code copied to clipboard'); Clipboard.setData(ClipboardData(text: code.value));
}, showMessage(context, 'Code copied to clipboard');
), },
if (manual) ),
MenuAction( if (manual)
text: 'Calculate', MenuAction(
icon: const Icon(Icons.refresh), text: 'Calculate',
action: ready icon: const Icon(Icons.refresh),
? (context) async { action: ready
try { ? (context) async {
await calculateCode(context, ref); try {
} on CancellationException catch (_) { await calculateCode(context, ref);
// ignored } on CancellationException catch (_) {
} // ignored
} }
: null, }
), : null,
MenuAction( ),
text: pinned ? 'Unpin account' : 'Pin account', MenuAction(
icon: pinned ? pushPinStrokeIcon : const Icon(Icons.push_pin_outlined), text: pinned ? 'Unpin account' : 'Pin account',
action: (context) { icon: pinned
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id); ? pushPinStrokeIcon
}, : const Icon(Icons.push_pin_outlined),
), action: (context) {
if (deviceData.info.version.isAtLeast(5, 3)) ref
MenuAction( .read(favoritesProvider.notifier)
icon: const Icon(Icons.edit_outlined), .toggleFavorite(credential.id);
text: 'Rename account', },
action: (context) async { ),
await renameCredential(context, ref); if (data.info.version.isAtLeast(5, 3))
}, MenuAction(
), icon: const Icon(Icons.edit_outlined),
MenuAction( text: 'Rename account',
text: 'Delete account', action: (context) async {
icon: const Icon(Icons.delete_outline), await renameCredential(context, ref);
action: (context) async { },
await deleteCredential(context, ref); ),
}, MenuAction(
), text: 'Delete account',
]; icon: const Icon(Icons.delete_outline),
} action: (context) async {
await deleteCredential(context, ref);
},
),
];
},
orElse: () => [],
);
@protected @protected
Widget buildCodeView(WidgetRef ref) { Widget buildCodeView(WidgetRef ref) {

View File

@ -1,13 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/cancellation_exception.dart'; import 'package:yubico_authenticator/cancellation_exception.dart';
import 'package:yubico_authenticator/app/state.dart'; import 'package:yubico_authenticator/app/state.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart'; import '../../app/shortcuts.dart';
import '../../app/state.dart'; import '../../app/state.dart';
import '../../widgets/menu_list_tile.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'account_dialog.dart'; import 'account_dialog.dart';
@ -16,8 +16,7 @@ import 'account_mixin.dart';
class AccountView extends ConsumerWidget with AccountMixin { class AccountView extends ConsumerWidget with AccountMixin {
@override @override
final OathCredential credential; final OathCredential credential;
final FocusNode? focusNode; AccountView(this.credential, {super.key});
AccountView(this.credential, {super.key, this.focusNode});
Color _iconColor(int shade) { Color _iconColor(int shade) {
final colors = [ final colors = [
@ -47,22 +46,16 @@ class AccountView extends ConsumerWidget with AccountMixin {
List<PopupMenuItem> _buildPopupMenu(BuildContext context, WidgetRef ref) { List<PopupMenuItem> _buildPopupMenu(BuildContext context, WidgetRef ref) {
return buildActions(context, ref).map((e) { return buildActions(context, ref).map((e) {
final action = e.action; final action = e.action;
return PopupMenuItem( return buildMenuItem(
enabled: action != null, leading: e.icon,
onTap: () { title: Text(e.text),
// As soon as onTap returns, the Navigator is popped, action: action != null
// closing the topmost item. Since we sometimes open new dialogs in ? () {
// the action, make sure that happens *after* the pop. ref.read(withContextProvider)((context) async {
Timer(Duration.zero, () { action.call(context);
action?.call(context); });
}); }
}, : null,
child: ListTile(
leading: e.icon,
title: Text(e.text),
dense: true,
contentPadding: EdgeInsets.zero,
),
); );
}).toList(); }).toList();
} }
@ -122,13 +115,10 @@ class AccountView extends ConsumerWidget with AccountMixin {
return ListTile( return ListTile(
shape: shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)), RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
focusNode: focusNode,
onTap: () { onTap: () {
showDialog( showBlurDialog(
context: context, context: context,
builder: (context) { builder: (context) => AccountDialog(credential),
return AccountDialog(credential);
},
); );
}, },
onLongPress: triggerCopy, onLongPress: triggerCopy,

View File

@ -6,14 +6,15 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:yubico_authenticator/cancellation_exception.dart'; import 'package:yubico_authenticator/cancellation_exception.dart';
import 'package:yubico_authenticator/app/logging.dart';
import '../../app/logging.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';
import '../../desktop/models.dart'; import '../../desktop/models.dart';
import '../../widgets/file_drop_target.dart'; import '../../widgets/file_drop_target.dart';
import '../../widgets/responsive_dialog.dart'; import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'utils.dart'; import 'utils.dart';
@ -213,6 +214,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
autofocus: true, autofocus: true,
enabled: issuerRemaining > 0, enabled: issuerRemaining > 0,
maxLength: max(issuerRemaining, 1), maxLength: max(issuerRemaining, 1),
inputFormatters: [limitBytesLength(issuerRemaining)],
buildCounter: buildByteCounterFor(_issuerController.text),
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Issuer (optional)', labelText: 'Issuer (optional)',
@ -232,6 +235,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
key: const Key('name'), key: const Key('name'),
controller: _accountController, controller: _accountController,
maxLength: max(nameRemaining, 1), maxLength: max(nameRemaining, 1),
buildCounter: buildByteCounterFor(_accountController.text),
inputFormatters: [limitBytesLength(nameRemaining)],
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Account name', labelText: 'Account name',

View File

@ -12,7 +12,7 @@ import '../../app/views/app_loading_screen.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 '../../theme.dart'; import '../../widgets/menu_list_tile.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'account_list.dart'; import 'account_list.dart';
@ -52,34 +52,26 @@ class _LockedView extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) => AppPage( Widget build(BuildContext context, WidgetRef ref) => AppPage(
title: const Text('Authenticator'), title: const Text('Authenticator'),
actions: [ keyActions: [
OutlinedButton.icon( buildMenuItem(
label: const Text('Options'), title: const Text('Manage password'),
icon: const Icon(Icons.tune), leading: const Icon(Icons.password),
onPressed: () { action: () {
showBottomMenu(context, [ showBlurDialog(
MenuAction( context: context,
text: 'Manage password', builder: (context) =>
icon: const Icon(Icons.password), ManagePasswordDialog(devicePath, oathState),
action: (context) { );
showDialog( },
context: context, ),
builder: (context) => buildMenuItem(
ManagePasswordDialog(devicePath, oathState), title: const Text('Reset OATH'),
); leading: const Icon(Icons.delete),
}, action: () {
), showBlurDialog(
MenuAction( context: context,
text: 'Reset OATH', builder: (context) => ResetDialog(devicePath),
icon: const Icon(Icons.delete), );
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(devicePath),
);
},
),
]);
}, },
), ),
], ],
@ -124,14 +116,17 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isEmpty = ref.watch(credentialListProvider(widget.devicePath) final credentials = ref.watch(credentialsProvider);
.select((value) => value?.isEmpty == true)); if (credentials?.isEmpty == true) {
if (isEmpty) {
return MessagePage( return MessagePage(
title: const Text('Authenticator'), title: const Text('Authenticator'),
graphic: noAccounts, graphic: noAccounts,
header: 'No accounts', header: 'No accounts',
actions: _buildActions(context, true), keyActions: _buildActions(
context,
used: 0,
capacity: widget.oathState.version.isAtLeast(4) ? 32 : null,
),
); );
} }
return Actions( return Actions(
@ -143,98 +138,92 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> {
return null; return null;
}), }),
}, },
child: Focus( child: AppPage(
autofocus: true, title: Focus(
child: AppPage( canRequestFocus: false,
title: Focus( onKeyEvent: (node, event) {
canRequestFocus: false, if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
onKeyEvent: (node, event) { node.focusInDirection(TraversalDirection.down);
if (event.logicalKey == LogicalKeyboardKey.arrowDown) { return KeyEventResult.handled;
node.focusInDirection(TraversalDirection.down); }
return KeyEventResult.handled; return KeyEventResult.ignored;
} },
return KeyEventResult.ignored; child: Builder(builder: (context) {
}, return TextFormField(
child: Builder(builder: (context) { key: const Key('search_accounts'),
return TextFormField( controller: searchController,
key: const Key('search_accounts'), focusNode: searchFocus,
controller: searchController, style: Theme.of(context).textTheme.titleSmall,
focusNode: searchFocus, decoration: const InputDecoration(
style: Theme.of(context).textTheme.titleSmall, hintText: 'Search accounts',
decoration: const InputDecoration( isDense: true,
hintText: 'Search accounts', prefixIcon: Icon(Icons.search_outlined),
isDense: true, prefixIconConstraints: BoxConstraints(
prefixIcon: Icon(Icons.search_outlined), minHeight: 30,
prefixIconConstraints: BoxConstraints( minWidth: 30,
minHeight: 30,
minWidth: 30,
),
border: InputBorder.none,
), ),
onChanged: (value) { border: InputBorder.none,
ref.read(searchProvider.notifier).setFilter(value); ),
}, onChanged: (value) {
textInputAction: TextInputAction.next, ref.read(searchProvider.notifier).setFilter(value);
onFieldSubmitted: (value) { },
Focus.of(context).focusInDirection(TraversalDirection.down); textInputAction: TextInputAction.next,
}, onFieldSubmitted: (value) {
); Focus.of(context).focusInDirection(TraversalDirection.down);
}), },
), );
actions: _buildActions(context, false), }),
child: AccountList(widget.devicePath, widget.oathState),
), ),
keyActions: _buildActions(
context,
used: credentials?.length ?? 0,
capacity: widget.oathState.version.isAtLeast(4) ? 32 : null,
),
child: AccountList(widget.devicePath, widget.oathState),
), ),
); );
} }
List<Widget> _buildActions(BuildContext context, bool isEmpty) { List<PopupMenuEntry> _buildActions(BuildContext context,
{required int used, int? capacity}) {
return [ return [
OutlinedButton.icon( buildMenuItem(
style: isEmpty ? AppTheme.primaryOutlinedButtonStyle(context) : null, title: const Text('Add account'),
label: const Text('Add account'), leading: const Icon(Icons.person_add_alt_1),
icon: const Icon(Icons.person_add_alt_1), trailing: capacity != null ? '$used/$capacity' : null,
onPressed: () { action: capacity == null || capacity > used
showDialog( ? () {
context: context, showBlurDialog(
builder: (context) => OathAddAccountPage(
widget.devicePath,
widget.oathState,
openQrScanner: Platform.isAndroid,
),
);
},
),
OutlinedButton.icon(
label: const Text('Options'),
icon: const Icon(Icons.tune),
onPressed: () {
showBottomMenu(context, [
MenuAction(
text:
widget.oathState.hasKey ? 'Manage password' : 'Set password',
icon: const Icon(Icons.password),
action: (context) {
showDialog(
context: context, context: context,
builder: (context) => builder: (context) => OathAddAccountPage(
ManagePasswordDialog(widget.devicePath, widget.oathState), widget.devicePath,
widget.oathState,
openQrScanner: Platform.isAndroid,
),
); );
}, }
), : null,
MenuAction(
text: 'Reset OATH',
icon: const Icon(Icons.delete),
action: (context) {
showDialog(
context: context,
builder: (context) => ResetDialog(widget.devicePath),
);
},
),
]);
},
), ),
buildMenuItem(
title: Text(
widget.oathState.hasKey ? 'Manage password' : 'Set password'),
leading: const Icon(Icons.password),
action: () {
showBlurDialog(
context: context,
builder: (context) =>
ManagePasswordDialog(widget.devicePath, widget.oathState),
);
}),
buildMenuItem(
title: const Text('Reset OATH'),
leading: const Icon(Icons.delete),
action: () {
showBlurDialog(
context: context,
builder: (context) => ResetDialog(widget.devicePath),
);
}),
]; ];
} }
} }

View File

@ -8,6 +8,7 @@ import '../../app/message.dart';
import '../../app/models.dart'; import '../../app/models.dart';
import '../../desktop/models.dart'; import '../../desktop/models.dart';
import '../../widgets/responsive_dialog.dart'; import '../../widgets/responsive_dialog.dart';
import '../../widgets/utf8_utils.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
import 'utils.dart'; import 'utils.dart';
@ -100,6 +101,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
initialValue: _issuer, initialValue: _issuer,
enabled: issuerRemaining > 0, enabled: issuerRemaining > 0,
maxLength: issuerRemaining > 0 ? issuerRemaining : null, maxLength: issuerRemaining > 0 ? issuerRemaining : null,
buildCounter: buildByteCounterFor(_issuer),
inputFormatters: [limitBytesLength(issuerRemaining)],
decoration: const InputDecoration( decoration: const InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Issuer (optional)', labelText: 'Issuer (optional)',
@ -115,6 +118,8 @@ class _RenameAccountDialogState extends ConsumerState<RenameAccountDialog> {
TextFormField( TextFormField(
initialValue: _account, initialValue: _account,
maxLength: nameRemaining, maxLength: nameRemaining,
inputFormatters: [limitBytesLength(nameRemaining)],
buildCounter: buildByteCounterFor(_account),
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: 'Account name', labelText: 'Account name',

View File

@ -1,5 +1,6 @@
import 'dart:math'; import 'dart:math';
import '../../widgets/utf8_utils.dart';
import '../models.dart'; import '../models.dart';
import '../../core/models.dart'; import '../../core/models.dart';
@ -18,7 +19,7 @@ Pair<int, int> getRemainingKeySpace(
// Non-standard TOTP periods are stored as part of this data, as a "D/"- prefix. // Non-standard TOTP periods are stored as part of this data, as a "D/"- prefix.
remaining -= '$period/'.length; remaining -= '$period/'.length;
} }
int issuerSpace = issuer.length; int issuerSpace = byteLength(issuer);
if (issuer.isNotEmpty) { if (issuer.isNotEmpty) {
// Issuer is separated from name with a ":", if present. // Issuer is separated from name with a ":", if present.
issuerSpace += 1; issuerSpace += 1;
@ -26,7 +27,7 @@ Pair<int, int> getRemainingKeySpace(
return Pair( return Pair(
// Always reserve at least one character for name // Always reserve at least one character for name
remaining - 1 - max(name.length, 1), remaining - 1 - max(byteLength(name), 1),
remaining - issuerSpace, remaining - issuerSpace,
); );
} }

View File

@ -1,22 +0,0 @@
import 'package:flutter/material.dart';
class DialogFrame extends StatelessWidget {
final Widget child;
const DialogFrame({super.key, required this.child});
@override
Widget build(BuildContext context) => GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
// Shows Snackbars above modal
child: Scaffold(
backgroundColor: Colors.transparent,
body: GestureDetector(
behavior: HitTestBehavior.deferToChild,
onTap: () {}, // Block onTap of parent gesture detector
child: child,
),
),
);
}

31
lib/widgets/menu_list_tile.dart Executable file
View File

@ -0,0 +1,31 @@
import 'dart:async';
import 'package:flutter/material.dart';
PopupMenuItem buildMenuItem({
required Widget title,
Widget? leading,
String? trailing,
void Function()? action,
}) =>
PopupMenuItem(
enabled: action != null,
onTap: () {
// Wait for popup menu to close before running action.
Timer.run(action!);
},
child: ListTile(
enabled: action != null,
dense: true,
contentPadding: EdgeInsets.zero,
minLeadingWidth: 0,
title: title,
leading: leading,
trailing: trailing != null
? Opacity(
opacity: 0.5,
child: Text(trailing, textScaleFactor: 0.7),
)
: null,
),
);

View File

@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dialog_frame.dart';
class ResponsiveDialog extends StatefulWidget { class ResponsiveDialog extends StatefulWidget {
final Widget? title; final Widget? title;
final Widget child; final Widget child;
@ -48,25 +46,23 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
final cancelText = widget.onCancel == null && widget.actions.isEmpty final cancelText = widget.onCancel == null && widget.actions.isEmpty
? 'Close' ? 'Close'
: 'Cancel'; : 'Cancel';
return DialogFrame( return AlertDialog(
child: AlertDialog( title: widget.title,
title: widget.title, scrollable: true,
scrollable: true, content: SizedBox(
content: SizedBox( width: 380,
width: 380, child: Container(key: _childKey, child: widget.child),
child: Container(key: _childKey, child: widget.child),
),
actions: [
TextButton(
child: Text(cancelText),
onPressed: () {
widget.onCancel?.call();
Navigator.of(context).pop();
},
),
...widget.actions
],
), ),
actions: [
TextButton(
child: Text(cancelText),
onPressed: () {
widget.onCancel?.call();
Navigator.of(context).pop();
},
),
...widget.actions
],
); );
} }
})); }));

126
lib/widgets/toast.dart Executable file
View File

@ -0,0 +1,126 @@
import 'dart:async';
import 'package:flutter/material.dart';
class Toast extends StatefulWidget {
final String message;
final Duration duration;
final void Function() onComplete;
final Color? backgroundColor;
final TextStyle? textStyle;
const Toast(
this.message,
this.duration, {
required this.onComplete,
this.backgroundColor,
this.textStyle,
super.key,
});
@override
State<StatefulWidget> createState() => _ToastState();
}
class _ToastState extends State<Toast> with SingleTickerProviderStateMixin {
late AnimationController _animator;
late Tween<double> _tween;
late Animation<double> _opacity;
@override
void initState() {
super.initState();
_animator = AnimationController(
duration: const Duration(milliseconds: 100), vsync: this);
_tween = Tween(begin: 0, end: 1);
_opacity = _tween.animate(_animator)
..addListener(() {
setState(() {});
});
_animate();
}
void _animate() async {
await _animator.forward();
if (mounted) {
await Future.delayed(widget.duration);
}
if (mounted) {
await _animator.reverse();
}
widget.onComplete();
}
@override
void dispose() {
_animator.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _opacity,
child: Material(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
color: widget.backgroundColor,
child: Center(
child: Text(
widget.message,
style: widget.textStyle,
)),
),
);
}
}
void Function() showToast(
BuildContext context,
String message, {
Duration duration = const Duration(seconds: 2),
}) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final bool isThemeDark = theme.brightness == Brightness.dark;
final Color backgroundColor = isThemeDark
? colorScheme.onSurface
: Color.alphaBlend(
colorScheme.onSurface.withOpacity(0.80), colorScheme.surface);
final textStyle =
ThemeData(brightness: isThemeDark ? Brightness.light : Brightness.dark)
.textTheme
.subtitle1;
OverlayEntry? entry;
void close() {
if (entry != null && entry.mounted) {
entry.remove();
}
}
entry = OverlayEntry(builder: (context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 50,
width: 400,
margin: const EdgeInsets.all(8),
child: Toast(
message,
duration,
backgroundColor: backgroundColor,
textStyle: textStyle,
onComplete: close,
),
),
);
});
Timer.run(() {
Overlay.of(context)!.insert(entry!);
});
return close;
}

29
lib/widgets/utf8_utils.dart Executable file
View File

@ -0,0 +1,29 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// Get the number of bytes used by a String when encoded to UTF-8.
int byteLength(String value) => utf8.encode(value).length;
/// Builds a counter widget showing number of bytes used/available.
///
/// Set this as a [TextField.buildCounter] callback to show the number of bytes
/// used rather than number of characters. [currentValue] should always match
/// the input text value to measure.
InputCounterWidgetBuilder buildByteCounterFor(String currentValue) =>
(context, {required currentLength, required isFocused, maxLength}) => Text(
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
style: Theme.of(context).textTheme.caption,
);
/// Limits the input in length based on the byte length when encoded.
/// This is generally used together with [buildByteCounterFor].
TextInputFormatter limitBytesLength(int maxByteLength) =>
TextInputFormatter.withFunction((oldValue, newValue) {
final newLength = byteLength(newValue.text);
if (newLength <= maxByteLength) {
return newValue;
}
return oldValue;
});

View File

@ -7,14 +7,14 @@ packages:
name: _fe_analyzer_shared name: _fe_analyzer_shared
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "38.0.0" version: "41.0.0"
analyzer: analyzer:
dependency: "direct overridden" dependency: transitive
description: description:
name: analyzer name: analyzer
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.4.1" version: "4.2.0"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -98,7 +98,7 @@ packages:
name: built_value name: built_value
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "8.3.2" version: "8.3.3"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@ -189,7 +189,7 @@ packages:
name: ffi name: ffi
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.1"
file: file:
dependency: transitive dependency: transitive
description: description:
@ -244,7 +244,7 @@ packages:
name: freezed name: freezed
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3+1" version: "2.0.4"
freezed_annotation: freezed_annotation:
dependency: "direct main" dependency: "direct main"
description: description:
@ -270,7 +270,7 @@ packages:
name: glob name: glob
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@ -284,7 +284,7 @@ packages:
name: http_multi_server name: http_multi_server
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.2.1"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@ -373,7 +373,7 @@ packages:
name: package_config name: package_config
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -408,7 +408,7 @@ packages:
name: pigeon name: pigeon
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.1.5" version: "3.2.3"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -429,7 +429,7 @@ packages:
name: pool name: pool
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0" version: "1.5.1"
process: process:
dependency: transitive dependency: transitive
description: description:
@ -534,14 +534,14 @@ packages:
name: shelf name: shelf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.3.1"
shelf_web_socket: shelf_web_socket:
dependency: transitive dependency: transitive
description: description:
name: shelf_web_socket name: shelf_web_socket
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" version: "1.0.2"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -644,7 +644,7 @@ packages:
name: url_launcher name: url_launcher
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.1.3" version: "6.1.4"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
@ -679,14 +679,14 @@ packages:
name: url_launcher_platform_interface name: url_launcher_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.5" version: "2.1.0"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.11" version: "2.0.12"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:

7
pubspec.yaml Executable file → Normal file
View File

@ -43,7 +43,7 @@ dependencies:
collection: ^1.16.0 collection: ^1.16.0
shared_preferences: ^2.0.12 shared_preferences: ^2.0.12
flutter_riverpod: ^1.0.0 flutter_riverpod: ^1.0.0
json_annotation: ^4.4.0 json_annotation: ^4.5.0
freezed_annotation: ^2.0.3 freezed_annotation: ^2.0.3
window_manager: ^0.2.0 window_manager: ^0.2.0
qrscanner_zxing: qrscanner_zxing:
@ -51,9 +51,6 @@ dependencies:
desktop_drop: ^0.3.3 desktop_drop: ^0.3.3
url_launcher: ^6.1.2 url_launcher: ^6.1.2
dependency_overrides:
analyzer: ^3.4.1
dev_dependencies: dev_dependencies:
integration_test: integration_test:
sdk: flutter sdk: flutter
@ -70,7 +67,7 @@ dev_dependencies:
build_runner: ^2.1.4 build_runner: ^2.1.4
freezed: ^2.0.3 freezed: ^2.0.3
json_serializable: ^6.0.0 json_serializable: ^6.0.0
pigeon: ^3.0.3 pigeon: ^3.1.6
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

BIN
resources/win/license.rtf Normal file

Binary file not shown.

View File

@ -0,0 +1,19 @@
$version=6.0.0-dev.0
echo "Renaming the Actions folder and moving it"
mv yubioath-desktop-* release
echo "Signing the executables"
signtool.exe sign /fd SHA256 /t http://timestamp.digicert.com/scripts/timstamp.dll release/authenticator.exe
signtool.exe sign /fd SHA256 /t http://timestamp.digicert.com/scripts/timstamp.dll release/helper/authenticator-helper.exe
echo "Setting env var and building installer"
$env:SRCDIR = ".\release\"
heat dir .\release -out fragment.wxs -gg -scom -srd -sfrag -dr INSTALLDIR -cg ApplicationFiles -var env.SRCDIR
candle .\fragment.wxs .\resources\win\yubioath-desktop.wxs -ext WixUtilExtension -arch x64
light fragment.wixobj yubioath-desktop.wixobj -ext WixUIExtension -ext WixUtilExtension -o yubioath-desktop-$version-win64.msi
echo "Signing the installer"
signtool.exe sign /d "Yubico Authenticator" /fd SHA256 /t http://timestamp.digicert.com/scripts/timstamp.dll yubioath-desktop-$version-win64.msi
echo "All done"

View File

@ -10,6 +10,7 @@
<WixVariable Id="WixUIDialogBmp" Value="resources\icons\yubico-msi-background.png" /> <WixVariable Id="WixUIDialogBmp" Value="resources\icons\yubico-msi-background.png" />
<WixVariable Id="WixUIBannerBmp" Value="resources\icons\yubico-msi-y-banner.png" /> <WixVariable Id="WixUIBannerBmp" Value="resources\icons\yubico-msi-y-banner.png" />
<WixVariable Id="WixUILicenseRtf" Value="license.rtf" />
<Icon Id="icon.ico" SourceFile="resources\icons\com.yubico.yubioath.ico"/> <Icon Id="icon.ico" SourceFile="resources\icons\com.yubico.yubioath.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" /> <Property Id="ARPPRODUCTICON" Value="icon.ico" />
@ -31,6 +32,11 @@
<UI> <UI>
<UIRef Id="WixUI_InstallDir" /> <UIRef Id="WixUI_InstallDir" />
<Publish Dialog="WelcomeDlg" <Publish Dialog="WelcomeDlg"
Control="Next"
Event="NewDialog"
Value="LicenseAgreementDlg"
Order="2">1</Publish>
<Publish Dialog="LicenseAgreementDlg"
Control="Next" Control="Next"
Event="NewDialog" Event="NewDialog"
Value="InstallDirDlg" Value="InstallDirDlg"

View File

@ -131,7 +131,15 @@ def update_helper_version(buf):
) )
return buf return buf
# release-win.ps1
def update_release_win(buf):
return sub1(
rf'\$version={version_pattern}',
f'$version={version}',
buf,
)
update_file("pubspec.yaml", update_pubspec) update_file("pubspec.yaml", update_pubspec)
update_file("windows/runner/Runner.rc", update_runner_rc) update_file("windows/runner/Runner.rc", update_runner_rc)
update_file("helper/version_info.txt", update_helper_version) update_file("helper/version_info.txt", update_helper_version)
update_file("resources/win/release-win.ps1", update_release_win)