Browse Source

feat: Add key backup reminder. Refactor Toast component to handle dark mode better

pull/360/head
Dan Ditomaso 1 year ago
parent
commit
f3a3741216
  1. 2
      package.json
  2. 214
      pnpm-lock.yaml
  3. 4
      src/App.tsx
  4. 2
      src/components/Dialog/DialogManager.tsx
  5. 13
      src/components/Dialog/PKIBackupDialog.tsx
  6. 21
      src/components/KeyBackupReminder.tsx
  7. 17
      src/components/Toaster.tsx
  8. 70
      src/components/UI/Toast.tsx
  9. 35
      src/core/hooks/useCookie.ts
  10. 85
      src/core/hooks/useKeyBackupReminder.tsx

2
package.json

@ -49,6 +49,7 @@
"cmdk": "^1.0.0",
"crypto-random-string": "^5.0.0",
"immer": "^10.1.1",
"js-cookie": "^3.0.5",
"lucide-react": "^0.363.0",
"mapbox-gl": "^3.6.0",
"maplibre-gl": "4.1.2",
@ -70,6 +71,7 @@
"@rsbuild/core": "^1.0.10",
"@rsbuild/plugin-react": "^1.0.3",
"@types/chrome": "^0.0.263",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.14.9",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",

214
pnpm-lock.yaml

@ -89,6 +89,9 @@ importers:
immer:
specifier: ^10.1.1
version: 10.1.1
js-cookie:
specifier: ^3.0.5
version: 3.0.5
lucide-react:
specifier: ^0.363.0
version: 0.363.0([email protected])
@ -127,7 +130,7 @@ importers:
version: 3.0.6([email protected])
vite-plugin-node-polyfills:
specifier: ^0.22.0
version: 0.22.0([email protected]4.0)([email protected](@types/[email protected]))
version: 0.22.0([email protected]9.1)([email protected](@types/[email protected]))
zustand:
specifier: 4.5.2
version: 4.5.2(@types/[email protected])([email protected])([email protected])
@ -147,6 +150,9 @@ importers:
'@types/chrome':
specifier: ^0.0.263
version: 0.0.263
'@types/js-cookie':
specifier: ^3.0.6
version: 3.0.6
'@types/node':
specifier: ^20.14.9
version: 20.14.9
@ -173,7 +179,7 @@ importers:
version: 8.4.38
rollup-plugin-visualizer:
specifier: ^5.12.0
version: 5.12.0([email protected]4.0)
version: 5.12.0([email protected]9.1)
tailwindcss:
specifier: ^3.4.4
version: 3.4.4
@ -1165,83 +1171,98 @@ packages:
rollup:
optional: true
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-ssKhA8RNltTZLpG6/QNkCSge+7mBQGUqJRisZ2MDQcEGaK93QESEgWK2iOpIDZ7k9zPVkG5AS3ksvD5ZWxmItw==}
cpu: [arm]
os: [android]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-CaRfrV0cd+NIIcVVN/jx+hVLN+VRqnuzLRmfmlzpOzB87ajixsN/+9L5xNmkaUUvEbI5BmIKS+XTwXsHEb65Ew==}
cpu: [arm64]
os: [android]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-2ORr7T31Y0Mnk6qNuwtyNmy14MunTAMx06VAPI6/Ju52W10zk1i7i5U3vlDRWjhOI5quBcrvhkCHyF76bI7kEw==}
cpu: [arm64]
os: [darwin]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-j/Ej1oanzPjmN0tirRd5K2/nncAhS9W6ICzgxV+9Y5ZsP0hiGhHJXZ2JQ53iSSjj8m6cRY6oB1GMzNn2EUt6Ng==}
cpu: [x64]
os: [darwin]
'@rollup/[email protected]':
resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==}
'@rollup/[email protected]':
resolution: {integrity: sha512-91C//G6Dm/cv724tpt7nTyP+JdN12iqeXGFM1SqnljCmi5yTXriH7B1r8AD9dAZByHpKAumqP1Qy2vVNIdLZqw==}
cpu: [arm64]
os: [freebsd]
'@rollup/[email protected]':
resolution: {integrity: sha512-hEioiEQ9Dec2nIRoeHUP6hr1PSkXzQaCUyqBDQ9I9ik4gCXQZjJMIVzoNLBRGet+hIUb3CISMh9KXuCcWVW/8w==}
cpu: [x64]
os: [freebsd]
'@rollup/[email protected]':
resolution: {integrity: sha512-Py5vFd5HWYN9zxBv3WMrLAXY3yYJ6Q/aVERoeUFwiDGiMOWsMs7FokXihSOaT/PMWUty/Pj60XDQndK3eAfE6A==}
cpu: [arm]
os: [linux]
'@rollup/[email protected]':
resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-RiWpGgbayf7LUcuSNIbahr0ys2YnEERD4gYdISA06wa0i8RALrnzflh9Wxii7zQJEB2/Eh74dX4y/sHKLWp5uQ==}
cpu: [arm]
os: [linux]
'@rollup/[email protected]':
resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-Z80O+taYxTQITWMjm/YqNoe9d10OX6kDh8X5/rFCMuPqsKsSyDilvfg+vd3iXIqtfmp+cnfL1UrYirkaF8SBZA==}
cpu: [arm64]
os: [linux]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-fOHRtF9gahwJk3QVp01a/GqS4hBEZCV1oKglVVq13kcK3NeVlS4BwIFzOHDbmKzt3i0OuHG4zfRP0YoG5OF/rA==}
cpu: [arm64]
os: [linux]
'@rollup/[email protected]':
resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==}
'@rollup/[email protected]':
resolution: {integrity: sha512-5a7q3tnlbcg0OodyxcAdrrCxFi0DgXJSoOuidFUzHZ2GixZXQs6Tc3CHmlvqKAmOs5eRde+JJxeIf9DonkmYkw==}
cpu: [loong64]
os: [linux]
'@rollup/[email protected]':
resolution: {integrity: sha512-9b4Mg5Yfz6mRnlSPIdROcfw1BU22FQxmfjlp/CShWwO3LilKQuMISMTtAu/bxmmrE6A902W2cZJuzx8+gJ8e9w==}
cpu: [ppc64]
os: [linux]
'@rollup/[email protected]':
resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-G5pn0NChlbRM8OJWpJFMX4/i8OEU538uiSv0P6roZcbpe/WfhEO+AT8SHVKfp8qhDQzaz7Q+1/ixMy7hBRidnQ==}
cpu: [riscv64]
os: [linux]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-WM9lIkNdkhVwiArmLxFXpWndFGuOka4oJOZh8EP3Vb8q5lzdSCBuhjavJsw68Q9AKDGeOOIHYzYm4ZFvmWez5g==}
cpu: [s390x]
os: [linux]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-87xYCwb0cPGZFoGiErT1eDcssByaLX4fc0z2nRM6eMtV9njAfEE6OW3UniAoDhX4Iq5xQVpE6qO9aJbCFumKYQ==}
cpu: [x64]
os: [linux]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-xufkSNppNOdVRCEC4WKvlR1FBDyqCSCpQeMMgv9ZyXqqtKBfkw1yfGMTUTs9Qsl6WQbJnsGboWCp7pJGkeMhKA==}
cpu: [x64]
os: [linux]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-F2OiJ42m77lSkizZQLuC+jiZ2cgueWQL5YC9tjo3AgaEw+KJmVxHGSyQfDUoYR9cci0lAywv2Clmckzulcq6ig==}
cpu: [arm64]
os: [win32]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-rYRe5S0FcjlOBZQHgbTKNrqxCBUmgDJem/VQTCcTnA2KCabYSWQDrytOzX7avb79cAAweNmMUb/Zw18RNd4mng==}
cpu: [ia32]
os: [win32]
'@rollup/[email protected]4.0':
resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==}
'@rollup/[email protected]9.1':
resolution: {integrity: sha512-+10CMg9vt1MoHj6x1pxyjPSMjHTIlqs8/tBztXvPAx24SKs9jwVnKqHJumlH/IzhaPUaj3T6T6wfZr8okdXaIg==}
cpu: [x64]
os: [win32]
@ -1690,6 +1711,9 @@ packages:
'@types/[email protected]':
resolution: {integrity: sha512-RpQH4rXLuvTXKR0zqHq3go0RVXYv/YVqv4TnPH95VbwUxZdQlK1EtcMvQvMpDngHbt13Csh9Z4qT9AbkiQH5BA==}
'@types/[email protected]':
resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==}
'@types/[email protected]':
resolution: {integrity: sha512-hI6cQDjw1bkJw7MC/eHMqq5TWUamLwsujnUUeiIX2KDRjxRNSYMjnHz07+LATz9I9XIsKumOtUz4gRYnZOJ/FA==}
@ -1864,8 +1888,8 @@ packages:
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
engines: {node: '>= 6'}
[email protected]38:
resolution: {integrity: sha512-5SuJUJ7cZnhPpeLHaH0c/HPAnAHZvS6ElWyHK9GSIbVOQABLzowiI2pjmpvZ1WEbkyz46iFd4UXlOHR5SqgfMQ==}
[email protected]90:
resolution: {integrity: sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==}
[email protected]:
resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==}
@ -2417,6 +2441,10 @@ packages:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
[email protected]:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
[email protected]:
resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==}
@ -2923,8 +2951,8 @@ packages:
rollup:
optional: true
[email protected]4.0:
resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==}
[email protected]9.1:
resolution: {integrity: sha512-RaJ45M/kmJUzSWDs1Nnd5DdV4eerC98idtUOVr6FfKcgxqvjwHmxc5upLF9qZU9EpsVzzhleFahrT3shLuJzIw==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@ -4258,68 +4286,77 @@ snapshots:
'@radix-ui/[email protected]': {}
'@rollup/[email protected]([email protected]4.0)':
'@rollup/[email protected]([email protected]9.1)':
dependencies:
'@rollup/pluginutils': 5.1.0([email protected]4.0)
'@rollup/pluginutils': 5.1.0([email protected]9.1)
estree-walker: 2.0.2
magic-string: 0.30.11
optionalDependencies:
rollup: 4.24.0
rollup: 4.29.1
'@rollup/[email protected]([email protected]4.0)':
'@rollup/[email protected]([email protected]9.1)':
dependencies:
'@types/estree': 1.0.5
estree-walker: 2.0.2
picomatch: 2.3.1
optionalDependencies:
rollup: 4.24.0
rollup: 4.29.1
'@rollup/[email protected]':
optional: true
'@rollup/[email protected]':
optional: true
'@rollup/[email protected]':
optional: true
'@rollup/[email protected]':
'@rollup/rollup-[email protected]':
optional: true
'@rollup/[email protected]':
'@rollup/rollup-[email protected]':
optional: true
'@rollup/[email protected]':
'@rollup/rollup-[email protected]':
optional: true
'@rollup/rollup-[email protected]':
'@rollup/rollup-[email protected]':
optional: true
'@rollup/rollup-linux-arm-[email protected]':
'@rollup/rollup-linux-arm-[email protected]':
optional: true
'@rollup/rollup-linux-arm[email protected]':
'@rollup/rollup-linux-arm[email protected]':
optional: true
'@rollup/rollup-linux-arm64-[email protected]':
'@rollup/rollup-linux-arm64-[email protected]':
optional: true
'@rollup/rollup-linux-[email protected]':
'@rollup/rollup-linux-[email protected]':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rollup/[email protected]4.0':
'@rollup/[email protected]9.1':
optional: true
'@rsbuild/[email protected]':
@ -4381,7 +4418,7 @@ snapshots:
'@module-federation/runtime-tools': 0.5.1
'@rspack/binding': 1.0.8
'@rspack/lite-tapable': 1.0.1
caniuse-lite: 1.0.30001638
caniuse-lite: 1.0.30001690
optionalDependencies:
'@swc/helpers': 0.5.13
@ -5255,6 +5292,8 @@ snapshots:
'@types/[email protected]': {}
'@types/[email protected]': {}
'@types/[email protected]':
dependencies:
'@types/geojson': 7946.0.14
@ -5343,7 +5382,7 @@ snapshots:
[email protected]([email protected]):
dependencies:
browserslist: 4.23.1
caniuse-lite: 1.0.30001638
caniuse-lite: 1.0.30001690
fraction.js: 4.3.7
normalize-range: 0.1.2
picocolors: 1.0.1
@ -5424,7 +5463,7 @@ snapshots:
[email protected]:
dependencies:
caniuse-lite: 1.0.30001638
caniuse-lite: 1.0.30001690
electron-to-chromium: 1.4.812
node-releases: 2.0.14
update-browserslist-db: 1.0.16([email protected])
@ -5459,7 +5498,7 @@ snapshots:
[email protected]: {}
[email protected]38: {}
[email protected]90: {}
[email protected]: {}
@ -6075,6 +6114,8 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
@ -6620,35 +6661,38 @@ snapshots:
[email protected]: {}
[email protected]([email protected]4.0):
[email protected]([email protected]9.1):
dependencies:
open: 8.4.2
picomatch: 2.3.1
source-map: 0.7.4
yargs: 17.7.2
optionalDependencies:
rollup: 4.24.0
rollup: 4.29.1
[email protected]4.0:
[email protected]9.1:
dependencies:
'@types/estree': 1.0.6
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.24.0
'@rollup/rollup-android-arm64': 4.24.0
'@rollup/rollup-darwin-arm64': 4.24.0
'@rollup/rollup-darwin-x64': 4.24.0
'@rollup/rollup-linux-arm-gnueabihf': 4.24.0
'@rollup/rollup-linux-arm-musleabihf': 4.24.0
'@rollup/rollup-linux-arm64-gnu': 4.24.0
'@rollup/rollup-linux-arm64-musl': 4.24.0
'@rollup/rollup-linux-powerpc64le-gnu': 4.24.0
'@rollup/rollup-linux-riscv64-gnu': 4.24.0
'@rollup/rollup-linux-s390x-gnu': 4.24.0
'@rollup/rollup-linux-x64-gnu': 4.24.0
'@rollup/rollup-linux-x64-musl': 4.24.0
'@rollup/rollup-win32-arm64-msvc': 4.24.0
'@rollup/rollup-win32-ia32-msvc': 4.24.0
'@rollup/rollup-win32-x64-msvc': 4.24.0
'@rollup/rollup-android-arm-eabi': 4.29.1
'@rollup/rollup-android-arm64': 4.29.1
'@rollup/rollup-darwin-arm64': 4.29.1
'@rollup/rollup-darwin-x64': 4.29.1
'@rollup/rollup-freebsd-arm64': 4.29.1
'@rollup/rollup-freebsd-x64': 4.29.1
'@rollup/rollup-linux-arm-gnueabihf': 4.29.1
'@rollup/rollup-linux-arm-musleabihf': 4.29.1
'@rollup/rollup-linux-arm64-gnu': 4.29.1
'@rollup/rollup-linux-arm64-musl': 4.29.1
'@rollup/rollup-linux-loongarch64-gnu': 4.29.1
'@rollup/rollup-linux-powerpc64le-gnu': 4.29.1
'@rollup/rollup-linux-riscv64-gnu': 4.29.1
'@rollup/rollup-linux-s390x-gnu': 4.29.1
'@rollup/rollup-linux-x64-gnu': 4.29.1
'@rollup/rollup-linux-x64-musl': 4.29.1
'@rollup/rollup-win32-arm64-msvc': 4.29.1
'@rollup/rollup-win32-ia32-msvc': 4.29.1
'@rollup/rollup-win32-x64-msvc': 4.29.1
fsevents: 2.3.3
[email protected]:
@ -6986,9 +7030,9 @@ snapshots:
[email protected]: {}
[email protected]([email protected]4.0)([email protected](@types/[email protected])):
[email protected]([email protected]9.1)([email protected](@types/[email protected])):
dependencies:
'@rollup/plugin-inject': 5.0.5([email protected]4.0)
'@rollup/plugin-inject': 5.0.5([email protected]9.1)
node-stdlib-browser: 1.2.0
vite: 5.3.6(@types/[email protected])
transitivePeerDependencies:
@ -6998,7 +7042,7 @@ snapshots:
dependencies:
esbuild: 0.21.5
postcss: 8.4.49
rollup: 4.24.0
rollup: 4.29.1
optionalDependencies:
'@types/node': 20.14.9
fsevents: 2.3.3

4
src/App.tsx

@ -2,7 +2,7 @@ import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
import { PageRouter } from "@app/PageRouter.tsx";
import { CommandPalette } from "@components/CommandPalette.tsx";
import { DeviceSelector } from "@components/DeviceSelector.tsx";
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
import { DialogManager } from "@components/Dialog/DialogManager";
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.tsx";
import { Toaster } from "@components/Toaster.tsx";
import Footer from "@components/UI/Footer.tsx";
@ -11,6 +11,7 @@ import { useAppStore } from "@core/stores/appStore.ts";
import { useDeviceStore } from "@core/stores/deviceStore.ts";
import { Dashboard } from "@pages/Dashboard/index.tsx";
import { MapProvider } from "react-map-gl";
import { KeyBackupReminder } from "@components/KeyBackupReminder";
export const App = (): JSX.Element => {
const { getDevice } = useDeviceStore();
@ -37,6 +38,7 @@ export const App = (): JSX.Element => {
{device ? (
<div className="flex h-screen">
<DialogManager />
<KeyBackupReminder />
<CommandPalette />
<PageRouter />
</div>

2
src/components/Dialog/DialogManager.tsx

@ -5,7 +5,7 @@ import { QRDialog } from "@components/Dialog/QRDialog.tsx";
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
import { useDevice } from "@core/stores/deviceStore.ts";
import { PkiBackupDialog } from "./PKIBackupDialog";
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog";
export const DialogManager = (): JSX.Element => {
const { channels, config, dialog, setDialogOpen } = useDevice();

13
src/components/Dialog/PKIBackupDialog.tsx

@ -21,7 +21,7 @@ export const PkiBackupDialog = ({
open,
onOpenChange,
}: PkiBackupDialogProps) => {
const { config } = useDevice();
const { config, setDialogOpen } = useDevice();
const privateKeyData = config.security?.privateKey
// If the private data doesn't exist return null
@ -31,6 +31,10 @@ export const PkiBackupDialog = ({
const getPrivateKey = React.useMemo(() => fromByteArray(config.security?.privateKey ?? new Uint8Array(0)), [config.security?.privateKey]);
const closeDialog = React.useCallback(() => {
setDialogOpen("pkiBackup", false)
}, [setDialogOpen])
const renderPrintWindow = React.useCallback(() => {
const printWindow = window.open("", "_blank");
if (printWindow) {
@ -52,8 +56,10 @@ export const PkiBackupDialog = ({
`);
printWindow.document.close();
printWindow.print();
closeDialog()
}
}, [getPrivateKey]);
}, [getPrivateKey, closeDialog]);
const createDownloadKeyFile = React.useCallback(() => {
const blob = new Blob([getPrivateKey], { type: "text/plain" });
@ -65,8 +71,9 @@ export const PkiBackupDialog = ({
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
closeDialog()
URL.revokeObjectURL(url);
}, [getPrivateKey]);
}, [getPrivateKey, closeDialog]);
return (

21
src/components/KeyBackupReminder.tsx

@ -0,0 +1,21 @@
import { useBackupReminder } from "@app/core/hooks/useKeyBackupReminder";
import { useDevice } from "@app/core/stores/deviceStore";
export const KeyBackupReminder = (): JSX.Element => {
const { setDialogOpen } = useDevice();
useBackupReminder({
suppressDays: 7,
message: "We recommend backing up your key data regularly. Would you like to back up now?",
onAccept: () => setDialogOpen("pkiBackup", true),
cookieOptions: {
secure: true,
sameSite: 'strict'
}
});
return (
<>
</>
);
};

17
src/components/Toaster.tsx

@ -1,5 +1,3 @@
import { useToast } from "@core/hooks/useToast.ts";
import {
Toast,
ToastClose,
@ -7,7 +5,8 @@ import {
ToastProvider,
ToastTitle,
ToastViewport,
} from "@components/UI/Toast.tsx";
} from "@components/UI/Toast";
import { useToast } from "@core/hooks/useToast";
export function Toaster() {
const { toasts } = useToast();
@ -15,16 +14,10 @@ export function Toaster() {
return (
<ToastProvider>
{toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}>
<Toast key={id} {...props} className="flex flex-col gap-4">
<div className="grid gap-1">
{title && (
<ToastTitle className="dark:text-white">{title}</ToastTitle>
)}
{description && (
<ToastDescription className="dark:text-white-400">
{description}
</ToastDescription>
)}
{title && <ToastTitle>{title}</ToastTitle>}
{description && <ToastDescription>{description}</ToastDescription>}
</div>
{action}
<ToastClose />

70
src/components/UI/Toast.tsx

@ -1,11 +1,11 @@
import * as ToastPrimitives from "@radix-ui/react-toast";
import { type VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from 'lucide-react'
import { cn } from "@core/utils/cn.ts";
import { cn } from "@core/utils/cn"
const ToastProvider = ToastPrimitives.Provider;
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
@ -14,35 +14,34 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-50 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-24 sm:right-6 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"data-[swipe=move]:transition-none grow-1 group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mt-4 data-[state=closed]:slide-out-to-right-full dark:border-slate-700 last:mt-0 sm:last:mt-4",
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default:
"bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700",
default: "border bg-background text-foreground dark:bg-slate-700 dark:border-slate-600 dark:text-slate-50",
destructive:
"group destructive bg-red-600 text-white border-red-600 dark:border-red-600",
"group destructive bg-red-600 text-white dark:border-red-900 dark:bg-red-900 dark:text-red-50"
},
},
defaultVariants: {
variant: "default",
},
},
);
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
@ -50,9 +49,9 @@ const Toast = React.forwardRef<
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
@ -61,13 +60,13 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-red-100 group-[.destructive]:hover:border-slate-50 group-[.destructive]:hover:bg-red-100 group-[.destructive]:hover:text-red-600 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:hover:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800",
className,
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
@ -76,16 +75,16 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-slate-500 opacity-0 transition-opacity hover:text-slate-900 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:hover:text-slate-50",
className,
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:text-slate-400 dark:hover:text-slate-50",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
@ -96,8 +95,8 @@ const ToastTitle = React.forwardRef<
className={cn("text-sm font-semibold", className)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
@ -108,12 +107,12 @@ const ToastDescription = React.forwardRef<
className={cn("text-sm opacity-90", className)}
{...props}
/>
));
ToastDescription.displayName = ToastPrimitives.Description.displayName;
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>;
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
@ -125,4 +124,5 @@ export {
ToastDescription,
ToastClose,
ToastAction,
};
}

35
src/core/hooks/useCookie.ts

@ -0,0 +1,35 @@
import React from "react";
import Cookies, { type CookieAttributes } from "js-cookie";
type Cookie<T> = [
T | undefined,
(value: T, options?: CookieAttributes) => void,
() => void,
];
const useCookie = <T>(
cookieName: string,
initialValue?: T,
): Cookie<T> => {
const [cookieValue, setCookieValue] = React.useState<T | undefined>(() => {
const cookie = Cookies.get(cookieName);
return cookie ? (JSON.parse(cookie) as T) : initialValue;
});
const setCookie = React.useCallback(
(value: T, options?: CookieAttributes) => {
Cookies.set(cookieName, JSON.stringify(value), options);
setCookieValue(value);
},
[cookieName],
);
const removeCookie = React.useCallback(() => {
Cookies.remove(cookieName);
setCookieValue(undefined);
}, [cookieName]);
return [cookieValue, setCookie, removeCookie];
};
export default useCookie;

85
src/core/hooks/useKeyBackupReminder.tsx

@ -0,0 +1,85 @@
import { useEffect, useCallback } from 'react';
import { useToast } from './useToast';
import useCookie from './useCookie';
import type { CookieAttributes } from 'js-cookie';
import { Button } from '@app/components/UI/Button';
interface UseBackupReminderOptions {
suppressDays?: number;
message?: string;
onAccept?: () => void | Promise<void>;
cookieName?: string;
cookieOptions?: CookieAttributes;
}
interface ReminderState {
suppressed: boolean;
lastShown: string;
}
export function useBackupReminder({
suppressDays = 365,
message = "It's time to back up your key data. Would you like to do this now?",
onAccept = () => { },
cookieName = "backup_reminder_state",
cookieOptions = {},
}: UseBackupReminderOptions = {}) {
const { toast } = useToast();
const [reminderState, setReminderState, resetReminderState] = useCookie<ReminderState>(cookieName);
const suppressReminder = useCallback(() => {
const expiryDate = new Date();
expiryDate.setDate(expiryDate.getDate() + suppressDays);
setReminderState(
{
suppressed: true,
lastShown: new Date().toISOString(),
},
{
...cookieOptions,
expires: expiryDate,
}
);
}, [setReminderState, suppressDays, cookieOptions]);
useEffect(() => {
if (!reminderState) {
const { dismiss: dimissToast } = toast({
title: "Backup Reminder",
description: message,
action: (
<div className="flex gap-2">
<Button
type="button"
variant={"default"}
onClick={async () => {
await onAccept();
dimissToast()
suppressReminder();
}}
>
Back up now
</Button>
<Button
type="button"
variant={"outline"}
onClick={() => {
dimissToast();
suppressReminder();
}}
>
Remind me later
</Button>
</div>
),
});
}
}, [reminderState]);
return {
resetReminder: resetReminderState
};
}
Loading…
Cancel
Save