فهرست منبع

Merge branch 'main' into f-droid

ashilkn 1 سال پیش
والد
کامیت
07808d6139
100فایلهای تغییر یافته به همراه1856 افزوده شده و 1250 حذف شده
  1. BIN
      .github/assets/github-badge.png
  2. 4 4
      .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. 4 4
      .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. 10 4
      .github/workflows/web-crowdin.yml
  13. 43 0
      .github/workflows/web-deploy-payments.yml
  14. 1 1
      .github/workflows/web-lint.yml
  15. 14 1
      .github/workflows/web-nightly.yml
  16. 1 0
      .github/workflows/web-preview.yml
  17. 1 1
      README.md
  18. 9 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. BIN
      auth/assets/icons/auth-icon.ico
  34. BIN
      auth/assets/icons/auth-icon.png
  35. 25 0
      auth/distribute_options.yaml
  36. 14 2
      auth/docs/release.md
  37. 1 1
      auth/fastlane/metadata/android/en-US/full_description.txt
  38. 1 1
      auth/fastlane/metadata/android/en-US/title.txt
  39. 6 6
      auth/fdroid_flutter_icons.yaml
  40. 1 1
      auth/flutter
  41. 93 59
      auth/ios/Podfile.lock
  42. 1 1
      auth/ios/Runner.xcodeproj/project.pbxproj
  43. 1 1
      auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  44. 12 2
      auth/ios/Runner/AppDelegate.swift
  45. 4 0
      auth/ios/Runner/Info.plist
  46. 61 2
      auth/lib/app/view/app.dart
  47. 0 4
      auth/lib/app/view/app_route.dart
  48. 0 163
      auth/lib/app/view/app_theme_extension.dart
  49. 2 0
      auth/lib/bootstrap.dart
  50. 56 51
      auth/lib/core/configuration.dart
  51. 2 1
      auth/lib/core/constants.dart
  52. 4 8
      auth/lib/core/errors.dart
  53. 5 5
      auth/lib/core/logging/super_logging.dart
  54. 7 7
      auth/lib/core/logging/tunneled_transport.dart
  55. 22 14
      auth/lib/core/network.dart
  56. 85 37
      auth/lib/ente_theme_data.dart
  57. 2 2
      auth/lib/gateway/authenticator.dart
  58. 2 1
      auth/lib/l10n/arb/app_de.arb
  59. 5 0
      auth/lib/l10n/arb/app_en.arb
  60. 2 1
      auth/lib/l10n/arb/app_ja.arb
  61. 1 0
      auth/lib/l10n/arb/app_ko.arb
  62. 13 1
      auth/lib/l10n/arb/app_nl.arb
  63. 18 1
      auth/lib/l10n/arb/app_pl.arb
  64. 5 4
      auth/lib/l10n/arb/app_pt.arb
  65. 2 1
      auth/lib/l10n/arb/app_zh.arb
  66. 63 6
      auth/lib/main.dart
  67. 16 30
      auth/lib/models/code.dart
  68. 0 9
      auth/lib/models/derived_key_result.dart
  69. 0 15
      auth/lib/models/encryption_result.dart
  70. 115 102
      auth/lib/onboarding/view/onboarding_page.dart
  71. 4 4
      auth/lib/onboarding/view/setup_enter_secret_key_page.dart
  72. 12 6
      auth/lib/onboarding/view/view_qr_page.dart
  73. 25 24
      auth/lib/services/authenticator_service.dart
  74. 12 12
      auth/lib/services/billing_service.dart
  75. 14 2
      auth/lib/services/local_authentication_service.dart
  76. 1 2
      auth/lib/services/notification_service.dart
  77. 6 1
      auth/lib/services/update_service.dart
  78. 2 2
      auth/lib/services/user_remote_flag_service.dart
  79. 23 23
      auth/lib/services/user_service.dart
  80. 36 0
      auth/lib/services/window_listener_service.dart
  81. 13 1
      auth/lib/store/authenticator_db.dart
  82. 13 2
      auth/lib/store/offline_authenticator_db.dart
  83. 1 0
      auth/lib/theme/colors.dart
  84. 1 1
      auth/lib/ui/account/change_email_dialog.dart
  85. 10 6
      auth/lib/ui/account/delete_account_page.dart
  86. 15 29
      auth/lib/ui/account/email_entry_page.dart
  87. 43 50
      auth/lib/ui/account/login_page.dart
  88. 13 9
      auth/lib/ui/account/login_pwd_verification_page.dart
  89. 24 17
      auth/lib/ui/account/ott_verification_page.dart
  90. 220 207
      auth/lib/ui/account/password_entry_page.dart
  91. 26 18
      auth/lib/ui/account/password_reentry_page.dart
  92. 159 69
      auth/lib/ui/account/recovery_key_page.dart
  93. 42 43
      auth/lib/ui/account/recovery_page.dart
  94. 10 8
      auth/lib/ui/account/request_pwd_verification_page.dart
  95. 1 1
      auth/lib/ui/account/sessions_page.dart
  96. 10 9
      auth/lib/ui/account/verify_recovery_page.dart
  97. 3 3
      auth/lib/ui/code_timer_progress.dart
  98. 125 82
      auth/lib/ui/code_widget.dart
  99. 1 2
      auth/lib/ui/common/bottom_shadow.dart
  100. 2 4
      auth/lib/ui/common/divider_with_padding.dart

BIN
.github/assets/github-badge.png


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

@@ -2,15 +2,15 @@ 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 every 24 hours]
-        - cron: "50 1 * * *"
+        # See: [Note: Run workflow on specific days of the week]
+        - cron: "50 1 * * 2,5"
     # Also allow manually running the workflow
     workflow_dispatch:
 
@@ -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"

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

@@ -2,15 +2,15 @@ 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 every 24 hours]
-        - cron: "40 1 * * *"
+        # See: [Note: Run workflow on specific days of the week]
+        - cron: "40 1 * * 2,5"
     # Also allow manually running the workflow
     workflow_dispatch:
 
@@ -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

+ 10 - 4
.github/workflows/web-crowdin.yml

@@ -2,15 +2,21 @@ 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:
-        # See: [Note: Run every 24 hours]
-        - cron: "20 1 * * *"
+        # [Note: Run workflow on specific days of the week]
+        #
+        # The last (5th) component of the cron syntax denotes the day of the
+        # week, with 0 == SUN and 6 == SAT. So, for example, to run on every TUE
+        # and FRI, this can be set to `2,5`.
+        #
+        # See also: [Note: Run workflow every 24 hours]
+        - cron: "20 1 * * 2,5"
     # Also allow manually running the workflow
     workflow_dispatch:
 
@@ -28,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"

+ 14 - 1
.github/workflows/web-nightly.yml

@@ -2,7 +2,7 @@ name: "Nightly (web)"
 
 on:
     schedule:
-        # [Note: Run every 24 hours]
+        # [Note: Run workflow every 24 hours]
         #
         # Run every 24 hours - First field is minute, second is hour of the day
         # This runs 23:15 UTC everyday - 1 and 15 are just arbitrary offset to
@@ -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>

+ 9 - 0
auth/.gitignore

@@ -9,12 +9,20 @@
 .history
 .svn/
 
+# Editors
+.vscode/
+
 # IntelliJ related
 *.iml
 *.ipr
 *.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/
@@ -32,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


BIN
auth/assets/icons/auth-icon.ico


BIN
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

+ 14 - 2
auth/docs/release.md

@@ -1,7 +1,14 @@
 # Releases
 
-Create a PR to bump up the version in `pubspec.yaml`. Once that is merged, tag
-main, and push the tag.
+Create a PR to bump up the version in `pubspec.yaml`.
+
+> [!NOTE]
+>
+> Use [semver](https://semver.org/) for the tags, with `auth-` as a prefix.
+> Multiple beta releases for the same upcoming version can be done by adding
+> build metadata at the end, e.g. `auth-v1.2.3-beta+3`.
+
+Once that is merged, tag main, and push the tag.
 
 ```sh
 git tag auth-v1.2.3
@@ -16,6 +23,11 @@ This'll trigger a GitHub workflow that:
 * Creates a new release in the internal track on Play Store.
 
 Once the workflow completes, go to the draft GitHub release that was created.
+
+> [!NOTE]
+>
+> Keep the title of the release same as the tag.
+
 Set "Previous tag" to the last release of auth and press "Generate release
 notes". The generated release note will contain all PRs and new contributors
 from all the releases in the monorepo, so you'll need to filter them to keep

+ 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"
+

+ 1 - 1
auth/flutter

@@ -1 +1 @@
-Subproject commit 41456452f29d64e8deb623a3c927524bcf9f111b
+Subproject commit ba393198430278b6595976de84fe170f553cc728

+ 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 - 1
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,7 +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...",
-  "launchPasskeyUrlAgain": "Passwort-URL erneut starten",
+  "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",

+ 2 - 1
auth/lib/l10n/arb/app_ja.arb

@@ -145,6 +145,7 @@
   "lostDeviceTitle": "デバイスを紛失しましたか?",
   "twoFactorAuthTitle": "2 要素認証",
   "passkeyAuthTitle": "パスキー認証",
+  "verifyPasskey": "パスキーの認証",
   "recoverAccount": "アカウントを回復",
   "enterRecoveryKeyHint": "回復キーを入力",
   "recover": "回復",
@@ -407,7 +408,7 @@
   "hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
   "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!",
   "waitingForBrowserRequest": "ブラウザのリクエストを待っています...",
-  "launchPasskeyUrlAgain": "パスキーのURLを再度起動する",
+  "waitingForVerification": "認証を待っています...",
   "passkey": "パスキー",
   "developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?",
   "developerSettings": "開発者向け設定",

+ 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 - 4
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,7 +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...",
-  "launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
+  "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 - 1
auth/lib/l10n/arb/app_zh.arb

@@ -145,6 +145,7 @@
   "lostDeviceTitle": "丢失了设备吗?",
   "twoFactorAuthTitle": "双因素认证",
   "passkeyAuthTitle": "通行密钥认证",
+  "verifyPasskey": "验证通行密钥",
   "recoverAccount": "恢复账户",
   "enterRecoveryKeyHint": "输入您的恢复密钥",
   "recover": "恢复",
@@ -407,7 +408,7 @@
   "hearUsWhereTitle": "您是如何知道Ente的? (可选的)",
   "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
   "waitingForBrowserRequest": "正在等待浏览器请求...",
-  "launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
+  "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) {

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است