瀏覽代碼

Merge branch 'main' of https://github.com/ente-io/auth into mobile_face

Neeraj Gupta 1 年之前
父節點
當前提交
fb3a77bf94
共有 100 個文件被更改,包括 1838 次插入1248 次删除
  1. 二進制
      .github/assets/github-badge.png
  2. 2 2
      .github/workflows/auth-crowdin.yml
  3. 2 2
      .github/workflows/auth-lint.yml
  4. 14 20
      .github/workflows/auth-release.yml
  5. 24 0
      .github/workflows/copycat-db-release.yaml
  6. 1 1
      .github/workflows/docs-verify-build.yml
  7. 2 2
      .github/workflows/mobile-crowdin.yml
  8. 1 1
      .github/workflows/mobile-lint.yml
  9. 1 1
      .github/workflows/server-lint.yml
  10. 38 0
      .github/workflows/server-publish.yml
  11. 4 4
      .github/workflows/server-release.yml
  12. 2 2
      .github/workflows/web-crowdin.yml
  13. 43 0
      .github/workflows/web-deploy-payments.yml
  14. 1 1
      .github/workflows/web-lint.yml
  15. 13 0
      .github/workflows/web-nightly.yml
  16. 1 0
      .github/workflows/web-preview.yml
  17. 1 1
      README.md
  18. 6 0
      auth/.gitignore
  19. 20 11
      auth/.metadata
  20. 7 5
      auth/README.md
  21. 8 7
      auth/android/app/build.gradle
  22. 54 7
      auth/assets/custom-icons/_data/custom-icons.json
  23. 47 0
      auth/assets/custom-icons/icons/CERN.svg
  24. 6 0
      auth/assets/custom-icons/icons/configcat.svg
  25. 9 0
      auth/assets/custom-icons/icons/dcs.svg
  26. 6 0
      auth/assets/custom-icons/icons/habbo.svg
  27. 6 0
      auth/assets/custom-icons/icons/mercado_livre.svg
  28. 0 0
      auth/assets/custom-icons/icons/rockstar_games.svg
  29. 6 0
      auth/assets/custom-icons/icons/sendgrid.svg
  30. 1 0
      auth/assets/custom-icons/icons/wyze.svg
  31. 0 0
      auth/assets/generation-icons/icon-light-adaptive-fg.png
  32. 0 0
      auth/assets/generation-icons/icon-light.png
  33. 二進制
      auth/assets/icons/auth-icon.ico
  34. 二進制
      auth/assets/icons/auth-icon.png
  35. 25 0
      auth/distribute_options.yaml
  36. 1 1
      auth/fastlane/metadata/android/en-US/full_description.txt
  37. 1 1
      auth/fastlane/metadata/android/en-US/title.txt
  38. 6 6
      auth/fdroid_flutter_icons.yaml
  39. 93 59
      auth/ios/Podfile.lock
  40. 1 1
      auth/ios/Runner.xcodeproj/project.pbxproj
  41. 1 1
      auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  42. 12 2
      auth/ios/Runner/AppDelegate.swift
  43. 4 0
      auth/ios/Runner/Info.plist
  44. 61 2
      auth/lib/app/view/app.dart
  45. 0 4
      auth/lib/app/view/app_route.dart
  46. 0 163
      auth/lib/app/view/app_theme_extension.dart
  47. 2 0
      auth/lib/bootstrap.dart
  48. 56 51
      auth/lib/core/configuration.dart
  49. 2 1
      auth/lib/core/constants.dart
  50. 4 8
      auth/lib/core/errors.dart
  51. 5 5
      auth/lib/core/logging/super_logging.dart
  52. 7 7
      auth/lib/core/logging/tunneled_transport.dart
  53. 22 14
      auth/lib/core/network.dart
  54. 85 37
      auth/lib/ente_theme_data.dart
  55. 2 2
      auth/lib/gateway/authenticator.dart
  56. 2 0
      auth/lib/l10n/arb/app_de.arb
  57. 5 0
      auth/lib/l10n/arb/app_en.arb
  58. 1 0
      auth/lib/l10n/arb/app_ko.arb
  59. 13 1
      auth/lib/l10n/arb/app_nl.arb
  60. 18 1
      auth/lib/l10n/arb/app_pl.arb
  61. 5 3
      auth/lib/l10n/arb/app_pt.arb
  62. 2 0
      auth/lib/l10n/arb/app_zh.arb
  63. 63 6
      auth/lib/main.dart
  64. 16 30
      auth/lib/models/code.dart
  65. 0 9
      auth/lib/models/derived_key_result.dart
  66. 0 15
      auth/lib/models/encryption_result.dart
  67. 115 102
      auth/lib/onboarding/view/onboarding_page.dart
  68. 4 4
      auth/lib/onboarding/view/setup_enter_secret_key_page.dart
  69. 12 6
      auth/lib/onboarding/view/view_qr_page.dart
  70. 25 24
      auth/lib/services/authenticator_service.dart
  71. 12 12
      auth/lib/services/billing_service.dart
  72. 14 2
      auth/lib/services/local_authentication_service.dart
  73. 1 2
      auth/lib/services/notification_service.dart
  74. 6 1
      auth/lib/services/update_service.dart
  75. 2 2
      auth/lib/services/user_remote_flag_service.dart
  76. 23 23
      auth/lib/services/user_service.dart
  77. 36 0
      auth/lib/services/window_listener_service.dart
  78. 13 1
      auth/lib/store/authenticator_db.dart
  79. 13 2
      auth/lib/store/offline_authenticator_db.dart
  80. 1 0
      auth/lib/theme/colors.dart
  81. 1 1
      auth/lib/ui/account/change_email_dialog.dart
  82. 10 6
      auth/lib/ui/account/delete_account_page.dart
  83. 15 29
      auth/lib/ui/account/email_entry_page.dart
  84. 43 50
      auth/lib/ui/account/login_page.dart
  85. 13 9
      auth/lib/ui/account/login_pwd_verification_page.dart
  86. 24 17
      auth/lib/ui/account/ott_verification_page.dart
  87. 220 207
      auth/lib/ui/account/password_entry_page.dart
  88. 26 18
      auth/lib/ui/account/password_reentry_page.dart
  89. 159 69
      auth/lib/ui/account/recovery_key_page.dart
  90. 42 43
      auth/lib/ui/account/recovery_page.dart
  91. 10 8
      auth/lib/ui/account/request_pwd_verification_page.dart
  92. 1 1
      auth/lib/ui/account/sessions_page.dart
  93. 10 9
      auth/lib/ui/account/verify_recovery_page.dart
  94. 3 3
      auth/lib/ui/code_timer_progress.dart
  95. 125 82
      auth/lib/ui/code_widget.dart
  96. 1 2
      auth/lib/ui/common/bottom_shadow.dart
  97. 2 4
      auth/lib/ui/common/divider_with_padding.dart
  98. 3 2
      auth/lib/ui/common/dynamic_fab.dart
  99. 9 5
      auth/lib/ui/common/gradient_button.dart
  100. 3 5
      auth/lib/ui/common/linear_progress_dialog.dart

二進制
.github/assets/github-badge.png


+ 2 - 2
.github/workflows/auth-crowdin.yml

@@ -2,12 +2,12 @@ name: "Sync Crowdin translations (auth)"
 
 on:
     push:
+        branches: [main]
         paths:
             # Run workflow when auth's intl_en.arb is changed
             - "mobile/lib/l10n/arb/app_en.arb"
             # Or the workflow itself is changed
             - ".github/workflows/auth-crowdin.yml"
-        branches: [main]
     schedule:
         # See: [Note: Run workflow on specific days of the week]
         - cron: "50 1 * * 2,5"
@@ -28,7 +28,7 @@ jobs:
                   base_path: "auth/"
                   config: "auth/crowdin.yml"
                   upload_sources: true
-                  upload_translations: true
+                  upload_translations: false
                   download_translations: true
                   localization_branch_name: crowdin-translations-auth
                   create_pull_request: true

+ 2 - 2
.github/workflows/auth-lint.yml

@@ -3,13 +3,13 @@ name: "Lint (auth)"
 on:
     # Run on every push to a branch other than main that changes auth/
     push:
-        branches-ignore: [main]
+        branches-ignore: [main, "deploy/**"]
         paths:
             - "auth/**"
             - ".github/workflows/auth-lint.yml"
 
 env:
-    FLUTTER_VERSION: "3.16.9"
+    FLUTTER_VERSION: "3.19.3"
 
 jobs:
     lint:

+ 14 - 20
.github/workflows/auth-release.yml

@@ -29,11 +29,11 @@ on:
             - "auth-v*"
 
 env:
-    FLUTTER_VERSION: "3.13.4"
+    FLUTTER_VERSION: "3.19.3"
 
 jobs:
     build-ubuntu:
-        runs-on: ubuntu-latest
+        runs-on: ubuntu-20.04
 
         defaults:
             run:
@@ -72,6 +72,8 @@ jobs:
                   SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
 
             - name: Build PlayStore AAB
+              # disable this step if release tag contains nightly or beta
+              if: startsWith(github.ref, 'refs/tags/auth-v') && !contains(github.ref, 'nightly') && !contains(github.ref, 'beta')
               run: |
                   flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore
               env:
@@ -83,7 +85,7 @@ jobs:
             - name: Install dependencies for desktop build
               run: |
                   sudo apt-get update -y
-                  sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate
+                  sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi7
 
             - name: Install appimagetool
               run: |
@@ -92,8 +94,6 @@ jobs:
                   mv appimagetool /usr/local/bin/
 
             - name: Build desktop app
-              # Temporarily disable desktop builds
-              if: false
               run: |
                   flutter config --enable-linux-desktop
                   dart pub global activate flutter_distributor
@@ -118,6 +118,8 @@ jobs:
                   updateOnlyUnreleased: true
 
             - name: Upload AAB to PlayStore
+              # disable this step if release tag contains nightly or beta
+              if: startsWith(github.ref, 'refs/tags/auth-v') && !contains(github.ref, 'nightly') && !contains(github.ref, 'beta')
               uses: r0adkll/upload-google-play@v1
               with:
                   serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
@@ -149,8 +151,6 @@ jobs:
               run: mkdir artifacts
 
             - name: Build Windows installer
-              # Temporarily disable desktop builds
-              if: false
               run: |
                   flutter config --enable-windows-desktop
                   dart pub global activate flutter_distributor
@@ -159,13 +159,9 @@ jobs:
                   mv dist/**/ente_auth-*-windows-setup.exe artifacts/ente-${{ github.ref_name }}-installer.exe
 
             - name: Retain Windows EXE and DLLs
-              # Temporarily disable desktop builds
-              if: false
               run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows
 
             - name: Code sign Windows installer and EXE
-              # Temporarily disable desktop builds
-              if: false
               uses: dlemstra/code-sign-action@v1
               with:
                   certificate: "${{ secrets.WINDOWS_CERTIFICATE }}"
@@ -175,9 +171,10 @@ jobs:
                       auth/ente-${{ github.ref_name }}-windows/auth.exe
 
             - name: Zip Windows EXE and DLLs
-              # Temporarily disable desktop builds
-              if: false
-              run: tar.exe -a -c -f auth/artifacts/ente-${{ github.ref_name }}-windows.zip auth/ente-${{ github.ref_name }}-windows
+              run: tar.exe -a -c -f artifacts/ente-${{ github.ref_name }}-windows.zip ente-${{ github.ref_name }}-windows
+
+            - name: Generate checksums
+              run: sha256sum artifacts/ente-* > artifacts/sha256sum-windows
 
             - name: Create a draft GitHub release
               uses: ncipollo/release-action@v1
@@ -248,8 +245,6 @@ jobs:
               run: mkdir artifacts
 
             - name: Build macOS DMG
-              # Temporarily disable desktop builds
-              if: false
               run: |
                   flutter config --enable-macos-desktop
                   dart pub global activate flutter_distributor
@@ -257,16 +252,12 @@ jobs:
                   mv dist/**/ente_auth-*-macos.dmg artifacts/ente-${{ github.ref_name }}.dmg
 
             - name: Code sign DMG
-              # Temporarily disable desktop builds
-              if: false
               run: |
                   CERT_NAME=$(security find-identity -v -p codesigning | grep "Developer ID Application" | awk -F'"' '{print $2}' | grep -m1 "")
                   codesign --force --timestamp --sign "$CERT_NAME" --options runtime artifacts/ente-${{ github.ref_name }}.dmg
                   codesign --verify --verbose=4 artifacts/ente-${{ github.ref_name }}.dmg
 
             - name: Notarize and staple DMG
-              # Temporarily disable desktop builds
-              if: false
               run: |
                   xcrun notarytool submit artifacts/ente-${{ github.ref_name }}.dmg \
                     --wait \
@@ -279,6 +270,9 @@ jobs:
                   APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
                   APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
 
+            - name: Generate checksums
+              run: shasum -a 256 artifacts/ente-* > artifacts/sha256sum-macos
+
             - name: Create a draft GitHub release
               uses: ncipollo/release-action@v1
               with:

+ 24 - 0
.github/workflows/copycat-db-release.yaml

@@ -0,0 +1,24 @@
+name: "Release (copycat-db)"
+
+on:
+    workflow_dispatch: # Run manually
+
+jobs:
+    build:
+        runs-on: ubuntu-latest
+        steps:
+            - uses: actions/checkout@v4
+              name: Check out code
+
+            - uses: mr-smithers-excellent/docker-build-push@v6
+              name: Build & Push
+              with:
+                  dockerfile: infra/copycat-db/Dockerfile
+                  directory: infra/copycat-db
+                  image: ente/copycat-db
+                  registry: rg.fr-par.scw.cloud
+                  enableBuildKit: true
+                  buildArgs: GIT_COMMIT=${GITHUB_SHA}
+                  tags: ${GITHUB_SHA}, latest
+                  username: ${{ secrets.DOCKER_USERNAME }}
+                  password: ${{ secrets.DOCKER_PASSWORD }}

+ 1 - 1
.github/workflows/docs-verify-build.yml

@@ -6,7 +6,7 @@ name: "Verify build (docs)"
 on:
     # Run on every push to a branch other than main that changes docs/
     push:
-        branches-ignore: [main]
+        branches-ignore: [main, "deploy/**"]
         paths:
             - "docs/**"
             - ".github/workflows/docs-verify-build.yml"

+ 2 - 2
.github/workflows/mobile-crowdin.yml

@@ -2,12 +2,12 @@ name: "Sync Crowdin translations (mobile)"
 
 on:
     push:
+        branches: [main]
         paths:
             # Run workflow when mobiles's intl_en.arb is changed
             - "mobile/lib/l10n/intl_en.arb"
             # Or the workflow itself is changed
             - ".github/workflows/mobile-crowdin.yml"
-        branches: [main]
     schedule:
         # See: [Note: Run workflow on specific days of the week]
         - cron: "40 1 * * 2,5"
@@ -28,7 +28,7 @@ jobs:
                   base_path: "mobile/"
                   config: "mobile/crowdin.yml"
                   upload_sources: true
-                  upload_translations: true
+                  upload_translations: false
                   download_translations: true
                   localization_branch_name: crowdin-translations-mobile
                   create_pull_request: true

+ 1 - 1
.github/workflows/mobile-lint.yml

@@ -3,7 +3,7 @@ name: "Lint (mobile)"
 on:
     # Run on every push to a branch other than main that changes mobile/
     push:
-        branches-ignore: [main, f-droid]
+        branches-ignore: [main, f-droid, "deploy/**"]
         paths:
             - "mobile/**"
             - ".github/workflows/mobile-lint.yml"

+ 1 - 1
.github/workflows/server-lint.yml

@@ -3,7 +3,7 @@ name: "Lint (server)"
 on:
     # Run on every push to a branch other than main that changes server/
     push:
-        branches-ignore: [main]
+        branches-ignore: [main, "deploy/**"]
         paths:
             - "server/**"
             - ".github/workflows/server-lint.yml"

+ 38 - 0
.github/workflows/server-publish.yml

@@ -0,0 +1,38 @@
+name: "Publish (server)"
+
+on:
+    # Run manually, providing it the commit.
+    #
+    # To obtain the commit from the currently deployed museum, do:
+    # curl -s https://api.ente.io/ping | jq -r '.id'
+    #
+    # See server/docs/publish.md for more details.
+    workflow_dispatch:
+        inputs:
+            commit:
+                description: "Commit to publish the image from"
+                type: string
+                required: true
+
+jobs:
+    publish:
+        runs-on: ubuntu-latest
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  ref: ${{ inputs.commit }}
+
+            - name: Build and push
+              uses: mr-smithers-excellent/docker-build-push@v6
+              with:
+                  dockerfile: server/Dockerfile
+                  directory: server
+                  # Resultant package name will be ghcr.io/ente-io/server
+                  image: server
+                  registry: ghcr.io
+                  enableBuildKit: true
+                  buildArgs: GIT_COMMIT=${{ inputs.commit }}
+                  tags: ${{ inputs.commit }}, latest
+                  username: ${{ github.actor }}
+                  password: ${{ secrets.GITHUB_TOKEN }}

+ 4 - 4
.github/workflows/server-release.yml

@@ -7,11 +7,11 @@ jobs:
     build:
         runs-on: ubuntu-latest
         steps:
-            - uses: actions/checkout@v4
-              name: Check out code
+            - name: Checkout code
+              uses: actions/checkout@v4
 
-            - uses: mr-smithers-excellent/docker-build-push@v6
-              name: Build & Push
+            - name: Build and push
+              uses: mr-smithers-excellent/docker-build-push@v6
               with:
                   dockerfile: server/Dockerfile
                   directory: server

+ 2 - 2
.github/workflows/web-crowdin.yml

@@ -2,12 +2,12 @@ name: "Sync Crowdin translations (web)"
 
 on:
     push:
+        branches: [main]
         paths:
             # Run workflow when web's en-US/translation.json is changed
             - "web/apps/photos/public/locales/en-US/translation.json"
             # Or the workflow itself is changed
             - ".github/workflows/web-crowdin.yml"
-        branches: [main]
     schedule:
         # [Note: Run workflow on specific days of the week]
         #
@@ -34,7 +34,7 @@ jobs:
                   base_path: "web/"
                   config: "web/crowdin.yml"
                   upload_sources: true
-                  upload_translations: true
+                  upload_translations: false
                   download_translations: true
                   localization_branch_name: crowdin-translations-web
                   create_pull_request: true

+ 43 - 0
.github/workflows/web-deploy-payments.yml

@@ -0,0 +1,43 @@
+name: "Deploy (payments)"
+
+on:
+    push:
+        # Run workflow on pushes to the deploy/payments
+        branches: [deploy/payments]
+
+jobs:
+    deploy:
+        runs-on: ubuntu-latest
+
+        defaults:
+            run:
+                working-directory: web
+
+        steps:
+            - name: Checkout code
+              uses: actions/checkout@v4
+              with:
+                  submodules: recursive
+
+            - name: Setup node and enable yarn caching
+              uses: actions/setup-node@v4
+              with:
+                  node-version: 20
+                  cache: "yarn"
+                  cache-dependency-path: "docs/yarn.lock"
+
+            - name: Install dependencies
+              run: yarn install
+
+            - name: Build payments
+              run: yarn build:payments
+
+            - name: Publish payments
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: deploy/payments
+                  directory: web/apps/payments/out
+                  wranglerVersion: "3"

+ 1 - 1
.github/workflows/web-lint.yml

@@ -3,7 +3,7 @@ name: "Lint (web)"
 on:
     # Run on every push to a branch other than main that changes web/
     push:
-        branches-ignore: [main]
+        branches-ignore: [main, "deploy/**"]
         paths:
             - "web/**"
             - ".github/workflows/web-lint.yml"

+ 13 - 0
.github/workflows/web-nightly.yml

@@ -78,6 +78,19 @@ jobs:
                   directory: web/apps/cast/out
                   wranglerVersion: "3"
 
+            - name: Build payments
+              run: yarn build:payments
+
+            - name: Publish payments
+              uses: cloudflare/pages-action@1
+              with:
+                  accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
+                  apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+                  projectName: ente
+                  branch: n-payments
+                  directory: web/apps/payments/out
+                  wranglerVersion: "3"
+
             - name: Build photos
               run: yarn build:photos
               env:

+ 1 - 0
.github/workflows/web-preview.yml

@@ -12,6 +12,7 @@ on:
                     - "accounts"
                     - "auth"
                     - "cast"
+                    - "payments"
                     - "photos"
 
 jobs:

+ 1 - 1
README.md

@@ -70,7 +70,7 @@ existing users will be grandfathered in.
 [<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/app/id6444121398)
 [<img height="42" src=".github/assets/play-store-badge.png">](https://play.google.com/store/apps/details?id=io.ente.auth)
 [<img height="42" src=".github/assets/f-droid-badge.png">](https://f-droid.org/packages/io.ente.auth/)
-[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
+[<img height="42" src=".github/assets/desktop-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
 [<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
 
 </div>

+ 6 - 0
auth/.gitignore

@@ -18,6 +18,11 @@
 *.iws
 .idea/
 
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
 # Flutter/Dart/Pub related
 **/doc/api/
 .dart_tool/
@@ -35,3 +40,4 @@ lib/generated_plugin_registrant.dart
 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
 
 android/key.properties
+dist/

+ 20 - 11
auth/.metadata

@@ -1,11 +1,11 @@
 # This file tracks properties of this Flutter project.
 # Used by Flutter tool to assess capabilities and perform upgrades etc.
 #
-# This file should be version controlled.
+# This file should be version controlled and should not be manually edited.
 
 version:
-  revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-  channel: unknown
+  revision: "ba393198430278b6595976de84fe170f553cc728"
+  channel: "[user-branch]"
 
 project_type: app
 
@@ -13,17 +13,26 @@ project_type: app
 migration:
   platforms:
     - platform: root
-      create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-      base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
+    - platform: android
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
+    - platform: ios
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
     - platform: linux
-      create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-      base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
     - platform: macos
-      create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-      base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
+    - platform: web
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
     - platform: windows
-      create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-      base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
 
   # User provided section
 

+ 7 - 5
auth/README.md

@@ -31,14 +31,16 @@ You can alternatively install the build from PlayStore or F-Droid.
   <img height="59" src="../.github/assets/app-store-badge.svg">
 </a>
 
-### Web
+### Desktop
 
-You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding
-or managing your secrets, please use our mobile app.
+You can [**download**](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
+a native desktop app from this repository's GitHub releases. The desktop app
+works on Windows, Linux and macOS.
 
-### Desktop
+### Web
 
-A native desktop app is coming soon!
+You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding
+or managing your secrets, please use our mobile or desktop app.
 
 ## 🧑‍💻 Build from source
 

+ 8 - 7
auth/android/app/build.gradle

@@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
 }
 
 android {
-    compileSdkVersion 33
+    compileSdkVersion 34
 
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
@@ -46,7 +46,7 @@ android {
 
     defaultConfig {
         applicationId "io.ente.auth"
-        minSdkVersion 20
+        minSdkVersion 21
         targetSdkVersion 33
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
@@ -56,11 +56,11 @@ android {
 
     signingConfigs {
        release {
-            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null
-            keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS")
-            keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD")
-            storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD")
-        }
+           storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null
+           keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS")
+           keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD")
+           storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD")
+       }
     }
     
     flavorDimensions "default"
@@ -109,6 +109,7 @@ dependencies {
     implementation 'io.sentry:sentry-android:2.0.0'
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
     implementation 'com.android.support:multidex:1.0.3'
+    implementation 'com.google.guava:guava:28.2-android'
     implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
     testImplementation 'junit:junit:4.12'
     androidTestImplementation 'androidx.test:runner:1.1.1'

+ 54 - 7
auth/assets/custom-icons/_data/custom-icons.json

@@ -36,7 +36,9 @@
     },
     {
       "title": "BorgBase",
-      "altNames": ["borg"],
+      "altNames": [
+        "borg"
+      ],
       "slug": "BorgBase"
     },
     {
@@ -46,11 +48,17 @@
     {
       "title": "Bybit"
     },
+    {
+      "title": "CERN"
+    },
     {
       "title": "Channel Island Hosting",
       "slug": "cih",
       "hex": "D14633"
     },
+    {
+      "title": "ConfigCat"
+    },
     {
       "title": "Cloudflare"
     },
@@ -62,6 +70,13 @@
     {
       "title": "Crowdpear"
     },
+    {
+      "title": "DCS",
+      "altNames": [
+        "Digital Combat Simulator"
+      ],
+      "slug": "dcs"
+    },
     {
       "title": "DEGIRO"
     },
@@ -107,9 +122,14 @@
     },
     {
       "title": "Gosuslugi",
-      "altNames": ["Госуслуги"],
+      "altNames": [
+        "Госуслуги"
+      ],
       "slug": "Gosuslugi"
     },
+    {
+      "title": "Habbo"
+    },
     {
       "title": "Healthchecks.io",
       "slug": "healthchecks"
@@ -172,13 +192,24 @@
     },
     {
       "title": "Mastodon",
-      "altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"],
+      "altNames": [
+        "mstdn",
+        "fediscience",
+        "mathstodon",
+        "fosstodon"
+      ],
       "slug": "mastodon",
       "hex": "6364FF"
     },
+    {
+      "title": "Mercado Livre",
+      "slug": "mercado_livre"
+    },
     {
       "title": "Murena",
-      "altNames": ["eCloud"],
+      "altNames": [
+        "eCloud"
+      ],
       "slug": "ecloud"
     },
     {
@@ -267,11 +298,18 @@
       "title": "Revolt",
       "hex": "858585"
     },
+    {
+      "title": "Rockstar Games",
+      "slug": "rockstar_games"
+    },
     {
       "title": "Rust Language Forum",
       "slug": "rust_language_forum",
       "hex": "000000"
     },
+    {
+      "title": "Sendgrid"
+    },
     {
       "title": "service-bw"
     },
@@ -356,15 +394,24 @@
     {
       "title": "Wise"
     },
+    {
+      "title": "WYZE",
+      "slug": "wyze"
+    },
     {
       "title": "X",
-      "altNames": ["twitter"],
+      "altNames": [
+        "twitter"
+      ],
       "slug": "x"
     },
     {
       "title": "Yandex",
-      "altNames": ["Ya", "Яндекс"],
+      "altNames": [
+        "Ya",
+        "Яндекс"
+      ],
       "slug": "Yandex"
     }
   ]
-}
+}

+ 47 - 0
auth/assets/custom-icons/icons/CERN.svg

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 25.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 283.465 283.465" enable-background="new 0 0 283.465 283.465" xml:space="preserve">
+<rect x="0.752" y="-0.337" fill="#FFFFFF" width="283.465" height="283.465"/>
+<path fill="#0033A0" d="M210.282,120.915c0.429,24.998-11.023,41.967-14.629,47.856c-3.143,5.13-10.654,15.024-25.11,30.374
+	c-18.235,19.365-75.231,79.947-79.029,83.983h154.558l-35.636-162.227L210.282,120.915z M224.498,203.72l1.277,5.803
+	c-15.302,16.528-38.73,28.912-66,28.912c-5.841,0-11.941-0.565-18.719-2.005c1.375-1.463,2.693-2.867,3.937-4.188
+	c4.352,0.773,9.361,1.284,14.506,1.286C186.023,233.541,209.198,221.605,224.498,203.72z M0.752-0.337v283.465h84.595l83.686-88.992
+	l-0.117-0.107c-13.455,9.491-30.532,14.646-48.943,14.646c-34.121,0-64.191-20.244-77.232-45.437l-0.132,0.088l17.755,59.9h-4.806
+	c0,0-8.809-30.352-16.33-56.812c-5.692-20.026-9.198-34.861-9.154-48.029c0.156-45.284,35.77-86.809,83.128-89.874
+	c1.3-0.086,5.328-0.506,11.328-0.511c36.48-0.022,148.884,0.84,159.687,0.923v-29.26H0.752z M100.261,210.453
+	c6.786,5.88,17.542,13.273,30.838,18.05c-1.113,1.185-2.321,2.469-3.603,3.833c-13.279-5.257-26.271-13.409-36.369-24.392
+	C93.969,208.901,97.099,209.768,100.261,210.453z M145.883,32.427c13.977,3.923,27.086,11.109,37.504,20.825
+	c-3.006-0.803-6.071-1.451-9.188-1.939c-14.647-11.578-33.714-18.482-53.64-18.482c-47.069,0-85.592,38.386-85.592,85.565
+	c0,47.18,38.382,85.564,85.565,85.564c47.179,0,85.565-38.384,85.565-85.564c0-18.252-6.876-36.487-16.318-49.367
+	c2.255,0.678,5.004,1.841,8.162,3.708c6.393,9.69,12.32,25.515,17.398,49.787c5.33,25.462,32.732,147.426,35.694,160.605h33.182
+	V33.152l-138.333-0.85C145.883,32.303,145.883,32.311,145.883,32.427z M50.595,116.383c0-11.441,8.652-19.051,20.412-19.051
+	c4.577,0,9.814,1.409,12.738,2.664c-0.611,1.353-1.113,3.142-1.326,4.258l-0.319,0.106c-2.261-2.503-5.898-4.672-11.279-4.672
+	c-6.83,0-14.689,5.528-14.689,16.557c0,10.737,8.009,16.442,15.169,16.442c6.434,0,9.513-3,12.278-5.337l0.212,0.213l-0.783,4.172
+	c-1.268,0.96-5.664,3.695-12.279,3.695C58.754,135.429,50.595,127.885,50.595,116.383z M78.765,187.656
+	c-6.344-12.899-9.219-25.995-9.6-38.519c1.612,0,3.481,0.067,5.093,0.067c0.556,11.987,3.274,26.892,12.648,43.087
+	C83.521,191.047,80.974,189.405,78.765,187.656z M112.787,134.755c0,0.001,0,0.001,0,0.001c-1.911-0.098-4.565-0.176-7.084-0.221
+	c-1.451-0.024-2.861-0.041-3.973-0.044c-0.158,0-0.319,0-0.47,0c-3.247,0-8.227,0.105-11.475,0.266
+	c0.214-4.631,0.429-9.26,0.429-13.836v-9.15c0-4.578-0.215-9.206-0.429-13.728c3.193,0.16,8.121,0.265,11.313,0.265
+	c3.193,0,9.149-0.141,10.991-0.265c-0.078,0.498-0.127,1.089-0.127,1.815c0,0.725,0.071,1.473,0.127,1.836
+	c-3.505-0.263-9.766-0.692-16.682-0.692c-0.056,2.287-0.162,11.991-0.162,13.324c6.278,0,10.301-0.269,13.441-0.532
+	c-0.105,0.532-0.16,1.486-0.16,2.017c0,0.532,0.054,1.32,0.16,1.853c-3.671-0.374-11.887-0.48-13.441-0.48
+	c-0.095,1.779-0.012,13.294,0.053,14.266c3.889-0.057,13.857-0.359,17.489-0.693c-0.057,0.403-0.125,1.233-0.125,2.042
+	C112.661,133.608,112.708,134.198,112.787,134.755z M144.039,134.58c-0.485,0-2.321,0.017-3.334,0.177
+	c-2.103-3.205-8.839-13.302-13.419-18.015c-0.137,0-2.708,0.003-2.708,0.003v4.23c0,4.575,0.212,9.205,0.426,13.781
+	c-0.906-0.161-2.542-0.177-2.877-0.177c-0.335,0-1.971,0.017-2.878,0.177c0.214-4.577,0.428-9.206,0.428-13.781v-9.152
+	c0-4.577-0.213-9.207-0.428-13.782c2.024,0.16,4.584,0.265,6.606,0.265c2.021,0,4.043-0.265,6.064-0.265
+	c6.013,0,11.523,1.776,11.523,8.472c0,7.084-7.06,9.632-11.104,10.163c2.606,3.246,11.946,14.619,15.032,18.08
+	C146.309,134.596,144.525,134.58,144.039,134.58z M184.319,135.253l-1.742-0.017c-2.13-2.888-24.461-26.18-26.395-28.237
+	c-0.053,1.967-0.055,6.062-0.055,10.043c0,5.287,0.4,13.354,0.634,17.714c-0.54-0.098-1.338-0.195-2.268-0.195
+	c-0.939,0-1.709,0.086-2.331,0.195c0.436-5.625,0.558-14.755,0.558-23.337c0-6.704-0.099-10.38-0.178-13.762l1.744,0.015
+	c2.256,2.45,24.457,25.428,26.392,27.487c0.053-1.967,0.057-5.441,0.057-9.424c0-5.285-0.402-13.354-0.636-17.711
+	c0.542,0.094,1.338,0.192,2.269,0.192c0.942,0,1.71-0.084,2.332-0.192c-0.438,5.624-0.56,14.753-0.56,23.336
+	C184.14,128.063,184.239,131.868,184.319,135.253z M230.093,88.11c9.889,12.128,17.896,31.314,19.065,47.379h0.156l8.24-85.104
+	l4.646-0.003c0,0-5.271,55.013-8.341,82.351c-3.844,34.241-8.729,48.448-18.071,63.403l-1.43-6.508
+	c7.28-12.787,9.357-23.946,10.306-29.823c2.956-18.304-1.273-43.291-14.025-62.789c-14.291-21.849-39.84-37.924-71.2-37.924
+	c-25.762,0-48.143,11.327-63.766,28.993l-3.789-3.003c16.539-18.764,40.576-30.737,67.558-30.737
+	C187.735,54.346,212.924,67.058,230.093,88.11z M138.305,107.241c0-5.29-4.629-6.973-8.248-6.973c-2.448,0-4.043,0.161-5.162,0.268
+	c-0.159,3.885-0.318,7.457-0.318,11.287v2.926c0.529,0.071,3.021,0.059,3.566,0.049
+	C132.537,114.707,138.305,113.312,138.305,107.241z"/>
+</svg>

文件差異過大導致無法顯示
+ 6 - 0
auth/assets/custom-icons/icons/configcat.svg


+ 9 - 0
auth/assets/custom-icons/icons/dcs.svg

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg width="100%" height="100%" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;">
+    <g transform="matrix(1.59257,0,0,1.59257,0,9.06171)">
+        <path d="M0.87,0.08L3.17,0.08C4.2,0.08 5.13,0.51 5.13,1.67C5.13,2.92 3.91,3.6 2.78,3.6L0.06,3.6C0.06,3.6 0.87,0.09 0.87,0.08ZM2.85,2.82C3.44,2.82 4.14,2.39 4.14,1.74C4.14,1.19 3.7,0.85 3.17,0.85L1.71,0.85L1.26,2.81L2.85,2.81L2.85,2.82Z" style="fill:rgb(245,158,15);fill-rule:nonzero;stroke:rgb(245,158,15);stroke-width:0.09px;"/>
+        <path d="M11.95,1.33C11.87,1.31 11.65,1.26 11.65,1.14C11.65,0.81 12.52,0.81 12.74,0.81C13.25,0.81 13.96,0.91 14.4,1.17L15,0.59C14.39,0.18 13.59,0.04 12.86,0.04C12.07,0.04 10.65,0.18 10.65,1.24C10.65,2.45 13.65,1.96 13.65,2.52C13.65,2.86 12.89,2.88 12.66,2.88C11.9,2.88 11.39,2.74 10.77,2.29C10.57,2.48 10.36,2.67 10.16,2.86C10.95,3.44 11.7,3.64 12.67,3.64C13.4,3.64 14.68,3.36 14.68,2.42C14.68,1.34 12.67,1.5 11.97,1.32L11.95,1.32L11.95,1.33Z" style="fill:rgb(245,158,15);fill-rule:nonzero;stroke:rgb(245,158,15);stroke-width:0.09px;"/>
+        <path d="M9.18,2.35L9.16,2.35C9.07,2.43 8.41,2.95 7.67,2.87C7.14,2.81 6.41,2.44 6.41,1.95C6.41,1.15 7.26,0.83 7.94,0.83C8.39,0.83 8.82,0.93 9.17,1.18C9.48,1.07 9.8,0.97 10.11,0.86C9.59,0.3 8.82,0.07 8.06,0.07C6.92,0.07 5.43,0.73 5.43,2.06C5.43,3.22 6.65,3.63 7.61,3.63C8.41,3.63 9.18,3.4 9.78,2.88C9.78,2.88 9.18,2.38 9.17,2.37L9.16,2.37L9.18,2.35Z" style="fill:rgb(245,158,15);fill-rule:nonzero;stroke:rgb(245,158,15);stroke-width:0.09px;"/>
+    </g>
+</svg>

文件差異過大導致無法顯示
+ 6 - 0
auth/assets/custom-icons/icons/habbo.svg


文件差異過大導致無法顯示
+ 6 - 0
auth/assets/custom-icons/icons/mercado_livre.svg


文件差異過大導致無法顯示
+ 0 - 0
auth/assets/custom-icons/icons/rockstar_games.svg


文件差異過大導致無法顯示
+ 6 - 0
auth/assets/custom-icons/icons/sendgrid.svg


+ 1 - 0
auth/assets/custom-icons/icons/wyze.svg

@@ -0,0 +1 @@
+<svg fill="#1DF0BB" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Wyze</title><path d="M5.475 13.171 7.3 9.469h.974L5.779 14.53h-.608l-1.034-2.082-1.034 2.082h-.609L0 9.469h.973l1.826 3.673.851-1.706-.973-1.967h.973l1.825 3.702Zm8.457-3.702-2.251 3.442v1.591h-.882v-1.591L8.517 9.469h1.034l1.673 2.545 1.673-2.545h1.035Zm5.444 4.194H24v.867h-4.624v-.867Zm0-4.194H24v.868h-4.624v-.868Zm0 2.083H24v.867h-4.624v-.867Zm-.273-2.083-3.438 4.223h3.133v.838H13.84l3.407-4.222h-3.042v-.839h4.898Z"/></svg>

+ 0 - 0
auth/assets/icon-light-adaptive-fg.png → auth/assets/generation-icons/icon-light-adaptive-fg.png


+ 0 - 0
auth/assets/icon-light.png → auth/assets/generation-icons/icon-light.png


二進制
auth/assets/icons/auth-icon.ico


二進制
auth/assets/icons/auth-icon.png


+ 25 - 0
auth/distribute_options.yaml

@@ -0,0 +1,25 @@
+output: dist/
+
+releases:
+  - name: dev
+    jobs:
+      - name: release-dev-linux-zip
+        package:
+          platform: linux
+          target: zip
+      - name: release-dev-linux-deb
+        package:
+          platform: linux
+          target: deb
+      - name: release-dev-linux-appimage
+        package:
+          platform: linux
+          target: appimage
+      - name: release-dev-windows-exe
+        package:
+          platform: windows
+          target: exe
+      - name: release-dev-macos-dmg
+        package:
+          platform: macos
+          target: dmg

+ 1 - 1
auth/fastlane/metadata/android/en-US/full_description.txt

@@ -37,4 +37,4 @@ file, that adheres to the above format.
 SUPPORT
 
 If you need help, please reach out to support@ente.io, and a human will get in touch with you.
-If you have feature requests, please create an issue @ https://github.com/ente-io/auth
+If you have feature requests, please create an issue @ https://github.com/ente-io/ente

+ 1 - 1
auth/fastlane/metadata/android/en-US/title.txt

@@ -1 +1 @@
-ente Authenticator
+Ente Authenticator

+ 6 - 6
auth/fdroid_flutter_icons.yaml

@@ -1,6 +1,6 @@
-flutter_icons:
-  android: "launcher_icon"
-  image_path: "assets/icon-light.png"
-  adaptive_icon_foreground: "assets/icon-light-adaptive-fg.png"
-  adaptive_icon_background: "#ffffff"
-
+flutter_icons:
+  android: "launcher_icon"
+  image_path: "assets/generation-icons/icon-light.png"
+  adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png"
+  adaptive_icon_background: "#ffffff"
+

+ 93 - 59
auth/ios/Podfile.lock

@@ -1,7 +1,9 @@
 PODS:
-  - connectivity (0.0.1):
+  - app_links (0.0.1):
     - Flutter
-    - Reachability
+  - connectivity_plus (0.0.1):
+    - Flutter
+    - ReachabilitySwift
   - device_info_plus (0.0.1):
     - Flutter
   - DKImagePickerController/Core (4.3.4):
@@ -45,27 +47,26 @@ PODS:
   - Flutter (1.0.0)
   - flutter_email_sender (0.0.1):
     - Flutter
-  - flutter_inappwebview (0.0.1):
+  - flutter_inappwebview_ios (0.0.1):
     - Flutter
-    - flutter_inappwebview/Core (= 0.0.1)
+    - flutter_inappwebview_ios/Core (= 0.0.1)
     - OrderedSet (~> 5.0)
-  - flutter_inappwebview/Core (0.0.1):
+  - flutter_inappwebview_ios/Core (0.0.1):
     - Flutter
     - OrderedSet (~> 5.0)
+  - flutter_local_authentication (1.2.0):
+    - Flutter
   - flutter_local_notifications (0.0.1):
     - Flutter
   - flutter_native_splash (0.0.1):
     - Flutter
   - flutter_secure_storage (6.0.0):
     - Flutter
-  - flutter_sodium (0.0.1):
-    - Flutter
   - fluttertoast (0.0.2):
     - Flutter
     - Toast
-  - FMDB (2.7.5):
-    - FMDB/standard (= 2.7.5)
-  - FMDB/standard (2.7.5)
+  - local_auth_darwin (0.0.1):
+    - Flutter
   - local_auth_ios (0.0.1):
     - Flutter
   - move_to_background (0.0.1):
@@ -82,46 +83,65 @@ PODS:
   - qr_code_scanner (0.2.0):
     - Flutter
     - MTBBarcodeScanner
-  - Reachability (3.2)
-  - SDWebImage (5.17.0):
-    - SDWebImage/Core (= 5.17.0)
-  - SDWebImage/Core (5.17.0)
-  - Sentry/HybridSDK (8.9.1):
-    - SentryPrivate (= 8.9.1)
+  - ReachabilitySwift (5.2.1)
+  - SDWebImage (5.19.0):
+    - SDWebImage/Core (= 5.19.0)
+  - SDWebImage/Core (5.19.0)
+  - Sentry/HybridSDK (8.21.0):
+    - SentryPrivate (= 8.21.0)
   - sentry_flutter (0.0.1):
     - Flutter
     - FlutterMacOS
-    - Sentry/HybridSDK (= 8.9.1)
-  - SentryPrivate (8.9.1)
+    - Sentry/HybridSDK (= 8.21.0)
+  - SentryPrivate (8.21.0)
   - share_plus (0.0.1):
     - Flutter
   - shared_preferences_foundation (0.0.1):
     - Flutter
     - FlutterMacOS
+  - smart_auth (0.0.1):
+    - Flutter
+  - sodium_libs (2.2.1):
+    - Flutter
   - sqflite (0.0.3):
     - Flutter
-    - FMDB (>= 2.7.5)
-  - SwiftyGif (5.4.4)
-  - Toast (4.0.0)
-  - uni_links (0.0.1):
+    - FlutterMacOS
+  - sqlite3 (3.45.1):
+    - sqlite3/common (= 3.45.1)
+  - sqlite3/common (3.45.1)
+  - sqlite3/fts5 (3.45.1):
+    - sqlite3/common
+  - sqlite3/perf-threadsafe (3.45.1):
+    - sqlite3/common
+  - sqlite3/rtree (3.45.1):
+    - sqlite3/common
+  - sqlite3_flutter_libs (0.0.1):
     - Flutter
+    - sqlite3 (~> 3.45.1)
+    - sqlite3/fts5
+    - sqlite3/perf-threadsafe
+    - sqlite3/rtree
+  - SwiftyGif (5.4.4)
+  - Toast (4.1.0)
   - url_launcher_ios (0.0.1):
     - Flutter
 
 DEPENDENCIES:
-  - connectivity (from `.symlinks/plugins/connectivity/ios`)
+  - app_links (from `.symlinks/plugins/app_links/ios`)
+  - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - file_picker (from `.symlinks/plugins/file_picker/ios`)
   - file_saver (from `.symlinks/plugins/file_saver/ios`)
   - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`)
   - Flutter (from `Flutter`)
   - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
-  - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
+  - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`)
+  - flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`)
   - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
-  - flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
+  - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
   - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
   - move_to_background (from `.symlinks/plugins/move_to_background/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@@ -131,27 +151,31 @@ DEPENDENCIES:
   - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
   - share_plus (from `.symlinks/plugins/share_plus/ios`)
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
-  - sqflite (from `.symlinks/plugins/sqflite/ios`)
-  - uni_links (from `.symlinks/plugins/uni_links/ios`)
+  - smart_auth (from `.symlinks/plugins/smart_auth/ios`)
+  - sodium_libs (from `.symlinks/plugins/sodium_libs/ios`)
+  - sqflite (from `.symlinks/plugins/sqflite/darwin`)
+  - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`)
   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
 
 SPEC REPOS:
   trunk:
     - DKImagePickerController
     - DKPhotoGallery
-    - FMDB
     - MTBBarcodeScanner
     - OrderedSet
-    - Reachability
+    - ReachabilitySwift
     - SDWebImage
     - Sentry
     - SentryPrivate
+    - sqlite3
     - SwiftyGif
     - Toast
 
 EXTERNAL SOURCES:
-  connectivity:
-    :path: ".symlinks/plugins/connectivity/ios"
+  app_links:
+    :path: ".symlinks/plugins/app_links/ios"
+  connectivity_plus:
+    :path: ".symlinks/plugins/connectivity_plus/ios"
   device_info_plus:
     :path: ".symlinks/plugins/device_info_plus/ios"
   file_picker:
@@ -164,18 +188,20 @@ EXTERNAL SOURCES:
     :path: Flutter
   flutter_email_sender:
     :path: ".symlinks/plugins/flutter_email_sender/ios"
-  flutter_inappwebview:
-    :path: ".symlinks/plugins/flutter_inappwebview/ios"
+  flutter_inappwebview_ios:
+    :path: ".symlinks/plugins/flutter_inappwebview_ios/ios"
+  flutter_local_authentication:
+    :path: ".symlinks/plugins/flutter_local_authentication/ios"
   flutter_local_notifications:
     :path: ".symlinks/plugins/flutter_local_notifications/ios"
   flutter_native_splash:
     :path: ".symlinks/plugins/flutter_native_splash/ios"
   flutter_secure_storage:
     :path: ".symlinks/plugins/flutter_secure_storage/ios"
-  flutter_sodium:
-    :path: ".symlinks/plugins/flutter_sodium/ios"
   fluttertoast:
     :path: ".symlinks/plugins/fluttertoast/ios"
+  local_auth_darwin:
+    :path: ".symlinks/plugins/local_auth_darwin/darwin"
   local_auth_ios:
     :path: ".symlinks/plugins/local_auth_ios/ios"
   move_to_background:
@@ -194,50 +220,58 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/share_plus/ios"
   shared_preferences_foundation:
     :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
+  smart_auth:
+    :path: ".symlinks/plugins/smart_auth/ios"
+  sodium_libs:
+    :path: ".symlinks/plugins/sodium_libs/ios"
   sqflite:
-    :path: ".symlinks/plugins/sqflite/ios"
-  uni_links:
-    :path: ".symlinks/plugins/uni_links/ios"
+    :path: ".symlinks/plugins/sqflite/darwin"
+  sqlite3_flutter_libs:
+    :path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
   url_launcher_ios:
     :path: ".symlinks/plugins/url_launcher_ios/ios"
 
 SPEC CHECKSUMS:
-  connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467
-  device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
+  app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
+  connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
+  device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
   DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
   DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
-  file_picker: ce3938a0df3cc1ef404671531facef740d03f920
+  file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
-  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
-  flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
-  flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
+  flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0
+  flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
+  flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
   flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
   flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
-  flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
   fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
-  FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
-  local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
+  local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
+  local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
   move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
   MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
   OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
-  package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
-  path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
+  package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
+  path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
   privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
   qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
-  Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
-  SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
-  Sentry: e3203780941722a1fcfee99e351de14244c7f806
-  sentry_flutter: 8f0ffd53088e6a4d50c095852c5cad9e4405025c
-  SentryPrivate: 5e3683390f66611fc7c6215e27645873adb55d13
+  ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66
+  SDWebImage: 981fd7e860af070920f249fd092420006014c3eb
+  Sentry: ebc12276bd17613a114ab359074096b6b3725203
+  sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e
+  SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe
   share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
-  shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
-  sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
+  shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
+  smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2
+  sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318
+  sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
+  sqlite3: 73b7fc691fdc43277614250e04d183740cb15078
+  sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80
   SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
-  Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
-  uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
-  url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
+  Toast: ec33c32b8688982cecc6348adeae667c1b9938da
+  url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
 
 PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb
 

+ 1 - 1
auth/ios/Runner.xcodeproj/project.pbxproj

@@ -159,7 +159,7 @@
 		97C146E61CF9000F007C117D /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
-				LastUpgradeCheck = 1430;
+				LastUpgradeCheck = 1510;
 				ORGANIZATIONNAME = "";
 				TargetAttributes = {
 					97C146ED1CF9000F007C117D = {

+ 1 - 1
auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1430"
+   LastUpgradeVersion = "1510"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"

+ 12 - 2
auth/ios/Runner/AppDelegate.swift

@@ -1,5 +1,6 @@
-import UIKit
 import Flutter
+import UIKit
+import app_links
 
 @UIApplicationMain
 @objc class AppDelegate: FlutterAppDelegate {
@@ -8,6 +9,15 @@ import Flutter
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
     GeneratedPluginRegistrant.register(with: self)
-    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+
+    super.application(application, didFinishLaunchingWithOptions: launchOptions)
+
+    if let url = AppLinks.shared.getLink(launchOptions: launchOptions) {
+      AppLinks.shared.handleLink(url: url)
+    }
+
+    return false
+
+    // return super.application(application, didFinishLaunchingWithOptions: launchOptions)
   }
 }

+ 4 - 0
auth/ios/Runner/Info.plist

@@ -78,5 +78,9 @@
 	</array>
 	<key>UIViewControllerBasedStatusBarAppearance</key>
 	<false/>
+	<key>LSSupportsOpeningDocumentsInPlace</key>
+	<true/>
+	<key>UIFileSharingEnabled</key>
+	<true/>
 </dict>
 </plist>

+ 61 - 2
auth/lib/app/view/app.dart

@@ -12,15 +12,18 @@ import 'package:ente_auth/locale.dart';
 import "package:ente_auth/onboarding/view/onboarding_page.dart";
 import 'package:ente_auth/services/update_service.dart';
 import 'package:ente_auth/services/user_service.dart';
+import 'package:ente_auth/services/window_listener_service.dart';
 import 'package:ente_auth/ui/home_page.dart';
 import 'package:ente_auth/ui/settings/app_update_dialog.dart';
 import 'package:flutter/foundation.dart';
 import "package:flutter/material.dart";
 import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:tray_manager/tray_manager.dart';
+import 'package:window_manager/window_manager.dart';
 
 class App extends StatefulWidget {
   final Locale locale;
-  const App({Key? key, this.locale = const Locale("en")}) : super(key: key);
+  const App({super.key, this.locale = const Locale("en")});
 
   static void setLocale(BuildContext context, Locale newLocale) {
     _AppState state = context.findAncestorStateOfType<_AppState>()!;
@@ -31,7 +34,7 @@ class App extends StatefulWidget {
   State<App> createState() => _AppState();
 }
 
-class _AppState extends State<App> {
+class _AppState extends State<App> with WindowListener, TrayListener {
   late StreamSubscription<SignedOutEvent> _signedOutEvent;
   late StreamSubscription<SignedInEvent> _signedInEvent;
   Locale? locale;
@@ -41,8 +44,19 @@ class _AppState extends State<App> {
     });
   }
 
+  Future<void> initWindowManager() async {
+    windowManager.addListener(this);
+  }
+
+  Future<void> initTrayManager() async {
+    trayManager.addListener(this);
+  }
+
   @override
   void initState() {
+    initWindowManager();
+    initTrayManager();
+
     _signedOutEvent = Bus.instance.on<SignedOutEvent>().listen((event) {
       if (mounted) {
         setState(() {});
@@ -76,6 +90,10 @@ class _AppState extends State<App> {
   @override
   void dispose() {
     super.dispose();
+
+    windowManager.removeListener(this);
+    trayManager.removeListener(this);
+
     _signedOutEvent.cancel();
     _signedInEvent.cancel();
   }
@@ -134,4 +152,45 @@ class _AppState extends State<App> {
           : const OnboardingPage(),
     };
   }
+
+  @override
+  void onWindowResize() {
+    WindowListenerService.instance.onWindowResize().ignore();
+  }
+
+  @override
+  void onTrayIconMouseDown() {
+    if (Platform.isWindows) {
+      windowManager.show();
+    } else {
+      trayManager.popUpContextMenu();
+    }
+  }
+
+  @override
+  void onTrayIconRightMouseDown() {
+    if (Platform.isWindows) {
+      trayManager.popUpContextMenu();
+    } else {
+      windowManager.show();
+    }
+  }
+
+  @override
+  void onTrayIconRightMouseUp() {}
+
+  @override
+  void onTrayMenuItemClick(MenuItem menuItem) {
+    switch (menuItem.key) {
+      case 'hide_window':
+        windowManager.hide();
+        break;
+      case 'show_window':
+        windowManager.show();
+        break;
+      case 'exit_app':
+        windowManager.close();
+        break;
+    }
+  }
 }

+ 0 - 4
auth/lib/app/view/app_route.dart

@@ -1,4 +0,0 @@
-class AppRoute {
-  static const String enterSecretKeyPage = "enterSecretKeyPage";
-  static const String settings = "settings";
-}

+ 0 - 163
auth/lib/app/view/app_theme_extension.dart

@@ -1,163 +0,0 @@
-import "package:flutter/material.dart";
-
-final lightTheme = ThemeData(
-  fontFamily: "Inter",
-  brightness: Brightness.light,
-  scaffoldBackgroundColor: Colors.white,
-  appBarTheme: const AppBarTheme().copyWith(
-    backgroundColor: Colors.white,
-    foregroundColor: Colors.black,
-    iconTheme: const IconThemeData(color: Colors.black),
-    elevation: 0,
-  ),
-  colorScheme: const ColorScheme.light(
-    primary: Colors.black,
-  ),
-  textTheme: _buildTextTheme(Colors.black),
-  outlinedButtonTheme: buildOutlinedButtonThemeData(
-    bgDisabled: Colors.grey.shade500,
-    bgEnabled: Colors.black,
-    fgDisabled: Colors.white,
-    fgEnabled: Colors.white,
-  ),
-  inputDecorationTheme: InputDecorationTheme(
-    fillColor: null,
-    filled: true,
-    contentPadding: const EdgeInsets.symmetric(
-      horizontal: 16,
-      vertical: 14,
-    ),
-    border: UnderlineInputBorder(
-      borderSide: BorderSide.none,
-      borderRadius: BorderRadius.circular(6),
-    ),
-  ),
-);
-
-final darkTheme = ThemeData(
-  fontFamily: "Inter",
-  brightness: Brightness.dark,
-  scaffoldBackgroundColor: Colors.black,
-  appBarTheme: const AppBarTheme(color: Colors.orange),
-  textTheme: _buildTextTheme(Colors.white),
-  outlinedButtonTheme: buildOutlinedButtonThemeData(
-    bgDisabled: Colors.grey.shade500,
-    bgEnabled: Colors.white,
-    fgDisabled: Colors.white,
-    fgEnabled: Colors.black,
-  ),
-  inputDecorationTheme: InputDecorationTheme(
-    fillColor: null,
-    filled: true,
-    contentPadding: const EdgeInsets.symmetric(
-      horizontal: 16,
-      vertical: 14,
-    ),
-    border: UnderlineInputBorder(
-      borderSide: BorderSide.none,
-      borderRadius: BorderRadius.circular(6),
-    ),
-  ), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: Colors.black),
-);
-
-TextTheme _buildTextTheme(Color textColor) {
-  return const TextTheme().copyWith(
-    headlineMedium: TextStyle(
-      color: textColor,
-      fontSize: 32,
-      fontWeight: FontWeight.w700,
-      fontFamily: "Inter",
-    ),
-    headlineSmall: TextStyle(
-      color: textColor,
-      fontSize: 24,
-      fontWeight: FontWeight.w600,
-      fontFamily: "Inter",
-    ),
-    // AG: Body
-    titleLarge: TextStyle(
-      color: textColor,
-      fontSize: 18,
-      fontFamily: "Inter",
-      fontWeight: FontWeight.w500,
-    ),
-    // Use labels for buttons or notifications
-    labelMedium: TextStyle(
-      color: textColor,
-      fontFamily: "Inter",
-      fontSize: 18,
-      fontWeight: FontWeight.w700,
-      height: 28,
-    ),
-
-    titleMedium: TextStyle(
-      color: textColor,
-      fontFamily: "Inter",
-      fontSize: 16,
-      fontWeight: FontWeight.w500,
-    ),
-    titleSmall: TextStyle(
-      color: textColor,
-      fontFamily: "Inter",
-      fontSize: 14,
-      fontWeight: FontWeight.w500,
-    ),
-    bodyLarge: TextStyle(
-      fontFamily: "Inter",
-      color: textColor,
-      fontSize: 16,
-      fontWeight: FontWeight.w500,
-    ),
-    bodyMedium: TextStyle(
-      fontFamily: "Inter",
-      color: textColor,
-      fontSize: 14,
-      fontWeight: FontWeight.w500,
-    ),
-    bodySmall: TextStyle(
-      color: textColor.withOpacity(0.6),
-      fontSize: 14,
-      fontWeight: FontWeight.w500,
-    ),
-  );
-}
-
-OutlinedButtonThemeData buildOutlinedButtonThemeData({
-  required Color bgDisabled,
-  required Color bgEnabled,
-  required Color fgDisabled,
-  required Color fgEnabled,
-}) {
-  return OutlinedButtonThemeData(
-    style: OutlinedButton.styleFrom(
-      shape: RoundedRectangleBorder(
-        borderRadius: BorderRadius.circular(8),
-      ),
-      alignment: Alignment.center,
-      padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 50),
-      textStyle: const TextStyle(
-        fontWeight: FontWeight.w700,
-        fontFamily: "Inter",
-        fontSize: 18,
-      ),
-    ).copyWith(
-      backgroundColor: MaterialStateProperty.resolveWith<Color>(
-        (Set<MaterialState> states) {
-          if (states.contains(MaterialState.disabled)) {
-            return bgDisabled;
-          }
-          return bgEnabled;
-        },
-      ),
-      foregroundColor: MaterialStateProperty.resolveWith<Color>(
-        (Set<MaterialState> states) {
-          if (states.contains(MaterialState.disabled)) {
-            return fgDisabled;
-          }
-          return fgEnabled;
-        },
-      ),
-      alignment: Alignment.center,
-    ),
-  );
-}

+ 2 - 0
auth/lib/bootstrap.dart

@@ -1,3 +1,5 @@
+// ignore_for_file: deprecated_member_use
+
 import "dart:async";
 import "dart:developer";
 

+ 56 - 51
auth/lib/core/configuration.dart

@@ -13,12 +13,12 @@ import 'package:ente_auth/models/key_attributes.dart';
 import 'package:ente_auth/models/key_gen_result.dart';
 import 'package:ente_auth/models/private_key_attributes.dart';
 import 'package:ente_auth/store/authenticator_db.dart';
-import 'package:ente_auth/utils/crypto_util.dart';
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter_secure_storage/flutter_secure_storage.dart';
-import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 import 'package:tuple/tuple.dart';
 
 class Configuration {
@@ -72,9 +72,10 @@ class Configuration {
 
   Future<void> init() async {
     _preferences = await SharedPreferences.getInstance();
+    sqfliteFfiInit();
     _secureStorage = const FlutterSecureStorage();
     _documentsDirectory = (await getApplicationDocumentsDirectory()).path;
-    _tempDirectory = _documentsDirectory + "/temp/";
+    _tempDirectory = "$_documentsDirectory/temp/";
     final tempDirectory = io.Directory(_tempDirectory);
     try {
       final currentTime = DateTime.now().microsecondsSinceEpoch;
@@ -162,7 +163,7 @@ class Configuration {
     // decrypt the master key
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
     final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
-      utf8.encode(password) as Uint8List,
+      utf8.encode(password),
       kekSalt,
     );
     final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
@@ -172,28 +173,28 @@ class Configuration {
         CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
 
     // Generate a public-private keypair and encrypt the latter
-    final keyPair = await CryptoUtil.generateKeyPair();
+    final keyPair = CryptoUtil.generateKeyPair();
     final encryptedSecretKeyData =
-        CryptoUtil.encryptSync(keyPair.sk, masterKey);
+        CryptoUtil.encryptSync(keyPair.secretKey.extractBytes(), masterKey);
 
     final attributes = KeyAttributes(
-      Sodium.bin2base64(kekSalt),
-      Sodium.bin2base64(encryptedKeyData.encryptedData!),
-      Sodium.bin2base64(encryptedKeyData.nonce!),
-      Sodium.bin2base64(keyPair.pk),
-      Sodium.bin2base64(encryptedSecretKeyData.encryptedData!),
-      Sodium.bin2base64(encryptedSecretKeyData.nonce!),
+      CryptoUtil.bin2base64(kekSalt),
+      CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
+      CryptoUtil.bin2base64(encryptedKeyData.nonce!),
+      CryptoUtil.bin2base64(keyPair.publicKey),
+      CryptoUtil.bin2base64(encryptedSecretKeyData.encryptedData!),
+      CryptoUtil.bin2base64(encryptedSecretKeyData.nonce!),
       derivedKeyResult.memLimit,
       derivedKeyResult.opsLimit,
-      Sodium.bin2base64(encryptedMasterKey.encryptedData!),
-      Sodium.bin2base64(encryptedMasterKey.nonce!),
-      Sodium.bin2base64(encryptedRecoveryKey.encryptedData!),
-      Sodium.bin2base64(encryptedRecoveryKey.nonce!),
+      CryptoUtil.bin2base64(encryptedMasterKey.encryptedData!),
+      CryptoUtil.bin2base64(encryptedMasterKey.nonce!),
+      CryptoUtil.bin2base64(encryptedRecoveryKey.encryptedData!),
+      CryptoUtil.bin2base64(encryptedRecoveryKey.nonce!),
     );
     final privateAttributes = PrivateKeyAttributes(
-      Sodium.bin2base64(masterKey),
-      Sodium.bin2hex(recoveryKey),
-      Sodium.bin2base64(keyPair.sk),
+      CryptoUtil.bin2base64(masterKey),
+      CryptoUtil.bin2hex(recoveryKey),
+      CryptoUtil.bin2base64(keyPair.secretKey.extractBytes()),
     );
     return KeyGenResult(attributes, privateAttributes, loginKey);
   }
@@ -208,7 +209,7 @@ class Configuration {
     // decrypt the master key
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
     final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
-      utf8.encode(password) as Uint8List,
+      utf8.encode(password),
       kekSalt,
     );
     final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
@@ -220,9 +221,9 @@ class Configuration {
     final existingAttributes = getKeyAttributes();
 
     final updatedAttributes = existingAttributes!.copyWith(
-      kekSalt: Sodium.bin2base64(kekSalt),
-      encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
-      keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
+      kekSalt: CryptoUtil.bin2base64(kekSalt),
+      encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
+      keyDecryptionNonce: CryptoUtil.bin2base64(encryptedKeyData.nonce!),
       memLimit: derivedKeyResult.memLimit,
       opsLimit: derivedKeyResult.opsLimit,
     );
@@ -240,8 +241,8 @@ class Configuration {
   }) async {
     _logger.info('Start decryptAndSaveSecrets');
     keyEncryptionKey ??= await CryptoUtil.deriveKey(
-      utf8.encode(password) as Uint8List,
-      Sodium.base642bin(attributes.kekSalt),
+      utf8.encode(password),
+      CryptoUtil.base642bin(attributes.kekSalt),
       attributes.memLimit,
       attributes.opsLimit,
     );
@@ -250,31 +251,31 @@ class Configuration {
     Uint8List key;
     try {
       key = CryptoUtil.decryptSync(
-        Sodium.base642bin(attributes.encryptedKey),
+        CryptoUtil.base642bin(attributes.encryptedKey),
         keyEncryptionKey,
-        Sodium.base642bin(attributes.keyDecryptionNonce),
+        CryptoUtil.base642bin(attributes.keyDecryptionNonce),
       );
     } catch (e) {
       _logger.severe('master-key failed, incorrect password?', e);
       throw Exception("Incorrect password");
     }
     _logger.info("master-key done");
-    await setKey(Sodium.bin2base64(key));
+    await setKey(CryptoUtil.bin2base64(key));
     final secretKey = CryptoUtil.decryptSync(
-      Sodium.base642bin(attributes.encryptedSecretKey),
+      CryptoUtil.base642bin(attributes.encryptedSecretKey),
       key,
-      Sodium.base642bin(attributes.secretKeyDecryptionNonce),
+      CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
     );
     _logger.info("secret-key done");
-    await setSecretKey(Sodium.bin2base64(secretKey));
+    await setSecretKey(CryptoUtil.bin2base64(secretKey));
     final token = CryptoUtil.openSealSync(
-      Sodium.base642bin(getEncryptedToken()!),
-      Sodium.base642bin(attributes.publicKey),
+      CryptoUtil.base642bin(getEncryptedToken()!),
+      CryptoUtil.base642bin(attributes.publicKey),
       secretKey,
     );
     _logger.info('appToken done');
     await setToken(
-      Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
+      CryptoUtil.bin2base64(token, urlSafe: true),
     );
     return keyEncryptionKey;
   }
@@ -293,28 +294,28 @@ class Configuration {
     Uint8List masterKey;
     try {
       masterKey = await CryptoUtil.decrypt(
-        Sodium.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
-        Sodium.hex2bin(recoveryKey),
-        Sodium.base642bin(attributes.masterKeyDecryptionNonce),
+        CryptoUtil.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
+        CryptoUtil.hex2bin(recoveryKey),
+        CryptoUtil.base642bin(attributes.masterKeyDecryptionNonce),
       );
     } catch (e) {
       _logger.severe(e);
       rethrow;
     }
-    await setKey(Sodium.bin2base64(masterKey));
+    await setKey(CryptoUtil.bin2base64(masterKey));
     final secretKey = CryptoUtil.decryptSync(
-      Sodium.base642bin(attributes.encryptedSecretKey),
+      CryptoUtil.base642bin(attributes.encryptedSecretKey),
       masterKey,
-      Sodium.base642bin(attributes.secretKeyDecryptionNonce),
+      CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
     );
-    await setSecretKey(Sodium.bin2base64(secretKey));
+    await setSecretKey(CryptoUtil.bin2base64(secretKey));
     final token = CryptoUtil.openSealSync(
-      Sodium.base642bin(getEncryptedToken()!),
-      Sodium.base642bin(attributes.publicKey),
+      CryptoUtil.base642bin(getEncryptedToken()!),
+      CryptoUtil.base642bin(attributes.publicKey),
       secretKey,
     );
     await setToken(
-      Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
+      CryptoUtil.bin2base64(token, urlSafe: true),
     );
   }
 
@@ -407,27 +408,31 @@ class Configuration {
   }
 
   Uint8List? getKey() {
-    return _key == null ? null : Sodium.base642bin(_key!);
+    return _key == null ? null : CryptoUtil.base642bin(_key!);
   }
 
   Uint8List? getSecretKey() {
-    return _secretKey == null ? null : Sodium.base642bin(_secretKey!);
+    return _secretKey == null ? null : CryptoUtil.base642bin(_secretKey!);
   }
 
   Uint8List? getAuthSecretKey() {
-    return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!);
+    return _authSecretKey == null
+        ? null
+        : CryptoUtil.base642bin(_authSecretKey!);
   }
 
   Uint8List? getOfflineSecretKey() {
-    return _offlineAuthKey == null ? null : Sodium.base642bin(_offlineAuthKey!);
+    return _offlineAuthKey == null
+        ? null
+        : CryptoUtil.base642bin(_offlineAuthKey!);
   }
 
   Uint8List getRecoveryKey() {
     final keyAttributes = getKeyAttributes()!;
     return CryptoUtil.decryptSync(
-      Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
+      CryptoUtil.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
       getKey()!,
-      Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
+      CryptoUtil.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
     );
   }
 
@@ -454,7 +459,7 @@ class Configuration {
         iOptions: _secureStorageOptionsIOS,
       );
     } else {
-      _offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey());
+      _offlineAuthKey = CryptoUtil.bin2base64(CryptoUtil.generateKey());
       await _secureStorage.write(
         key: offlineAuthSecretKey,
         value: _offlineAuthKey,

+ 2 - 1
auth/lib/core/constants.dart

@@ -7,7 +7,8 @@ const String sentryDSN =
     "https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
 const String sentryTunnel = "https://sentry-reporter.ente.io";
 const String roadmapURL = "https://roadmap.ente.io";
-const String githubDiscussionsUrl = "https://github.com/ente-io/ente/discussions";
+const String githubIssuesUrl =
+    "https://github.com/ente-io/ente/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc";
 const int microSecondsInDay = 86400000000;
 const int android11SDKINT = 30;
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748

+ 4 - 8
auth/lib/core/errors.dart

@@ -1,9 +1,9 @@
 class InvalidFileError extends ArgumentError {
-  InvalidFileError(String message) : super(message);
+  InvalidFileError(String super.message);
 }
 
 class InvalidFileUploadState extends AssertionError {
-  InvalidFileUploadState(String message) : super(message);
+  InvalidFileUploadState(String super.message);
 }
 
 class SubscriptionAlreadyClaimedError extends Error {}
@@ -30,19 +30,15 @@ class UnauthorizedError extends Error {}
 class RequestCancelledError extends Error {}
 
 class InvalidSyncStatusError extends AssertionError {
-  InvalidSyncStatusError(String message) : super(message);
+  InvalidSyncStatusError(String super.message);
 }
 
 class UnauthorizedEditError extends AssertionError {}
 
 class InvalidStateError extends AssertionError {
-  InvalidStateError(String message) : super(message);
+  InvalidStateError(String super.message);
 }
 
-class KeyDerivationError extends Error {}
-
-class LoginKeyDerivationError extends Error {}
-
 class SrpSetupNotCompleteError extends Error {}
 
 class AuthenticatorKeyNotFound extends Error {}

+ 5 - 5
auth/lib/core/logging/super_logging.dart

@@ -235,14 +235,14 @@ class SuperLogging {
       extraLines = null;
     }
 
-    final str = (config.prefix) + " " + rec.toPrettyString(extraLines);
+    final str = "${config.prefix} ${rec.toPrettyString(extraLines)}";
 
     // write to stdout
     printLog(str);
 
     // push to log queue
     if (fileIsEnabled) {
-      fileQueueEntries.add(str + '\n');
+      fileQueueEntries.add('$str\n');
       if (fileQueueEntries.length == 1) {
         flushQueue();
       }
@@ -275,7 +275,7 @@ class SuperLogging {
   static var logChunkSize = 800;
 
   static void printLog(String text) {
-    text.chunked(logChunkSize).forEach(print);
+    text.chunked(logChunkSize).forEach(debugPrint);
   }
 
   /// A queue to be consumed by [setupSentry].
@@ -354,7 +354,7 @@ class SuperLogging {
         final date = config.dateFmt!.parse(basename(file.path));
         dates[file as File] = date;
         files.add(file);
-      } on FormatException {}
+      } on Exception catch (_) {}
     }
     final nowTime = DateTime.now();
 
@@ -374,7 +374,7 @@ class SuperLogging {
             "deleting log file ${file.path}",
           );
           await file.delete();
-        } catch (ignore) {}
+        } on Exception catch (_) {}
       }
     }
 

+ 7 - 7
auth/lib/core/logging/tunneled_transport.dart

@@ -46,7 +46,7 @@ class TunneledTransport implements Transport {
         _options.logger(
           SentryLevel.error,
           'API returned an error, statusCode = ${response.statusCode}, '
-              'body = ${response.body}',
+          'body = ${response.body}',
         );
       }
       return const SentryId.empty();
@@ -65,8 +65,8 @@ class TunneledTransport implements Transport {
   }
 
   Future<StreamedRequest> _createStreamedRequest(
-      SentryEnvelope envelope,
-      ) async {
+    SentryEnvelope envelope,
+  ) async {
     final streamedRequest = StreamedRequest('POST', _tunnel);
     envelope
         .envelopeStream(_options)
@@ -91,10 +91,10 @@ class _CredentialBuilder {
         _clock = clock;
 
   factory _CredentialBuilder(
-      Dsn? dsn,
-      String sdkIdentifier,
-      ClockProvider clock,
-      ) {
+    Dsn? dsn,
+    String sdkIdentifier,
+    ClockProvider clock,
+  ) {
     final authHeader = _buildAuthHeader(
       publicKey: dsn?.publicKey,
       secretKey: dsn?.secretKey,

+ 22 - 14
auth/lib/core/network.dart

@@ -4,9 +4,10 @@ import 'package:dio/dio.dart';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/event_bus.dart';
 import 'package:ente_auth/events/endpoint_updated_event.dart';
+import 'package:ente_auth/utils/package_info_util.dart';
+import 'package:ente_auth/utils/platform_util.dart';
 import 'package:fk_user_agent/fk_user_agent.dart';
 import 'package:flutter/foundation.dart';
-import 'package:package_info_plus/package_info_plus.dart';
 import 'package:uuid/uuid.dart';
 
 int kConnectTimeout = 15000;
@@ -16,34 +17,41 @@ class Network {
   late Dio _enteDio;
 
   Future<void> init() async {
-    await FkUserAgent.init();
-    final packageInfo = await PackageInfo.fromPlatform();
+    if (PlatformUtil.isMobile()) await FkUserAgent.init();
+    final packageInfo = await PackageInfoUtil().getPackageInfo();
+    final version = PackageInfoUtil().getVersion(packageInfo);
+    final packageName = PackageInfoUtil().getPackageName(packageInfo);
     final endpoint = Configuration.instance.getHttpEndpoint();
-    
+
     _dio = Dio(
       BaseOptions(
-        connectTimeout: kConnectTimeout,
+        connectTimeout: Duration(milliseconds: kConnectTimeout),
         headers: {
-          HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
-          'X-Client-Version': packageInfo.version,
-          'X-Client-Package': packageInfo.packageName,
+          HttpHeaders.userAgentHeader: PlatformUtil.isMobile()
+              ? FkUserAgent.userAgent
+              : Platform.operatingSystem,
+          'X-Client-Version': version,
+          'X-Client-Package': packageName,
         },
       ),
     );
-    
+
     _enteDio = Dio(
       BaseOptions(
         baseUrl: endpoint,
-        connectTimeout: kConnectTimeout,
+        connectTimeout: Duration(milliseconds: kConnectTimeout),
         headers: {
-          HttpHeaders.userAgentHeader: FkUserAgent.userAgent,
-          'X-Client-Version': packageInfo.version,
-          'X-Client-Package': packageInfo.packageName,
+          if (PlatformUtil.isMobile())
+            HttpHeaders.userAgentHeader: FkUserAgent.userAgent
+          else
+            HttpHeaders.userAgentHeader: Platform.operatingSystem,
+          'X-Client-Version': version,
+          'X-Client-Package': packageName,
         },
       ),
     );
     _setupInterceptors(endpoint);
-    
+
     Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
       final endpoint = Configuration.instance.getHttpEndpoint();
       _enteDio.options.baseUrl = endpoint;

+ 85 - 37
auth/lib/ente_theme_data.dart

@@ -5,12 +5,16 @@ import 'package:flutter/material.dart';
 final lightThemeData = ThemeData(
   fontFamily: 'Inter',
   brightness: Brightness.light,
+  dividerTheme: const DividerThemeData(
+    color: Colors.black12,
+  ),
   hintColor: const Color.fromRGBO(158, 158, 158, 1),
   primaryColor: const Color.fromRGBO(255, 110, 64, 1),
   primaryColorLight: const Color.fromRGBO(0, 0, 0, 0.541),
   iconTheme: const IconThemeData(color: Colors.black),
   primaryIconTheme:
       const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0),
+  buttonTheme: const ButtonThemeData(),
   outlinedButtonTheme: buildOutlinedButtonThemeData(
     bgDisabled: const Color.fromRGBO(158, 158, 158, 1),
     bgEnabled: const Color.fromRGBO(0, 0, 0, 1),
@@ -72,24 +76,42 @@ final lightThemeData = ThemeData(
           ? const Color.fromRGBO(255, 255, 255, 1)
           : const Color.fromRGBO(0, 0, 0, 1);
     }),
-  ),  radioTheme: RadioThemeData(
- fillColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
- if (states.contains(MaterialState.disabled)) { return null; }
- if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
- return null;
- }),
- ), switchTheme: SwitchThemeData(
- thumbColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
- if (states.contains(MaterialState.disabled)) { return null; }
- if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
- return null;
- }),
- trackColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
- if (states.contains(MaterialState.disabled)) { return null; }
- if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
- return null;
- }),
- ), colorScheme: const ColorScheme.light(
+  ),
+  radioTheme: RadioThemeData(
+    fillColor:
+        MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return null;
+      }
+      if (states.contains(MaterialState.selected)) {
+        return const Color.fromRGBO(102, 187, 106, 1);
+      }
+      return null;
+    }),
+  ),
+  switchTheme: SwitchThemeData(
+    thumbColor:
+        MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return null;
+      }
+      if (states.contains(MaterialState.selected)) {
+        return const Color.fromRGBO(102, 187, 106, 1);
+      }
+      return null;
+    }),
+    trackColor:
+        MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return null;
+      }
+      if (states.contains(MaterialState.selected)) {
+        return const Color.fromRGBO(102, 187, 106, 1);
+      }
+      return null;
+    }),
+  ),
+  colorScheme: const ColorScheme.light(
     primary: Colors.black,
     secondary: Color.fromARGB(255, 163, 163, 163),
   ).copyWith(background: const Color.fromRGBO(255, 255, 255, 1)),
@@ -98,6 +120,9 @@ final lightThemeData = ThemeData(
 final darkThemeData = ThemeData(
   fontFamily: 'Inter',
   brightness: Brightness.dark,
+  dividerTheme: const DividerThemeData(
+    color: Colors.white12,
+  ),
   primaryColorLight: const Color.fromRGBO(255, 255, 255, 0.702),
   iconTheme: const IconThemeData(color: Colors.white),
   primaryIconTheme:
@@ -105,6 +130,7 @@ final darkThemeData = ThemeData(
   hintColor: const Color.fromRGBO(158, 158, 158, 1),
   buttonTheme: const ButtonThemeData().copyWith(
     buttonColor: const Color.fromRGBO(45, 194, 98, 1.0),
+    height: 56,
   ),
   textTheme: _buildTextTheme(const Color.fromRGBO(255, 255, 255, 1)),
   outlinedButtonTheme: buildOutlinedButtonThemeData(
@@ -164,24 +190,43 @@ final darkThemeData = ThemeData(
         return const Color.fromRGBO(158, 158, 158, 1);
       }
     }),
-  ), radioTheme: RadioThemeData(
- fillColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
- if (states.contains(MaterialState.disabled)) { return null; }
- if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
- return null;
- }),
- ), switchTheme: SwitchThemeData(
- thumbColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
- if (states.contains(MaterialState.disabled)) { return null; }
- if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
- return null;
- }),
- trackColor: MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
- if (states.contains(MaterialState.disabled)) { return null; }
- if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); }
- return null;
- }),
- ), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: const Color.fromRGBO(0, 0, 0, 1)),
+  ),
+  radioTheme: RadioThemeData(
+    fillColor:
+        MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return null;
+      }
+      if (states.contains(MaterialState.selected)) {
+        return const Color.fromRGBO(102, 187, 106, 1);
+      }
+      return null;
+    }),
+  ),
+  switchTheme: SwitchThemeData(
+    thumbColor:
+        MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return null;
+      }
+      if (states.contains(MaterialState.selected)) {
+        return const Color.fromRGBO(102, 187, 106, 1);
+      }
+      return null;
+    }),
+    trackColor:
+        MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
+      if (states.contains(MaterialState.disabled)) {
+        return null;
+      }
+      if (states.contains(MaterialState.selected)) {
+        return const Color.fromRGBO(102, 187, 106, 1);
+      }
+      return null;
+    }),
+  ),
+  colorScheme: const ColorScheme.dark(primary: Colors.white)
+      .copyWith(background: const Color.fromRGBO(0, 0, 0, 1)),
 );
 
 TextTheme _buildTextTheme(Color textColor) {
@@ -400,6 +445,7 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData({
       shape: RoundedRectangleBorder(
         borderRadius: BorderRadius.circular(8),
       ),
+      fixedSize: const Size.fromHeight(56),
       alignment: Alignment.center,
       padding: const EdgeInsets.fromLTRB(50, 16, 50, 16),
       textStyle: const TextStyle(
@@ -436,7 +482,9 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({
 }) {
   return ElevatedButtonThemeData(
     style: ElevatedButton.styleFrom(
-      foregroundColor: onPrimary, backgroundColor: primary, elevation: elevation,
+      foregroundColor: onPrimary,
+      backgroundColor: primary,
+      elevation: elevation,
       alignment: Alignment.center,
       textStyle: const TextStyle(
         fontWeight: FontWeight.w600,

+ 2 - 2
auth/lib/gateway/authenticator.dart

@@ -25,7 +25,7 @@ class AuthenticatorGateway {
     try {
       final response = await _enteDio.get("/authenticator/key");
       return AuthKey.fromMap(response.data);
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       if (e.response != null && (e.response!.statusCode ?? 0) == 404) {
         throw AuthenticatorKeyNotFound();
       } else {
@@ -90,7 +90,7 @@ class AuthenticatorGateway {
       }
       return authEntities;
     } catch (e) {
-      if (e is DioError && e.response?.statusCode == 401) {
+      if (e is DioException && e.response?.statusCode == 401) {
         throw UnauthorizedError();
       } else {
         rethrow;

+ 2 - 0
auth/lib/l10n/arb/app_de.arb

@@ -145,6 +145,7 @@
   "lostDeviceTitle": "Gerät verloren?",
   "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
   "passkeyAuthTitle": "Passkey Authentifizierung",
+  "verifyPasskey": "Passkey verifizieren",
   "recoverAccount": "Konto wiederherstellen",
   "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
   "recover": "Wiederherstellen",
@@ -407,6 +408,7 @@
   "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
   "hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
   "waitingForBrowserRequest": "Warten auf Browseranfrage...",
+  "waitingForVerification": "Warte auf Bestätigung...",
   "passkey": "Passkey",
   "developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?",
   "developerSettings": "Entwicklereinstellungen",

+ 5 - 0
auth/lib/l10n/arb/app_en.arb

@@ -199,6 +199,10 @@
   "recoveryKeySaveDescription": "We don't store this key, please save this 24 word key in a safe place.",
   "doThisLater": "Do this later",
   "saveKey": "Save key",
+  "save": "Save",
+  "send": "Send",
+  "saveOrSendDescription": "Do you want to save this to your storage (Downloads folder by default) or send it to other apps?",
+  "saveOnlyDescription": "Do you want to save this to your storage (Downloads folder by default)?",
   "back": "Back",
   "createAccount": "Create account",
   "passwordStrength": "Password strength: {passwordStrengthValue}",
@@ -407,6 +411,7 @@
   "doNotSignOut": "Do not sign out",
   "hearUsWhereTitle": "How did you hear about Ente? (optional)",
   "hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
+  "recoveryKeySaved": "Recovery key saved in Downloads folder!",
   "waitingForBrowserRequest": "Waiting for browser request...",
   "waitingForVerification": "Waiting for verification...",
   "passkey": "Passkey",

+ 1 - 0
auth/lib/l10n/arb/app_ko.arb

@@ -0,0 +1 @@
+{}

+ 13 - 1
auth/lib/l10n/arb/app_nl.arb

@@ -144,6 +144,8 @@
   "enterCodeHint": "Voer de 6-cijferige code van je verificatie-app in",
   "lostDeviceTitle": "Apparaat verloren?",
   "twoFactorAuthTitle": "Tweestapsverificatie",
+  "passkeyAuthTitle": "Passkey verificatie",
+  "verifyPasskey": "Bevestig passkey",
   "recoverAccount": "Account herstellen",
   "enterRecoveryKeyHint": "Voer je herstelsleutel in",
   "recover": "Herstellen",
@@ -404,5 +406,15 @@
   "signOutOtherDevices": "Afmelden bij andere apparaten",
   "doNotSignOut": "Niet uitloggen",
   "hearUsWhereTitle": "Hoe hoorde je over Ente? (optioneel)",
-  "hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!"
+  "hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!",
+  "waitingForBrowserRequest": "Wachten op browserverzoek...",
+  "waitingForVerification": "Wachten op verificatie...",
+  "passkey": "Passkey",
+  "developerSettingsWarning": "Weet u zeker dat u de ontwikkelaarsinstellingen wilt wijzigen?",
+  "developerSettings": "Ontwikkelaarsinstellingen",
+  "serverEndpoint": "Server eindpunt",
+  "invalidEndpoint": "Ongeldig eindpunt",
+  "invalidEndpointMessage": "Sorry, het eindpunt dat u hebt ingevoerd is ongeldig. Voer een geldig eindpunt in en probeer het opnieuw.",
+  "endpointUpdatedMessage": "Eindpunt met succes bijgewerkt",
+  "customEndpoint": "Verbonden met {endpoint}"
 }

+ 18 - 1
auth/lib/l10n/arb/app_pl.arb

@@ -338,5 +338,22 @@
   "deleteCodeAuthMessage": "Uwierzytelnij, aby usunąć kod",
   "showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR",
   "confirmAccountDeleteTitle": "Potwierdź usunięcie konta",
-  "confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte."
+  "confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte.",
+  "androidBiometricNotRecognized": "Nie rozpoznano. Spróbuj ponownie.",
+  "@androidBiometricNotRecognized": {
+    "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
+  },
+  "androidSignInTitle": "Wymagana autoryzacja",
+  "@androidSignInTitle": {
+    "description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters."
+  },
+  "goToSettings": "Przejdź do Ustawień",
+  "@goToSettings": {
+    "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters."
+  },
+  "noInternetConnection": "Brak połączenia z Internetem",
+  "pleaseCheckYourInternetConnectionAndTryAgain": "Proszę sprawdzić połączenie internetowe i spróbować ponownie.",
+  "hearUsWhereTitle": "Jak usłyszałeś o Ente? (opcjonalnie)",
+  "waitingForVerification": "Oczekiwanie na weryfikację...",
+  "developerSettings": "Ustawienia deweloperskie"
 }

+ 5 - 3
auth/lib/l10n/arb/app_pt.arb

@@ -47,9 +47,9 @@
   },
   "copyEmailAction": "Copiar e-mail",
   "exportLogsAction": "Exportar logs",
-  "reportABug": "Reportar um bug",
+  "reportABug": "Reportar um problema",
   "crashAndErrorReporting": "Reporte de erros e falhas",
-  "reportBug": "Reportar bug",
+  "reportBug": "Reportar problema",
   "emailUsMessage": "Por favor, envie um e-mail para {email}",
   "@emailUsMessage": {
     "placeholders": {
@@ -145,6 +145,7 @@
   "lostDeviceTitle": "Perdeu seu dispositivo?",
   "twoFactorAuthTitle": "Autenticação de dois fatores",
   "passkeyAuthTitle": "Autenticação via Chave de acesso",
+  "verifyPasskey": "Verificar chave de acesso",
   "recoverAccount": "Recuperar conta",
   "enterRecoveryKeyHint": "Digite sua chave de recuperação",
   "recover": "Recuperar",
@@ -162,7 +163,7 @@
   "invalidEmailMessage": "Por favor, insira um endereço de e-mail válido.",
   "deleteAccount": "Excluir conta",
   "deleteAccountQuery": "Sentiremos muito por vê-lo partir. Você está enfrentando algum problema?",
-  "yesSendFeedbackAction": "Sim, enviar feedback",
+  "yesSendFeedbackAction": "Sim, enviar comentário",
   "noDeleteAccountAction": "Não, excluir conta",
   "initiateAccountDeleteTitle": "Por favor, autentique-se para iniciar a exclusão de conta",
   "sendEmail": "Enviar e-mail",
@@ -407,6 +408,7 @@
   "hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
   "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!",
   "waitingForBrowserRequest": "Aguardando solicitação do navegador...",
+  "waitingForVerification": "Esperando por verificação...",
   "passkey": "Chave de acesso",
   "developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?",
   "developerSettings": "Configurações de desenvolvedor",

+ 2 - 0
auth/lib/l10n/arb/app_zh.arb

@@ -145,6 +145,7 @@
   "lostDeviceTitle": "丢失了设备吗?",
   "twoFactorAuthTitle": "双因素认证",
   "passkeyAuthTitle": "通行密钥认证",
+  "verifyPasskey": "验证通行密钥",
   "recoverAccount": "恢复账户",
   "enterRecoveryKeyHint": "输入您的恢复密钥",
   "recover": "恢复",
@@ -407,6 +408,7 @@
   "hearUsWhereTitle": "您是如何知道Ente的? (可选的)",
   "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
   "waitingForBrowserRequest": "正在等待浏览器请求...",
+  "waitingForVerification": "等待验证...",
   "passkey": "通行密钥",
   "developerSettingsWarning": "您确定要修改开发者设置吗?",
   "developerSettings": "开发者设置",

+ 63 - 6
auth/lib/main.dart

@@ -2,7 +2,6 @@ import 'dart:async';
 import 'dart:io';
 
 import 'package:adaptive_theme/adaptive_theme.dart';
-import 'package:computer/computer.dart';
 import "package:ente_auth/app/view/app.dart";
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/constants.dart';
@@ -17,11 +16,14 @@ import 'package:ente_auth/services/preference_service.dart';
 import 'package:ente_auth/services/update_service.dart';
 import 'package:ente_auth/services/user_remote_flag_service.dart';
 import 'package:ente_auth/services/user_service.dart';
+import 'package:ente_auth/services/window_listener_service.dart';
 import 'package:ente_auth/store/code_store.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/ui/tools/lock_screen.dart';
 import 'package:ente_auth/ui/utils/icon_utils.dart';
-import 'package:ente_auth/utils/crypto_util.dart';
+import 'package:ente_auth/utils/platform_util.dart';
+import 'package:ente_auth/utils/window_protocol_handler.dart';
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/foundation.dart';
 import "package:flutter/material.dart";
 import 'package:flutter/scheduler.dart';
@@ -29,11 +31,52 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
 import 'package:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:privacy_screen/privacy_screen.dart';
+import 'package:tray_manager/tray_manager.dart';
+import 'package:window_manager/window_manager.dart';
 
 final _logger = Logger("main");
 
+Future<void> initSystemTray() async {
+  String path = Platform.isWindows
+      ? 'assets/icons/auth-icon.ico'
+      : 'assets/icons/auth-icon.png';
+  await trayManager.setIcon(path);
+  Menu menu = Menu(
+    items: [
+      MenuItem(
+        key: 'hide_window',
+        label: 'Hide Window',
+      ),
+      MenuItem(
+        key: 'show_window',
+        label: 'Show Window',
+      ),
+      MenuItem.separator(),
+      MenuItem(
+        key: 'exit_app',
+        label: 'Exit App',
+      ),
+    ],
+  );
+  await trayManager.setContextMenu(menu);
+}
+
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
+
+  initSystemTray().ignore();
+
+  if (PlatformUtil.isDesktop()) {
+    await windowManager.ensureInitialized();
+    await WindowListenerService.instance.init();
+    WindowOptions windowOptions = WindowOptions(
+      size: WindowListenerService.instance.getWindowSize(),
+    );
+    await windowManager.waitUntilReadyToShow(windowOptions, () async {
+      await windowManager.show();
+      await windowManager.focus();
+    });
+  }
   await _runInForeground();
   await _setupPrivacyScreen();
   if (Platform.isAndroid) {
@@ -70,10 +113,14 @@ ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) {
 }
 
 Future _runWithLogs(Function() function, {String prefix = ""}) async {
+  String dir = "";
+  try {
+    dir = "${(await getApplicationSupportDirectory()).path}/logs";
+  } catch (_) {}
   await SuperLogging.main(
     LogConfig(
       body: function,
-      logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
+      logDirPath: dir,
       maxLogFiles: 5,
       sentryDsn: sentryDSN,
       enableInDebugMode: true,
@@ -82,10 +129,19 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
   );
 }
 
+void _registerWindowsProtocol() {
+  const kWindowsScheme = 'ente';
+  // Register our protocol only on Windows platform
+  if (!kIsWeb && Platform.isWindows) {
+    WindowsProtocolHandler()
+        .register(kWindowsScheme, executable: null, arguments: null);
+  }
+}
+
 Future<void> _init(bool bool, {String? via}) async {
-  // Start workers asynchronously. No need to wait for them to start
-  Computer.shared().turnOn(workersCount: 4, verbose: kDebugMode).ignore();
-  CryptoUtil.init();
+  _registerWindowsProtocol();
+  await initCryptoUtil();
+
   await PreferenceService.instance.init();
   await CodeStore.instance.init();
   await Configuration.instance.init();
@@ -100,6 +156,7 @@ Future<void> _init(bool bool, {String? via}) async {
 }
 
 Future<void> _setupPrivacyScreen() async {
+  if (!PlatformUtil.isMobile()) return;
   final brightness =
       SchedulerBinding.instance.platformDispatcher.platformBrightness;
   bool isInDarkMode = brightness == Brightness.dark;

+ 16 - 30
auth/lib/models/code.dart

@@ -57,14 +57,7 @@ class Code {
       updatedAlgo,
       updatedType,
       updatedCounter,
-      "otpauth://${updatedType.name}/" +
-          updateIssuer +
-          ":" +
-          updateAccount +
-          "?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=" +
-          updateIssuer +
-          "&period=$updatePeriod&secret=" +
-          updatedSecret + (updatedType == Type.hotp ? "&counter=$updatedCounter" : ""),
+      "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
       generatedID: generatedID,
     );
   }
@@ -83,35 +76,28 @@ class Code {
       Algorithm.sha1,
       Type.totp,
       0,
-      "otpauth://totp/" +
-          issuer +
-          ":" +
-          account +
-          "?algorithm=SHA1&digits=6&issuer=" +
-          issuer +
-          "&period=30&secret=" +
-          secret,
+      "otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret",
     );
   }
 
   static Code fromRawData(String rawData) {
     Uri uri = Uri.parse(rawData);
     try {
-    return Code(
-      _getAccount(uri),
-      _getIssuer(uri),
-      _getDigits(uri),
-      _getPeriod(uri),
-      getSanitizedSecret(uri.queryParameters['secret']!),
-      _getAlgorithm(uri),
-      _getType(uri),
-      _getCounter(uri),
-      rawData,
-    );
-    } catch(e) {
+      return Code(
+        _getAccount(uri),
+        _getIssuer(uri),
+        _getDigits(uri),
+        _getPeriod(uri),
+        getSanitizedSecret(uri.queryParameters['secret']!),
+        _getAlgorithm(uri),
+        _getType(uri),
+        _getCounter(uri),
+        rawData,
+      );
+    } catch (e) {
       // if account name contains # without encoding,
       // rest of the url are treated as url fragment
-      if(rawData.contains("#")) {
+      if (rawData.contains("#")) {
         return Code.fromRawData(rawData.replaceAll("#", '%23'));
       } else {
         rethrow;
@@ -141,7 +127,7 @@ class Code {
       if (uri.queryParameters.containsKey("issuer")) {
         String issuerName = uri.queryParameters['issuer']!;
         // Handle issuer name with period
-        // See https://github.com/ente-io/auth/pull/77
+        // See https://github.com/ente-io/ente/pull/77
         if (issuerName.contains("period=")) {
           return issuerName.substring(0, issuerName.indexOf("period="));
         }

+ 0 - 9
auth/lib/models/derived_key_result.dart

@@ -1,9 +0,0 @@
-import 'dart:typed_data';
-
-class DerivedKeyResult {
-  final Uint8List key;
-  final int memLimit;
-  final int opsLimit;
-
-  DerivedKeyResult(this.key, this.memLimit, this.opsLimit);
-}

+ 0 - 15
auth/lib/models/encryption_result.dart

@@ -1,15 +0,0 @@
-import 'dart:typed_data';
-
-class EncryptionResult {
-  final Uint8List? encryptedData;
-  final Uint8List? key;
-  final Uint8List? header;
-  final Uint8List? nonce;
-
-  EncryptionResult({
-    this.encryptedData,
-    this.key,
-    this.header,
-    this.nonce,
-  });
-}

+ 115 - 102
auth/lib/onboarding/view/onboarding_page.dart

@@ -1,4 +1,5 @@
 import 'dart:async';
+import 'dart:io';
 
 import 'package:ente_auth/app/view/app.dart';
 import 'package:ente_auth/core/configuration.dart';
@@ -28,7 +29,7 @@ import "package:flutter/material.dart";
 import 'package:local_auth/local_auth.dart';
 
 class OnboardingPage extends StatefulWidget {
-  const OnboardingPage({Key? key}) : super(key: key);
+  const OnboardingPage({super.key});
 
   @override
   State<OnboardingPage> createState() => _OnboardingPageState();
@@ -86,118 +87,128 @@ class _OnboardingPageState extends State<OnboardingPage> {
               }
             }
           },
-          child: Center(
-            child: SingleChildScrollView(
-              child: Padding(
-                padding:
-                    const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
-                child: Column(
-                  children: [
-                    Column(
-                      children: [
-                        kDebugMode
-                            ? GestureDetector(
-                                child: const Align(
-                                  alignment: Alignment.topRight,
-                                  child: Text("Lang"),
+          child: SingleChildScrollView(
+            child: Center(
+              child: ConstrainedBox(
+                constraints:
+                    const BoxConstraints.tightFor(height: 800, width: 450),
+                child: Padding(
+                  padding: const EdgeInsets.symmetric(
+                    vertical: 40.0,
+                    horizontal: 40,
+                  ),
+                  child: Column(
+                    children: [
+                      Column(
+                        children: [
+                          kDebugMode
+                              ? GestureDetector(
+                                  child: const Align(
+                                    alignment: Alignment.topRight,
+                                    child: Text("Lang"),
+                                  ),
+                                  onTap: () async {
+                                    final locale = await getLocale();
+                                    // ignore: unawaited_futures
+                                    routeToPage(
+                                      context,
+                                      LanguageSelectorPage(
+                                        appSupportedLocales,
+                                        (locale) async {
+                                          await setLocale(locale);
+                                          App.setLocale(context, locale);
+                                        },
+                                        locale,
+                                      ),
+                                    ).then((value) {
+                                      setState(() {});
+                                    });
+                                  },
+                                )
+                              : const SizedBox(),
+                          Image.asset(
+                            "assets/sheild-front-gradient.png",
+                            width: 200,
+                            height: 200,
+                          ),
+                          const SizedBox(height: 12),
+                          const Text(
+                            "ente",
+                            style: TextStyle(
+                              fontWeight: FontWeight.bold,
+                              fontFamily: 'Montserrat',
+                              fontSize: 42,
+                            ),
+                          ),
+                          const SizedBox(height: 4),
+                          Text(
+                            "Authenticator",
+                            style: Theme.of(context).textTheme.headlineMedium,
+                          ),
+                          const SizedBox(height: 32),
+                          Text(
+                            l10n.onBoardingBody,
+                            textAlign: TextAlign.center,
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleLarge!
+                                .copyWith(
+                                  color: Colors.white38,
                                 ),
-                                onTap: () async {
-                                  final locale = await getLocale();
-                                  // ignore: unawaited_futures
-                                  routeToPage(
-                                    context,
-                                    LanguageSelectorPage(
-                                      appSupportedLocales,
-                                      (locale) async {
-                                        await setLocale(locale);
-                                        App.setLocale(context, locale);
-                                      },
-                                      locale,
-                                    ),
-                                  ).then((value) {
-                                    setState(() {});
-                                  });
-                                },
-                              )
-                            : const SizedBox(),
-                        Image.asset(
-                          "assets/sheild-front-gradient.png",
-                          width: 200,
-                          height: 200,
-                        ),
-                        const SizedBox(height: 12),
-                        const Text(
-                          "ente",
-                          style: TextStyle(
-                            fontWeight: FontWeight.bold,
-                            fontFamily: 'Montserrat',
-                            fontSize: 42,
                           ),
+                        ],
+                      ),
+                      const SizedBox(height: 100),
+                      Container(
+                        width: double.infinity,
+                        padding: const EdgeInsets.symmetric(horizontal: 20),
+                        child: GradientButton(
+                          onTap: _navigateToSignUpPage,
+                          text: l10n.newUser,
                         ),
-                        const SizedBox(height: 4),
-                        Text(
-                          "Authenticator",
-                          style: Theme.of(context).textTheme.headlineMedium,
-                        ),
-                        const SizedBox(height: 32),
-                        Text(
-                          l10n.onBoardingBody,
-                          textAlign: TextAlign.center,
-                          style:
-                              Theme.of(context).textTheme.titleLarge!.copyWith(
-                                    color: Colors.white38,
-                                  ),
-                        ),
-                      ],
-                    ),
-                    const SizedBox(height: 100),
-                    Container(
-                      width: double.infinity,
-                      padding: const EdgeInsets.symmetric(horizontal: 20),
-                      child: GradientButton(
-                        onTap: _navigateToSignUpPage,
-                        text: l10n.newUser,
                       ),
-                    ),
-                    const SizedBox(height: 4),
-                    Container(
-                      width: double.infinity,
-                      padding: const EdgeInsets.fromLTRB(20, 12, 20, 0),
-                      child: Hero(
-                        tag: "log_in",
-                        child: ElevatedButton(
-                          style: Theme.of(context)
-                              .colorScheme
-                              .optionalActionButtonStyle,
-                          onPressed: _navigateToSignInPage,
-                          child: Text(
-                            l10n.existingUser,
-                            style: const TextStyle(
-                              color: Colors.black, // same for both themes
+                      const SizedBox(height: 16),
+                      Container(
+                        height: 56,
+                        width: double.infinity,
+                        padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
+                        child: Hero(
+                          tag: "log_in",
+                          child: ElevatedButton(
+                            style: Theme.of(context)
+                                .colorScheme
+                                .optionalActionButtonStyle,
+                            onPressed: _navigateToSignInPage,
+                            child: Text(
+                              l10n.existingUser,
+                              style: const TextStyle(
+                                color: Colors.black, // same for both themes
+                              ),
                             ),
                           ),
                         ),
                       ),
-                    ),
-                    const SizedBox(height: 4),
-                    Container(
-                      width: double.infinity,
-                      padding: const EdgeInsets.only(top: 20, bottom: 20),
-                      child: GestureDetector(
-                        onTap: _optForOfflineMode,
-                        child: Center(
-                          child: Text(
-                            l10n.useOffline,
-                            style: body.copyWith(
-                              color:
-                                  Theme.of(context).colorScheme.mutedTextColor,
+                      const SizedBox(height: 4),
+                      Container(
+                        width: double.infinity,
+                        padding: const EdgeInsets.only(top: 20, bottom: 20),
+                        child: GestureDetector(
+                          onTap: _optForOfflineMode,
+                          child: Center(
+                            child: Text(
+                              l10n.useOffline,
+                              style: body.copyWith(
+                                color: Theme.of(context)
+                                    .colorScheme
+                                    .mutedTextColor,
+                              ),
                             ),
                           ),
                         ),
                       ),
-                    ),
-                    const DeveloperSettingsWidget(),
-                  ],
+                      const DeveloperSettingsWidget(),
+                    ],
+                  ),
                 ),
               ),
             ),
@@ -208,7 +219,9 @@ class _OnboardingPageState extends State<OnboardingPage> {
   }
 
   Future<void> _optForOfflineMode() async {
-    bool canCheckBio = await LocalAuthentication().canCheckBiometrics;
+    bool canCheckBio = Platform.isMacOS || Platform.isLinux
+        ? true
+        : await LocalAuthentication().canCheckBiometrics;
     if (!canCheckBio) {
       showToast(
         context,

+ 4 - 4
auth/lib/onboarding/view/setup_enter_secret_key_page.dart

@@ -9,7 +9,7 @@ import "package:flutter/material.dart";
 class SetupEnterSecretKeyPage extends StatefulWidget {
   final Code? code;
 
-  SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key);
+  SetupEnterSecretKeyPage({this.code, super.key});
 
   @override
   State<SetupEnterSecretKeyPage> createState() =>
@@ -32,7 +32,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
           widget.code != null ? safeDecode(widget.code!.account).trim() : null,
     );
     _secretController = TextEditingController(
-      text: widget.code != null ? widget.code!.secret : null,
+      text: widget.code?.secret,
     );
     _secretKeyObscured = widget.code != null;
     super.initState();
@@ -45,8 +45,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
       appBar: AppBar(
         title: Text(l10n.importAccountPageTitle),
       ),
-      body: SafeArea(
-        child: Center(
+      body: Center(
+        child: SingleChildScrollView(
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
             child: Column(

+ 12 - 6
auth/lib/onboarding/view/view_qr_page.dart

@@ -1,4 +1,3 @@
-
 import 'dart:math';
 
 import "package:ente_auth/l10n/l10n.dart";
@@ -10,7 +9,7 @@ import 'package:qr_flutter/qr_flutter.dart';
 class ViewQrPage extends StatelessWidget {
   final Code? code;
 
-  ViewQrPage({this.code, Key? key}) : super(key: key);
+  ViewQrPage({this.code, super.key});
 
   @override
   Widget build(BuildContext context) {
@@ -22,15 +21,22 @@ class ViewQrPage extends StatelessWidget {
       appBar: AppBar(
         title: Text(l10n.qrCode),
       ),
-      body: SafeArea(
-        child: Center(
+      body: Center(
+        child: SingleChildScrollView(
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
             child: Column(
               children: [
-                QrImage(
+                QrImageView(
                   data: code!.rawData,
-                  foregroundColor: Theme.of(context).colorScheme.onBackground,
+                  eyeStyle: QrEyeStyle(
+                    eyeShape: QrEyeShape.square,
+                    color: Theme.of(context).colorScheme.onBackground,
+                  ),
+                  dataModuleStyle: QrDataModuleStyle(
+                    dataModuleShape: QrDataModuleShape.square,
+                    color: Theme.of(context).colorScheme.onBackground,
+                  ),
                   version: QrVersions.auto,
                   size: qrSize,
                 ),

+ 25 - 24
auth/lib/services/authenticator_service.dart

@@ -15,9 +15,8 @@ import 'package:ente_auth/models/authenticator/entity_result.dart';
 import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
 import 'package:ente_auth/store/authenticator_db.dart';
 import 'package:ente_auth/store/offline_authenticator_db.dart';
-import 'package:ente_auth/utils/crypto_util.dart';
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/foundation.dart';
-import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
@@ -75,10 +74,10 @@ class AuthenticatorService {
     final key = await getOrCreateAuthDataKey(mode);
     for (LocalAuthEntity e in result) {
       try {
-        final decryptedValue = await CryptoUtil.decryptChaCha(
-          Sodium.base642bin(e.encryptedData),
+        final decryptedValue = await CryptoUtil.decryptData(
+          CryptoUtil.base642bin(e.encryptedData),
           key,
-          Sodium.base642bin(e.header),
+          CryptoUtil.base642bin(e.header),
         );
         final hasSynced = !(e.id == null || e.shouldSync);
         entities.add(
@@ -101,12 +100,13 @@ class AuthenticatorService {
     AccountMode accountMode,
   ) async {
     var key = await getOrCreateAuthDataKey(accountMode);
-    final encryptedKeyData = await CryptoUtil.encryptChaCha(
-      utf8.encode(plainText) as Uint8List,
+    final encryptedKeyData = await CryptoUtil.encryptData(
+      utf8.encode(plainText),
       key,
     );
-    String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
-    String header = Sodium.bin2base64(encryptedKeyData.header!);
+    String encryptedData =
+        CryptoUtil.bin2base64(encryptedKeyData.encryptedData!);
+    String header = CryptoUtil.bin2base64(encryptedKeyData.header!);
     final insertedID = accountMode.isOnline
         ? await _db.insert(encryptedData, header)
         : await _offlineDb.insert(encryptedData, header);
@@ -123,12 +123,13 @@ class AuthenticatorService {
     AccountMode accountMode,
   ) async {
     var key = await getOrCreateAuthDataKey(accountMode);
-    final encryptedKeyData = await CryptoUtil.encryptChaCha(
-      utf8.encode(plainText) as Uint8List,
+    final encryptedKeyData = await CryptoUtil.encryptData(
+      utf8.encode(plainText),
       key,
     );
-    String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
-    String header = Sodium.bin2base64(encryptedKeyData.header!);
+    String encryptedData =
+        CryptoUtil.bin2base64(encryptedKeyData.encryptedData!);
+    String header = CryptoUtil.bin2base64(encryptedKeyData.header!);
     final int affectedRows = accountMode.isOnline
         ? await _db.updateEntry(generatedID, encryptedData, header)
         : await _offlineDb.updateEntry(generatedID, encryptedData, header);
@@ -191,25 +192,25 @@ class AuthenticatorService {
   Future<void> _remoteToLocalSync() async {
     _logger.info('Initiating remote to local sync');
     final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
-    _logger.info("Current sync is " + lastSyncTime.toString());
+    _logger.info("Current sync is $lastSyncTime");
     const int fetchLimit = 500;
     final List<AuthEntity> result =
         await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
-    _logger.info(result.length.toString() + " entries fetched from remote");
+    _logger.info("${result.length} entries fetched from remote");
     if (result.isEmpty) {
       return;
     }
     final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
     List<String> deletedIDs =
         result.where((element) => element.isDeleted).map((e) => e.id).toList();
-    _logger.info(deletedIDs.length.toString() + " entries deleted");
+    _logger.info("${deletedIDs.length} entries deleted");
     result.removeWhere((element) => element.isDeleted);
     await _db.insertOrReplace(result);
     if (deletedIDs.isNotEmpty) {
       await _db.deleteByIDs(ids: deletedIDs);
     }
     await _prefs.setInt(_lastEntitySyncTime, maxSyncTime);
-    _logger.info("Setting synctime to " + maxSyncTime.toString());
+    _logger.info("Setting synctime to $maxSyncTime");
     if (result.length == fetchLimit) {
       _logger.info("Diff limit reached, pulling again");
       await _remoteToLocalSync();
@@ -223,7 +224,7 @@ class AuthenticatorService {
         .where((element) => element.shouldSync || element.id == null)
         .toList();
     _logger.info(
-      pendingUpdate.length.toString() + " entries to be updated at remote",
+      "${pendingUpdate.length} entries to be updated at remote",
     );
     for (LocalAuthEntity entity in pendingUpdate) {
       if (entity.id == null) {
@@ -262,21 +263,21 @@ class AuthenticatorService {
     try {
       final AuthKey response = await _gateway.getKey();
       final authKey = CryptoUtil.decryptSync(
-        Sodium.base642bin(response.encryptedKey),
+        CryptoUtil.base642bin(response.encryptedKey),
         _config.getKey()!,
-        Sodium.base642bin(response.header),
+        CryptoUtil.base642bin(response.header),
       );
-      await _config.setAuthSecretKey(Sodium.bin2base64(authKey));
+      await _config.setAuthSecretKey(CryptoUtil.bin2base64(authKey));
       return authKey;
     } on AuthenticatorKeyNotFound catch (e) {
       _logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}");
       final key = CryptoUtil.generateKey();
       final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
       await _gateway.createKey(
-        Sodium.bin2base64(encryptedKeyData.encryptedData!),
-        Sodium.bin2base64(encryptedKeyData.nonce!),
+        CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
+        CryptoUtil.bin2base64(encryptedKeyData.nonce!),
       );
-      await _config.setAuthSecretKey(Sodium.bin2base64(key));
+      await _config.setAuthSecretKey(CryptoUtil.bin2base64(key));
       return key;
     } catch (e, s) {
       _logger.severe("Failed to getOrCreateAuthDataKey", e, s);

+ 12 - 12
auth/lib/services/billing_service.dart

@@ -50,7 +50,7 @@ class BillingService {
 
   Future<Response<dynamic>> _fetchPrivateBillingPlans() {
     return _dio.get(
-      _config.getHttpEndpoint() + "/billing/user-plans/",
+      "${_config.getHttpEndpoint()}/billing/user-plans/",
       options: Options(
         headers: {
           "X-Auth-Token": _config.getToken(),
@@ -60,7 +60,7 @@ class BillingService {
   }
 
   Future<Response<dynamic>> _fetchPublicBillingPlans() {
-    return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
+    return _dio.get("${_config.getHttpEndpoint()}/billing/plans/v2");
   }
 
   Future<Subscription> verifySubscription(
@@ -70,7 +70,7 @@ class BillingService {
   }) async {
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/billing/verify-subscription",
+        "${_config.getHttpEndpoint()}/billing/verify-subscription",
         data: {
           "paymentProvider": paymentProvider ??
               (Platform.isAndroid ? "playstore" : "appstore"),
@@ -84,7 +84,7 @@ class BillingService {
         ),
       );
       return Subscription.fromMap(response.data["subscription"]);
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       if (e.response != null && e.response!.statusCode == 409) {
         throw SubscriptionAlreadyClaimedError();
       } else {
@@ -100,7 +100,7 @@ class BillingService {
     if (_cachedSubscription == null) {
       try {
         final response = await _dio.get(
-          _config.getHttpEndpoint() + "/billing/subscription",
+          "${_config.getHttpEndpoint()}/billing/subscription",
           options: Options(
             headers: {
               "X-Auth-Token": _config.getToken(),
@@ -109,7 +109,7 @@ class BillingService {
         );
         _cachedSubscription =
             Subscription.fromMap(response.data["subscription"]);
-      } on DioError catch (e, s) {
+      } on DioException catch (e, s) {
         _logger.severe(e, s);
         rethrow;
       }
@@ -120,7 +120,7 @@ class BillingService {
   Future<Subscription> cancelStripeSubscription() async {
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/billing/stripe/cancel-subscription",
+        "${_config.getHttpEndpoint()}/billing/stripe/cancel-subscription",
         options: Options(
           headers: {
             "X-Auth-Token": _config.getToken(),
@@ -129,7 +129,7 @@ class BillingService {
       );
       final subscription = Subscription.fromMap(response.data["subscription"]);
       return subscription;
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       _logger.severe(e, s);
       rethrow;
     }
@@ -138,7 +138,7 @@ class BillingService {
   Future<Subscription> activateStripeSubscription() async {
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/billing/stripe/activate-subscription",
+        "${_config.getHttpEndpoint()}/billing/stripe/activate-subscription",
         options: Options(
           headers: {
             "X-Auth-Token": _config.getToken(),
@@ -147,7 +147,7 @@ class BillingService {
       );
       final subscription = Subscription.fromMap(response.data["subscription"]);
       return subscription;
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       _logger.severe(e, s);
       rethrow;
     }
@@ -158,7 +158,7 @@ class BillingService {
   }) async {
     try {
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/billing/stripe/customer-portal",
+        "${_config.getHttpEndpoint()}/billing/stripe/customer-portal",
         queryParameters: {
           "redirectURL": kWebPaymentRedirectUrl,
         },
@@ -169,7 +169,7 @@ class BillingService {
         ),
       );
       return response.data["url"];
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       _logger.severe(e, s);
       rethrow;
     }

+ 14 - 2
auth/lib/services/local_authentication_service.dart

@@ -1,15 +1,21 @@
+import 'dart:io';
+
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/utils/auth_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_local_authentication/flutter_local_authentication.dart';
 import 'package:local_auth/local_auth.dart';
+import 'package:logging/logging.dart';
 
 class LocalAuthenticationService {
   LocalAuthenticationService._privateConstructor();
   static final LocalAuthenticationService instance =
       LocalAuthenticationService._privateConstructor();
+  final logger = Logger((LocalAuthenticationService).toString());
 
   Future<bool> requestLocalAuthentication(
     BuildContext context,
@@ -38,7 +44,7 @@ class LocalAuthenticationService {
     String errorDialogContent, [
     String errorDialogTitle = "",
   ]) async {
-    if (await LocalAuthentication().isDeviceSupported()) {
+    if (await _isLocalAuthSupportedOnDevice()) {
       AppLock.of(context)!.disable();
       final result = await requestAuthentication(
         context,
@@ -65,6 +71,12 @@ class LocalAuthenticationService {
   }
 
   Future<bool> _isLocalAuthSupportedOnDevice() async {
-    return await LocalAuthentication().isDeviceSupported();
+    try {
+      return Platform.isMacOS || Platform.isLinux
+          ? await FlutterLocalAuthentication().canAuthenticate()
+          : await LocalAuthentication().isDeviceSupported();
+    } on MissingPluginException {
+      return false;
+    }
   }
 }

+ 1 - 2
auth/lib/services/notification_service.dart

@@ -27,8 +27,7 @@ class NotificationService {
         _flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
             AndroidFlutterLocalNotificationsPlugin>();
     if (implementation != null) {
-      // ignore: unawaited_futures
-      implementation.requestPermission();
+      await implementation.requestNotificationsPermission();
     }
   }
 

+ 6 - 1
auth/lib/services/update_service.dart

@@ -4,6 +4,7 @@ import 'dart:io';
 import 'package:ente_auth/core/constants.dart';
 import 'package:ente_auth/core/network.dart';
 import 'package:ente_auth/services/notification_service.dart';
+import 'package:ente_auth/utils/platform_util.dart';
 import 'package:logging/logging.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:shared_preferences/shared_preferences.dart';
@@ -130,7 +131,8 @@ class UpdateService {
 
   bool isIndependent() {
     return flavor == "independent" ||
-        _packageInfo.packageName.endsWith("independent");
+        _packageInfo.packageName.endsWith("independent") ||
+        PlatformUtil.isDesktop();
   }
 }
 
@@ -141,6 +143,7 @@ class LatestVersionInfo {
   final bool? shouldForceUpdate;
   final int lastSupportedVersionCode;
   final String? url;
+  final String? release;
   final int? size;
   final bool? shouldNotify;
 
@@ -151,6 +154,7 @@ class LatestVersionInfo {
     this.shouldForceUpdate,
     this.lastSupportedVersionCode,
     this.url,
+    this.release,
     this.size,
     this.shouldNotify,
   );
@@ -163,6 +167,7 @@ class LatestVersionInfo {
       map['shouldForceUpdate'],
       map['lastSupportedVersionCode'] ?? 1,
       map['url'],
+      map['release'],
       map['size'],
       map['shouldNotify'],
     );

+ 2 - 2
auth/lib/services/user_remote_flag_service.dart

@@ -96,7 +96,7 @@ class UserRemoteFlagService {
         queryParams["defaultValue"] = defaultValue;
       }
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/remote-store",
+        "${_config.getHttpEndpoint()}/remote-store",
         queryParameters: queryParams,
         options: Options(
           headers: {
@@ -119,7 +119,7 @@ class UserRemoteFlagService {
   Future<void> _updateKeyValue(String key, String value) async {
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/remote-store/update",
+        "${_config.getHttpEndpoint()}/remote-store/update",
         data: {
           "key": key,
           "value": value,

+ 23 - 23
auth/lib/services/user_service.dart

@@ -30,9 +30,9 @@ import 'package:ente_auth/ui/home_page.dart';
 import 'package:ente_auth/ui/passkey_page.dart';
 import 'package:ente_auth/ui/two_factor_authentication_page.dart';
 import 'package:ente_auth/ui/two_factor_recovery_page.dart';
-import 'package:ente_auth/utils/crypto_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import "package:flutter/foundation.dart";
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
@@ -80,7 +80,7 @@ class UserService {
     await dialog.show();
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/ott",
+        "${_config.getHttpEndpoint()}/users/ott",
         data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
       );
       await dialog.hide();
@@ -102,7 +102,7 @@ class UserService {
         return;
       }
       unawaited(showGenericErrorDialog(context: context));
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       _logger.info(e);
       if (e.response != null && e.response!.statusCode == 403) {
@@ -129,7 +129,7 @@ class UserService {
     String type = "SubCancellation",
   }) async {
     await _dio.post(
-      _config.getHttpEndpoint() + "/anonymous/feedback",
+      "${_config.getHttpEndpoint()}/anonymous/feedback",
       data: {"feedback": feedback, "type": "type"},
     );
   }
@@ -173,7 +173,7 @@ class UserService {
     try {
       final response = await _enteDio.get("/users/sessions");
       return Sessions.fromMap(response.data);
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.info(e);
       rethrow;
     }
@@ -187,7 +187,7 @@ class UserService {
           "token": token,
         },
       );
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.info(e);
       rethrow;
     }
@@ -196,7 +196,7 @@ class UserService {
   Future<void> leaveFamilyPlan() async {
     try {
       await _enteDio.delete("/family/leave");
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.warning('failed to leave family plan', e);
       rethrow;
     }
@@ -306,11 +306,11 @@ class UserService {
       "ott": ott,
     };
     if (!_config.isLoggedIn()) {
-      verifyData["source"] = 'auth:' + _getRefSource();
+      verifyData["source"] = 'auth:${_getRefSource()}';
     }
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/verify-email",
+        "${_config.getHttpEndpoint()}/users/verify-email",
         data: verifyData,
       );
       await dialog.hide();
@@ -346,7 +346,7 @@ class UserService {
         // should never reach here
         throw Exception("unexpected response during email verification");
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.info(e);
       await dialog.hide();
       if (e.response != null && e.response!.statusCode == 410) {
@@ -410,7 +410,7 @@ class UserService {
         context.l10n.oops,
         context.l10n.verificationFailedPleaseTryAgain,
       );
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       if (e.response != null && e.response!.statusCode == 403) {
         // ignore: unawaited_futures
@@ -460,7 +460,7 @@ class UserService {
   Future<SrpAttributes> getSrpAttributes(String email) async {
     try {
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/users/srp/attributes",
+        "${_config.getHttpEndpoint()}/users/srp/attributes",
         queryParameters: {
           "email": email,
         },
@@ -470,7 +470,7 @@ class UserService {
       } else {
         throw Exception("get-srp-attributes action failed");
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       if (e.response != null && e.response!.statusCode == 404) {
         throw SrpSetupNotCompleteError();
       }
@@ -523,7 +523,7 @@ class UserService {
         // ignore: need to calculate secret to get M1, unused_local_variable
         final clientS = client.calculateSecret(serverB);
         final clientM = client.calculateClientEvidenceMessage();
-        // ignore: unused_local_variable
+
         late Response _;
         if (setKeysRequest == null) {
           _ = await _enteDio.post(
@@ -573,7 +573,7 @@ class UserService {
     late Uint8List keyEncryptionKey;
     _logger.finest('Start deriving key');
     keyEncryptionKey = await CryptoUtil.deriveKey(
-      utf8.encode(userPassword) as Uint8List,
+      utf8.encode(userPassword),
       CryptoUtil.base642bin(srpAttributes.kekSalt),
       srpAttributes.memLimit,
       srpAttributes.opsLimit,
@@ -596,7 +596,7 @@ class UserService {
 
     final A = client.generateClientCredentials(salt, identity, password);
     final createSessionResponse = await _dio.post(
-      _config.getHttpEndpoint() + "/users/srp/create-session",
+      "${_config.getHttpEndpoint()}/users/srp/create-session",
       data: {
         "srpUserID": srpAttributes.srpUserID,
         "srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
@@ -610,7 +610,7 @@ class UserService {
     final clientS = client.calculateSecret(serverB);
     final clientM = client.calculateClientEvidenceMessage();
     final response = await _dio.post(
-      _config.getHttpEndpoint() + "/users/srp/verify-session",
+      "${_config.getHttpEndpoint()}/users/srp/verify-session",
       data: {
         "sessionID": sessionID,
         "srpUserID": srpAttributes.srpUserID,
@@ -709,7 +709,7 @@ class UserService {
     await dialog.show();
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/two-factor/verify",
+        "${_config.getHttpEndpoint()}/users/two-factor/verify",
         data: {
           "sessionID": sessionID,
           "code": code,
@@ -729,7 +729,7 @@ class UserService {
           (route) => route.isFirst,
         );
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
@@ -772,7 +772,7 @@ class UserService {
     await dialog.show();
     try {
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/users/two-factor/recover",
+        "${_config.getHttpEndpoint()}/users/two-factor/recover",
         queryParameters: {
           "sessionID": sessionID,
           "twoFactorType": twoFactorTypeToString(type),
@@ -794,7 +794,7 @@ class UserService {
           (route) => route.isFirst,
         );
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
@@ -868,7 +868,7 @@ class UserService {
     }
     try {
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/two-factor/remove",
+        "${_config.getHttpEndpoint()}/users/two-factor/remove",
         data: {
           "sessionID": sessionID,
           "secret": secret,
@@ -891,7 +891,7 @@ class UserService {
           (route) => route.isFirst,
         );
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {

+ 36 - 0
auth/lib/services/window_listener_service.dart

@@ -0,0 +1,36 @@
+import 'dart:async';
+import 'dart:ui';
+
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:window_manager/window_manager.dart';
+
+class WindowListenerService {
+  late SharedPreferences _preferences;
+
+  WindowListenerService._privateConstructor();
+
+  static final WindowListenerService instance =
+      WindowListenerService._privateConstructor();
+
+  Future<void> init() async {
+    _preferences = await SharedPreferences.getInstance();
+  }
+
+  Size getWindowSize() {
+    final double windowWidth = _preferences.getDouble('windowWidth') ?? 450.0;
+    final double windowHeight = _preferences.getDouble('windowHeight') ?? 800.0;
+    return Size(windowWidth, windowHeight);
+  }
+
+  Future<void> onWindowResize() async {
+    // Save the window size to shared preferences
+    await _preferences.setDouble(
+      'windowWidth',
+      (await windowManager.getSize()).width,
+    );
+    await _preferences.setDouble(
+      'windowHeight',
+      (await windowManager.getSize()).height,
+    );
+  }
+}

+ 13 - 1
auth/lib/store/authenticator_db.dart

@@ -3,10 +3,12 @@ import 'dart:io';
 
 import 'package:ente_auth/models/authenticator/auth_entity.dart';
 import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
+import 'package:ente_auth/utils/directory_utils.dart';
 import 'package:flutter/foundation.dart';
 import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:sqflite/sqflite.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 
 class AuthenticatorDB {
   static const _databaseName = "ente.authenticator.db";
@@ -25,6 +27,16 @@ class AuthenticatorDB {
   }
 
   Future<Database> _initDatabase() async {
+    if (Platform.isWindows || Platform.isLinux) {
+      var databaseFactory = databaseFactoryFfi;
+      return await databaseFactory.openDatabase(
+        await DirectoryUtils.getDatabasePath(_databaseName),
+        options: OpenDatabaseOptions(
+          version: _databaseVersion,
+          onCreate: _onCreate,
+        ),
+      );
+    }
     final Directory documentsDirectory =
         await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
@@ -166,7 +178,7 @@ class AuthenticatorDB {
         batch.delete(entityTable, where: whereID, whereArgs: [id]);
       }
     }
-    await batch.commit();
+    final _ = await batch.commit();
     debugPrint("Done");
   }
 

+ 13 - 2
auth/lib/store/offline_authenticator_db.dart

@@ -3,10 +3,11 @@ import 'dart:io';
 
 import 'package:ente_auth/models/authenticator/auth_entity.dart';
 import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
+import 'package:ente_auth/utils/directory_utils.dart';
 import 'package:flutter/foundation.dart';
 import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
-import 'package:sqflite/sqflite.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 
 class OfflineAuthenticatorDB {
   static const _databaseName = "ente.offline_authenticator.db";
@@ -26,6 +27,16 @@ class OfflineAuthenticatorDB {
   }
 
   Future<Database> _initDatabase() async {
+    if (Platform.isWindows || Platform.isLinux) {
+      var databaseFactory = databaseFactoryFfi;
+      return await databaseFactory.openDatabase(
+        await DirectoryUtils.getDatabasePath(_databaseName),
+        options: OpenDatabaseOptions(
+          version: _databaseVersion,
+          onCreate: _onCreate,
+        ),
+      );
+    }
     final Directory documentsDirectory =
         await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
@@ -152,7 +163,7 @@ class OfflineAuthenticatorDB {
         batch.delete(entityTable, where: whereID, whereArgs: [id]);
       }
     }
-    await batch.commit();
+    final _ = await batch.commit();
     debugPrint("Done");
   }
 

+ 1 - 0
auth/lib/theme/colors.dart

@@ -204,6 +204,7 @@ const Color _warning700 = Color.fromRGBO(234, 63, 63, 1);
 const Color _warning500 = Color.fromRGBO(255, 101, 101, 1);
 const Color _warning800 = Color(0xFFF53434);
 const Color warning500 = Color.fromRGBO(255, 101, 101, 1);
+// ignore: unused_element
 const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
 
 const Color _caution500 = Color.fromRGBO(255, 194, 71, 1);

+ 1 - 1
auth/lib/ui/account/change_email_dialog.dart

@@ -5,7 +5,7 @@ import 'package:ente_auth/utils/email_util.dart';
 import 'package:flutter/material.dart';
 
 class ChangeEmailDialog extends StatefulWidget {
-  const ChangeEmailDialog({Key? key}) : super(key: key);
+  const ChangeEmailDialog({super.key});
 
   @override
   State<ChangeEmailDialog> createState() => _ChangeEmailDialogState();

+ 10 - 6
auth/lib/ui/account/delete_account_page.dart

@@ -7,15 +7,15 @@ import 'package:ente_auth/services/local_authentication_service.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/ui/common/dialogs.dart';
 import 'package:ente_auth/ui/common/gradient_button.dart';
-import 'package:ente_auth/utils/crypto_util.dart';
 import 'package:ente_auth/utils/email_util.dart';
+import 'package:ente_auth/utils/platform_util.dart';
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_sodium/flutter_sodium.dart';
 
 class DeleteAccountPage extends StatelessWidget {
   const DeleteAccountPage({
-    Key? key,
-  }) : super(key: key);
+    super.key,
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -150,6 +150,8 @@ class DeleteAccountPage extends StatelessWidget {
       l10n.initiateAccountDeleteTitle,
     );
 
+    await PlatformUtil.refocusWindows();
+
     if (hasAuthenticated) {
       final choice = await showChoiceDialogOld(
         context,
@@ -164,8 +166,10 @@ class DeleteAccountPage extends StatelessWidget {
         return;
       }
       final decryptChallenge = CryptoUtil.openSealSync(
-        Sodium.base642bin(response.encryptedChallenge),
-        Sodium.base642bin(Configuration.instance.getKeyAttributes()!.publicKey),
+        CryptoUtil.base642bin(response.encryptedChallenge),
+        CryptoUtil.base642bin(
+          Configuration.instance.getKeyAttributes()!.publicKey,
+        ),
         Configuration.instance.getSecretKey()!,
       );
       final challengeResponseStr = utf8.decode(decryptChallenge);

+ 15 - 29
auth/lib/ui/account/email_entry_page.dart

@@ -5,7 +5,7 @@ import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/theme/ente_theme.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
-import 'package:ente_auth/ui/common/web_page.dart';
+import 'package:ente_auth/utils/platform_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -14,7 +14,7 @@ import 'package:step_progress_indicator/step_progress_indicator.dart';
 import "package:styled_text/styled_text.dart";
 
 class EmailEntryPage extends StatefulWidget {
-  const EmailEntryPage({Key? key}) : super(key: key);
+  const EmailEntryPage({super.key});
 
   @override
   State<EmailEntryPage> createState() => _EmailEntryPageState();
@@ -190,6 +190,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                   padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
                   child: TextFormField(
                     keyboardType: TextInputType.text,
+                    textInputAction: TextInputAction.next,
                     controller: _passwordController1,
                     obscureText: !_password1Visible,
                     enableSuggestions: true,
@@ -427,15 +428,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
               tags: {
                 'u-terms': StyledTextActionTag(
                   (String? text, Map<String?, String?> attrs) =>
-                      Navigator.of(context).push(
-                    MaterialPageRoute(
-                      builder: (BuildContext context) {
-                        return WebPage(
-                          context.l10n.termsOfServicesTitle,
-                          "https://ente.io/terms",
-                        );
-                      },
-                    ),
+                      PlatformUtil.openWebView(
+                    context,
+                    context.l10n.termsOfServicesTitle,
+                    "https://ente.io/terms",
                   ),
                   style: const TextStyle(
                     decoration: TextDecoration.underline,
@@ -443,15 +439,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                 ),
                 'u-policy': StyledTextActionTag(
                   (String? text, Map<String?, String?> attrs) =>
-                      Navigator.of(context).push(
-                    MaterialPageRoute(
-                      builder: (BuildContext context) {
-                        return WebPage(
-                          context.l10n.privacyPolicyTitle,
-                          "https://ente.io/privacy",
-                        );
-                      },
-                    ),
+                      PlatformUtil.openWebView(
+                    context,
+                    context.l10n.privacyPolicyTitle,
+                    "https://ente.io/privacy",
                   ),
                   style: const TextStyle(
                     decoration: TextDecoration.underline,
@@ -494,15 +485,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
               tags: {
                 'underline': StyledTextActionTag(
                   (String? text, Map<String?, String?> attrs) =>
-                      Navigator.of(context).push(
-                    MaterialPageRoute(
-                      builder: (BuildContext context) {
-                        return WebPage(
-                          context.l10n.encryption,
-                          "https://ente.io/architecture",
-                        );
-                      },
-                    ),
+                      PlatformUtil.openWebView(
+                    context,
+                    context.l10n.encryption,
+                    "https://ente.io/architecture",
                   ),
                   style: const TextStyle(
                     decoration: TextDecoration.underline,

+ 43 - 50
auth/lib/ui/account/login_page.dart

@@ -6,13 +6,13 @@ import 'package:ente_auth/models/api/user/srp.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/ui/account/login_pwd_verification_page.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
-import 'package:ente_auth/ui/common/web_page.dart';
+import 'package:ente_auth/utils/platform_util.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import "package:styled_text/styled_text.dart";
 
 class LoginPage extends StatefulWidget {
-  const LoginPage({Key? key}) : super(key: key);
+  const LoginPage({super.key});
 
   @override
   State<LoginPage> createState() => _LoginPageState();
@@ -25,6 +25,36 @@ class _LoginPageState extends State<LoginPage> {
   Color? _emailInputFieldColor;
   final Logger _logger = Logger('_LoginPageState');
 
+  Future<void> onPressed() async {
+    await UserService.instance.setEmail(_email!);
+    Configuration.instance.resetVolatilePassword();
+    SrpAttributes? attr;
+    bool isEmailVerificationEnabled = true;
+    try {
+      attr = await UserService.instance.getSrpAttributes(_email!);
+      isEmailVerificationEnabled = attr.isEmailMFAEnabled;
+    } catch (e) {
+      if (e is! SrpSetupNotCompleteError) {
+        _logger.severe('Error getting SRP attributes', e);
+      }
+    }
+    if (attr != null && !isEmailVerificationEnabled) {
+      await Navigator.of(context).push(
+        MaterialPageRoute(
+          builder: (BuildContext context) {
+            return LoginPasswordVerificationPage(
+              srpAttributes: attr!,
+            );
+          },
+        ),
+      );
+    } else {
+      await UserService.instance
+          .sendOtt(context, _email!, isCreateAccountScreen: false);
+    }
+    FocusScope.of(context).unfocus();
+  }
+
   @override
   void initState() {
     _email = _config.getEmail();
@@ -60,36 +90,7 @@ class _LoginPageState extends State<LoginPage> {
         isKeypadOpen: isKeypadOpen,
         isFormValid: _emailIsValid,
         buttonText: context.l10n.logInLabel,
-        onPressedFunction: () async {
-          await UserService.instance.setEmail(_email!);
-          Configuration.instance.resetVolatilePassword();
-          SrpAttributes? attr;
-          bool isEmailVerificationEnabled = true;
-          try {
-            attr = await UserService.instance.getSrpAttributes(_email!);
-            isEmailVerificationEnabled = attr.isEmailMFAEnabled;
-          } catch (e) {
-            if (e is! SrpSetupNotCompleteError) {
-              _logger.severe('Error getting SRP attributes', e);
-            }
-          }
-          if (attr != null && !isEmailVerificationEnabled) {
-            // ignore: unawaited_futures
-            Navigator.of(context).push(
-              MaterialPageRoute(
-                builder: (BuildContext context) {
-                  return LoginPasswordVerificationPage(
-                    srpAttributes: attr!,
-                  );
-                },
-              ),
-            );
-          } else {
-            await UserService.instance
-                .sendOtt(context, _email!, isCreateAccountScreen: false);
-          }
-          FocusScope.of(context).unfocus();
-        },
+        onPressedFunction: onPressed,
       ),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -116,6 +117,8 @@ class _LoginPageState extends State<LoginPage> {
                   padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
                   child: TextFormField(
                     autofillHints: const [AutofillHints.email],
+                    onFieldSubmitted:
+                        _emailIsValid ? (value) => onPressed() : null,
                     decoration: InputDecoration(
                       fillColor: _emailInputFieldColor,
                       filled: true,
@@ -179,15 +182,10 @@ class _LoginPageState extends State<LoginPage> {
                           tags: {
                             'u-terms': StyledTextActionTag(
                               (String? text, Map<String?, String?> attrs) =>
-                                  Navigator.of(context).push(
-                                MaterialPageRoute(
-                                  builder: (BuildContext context) {
-                                    return WebPage(
-                                      context.l10n.termsOfServicesTitle,
-                                      "https://ente.io/terms",
-                                    );
-                                  },
-                                ),
+                                  PlatformUtil.openWebView(
+                                context,
+                                context.l10n.termsOfServicesTitle,
+                                "https://ente.io/terms",
                               ),
                               style: const TextStyle(
                                 decoration: TextDecoration.underline,
@@ -195,15 +193,10 @@ class _LoginPageState extends State<LoginPage> {
                             ),
                             'u-policy': StyledTextActionTag(
                               (String? text, Map<String?, String?> attrs) =>
-                                  Navigator.of(context).push(
-                                MaterialPageRoute(
-                                  builder: (BuildContext context) {
-                                    return WebPage(
-                                      context.l10n.privacyPolicyTitle,
-                                      "https://ente.io/privacy",
-                                    );
-                                  },
-                                ),
+                                  PlatformUtil.openWebView(
+                                context,
+                                context.l10n.privacyPolicyTitle,
+                                "https://ente.io/privacy",
                               ),
                               style: const TextStyle(
                                 decoration: TextDecoration.underline,

+ 13 - 9
auth/lib/ui/account/login_pwd_verification_page.dart

@@ -1,6 +1,5 @@
 import "package:dio/dio.dart";
 import 'package:ente_auth/core/configuration.dart';
-import "package:ente_auth/core/errors.dart";
 import "package:ente_auth/l10n/l10n.dart";
 import "package:ente_auth/models/api/user/srp.dart";
 import "package:ente_auth/services/user_service.dart";
@@ -9,6 +8,7 @@ import 'package:ente_auth/ui/common/dynamic_fab.dart';
 import "package:ente_auth/ui/components/buttons/button_widget.dart";
 import "package:ente_auth/utils/dialog_util.dart";
 import "package:ente_auth/utils/email_util.dart";
+import "package:ente_crypto_dart/ente_crypto_dart.dart";
 import 'package:flutter/material.dart';
 import "package:logging/logging.dart";
 
@@ -19,8 +19,7 @@ import "package:logging/logging.dart";
 // volatile password.
 class LoginPasswordVerificationPage extends StatefulWidget {
   final SrpAttributes srpAttributes;
-  const LoginPasswordVerificationPage({Key? key, required this.srpAttributes})
-      : super(key: key);
+  const LoginPasswordVerificationPage({super.key, required this.srpAttributes});
 
   @override
   State<LoginPasswordVerificationPage> createState() =>
@@ -36,6 +35,11 @@ class _LoginPasswordVerificationPageState
   bool _passwordInFocus = false;
   bool _passwordVisible = false;
 
+  Future<void> onPressed() async {
+    FocusScope.of(context).unfocus();
+    await verifyPassword(context, _passwordController.text);
+  }
+
   @override
   void initState() {
     super.initState();
@@ -77,10 +81,7 @@ class _LoginPasswordVerificationPageState
         isKeypadOpen: isKeypadOpen,
         isFormValid: _passwordController.text.isNotEmpty,
         buttonText: context.l10n.logInLabel,
-        onPressedFunction: () async {
-          FocusScope.of(context).unfocus();
-          await verifyPassword(context, _passwordController.text);
-        },
+        onPressedFunction: onPressed,
       ),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -101,7 +102,7 @@ class _LoginPasswordVerificationPageState
         password,
         dialog,
       );
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       await dialog.hide();
       if (e.response != null && e.response!.statusCode == 401) {
         _logger.severe('server reject, failed verify SRP login', e, s);
@@ -112,7 +113,7 @@ class _LoginPasswordVerificationPageState
         );
       } else {
         _logger.severe('API failure during SRP login', e, s);
-        if (e.type == DioErrorType.other) {
+        if (e.type == DioExceptionType.unknown) {
           await _showContactSupportDialog(
             context,
             context.l10n.noInternetConnection,
@@ -229,6 +230,9 @@ class _LoginPasswordVerificationPageState
                 Padding(
                   padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
                   child: TextFormField(
+                    onFieldSubmitted: _passwordController.text.isNotEmpty
+                        ? (_) => onPressed()
+                        : null,
                     key: const ValueKey("passwordInputField"),
                     autofillHints: const [AutofillHints.password],
                     decoration: InputDecoration(

+ 24 - 17
auth/lib/ui/account/ott_verification_page.dart

@@ -17,8 +17,8 @@ class OTTVerificationPage extends StatefulWidget {
     this.isChangeEmail = false,
     this.isCreateAccountScreen = false,
     this.isResetPasswordScreen = false,
-    Key? key,
-  }) : super(key: key);
+    super.key,
+  });
 
   @override
   State<OTTVerificationPage> createState() => _OTTVerificationPageState();
@@ -27,6 +27,23 @@ class OTTVerificationPage extends StatefulWidget {
 class _OTTVerificationPageState extends State<OTTVerificationPage> {
   final _verificationCodeController = TextEditingController();
 
+  Future<void> onPressed() async {
+    if (widget.isChangeEmail) {
+      await UserService.instance.changeEmail(
+        context,
+        widget.email,
+        _verificationCodeController.text,
+      );
+    } else {
+      await UserService.instance.verifyEmail(
+        context,
+        _verificationCodeController.text,
+        isResettingPasswordScreen: widget.isResetPasswordScreen,
+      );
+    }
+    FocusScope.of(context).unfocus();
+  }
+
   @override
   Widget build(BuildContext context) {
     final l10n = context.l10n;
@@ -68,22 +85,9 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
       body: _getBody(),
       floatingActionButton: DynamicFAB(
         isKeypadOpen: isKeypadOpen,
-        isFormValid: !(_verificationCodeController.text.isEmpty),
+        isFormValid: _verificationCodeController.text.isNotEmpty,
         buttonText: l10n.verify,
-        onPressedFunction: () {
-          if (widget.isChangeEmail) {
-            UserService.instance.changeEmail(
-              context,
-              widget.email,
-              _verificationCodeController.text,
-            );
-          } else {
-            UserService.instance
-                .verifyEmail(context, _verificationCodeController.text,
-              isResettingPasswordScreen: widget.isResetPasswordScreen,);
-          }
-          FocusScope.of(context).unfocus();
-        },
+        onPressedFunction: onPressed,
       ),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -160,6 +164,9 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
               padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
               child: TextFormField(
                 style: Theme.of(context).textTheme.titleMedium,
+                onFieldSubmitted: _verificationCodeController.text.isNotEmpty
+                    ? (_) => onPressed()
+                    : null,
                 decoration: InputDecoration(
                   filled: true,
                   hintText: l10n.tapToEnterCode,

+ 220 - 207
auth/lib/ui/account/password_entry_page.dart

@@ -4,11 +4,11 @@ import 'package:ente_auth/models/key_gen_result.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/ui/account/recovery_key_page.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
-import 'package:ente_auth/ui/common/web_page.dart';
 import 'package:ente_auth/ui/components/models/button_type.dart';
 import 'package:ente_auth/ui/home_page.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/navigation_util.dart';
+import 'package:ente_auth/utils/platform_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -25,7 +25,7 @@ enum PasswordEntryMode {
 class PasswordEntryPage extends StatefulWidget {
   final PasswordEntryMode mode;
 
-  const PasswordEntryPage({required this.mode, Key? key}) : super(key: key);
+  const PasswordEntryPage({required this.mode, super.key});
 
   @override
   State<PasswordEntryPage> createState() => _PasswordEntryPageState();
@@ -149,227 +149,239 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
       children: [
         Expanded(
           child: AutofillGroup(
-            child: ListView(
-              children: [
-                Padding(
-                  padding:
-                      const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
-                  child: Text(
-                    buttonTextAndHeading,
-                    style: Theme.of(context).textTheme.headlineMedium,
-                  ),
-                ),
-                Padding(
-                  padding: const EdgeInsets.symmetric(horizontal: 20),
-                  child: Text(
-                    widget.mode == PasswordEntryMode.set
-                        ? context.l10n.enterPasswordToEncrypt
-                        : context.l10n.enterNewPasswordToEncrypt,
-                    textAlign: TextAlign.start,
-                    style: Theme.of(context)
-                        .textTheme
-                        .titleMedium!
-                        .copyWith(fontSize: 14),
+            child: FocusTraversalGroup(
+              policy: OrderedTraversalPolicy(),
+              child: ListView(
+                children: [
+                  Padding(
+                    padding: const EdgeInsets.symmetric(
+                      vertical: 30,
+                      horizontal: 20,
+                    ),
+                    child: Text(
+                      buttonTextAndHeading,
+                      style: Theme.of(context).textTheme.headlineMedium,
+                    ),
                   ),
-                ),
-                const Padding(padding: EdgeInsets.all(8)),
-                Padding(
-                  padding: const EdgeInsets.symmetric(horizontal: 20),
-                  child: StyledText(
-                    text: context.l10n.passwordWarning,
-                    style: Theme.of(context)
-                        .textTheme
-                        .titleMedium!
-                        .copyWith(fontSize: 14),
-                    tags: {
-                      'underline': StyledTextTag(
-                        style:
-                            Theme.of(context).textTheme.titleMedium!.copyWith(
-                                  fontSize: 14,
-                                  decoration: TextDecoration.underline,
-                                ),
-                      ),
-                    },
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 20),
+                    child: Text(
+                      widget.mode == PasswordEntryMode.set
+                          ? context.l10n.enterPasswordToEncrypt
+                          : context.l10n.enterNewPasswordToEncrypt,
+                      textAlign: TextAlign.start,
+                      style: Theme.of(context)
+                          .textTheme
+                          .titleMedium!
+                          .copyWith(fontSize: 14),
+                    ),
                   ),
-                ),
-                const Padding(padding: EdgeInsets.all(12)),
-                Visibility(
-                  // hidden textForm for suggesting auto-fill service for saving
-                  // password
-                  visible: false,
-                  child: TextFormField(
-                    autofillHints: const [
-                      AutofillHints.email,
-                    ],
-                    autocorrect: false,
-                    keyboardType: TextInputType.emailAddress,
-                    initialValue: email,
-                    textInputAction: TextInputAction.next,
+                  const Padding(padding: EdgeInsets.all(8)),
+                  Padding(
+                    padding: const EdgeInsets.symmetric(horizontal: 20),
+                    child: StyledText(
+                      text: context.l10n.passwordWarning,
+                      style: Theme.of(context)
+                          .textTheme
+                          .titleMedium!
+                          .copyWith(fontSize: 14),
+                      tags: {
+                        'underline': StyledTextTag(
+                          style:
+                              Theme.of(context).textTheme.titleMedium!.copyWith(
+                                    fontSize: 14,
+                                    decoration: TextDecoration.underline,
+                                  ),
+                        ),
+                      },
+                    ),
                   ),
-                ),
-                Padding(
-                  padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
-                  child: TextFormField(
-                    autofillHints: const [AutofillHints.newPassword],
-                    decoration: InputDecoration(
-                      fillColor:
-                          _isPasswordValid ? _validFieldValueColor : null,
-                      filled: true,
-                      hintText: context.l10n.password,
-                      contentPadding: const EdgeInsets.all(20),
-                      border: UnderlineInputBorder(
-                        borderSide: BorderSide.none,
-                        borderRadius: BorderRadius.circular(6),
-                      ),
-                      suffixIcon: _password1InFocus
-                          ? IconButton(
-                              icon: Icon(
-                                _password1Visible
-                                    ? Icons.visibility
-                                    : Icons.visibility_off,
-                                color: Theme.of(context).iconTheme.color,
-                                size: 20,
-                              ),
-                              onPressed: () {
-                                setState(() {
-                                  _password1Visible = !_password1Visible;
-                                });
-                              },
-                            )
-                          : _isPasswordValid
-                              ? Icon(
-                                  Icons.check,
-                                  color: Theme.of(context)
-                                      .inputDecorationTheme
-                                      .focusedBorder!
-                                      .borderSide
-                                      .color,
-                                )
-                              : null,
+                  const Padding(padding: EdgeInsets.all(12)),
+                  Visibility(
+                    // hidden textForm for suggesting auto-fill service for saving
+                    // password
+                    visible: false,
+                    child: TextFormField(
+                      autofillHints: const [
+                        AutofillHints.email,
+                      ],
+                      autocorrect: false,
+                      keyboardType: TextInputType.emailAddress,
+                      initialValue: email,
+                      textInputAction: TextInputAction.next,
                     ),
-                    obscureText: !_password1Visible,
-                    controller: _passwordController1,
-                    autofocus: false,
-                    autocorrect: false,
-                    keyboardType: TextInputType.visiblePassword,
-                    onChanged: (password) {
-                      setState(() {
-                        _passwordInInputBox = password;
-                        _passwordStrength = estimatePasswordStrength(password);
-                        _isPasswordValid =
-                            _passwordStrength >= kMildPasswordStrengthThreshold;
-                        _passwordsMatch = _passwordInInputBox ==
-                            _passwordInInputConfirmationBox;
-                      });
-                    },
-                    textInputAction: TextInputAction.next,
-                    focusNode: _password1FocusNode,
                   ),
-                ),
-                const SizedBox(height: 8),
-                Padding(
-                  padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
-                  child: TextFormField(
-                    keyboardType: TextInputType.visiblePassword,
-                    controller: _passwordController2,
-                    obscureText: !_password2Visible,
-                    autofillHints: const [AutofillHints.newPassword],
-                    onEditingComplete: () => TextInput.finishAutofillContext(),
-                    decoration: InputDecoration(
-                      fillColor: _passwordsMatch ? _validFieldValueColor : null,
-                      filled: true,
-                      hintText: context.l10n.confirmPassword,
-                      contentPadding: const EdgeInsets.symmetric(
-                        horizontal: 20,
-                        vertical: 20,
-                      ),
-                      suffixIcon: _password2InFocus
-                          ? IconButton(
-                              icon: Icon(
-                                _password2Visible
-                                    ? Icons.visibility
-                                    : Icons.visibility_off,
-                                color: Theme.of(context).iconTheme.color,
-                                size: 20,
-                              ),
-                              onPressed: () {
-                                setState(() {
-                                  _password2Visible = !_password2Visible;
-                                });
-                              },
-                            )
-                          : _passwordsMatch
-                              ? Icon(
-                                  Icons.check,
-                                  color: Theme.of(context)
-                                      .inputDecorationTheme
-                                      .focusedBorder!
-                                      .borderSide
-                                      .color,
-                                )
-                              : null,
-                      border: UnderlineInputBorder(
-                        borderSide: BorderSide.none,
-                        borderRadius: BorderRadius.circular(6),
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
+                    child: TextFormField(
+                      autofillHints: const [AutofillHints.newPassword],
+                      onFieldSubmitted: (_) {
+                        do {
+                          FocusScope.of(context).nextFocus();
+                        } while (FocusScope.of(context).focusedChild!.context ==
+                            null);
+                      },
+                      decoration: InputDecoration(
+                        fillColor:
+                            _isPasswordValid ? _validFieldValueColor : null,
+                        filled: true,
+                        hintText: context.l10n.password,
+                        contentPadding: const EdgeInsets.all(20),
+                        border: UnderlineInputBorder(
+                          borderSide: BorderSide.none,
+                          borderRadius: BorderRadius.circular(6),
+                        ),
+                        suffixIcon: _password1InFocus
+                            ? IconButton(
+                                icon: Icon(
+                                  _password1Visible
+                                      ? Icons.visibility
+                                      : Icons.visibility_off,
+                                  color: Theme.of(context).iconTheme.color,
+                                  size: 20,
+                                ),
+                                onPressed: () {
+                                  setState(() {
+                                    _password1Visible = !_password1Visible;
+                                  });
+                                },
+                              )
+                            : _isPasswordValid
+                                ? Icon(
+                                    Icons.check,
+                                    color: Theme.of(context)
+                                        .inputDecorationTheme
+                                        .focusedBorder!
+                                        .borderSide
+                                        .color,
+                                  )
+                                : null,
                       ),
-                    ),
-                    focusNode: _password2FocusNode,
-                    onChanged: (cnfPassword) {
-                      setState(() {
-                        _passwordInInputConfirmationBox = cnfPassword;
-                        if (_passwordInInputBox != '') {
+                      obscureText: !_password1Visible,
+                      controller: _passwordController1,
+                      autofocus: false,
+                      autocorrect: false,
+                      keyboardType: TextInputType.visiblePassword,
+                      onChanged: (password) {
+                        setState(() {
+                          _passwordInInputBox = password;
+                          _passwordStrength =
+                              estimatePasswordStrength(password);
+                          _isPasswordValid = _passwordStrength >=
+                              kMildPasswordStrengthThreshold;
                           _passwordsMatch = _passwordInInputBox ==
                               _passwordInInputConfirmationBox;
-                        }
-                      });
-                    },
+                        });
+                      },
+                      textInputAction: TextInputAction.next,
+                      focusNode: _password1FocusNode,
+                    ),
                   ),
-                ),
-                Opacity(
-                  opacity:
-                      (_passwordInInputBox != '') && _password1InFocus ? 1 : 0,
-                  child: Padding(
-                    padding:
-                        const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
-                    child: Text(
-                      context.l10n.passwordStrength(passwordStrengthText),
-                      style: TextStyle(
-                        color: passwordStrengthColor,
+                  const SizedBox(height: 8),
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
+                    child: TextFormField(
+                      keyboardType: TextInputType.visiblePassword,
+                      controller: _passwordController2,
+                      obscureText: !_password2Visible,
+                      autofillHints: const [AutofillHints.newPassword],
+                      onEditingComplete: () =>
+                          TextInput.finishAutofillContext(),
+                      decoration: InputDecoration(
+                        fillColor:
+                            _passwordsMatch ? _validFieldValueColor : null,
+                        filled: true,
+                        hintText: context.l10n.confirmPassword,
+                        contentPadding: const EdgeInsets.symmetric(
+                          horizontal: 20,
+                          vertical: 20,
+                        ),
+                        suffixIcon: _password2InFocus
+                            ? IconButton(
+                                icon: Icon(
+                                  _password2Visible
+                                      ? Icons.visibility
+                                      : Icons.visibility_off,
+                                  color: Theme.of(context).iconTheme.color,
+                                  size: 20,
+                                ),
+                                onPressed: () {
+                                  setState(() {
+                                    _password2Visible = !_password2Visible;
+                                  });
+                                },
+                              )
+                            : _passwordsMatch
+                                ? Icon(
+                                    Icons.check,
+                                    color: Theme.of(context)
+                                        .inputDecorationTheme
+                                        .focusedBorder!
+                                        .borderSide
+                                        .color,
+                                  )
+                                : null,
+                        border: UnderlineInputBorder(
+                          borderSide: BorderSide.none,
+                          borderRadius: BorderRadius.circular(6),
+                        ),
                       ),
+                      focusNode: _password2FocusNode,
+                      onChanged: (cnfPassword) {
+                        setState(() {
+                          _passwordInInputConfirmationBox = cnfPassword;
+                          if (_passwordInInputBox != '') {
+                            _passwordsMatch = _passwordInInputBox ==
+                                _passwordInInputConfirmationBox;
+                          }
+                        });
+                      },
                     ),
                   ),
-                ),
-                const SizedBox(height: 8),
-                GestureDetector(
-                  behavior: HitTestBehavior.translucent,
-                  onTap: () {
-                    Navigator.of(context).push(
-                      MaterialPageRoute(
-                        builder: (BuildContext context) {
-                          return WebPage(
-                            context.l10n.howItWorks,
-                            "https://ente.io/architecture",
-                          );
-                        },
+                  Opacity(
+                    opacity: (_passwordInInputBox != '') && _password1InFocus
+                        ? 1
+                        : 0,
+                    child: Padding(
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 20,
+                        vertical: 8,
                       ),
-                    );
-                  },
-                  child: Container(
-                    padding: const EdgeInsets.symmetric(horizontal: 20),
-                    child: RichText(
-                      text: TextSpan(
-                        text: context.l10n.howItWorks,
-                        style:
-                            Theme.of(context).textTheme.titleMedium!.copyWith(
-                                  fontSize: 14,
-                                  decoration: TextDecoration.underline,
-                                ),
+                      child: Text(
+                        context.l10n.passwordStrength(passwordStrengthText),
+                        style: TextStyle(
+                          color: passwordStrengthColor,
+                        ),
                       ),
                     ),
                   ),
-                ),
-                const Padding(padding: EdgeInsets.all(20)),
-              ],
+                  const SizedBox(height: 8),
+                  GestureDetector(
+                    behavior: HitTestBehavior.translucent,
+                    onTap: () {
+                      PlatformUtil.openWebView(
+                        context,
+                        context.l10n.howItWorks,
+                        "https://ente.io/architecture",
+                      );
+                    },
+                    child: Container(
+                      padding: const EdgeInsets.symmetric(horizontal: 20),
+                      child: RichText(
+                        text: TextSpan(
+                          text: context.l10n.howItWorks,
+                          style:
+                              Theme.of(context).textTheme.titleMedium!.copyWith(
+                                    fontSize: 14,
+                                    decoration: TextDecoration.underline,
+                                  ),
+                        ),
+                      ),
+                    ),
+                  ),
+                  const Padding(padding: EdgeInsets.all(20)),
+                ],
+              ),
             ),
           ),
         ),
@@ -463,6 +475,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
           showGenericErrorDialog(context: context);
         }
       }
+
       // ignore: unawaited_futures
       routeToPage(
         context,

+ 26 - 18
auth/lib/ui/account/password_reentry_page.dart

@@ -9,14 +9,14 @@ import 'package:ente_auth/ui/account/recovery_page.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
 import 'package:ente_auth/ui/components/buttons/button_widget.dart';
 import 'package:ente_auth/ui/home_page.dart';
-import 'package:ente_auth/utils/crypto_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/email_util.dart';
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 
 class PasswordReentryPage extends StatefulWidget {
-  const PasswordReentryPage({Key? key}) : super(key: key);
+  const PasswordReentryPage({super.key});
 
   @override
   State<PasswordReentryPage> createState() => _PasswordReentryPageState();
@@ -261,8 +261,8 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                 ),
                 Padding(
                   padding: const EdgeInsets.symmetric(horizontal: 20),
-                  child: Wrap(
-                    alignment: WrapAlignment.spaceBetween,
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     children: [
                       GestureDetector(
                         behavior: HitTestBehavior.opaque,
@@ -275,13 +275,17 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                             ),
                           );
                         },
-                        child: Text(
-                          context.l10n.forgotPassword,
-                          style:
-                              Theme.of(context).textTheme.titleMedium!.copyWith(
-                                    fontSize: 14,
-                                    decoration: TextDecoration.underline,
-                                  ),
+                        child: Center(
+                          child: Text(
+                            context.l10n.forgotPassword,
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleMedium!
+                                .copyWith(
+                                  fontSize: 14,
+                                  decoration: TextDecoration.underline,
+                                ),
+                          ),
                         ),
                       ),
                       GestureDetector(
@@ -297,13 +301,17 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                           Navigator.of(context)
                               .popUntil((route) => route.isFirst);
                         },
-                        child: Text(
-                          context.l10n.changeEmail,
-                          style:
-                              Theme.of(context).textTheme.titleMedium!.copyWith(
-                                    fontSize: 14,
-                                    decoration: TextDecoration.underline,
-                                  ),
+                        child: Center(
+                          child: Text(
+                            context.l10n.changeEmail,
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleMedium!
+                                .copyWith(
+                                  fontSize: 14,
+                                  decoration: TextDecoration.underline,
+                                ),
+                          ),
                         ),
                       ),
                     ],

+ 159 - 69
auth/lib/ui/account/recovery_key_page.dart

@@ -1,3 +1,4 @@
+import 'dart:convert';
 import 'dart:io' as io;
 
 import 'package:bip39/bip39.dart' as bip39;
@@ -7,7 +8,10 @@ import 'package:ente_auth/core/constants.dart';
 import 'package:ente_auth/ente_theme_data.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/ui/common/gradient_button.dart';
+import 'package:ente_auth/utils/platform_util.dart';
+import 'package:ente_auth/utils/share_utils.dart';
 import 'package:ente_auth/utils/toast_util.dart';
+import 'package:file_saver/file_saver.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:share_plus/share_plus.dart';
@@ -27,7 +31,7 @@ class RecoveryKeyPage extends StatefulWidget {
   const RecoveryKeyPage(
     this.recoveryKey,
     this.doneText, {
-    Key? key,
+    super.key,
     this.showAppBar,
     this.onDone,
     this.isDismissible,
@@ -35,7 +39,7 @@ class RecoveryKeyPage extends StatefulWidget {
     this.text,
     this.subText,
     this.showProgressBar = false,
-  }) : super(key: key);
+  });
 
   @override
   State<RecoveryKeyPage> createState() => _RecoveryKeyPageState();
@@ -44,7 +48,7 @@ class RecoveryKeyPage extends StatefulWidget {
 class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
   bool _hasTriedToSave = false;
   final _recoveryKeyFile = io.File(
-    Configuration.instance.getTempDirectory() + "ente-recovery-key.txt",
+    "${Configuration.instance.getTempDirectory()}ente-recovery-key.txt",
   );
 
   @override
@@ -61,6 +65,21 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
             ? 32
             : 120;
 
+    Future<void> copy() async {
+      await Clipboard.setData(
+        ClipboardData(
+          text: recoveryKey,
+        ),
+      );
+      showShortToast(
+        context,
+        context.l10n.recoveryKeyCopiedToClipboard,
+      );
+      setState(() {
+        _hasTriedToSave = true;
+      });
+    }
+
     return Scaffold(
       appBar: widget.showProgressBar
           ? AppBar(
@@ -113,62 +132,73 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
                         style: Theme.of(context).textTheme.titleMedium,
                       ),
                       const Padding(padding: EdgeInsets.only(top: 24)),
-                      DottedBorder(
-                        color: const Color.fromARGB(255, 105, 17, 127),
-                        //color of dotted/dash line
-                        strokeWidth: 1,
-                        //thickness of dash/dots
-                        dashPattern: const [6, 6],
-                        radius: const Radius.circular(8),
-                        //dash patterns, 10 is dash width, 6 is space width
-                        child: SizedBox(
-                          //inner container
-                          // height: 120, //height of inner container
-                          width: double
-                              .infinity, //width to 100% match to parent container.
-                          // ignore: prefer_const_literals_to_create_immutables
-                          child: Column(
-                            children: [
-                              GestureDetector(
-                                onTap: () async {
-                                  await Clipboard.setData(
-                                    ClipboardData(text: recoveryKey),
-                                  );
-                                  showShortToast(
-                                    context,
-                                    context.l10n.recoveryKeyCopiedToClipboard,
-                                  );
-                                  setState(() {
-                                    _hasTriedToSave = true;
-                                  });
-                                },
-                                child: Container(
-                                  decoration: BoxDecoration(
-                                    border: Border.all(
-                                      color: const Color.fromRGBO(
-                                        49,
-                                        155,
-                                        86,
-                                        .2,
-                                      ),
-                                    ),
-                                    borderRadius: const BorderRadius.all(
-                                      Radius.circular(2),
+                      Container(
+                        padding: const EdgeInsets.all(1),
+                        decoration: BoxDecoration(
+                          borderRadius: BorderRadius.circular(8),
+                          gradient: const LinearGradient(
+                            begin: Alignment.topCenter,
+                            end: Alignment.bottomCenter,
+                            colors: [
+                              Color(0x8E9610D6),
+                              Color(0x8E9F4FC6),
+                            ],
+                            stops: [0.0, 0.9753],
+                          ),
+                        ),
+                        child: DottedBorder(
+                          padding: EdgeInsets.zero,
+                          borderType: BorderType.RRect,
+                          strokeWidth: 1,
+                          color: const Color(0xFF6B6B6B),
+                          dashPattern: const [6, 6],
+                          radius: const Radius.circular(8),
+                          child: SizedBox(
+                            width: double.infinity,
+                            child: Stack(
+                              children: [
+                                Column(
+                                  children: [
+                                    Builder(
+                                      builder: (context) {
+                                        final content = Container(
+                                          padding: const EdgeInsets.all(20),
+                                          width: double.infinity,
+                                          child: Text(
+                                            recoveryKey,
+                                            textAlign: TextAlign.justify,
+                                            style: Theme.of(context)
+                                                .textTheme
+                                                .bodyLarge,
+                                          ),
+                                        );
+
+                                        if (PlatformUtil.isMobile()) {
+                                          return GestureDetector(
+                                            onTap: () async => await copy(),
+                                            child: content,
+                                          );
+                                        } else {
+                                          return SelectableRegion(
+                                            focusNode: FocusNode(),
+                                            selectionControls:
+                                                PlatformUtil.selectionControls,
+                                            child: content,
+                                          );
+                                        }
+                                      },
                                     ),
-                                    color: Theme.of(context)
-                                        .colorScheme
-                                        .recoveryKeyBoxColor,
-                                  ),
-                                  padding: const EdgeInsets.all(20),
-                                  width: double.infinity,
-                                  child: Text(
-                                    recoveryKey,
-                                    style:
-                                        Theme.of(context).textTheme.bodyLarge,
+                                  ],
+                                ),
+                                Positioned(
+                                  right: 0,
+                                  top: 0,
+                                  child: PlatformCopy(
+                                    onPressed: copy,
                                   ),
                                 ),
-                              ),
-                            ],
+                              ],
+                            ),
                           ),
                         ),
                       ),
@@ -193,7 +223,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
                         ),
                       ),
                     ],
-                  ), // columnEnds
+                  ),
                 ),
               ),
             );
@@ -207,12 +237,15 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
     final List<Widget> childrens = [];
     if (!_hasTriedToSave) {
       childrens.add(
-        ElevatedButton(
-          style: Theme.of(context).colorScheme.optionalActionButtonStyle,
-          onPressed: () async {
-            await _saveKeys();
-          },
-          child: Text(context.l10n.doThisLater),
+        SizedBox(
+          height: 56,
+          child: ElevatedButton(
+            style: Theme.of(context).colorScheme.optionalActionButtonStyle,
+            onPressed: () async {
+              await _saveKeys();
+            },
+            child: Text(context.l10n.doThisLater),
+          ),
         ),
       );
       childrens.add(const SizedBox(height: 10));
@@ -221,19 +254,32 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
     childrens.add(
       GradientButton(
         onTap: () async {
-          await _shareRecoveryKey(recoveryKey);
+          await shareDialog(
+            context,
+            context.l10n.recoveryKey,
+            saveAction: () async {
+              await _saveRecoveryKey(recoveryKey);
+            },
+            sendAction: () async {
+              await _shareRecoveryKey(recoveryKey);
+            },
+          );
         },
         text: context.l10n.saveKey,
       ),
     );
+
     if (_hasTriedToSave) {
       childrens.add(const SizedBox(height: 10));
       childrens.add(
-        ElevatedButton(
-          child: Text(widget.doneText),
-          onPressed: () async {
-            await _saveKeys();
-          },
+        SizedBox(
+          height: 56,
+          child: ElevatedButton(
+            child: Text(widget.doneText),
+            onPressed: () async {
+              await _saveKeys();
+            },
+          ),
         ),
       );
     }
@@ -241,11 +287,34 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
     return childrens;
   }
 
+  Future _saveRecoveryKey(String recoveryKey) async {
+    final bytes = utf8.encode(recoveryKey);
+    final time = DateTime.now().millisecondsSinceEpoch;
+
+    await PlatformUtil.shareFile(
+      "ente_recovery_key_$time",
+      "txt",
+      bytes,
+      MimeType.text,
+    );
+
+    if (mounted) {
+      showToast(
+        context,
+        context.l10n.recoveryKeySaved,
+      );
+      setState(() {
+        _hasTriedToSave = true;
+      });
+    }
+  }
+
   Future _shareRecoveryKey(String recoveryKey) async {
     if (_recoveryKeyFile.existsSync()) {
       await _recoveryKeyFile.delete();
     }
     _recoveryKeyFile.writeAsStringSync(recoveryKey);
+    // ignore: deprecated_member_use
     await Share.shareFiles([_recoveryKeyFile.path]);
     Future.delayed(const Duration(milliseconds: 500), () {
       if (mounted) {
@@ -264,3 +333,24 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
     widget.onDone!();
   }
 }
+
+class PlatformCopy extends StatelessWidget {
+  const PlatformCopy({
+    super.key,
+    required this.onPressed,
+  });
+
+  final void Function() onPressed;
+
+  @override
+  Widget build(BuildContext context) {
+    return IconButton(
+      onPressed: () => onPressed(),
+      visualDensity: VisualDensity.compact,
+      icon: const Icon(
+        Icons.copy,
+        size: 16,
+      ),
+    );
+  }
+}

+ 42 - 43
auth/lib/ui/account/recovery_page.dart

@@ -1,8 +1,5 @@
-
-
-import 'dart:ui';
-
 import 'package:ente_auth/core/configuration.dart';
+import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/ui/account/password_entry_page.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
@@ -10,7 +7,7 @@ import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 
 class RecoveryPage extends StatefulWidget {
-  const RecoveryPage({Key? key}) : super(key: key);
+  const RecoveryPage({super.key});
 
   @override
   State<RecoveryPage> createState() => _RecoveryPageState();
@@ -19,6 +16,36 @@ class RecoveryPage extends StatefulWidget {
 class _RecoveryPageState extends State<RecoveryPage> {
   final _recoveryKey = TextEditingController();
 
+  Future<void> onPressed() async {
+    FocusScope.of(context).unfocus();
+    final dialog = createProgressDialog(context, "Decrypting...");
+    await dialog.show();
+    try {
+      await Configuration.instance.recover(_recoveryKey.text.trim());
+      await dialog.hide();
+      showToast(context, "Recovery successful!");
+      await Navigator.of(context).pushReplacement(
+        MaterialPageRoute(
+          builder: (BuildContext context) {
+            return const PopScope(
+              canPop: false,
+              child: PasswordEntryPage(
+                mode: PasswordEntryMode.reset,
+              ),
+            );
+          },
+        ),
+      );
+    } catch (e) {
+      await dialog.hide();
+      String errMessage = 'The recovery key you entered is incorrect';
+      if (e is AssertionError) {
+        errMessage = '$errMessage : ${e.message}';
+      }
+      await showErrorDialog(context, "Incorrect recovery key", errMessage);
+    }
+  }
+
   @override
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
@@ -46,37 +73,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
         isKeypadOpen: isKeypadOpen,
         isFormValid: _recoveryKey.text.isNotEmpty,
         buttonText: 'Recover',
-        onPressedFunction: () async {
-          FocusScope.of(context).unfocus();
-          final dialog = createProgressDialog(context, "Decrypting...");
-          await dialog.show();
-          try {
-            await Configuration.instance.recover(_recoveryKey.text.trim());
-            await dialog.hide();
-            showToast(context, "Recovery successful!");
-            // ignore: unawaited_futures
-            Navigator.of(context).pushReplacement(
-              MaterialPageRoute(
-                builder: (BuildContext context) {
-                  return WillPopScope(
-                    onWillPop: () async => false,
-                    child: const PasswordEntryPage(
-                      mode: PasswordEntryMode.reset,
-                    ),
-                  );
-                },
-              ),
-            );
-          } catch (e) {
-            await dialog.hide();
-            String errMessage = 'The recovery key you entered is incorrect';
-            if (e is AssertionError) {
-              errMessage = '$errMessage : ${e.message}';
-            }
-            // ignore: unawaited_futures
-            showErrorDialog(context, "Incorrect recovery key", errMessage);
-          }
-        },
+        onPressedFunction: onPressed,
       ),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -89,7 +86,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
                   padding:
                       const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
                   child: Text(
-                    'Forgot password',
+                    context.l10n.forgotPassword,
                     style: Theme.of(context).textTheme.headlineMedium,
                   ),
                 ),
@@ -140,12 +137,14 @@ class _RecoveryPageState extends State<RecoveryPage> {
                         padding: const EdgeInsets.symmetric(horizontal: 20),
                         child: Center(
                           child: Text(
-                            "No recovery key?",
-                            style:
-                                Theme.of(context).textTheme.titleMedium!.copyWith(
-                                      fontSize: 14,
-                                      decoration: TextDecoration.underline,
-                                    ),
+                            context.l10n.noRecoveryKeyTitle,
+                            style: Theme.of(context)
+                                .textTheme
+                                .titleMedium!
+                                .copyWith(
+                                  fontSize: 14,
+                                  decoration: TextDecoration.underline,
+                                ),
                           ),
                         ),
                       ),

+ 10 - 8
auth/lib/ui/account/request_pwd_verification_page.dart

@@ -5,10 +5,9 @@ import 'package:ente_auth/core/configuration.dart';
 import "package:ente_auth/l10n/l10n.dart";
 import "package:ente_auth/theme/ente_theme.dart";
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
-import "package:ente_auth/utils/crypto_util.dart";
 import "package:ente_auth/utils/dialog_util.dart";
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/material.dart';
-import "package:flutter_sodium/flutter_sodium.dart";
 import "package:logging/logging.dart";
 
 typedef OnPasswordVerifiedFn = Future<void> Function(Uint8List bytes);
@@ -17,8 +16,11 @@ class RequestPasswordVerificationPage extends StatefulWidget {
   final OnPasswordVerifiedFn onPasswordVerified;
   final Function? onPasswordError;
 
-  const RequestPasswordVerificationPage(
-      {super.key, required this.onPasswordVerified, this.onPasswordError,});
+  const RequestPasswordVerificationPage({
+    super.key,
+    required this.onPasswordVerified,
+    this.onPasswordError,
+  });
 
   @override
   State<RequestPasswordVerificationPage> createState() =>
@@ -82,15 +84,15 @@ class _RequestPasswordVerificationPageState
           try {
             final attributes = Configuration.instance.getKeyAttributes()!;
             final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey(
-              utf8.encode(_passwordController.text) as Uint8List,
-              Sodium.base642bin(attributes.kekSalt),
+              utf8.encode(_passwordController.text),
+              CryptoUtil.base642bin(attributes.kekSalt),
               attributes.memLimit,
               attributes.opsLimit,
             );
             CryptoUtil.decryptSync(
-              Sodium.base642bin(attributes.encryptedKey),
+              CryptoUtil.base642bin(attributes.encryptedKey),
               keyEncryptionKey,
-              Sodium.base642bin(attributes.keyDecryptionNonce),
+              CryptoUtil.base642bin(attributes.keyDecryptionNonce),
             );
             await dialog.show();
             // pop

+ 1 - 1
auth/lib/ui/account/sessions_page.dart

@@ -11,7 +11,7 @@ import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 
 class SessionsPage extends StatefulWidget {
-  const SessionsPage({Key? key}) : super(key: key);
+  const SessionsPage({super.key});
 
   @override
   State<SessionsPage> createState() => _SessionsPageState();

+ 10 - 9
auth/lib/ui/account/verify_recovery_page.dart

@@ -1,5 +1,3 @@
-import 'dart:ui';
-
 import 'package:bip39/bip39.dart' as bip39;
 import 'package:dio/dio.dart';
 import 'package:ente_auth/core/configuration.dart';
@@ -12,12 +10,13 @@ import 'package:ente_auth/ui/common/gradient_button.dart';
 import 'package:ente_auth/ui/components/buttons/button_widget.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/navigation_util.dart';
+import 'package:ente_auth/utils/platform_util.dart';
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 
 class VerifyRecoveryPage extends StatefulWidget {
-  const VerifyRecoveryPage({Key? key}) : super(key: key);
+  const VerifyRecoveryPage({super.key});
 
   @override
   State<VerifyRecoveryPage> createState() => _VerifyRecoveryPageState();
@@ -34,14 +33,14 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
     try {
       final String inputKey = _recoveryKey.text.trim();
       final String recoveryKey =
-          Sodium.bin2hex(Configuration.instance.getRecoveryKey());
+          CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey());
       final String recoveryKeyWords = bip39.entropyToMnemonic(recoveryKey);
       if (inputKey == recoveryKey || inputKey == recoveryKeyWords) {
         try {
           await UserRemoteFlagService.instance.markRecoveryVerificationAsDone();
         } catch (e) {
           await dialog.hide();
-          if (e is DioError && e.type == DioErrorType.other) {
+          if (e is DioException && e.type == DioExceptionType.unknown) {
             await showErrorDialog(
               context,
               "No internet connection",
@@ -88,12 +87,14 @@ class _VerifyRecoveryPageState extends State<VerifyRecoveryPage> {
       context,
       "Please authenticate to view your recovery key",
     );
+    await PlatformUtil.refocusWindows();
+
     if (hasAuthenticated) {
       String recoveryKey;
       try {
-        recoveryKey = Sodium.bin2hex(Configuration.instance.getRecoveryKey());
-        // ignore: unawaited_futures
-        routeToPage(
+        recoveryKey =
+            CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey());
+        await routeToPage(
           context,
           RecoveryKeyPage(
             recoveryKey,

+ 3 - 3
auth/lib/ui/code_timer_progress.dart

@@ -6,12 +6,12 @@ class CodeTimerProgress extends StatefulWidget {
   final int period;
 
   CodeTimerProgress({
-    Key? key,
+    super.key,
     required this.period,
-  }) : super(key: key);
+  });
 
   @override
-  _CodeTimerProgressState createState() => _CodeTimerProgressState();
+  State createState() => _CodeTimerProgressState();
 }
 
 class _CodeTimerProgressState extends State<CodeTimerProgress>

+ 125 - 82
auth/lib/ui/code_widget.dart

@@ -14,9 +14,11 @@ import 'package:ente_auth/store/code_store.dart';
 import 'package:ente_auth/ui/code_timer_progress.dart';
 import 'package:ente_auth/ui/utils/icon_utils.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
+import 'package:ente_auth/utils/platform_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:ente_auth/utils/totp_util.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_context_menu/flutter_context_menu.dart';
 import 'package:flutter_slidable/flutter_slidable.dart';
 import 'package:logging/logging.dart';
 import 'package:move_to_background/move_to_background.dart';
@@ -24,7 +26,7 @@ import 'package:move_to_background/move_to_background.dart';
 class CodeWidget extends StatefulWidget {
   final Code code;
 
-  const CodeWidget(this.code, {Key? key}) : super(key: key);
+  const CodeWidget(this.code, {super.key});
 
   @override
   State<CodeWidget> createState() => _CodeWidgetState();
@@ -84,83 +86,121 @@ class _CodeWidgetState extends State<CodeWidget> {
     final l10n = context.l10n;
     return Container(
       margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
-      child: Slidable(
-        key: ValueKey(widget.code.hashCode),
-        endActionPane: ActionPane(
-          extentRatio: 0.60,
-          motion: const ScrollMotion(),
-          children: [
-            const SizedBox(
-              width: 4,
-            ),
-            SlidableAction(
-              onPressed: _onShowQrPressed,
-              backgroundColor: Colors.grey.withOpacity(0.1),
-              borderRadius: const BorderRadius.all(Radius.circular(12.0)),
-              foregroundColor:
-                  Theme.of(context).colorScheme.inverseBackgroundColor,
-              icon: Icons.qr_code_2_outlined,
-              label: "QR",
-              padding: const EdgeInsets.only(left: 4, right: 0),
-              spacing: 8,
-            ),
-            const SizedBox(
-              width: 4,
-            ),
-            SlidableAction(
-              onPressed: _onEditPressed,
-              backgroundColor: Colors.grey.withOpacity(0.1),
-              borderRadius: const BorderRadius.all(Radius.circular(12.0)),
-              foregroundColor:
-                  Theme.of(context).colorScheme.inverseBackgroundColor,
-              icon: Icons.edit_outlined,
-              label: l10n.edit,
-              padding: const EdgeInsets.only(left: 4, right: 0),
-              spacing: 8,
-            ),
-            const SizedBox(
-              width: 4,
+      child: Builder(
+        builder: (context) {
+          if (PlatformUtil.isDesktop()) {
+            return ContextMenuRegion(
+              contextMenu: ContextMenu(
+                entries: <ContextMenuEntry>[
+                  MenuItem(
+                    label: 'QR',
+                    icon: Icons.qr_code_2_outlined,
+                    onSelected: () => _onShowQrPressed(null),
+                  ),
+                  MenuItem(
+                    label: l10n.edit,
+                    icon: Icons.edit,
+                    onSelected: () => _onEditPressed(null),
+                  ),
+                  const MenuDivider(),
+                  MenuItem(
+                    label: l10n.delete,
+                    value: "Delete",
+                    icon: Icons.delete,
+                    onSelected: () => _onDeletePressed(null),
+                  ),
+                ],
+                padding: const EdgeInsets.all(8.0),
+              ),
+              child: _clippedCard(l10n),
+            );
+          }
+
+          return Slidable(
+            key: ValueKey(widget.code.hashCode),
+            endActionPane: ActionPane(
+              extentRatio: 0.60,
+              motion: const ScrollMotion(),
+              children: [
+                const SizedBox(
+                  width: 4,
+                ),
+                SlidableAction(
+                  onPressed: _onShowQrPressed,
+                  backgroundColor: Colors.grey.withOpacity(0.1),
+                  borderRadius: const BorderRadius.all(Radius.circular(12.0)),
+                  foregroundColor:
+                      Theme.of(context).colorScheme.inverseBackgroundColor,
+                  icon: Icons.qr_code_2_outlined,
+                  label: "QR",
+                  padding: const EdgeInsets.only(left: 4, right: 0),
+                  spacing: 8,
+                ),
+                const SizedBox(
+                  width: 4,
+                ),
+                SlidableAction(
+                  onPressed: _onEditPressed,
+                  backgroundColor: Colors.grey.withOpacity(0.1),
+                  borderRadius: const BorderRadius.all(Radius.circular(12.0)),
+                  foregroundColor:
+                      Theme.of(context).colorScheme.inverseBackgroundColor,
+                  icon: Icons.edit_outlined,
+                  label: l10n.edit,
+                  padding: const EdgeInsets.only(left: 4, right: 0),
+                  spacing: 8,
+                ),
+                const SizedBox(
+                  width: 4,
+                ),
+                SlidableAction(
+                  onPressed: _onDeletePressed,
+                  backgroundColor: Colors.grey.withOpacity(0.1),
+                  borderRadius: const BorderRadius.all(Radius.circular(12.0)),
+                  foregroundColor: const Color(0xFFFE4A49),
+                  icon: Icons.delete,
+                  label: l10n.delete,
+                  padding: const EdgeInsets.only(left: 0, right: 0),
+                  spacing: 8,
+                ),
+              ],
             ),
-            SlidableAction(
-              onPressed: _onDeletePressed,
-              backgroundColor: Colors.grey.withOpacity(0.1),
-              borderRadius: const BorderRadius.all(Radius.circular(12.0)),
-              foregroundColor: const Color(0xFFFE4A49),
-              icon: Icons.delete,
-              label: l10n.delete,
-              padding: const EdgeInsets.only(left: 0, right: 0),
-              spacing: 8,
+            child: Builder(
+              builder: (context) => _clippedCard(l10n),
             ),
-          ],
-        ),
-        child: ClipRRect(
-          borderRadius: BorderRadius.circular(8),
-          child: Container(
-            color: Theme.of(context).colorScheme.codeCardBackgroundColor,
-            child: Material(
-              color: Colors.transparent,
-              child: InkWell(
-                customBorder: RoundedRectangleBorder(
-                  borderRadius: BorderRadius.circular(10),
-                ),
-                onTap: () {
-                  _copyCurrentOTPToClipboard();
-                },
-                onDoubleTap: isMaskingEnabled
-                    ? () {
-                        setState(
-                          () {
-                            _hideCode = !_hideCode;
-                          },
-                        );
-                      }
-                    : null,
-                onLongPress: () {
-                  _copyCurrentOTPToClipboard();
-                },
-                child: _getCardContents(l10n),
-              ),
+          );
+        },
+      ),
+    );
+  }
+
+  Widget _clippedCard(AppLocalizations l10n) {
+    return ClipRRect(
+      borderRadius: BorderRadius.circular(8),
+      child: Container(
+        color: Theme.of(context).colorScheme.codeCardBackgroundColor,
+        child: Material(
+          color: Colors.transparent,
+          child: InkWell(
+            customBorder: RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(10),
             ),
+            onTap: () {
+              _copyCurrentOTPToClipboard();
+            },
+            onDoubleTap: isMaskingEnabled
+                ? () {
+                    setState(
+                      () {
+                        _hideCode = !_hideCode;
+                      },
+                    );
+                  }
+                : null,
+            onLongPress: () {
+              _copyCurrentOTPToClipboard();
+            },
+            child: _getCardContents(l10n),
           ),
         ),
       ),
@@ -373,9 +413,10 @@ class _CodeWidgetState extends State<CodeWidget> {
   }
 
   Future<void> _onEditPressed(_) async {
-    bool _isAuthSuccessful = await LocalAuthenticationService.instance
+    bool isAuthSuccessful = await LocalAuthenticationService.instance
         .requestLocalAuthentication(context, context.l10n.editCodeAuthMessage);
-    if (!_isAuthSuccessful) {
+    await PlatformUtil.refocusWindows();
+    if (!isAuthSuccessful) {
       return;
     }
     final Code? code = await Navigator.of(context).push(
@@ -391,9 +432,10 @@ class _CodeWidgetState extends State<CodeWidget> {
   }
 
   Future<void> _onShowQrPressed(_) async {
-    bool _isAuthSuccessful = await LocalAuthenticationService.instance
+    bool isAuthSuccessful = await LocalAuthenticationService.instance
         .requestLocalAuthentication(context, context.l10n.showQRAuthMessage);
-    if (!_isAuthSuccessful) {
+    await PlatformUtil.refocusWindows();
+    if (!isAuthSuccessful) {
       return;
     }
     // ignore: unused_local_variable
@@ -407,14 +449,15 @@ class _CodeWidgetState extends State<CodeWidget> {
   }
 
   void _onDeletePressed(_) async {
-    bool _isAuthSuccessful =
+    bool isAuthSuccessful =
         await LocalAuthenticationService.instance.requestLocalAuthentication(
       context,
       context.l10n.deleteCodeAuthMessage,
     );
-    if (!_isAuthSuccessful) {
+    if (!isAuthSuccessful) {
       return;
     }
+    FocusScope.of(context).requestFocus();
     final l10n = context.l10n;
     await showChoiceActionSheet(
       context,
@@ -451,7 +494,7 @@ class _CodeWidgetState extends State<CodeWidget> {
       code = code.replaceAll(RegExp(r'\d'), '•');
     }
     if (code.length == 6) {
-      return code.substring(0, 3) + " " + code.substring(3, 6);
+      return "${code.substring(0, 3)} ${code.substring(3, 6)}";
     }
     return code;
   }

+ 1 - 2
auth/lib/ui/common/bottom_shadow.dart

@@ -5,8 +5,7 @@ import 'package:flutter/material.dart';
 class BottomShadowWidget extends StatelessWidget {
   final double offsetDy;
   final Color? shadowColor;
-  const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, Key? key})
-      : super(key: key);
+  const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, super.key});
 
   @override
   Widget build(BuildContext context) {

+ 2 - 4
auth/lib/ui/common/DividerWithPadding.dart → auth/lib/ui/common/divider_with_padding.dart

@@ -1,17 +1,15 @@
-
-
 import 'package:flutter/material.dart';
 
 class DividerWithPadding extends StatelessWidget {
   final double left, top, right, bottom, thinckness;
   const DividerWithPadding({
-    Key? key,
+    super.key,
     this.left = 0,
     this.top = 0,
     this.right = 0,
     this.bottom = 0,
     this.thinckness = 0.5,
-  }) : super(key: key);
+  });
 
   @override
   Widget build(BuildContext context) {

+ 3 - 2
auth/lib/ui/common/dynamic_fab.dart

@@ -10,12 +10,12 @@ class DynamicFAB extends StatelessWidget {
   final Function? onPressedFunction;
 
   const DynamicFAB({
-    Key? key,
+    super.key,
     this.isKeypadOpen,
     this.buttonText,
     this.isFormValid,
     this.onPressedFunction,
-  }) : super(key: key);
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -60,6 +60,7 @@ class DynamicFAB extends StatelessWidget {
     } else {
       return Container(
         width: double.infinity,
+        height: 56,
         padding: const EdgeInsets.symmetric(horizontal: 20),
         child: OutlinedButton(
           onPressed:

+ 9 - 5
auth/lib/ui/common/gradient_button.dart

@@ -1,5 +1,3 @@
-
-
 import 'package:flutter/material.dart';
 
 class GradientButton extends StatelessWidget {
@@ -15,17 +13,21 @@ class GradientButton extends StatelessWidget {
   // padding between the text and icon
   final double paddingValue;
 
+  // used when two icons are in row
+  final bool reversedGradient;
+
   const GradientButton({
-    Key? key,
+    super.key,
     this.linearGradientColors = const [
       Color.fromARGB(255, 133, 44, 210),
       Color.fromARGB(255, 187, 26, 93),
     ],
+    this.reversedGradient = false,
     this.onTap,
     this.text = '',
     this.iconData,
     this.paddingValue = 0.0,
-  }) : super(key: key);
+  });
 
   @override
   Widget build(BuildContext context) {
@@ -71,7 +73,9 @@ class GradientButton extends StatelessWidget {
           gradient: LinearGradient(
             begin: const Alignment(0.1, -0.9),
             end: const Alignment(-0.6, 0.9),
-            colors: linearGradientColors,
+            colors: reversedGradient
+                ? linearGradientColors.reversed.toList()
+                : linearGradientColors,
           ),
           borderRadius: BorderRadius.circular(8),
         ),

+ 3 - 5
auth/lib/ui/common/linear_progress_dialog.dart

@@ -1,12 +1,10 @@
-
-
 import 'package:ente_auth/ente_theme_data.dart';
 import 'package:flutter/material.dart';
 
 class LinearProgressDialog extends StatefulWidget {
   final String message;
 
-  const LinearProgressDialog(this.message, {Key? key}) : super(key: key);
+  const LinearProgressDialog(this.message, {super.key});
 
   @override
   LinearProgressDialogState createState() => LinearProgressDialogState();
@@ -29,8 +27,8 @@ class LinearProgressDialogState extends State<LinearProgressDialog> {
 
   @override
   Widget build(BuildContext context) {
-    return WillPopScope(
-      onWillPop: () async => false,
+    return PopScope(
+      canPop: false,
       child: AlertDialog(
         title: Text(
           widget.message,

部分文件因文件數量過多而無法顯示