Browse Source

Merge branch 'main' into f-droid

ashilkn 1 year ago
parent
commit
07808d6139
100 changed files with 1856 additions and 1250 deletions
  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:
 on:
     push:
     push:
+        branches: [main]
         paths:
         paths:
             # Run workflow when auth's intl_en.arb is changed
             # Run workflow when auth's intl_en.arb is changed
             - "mobile/lib/l10n/arb/app_en.arb"
             - "mobile/lib/l10n/arb/app_en.arb"
             # Or the workflow itself is changed
             # Or the workflow itself is changed
             - ".github/workflows/auth-crowdin.yml"
             - ".github/workflows/auth-crowdin.yml"
-        branches: [main]
     schedule:
     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
     # Also allow manually running the workflow
     workflow_dispatch:
     workflow_dispatch:
 
 
@@ -28,7 +28,7 @@ jobs:
                   base_path: "auth/"
                   base_path: "auth/"
                   config: "auth/crowdin.yml"
                   config: "auth/crowdin.yml"
                   upload_sources: true
                   upload_sources: true
-                  upload_translations: true
+                  upload_translations: false
                   download_translations: true
                   download_translations: true
                   localization_branch_name: crowdin-translations-auth
                   localization_branch_name: crowdin-translations-auth
                   create_pull_request: true
                   create_pull_request: true

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

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

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

@@ -29,11 +29,11 @@ on:
             - "auth-v*"
             - "auth-v*"
 
 
 env:
 env:
-    FLUTTER_VERSION: "3.13.4"
+    FLUTTER_VERSION: "3.19.3"
 
 
 jobs:
 jobs:
     build-ubuntu:
     build-ubuntu:
-        runs-on: ubuntu-latest
+        runs-on: ubuntu-20.04
 
 
         defaults:
         defaults:
             run:
             run:
@@ -72,6 +72,8 @@ jobs:
                   SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
                   SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
 
 
             - name: Build PlayStore AAB
             - 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: |
               run: |
                   flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore
                   flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore
               env:
               env:
@@ -83,7 +85,7 @@ jobs:
             - name: Install dependencies for desktop build
             - name: Install dependencies for desktop build
               run: |
               run: |
                   sudo apt-get update -y
                   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
             - name: Install appimagetool
               run: |
               run: |
@@ -92,8 +94,6 @@ jobs:
                   mv appimagetool /usr/local/bin/
                   mv appimagetool /usr/local/bin/
 
 
             - name: Build desktop app
             - name: Build desktop app
-              # Temporarily disable desktop builds
-              if: false
               run: |
               run: |
                   flutter config --enable-linux-desktop
                   flutter config --enable-linux-desktop
                   dart pub global activate flutter_distributor
                   dart pub global activate flutter_distributor
@@ -118,6 +118,8 @@ jobs:
                   updateOnlyUnreleased: true
                   updateOnlyUnreleased: true
 
 
             - name: Upload AAB to PlayStore
             - 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
               uses: r0adkll/upload-google-play@v1
               with:
               with:
                   serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
                   serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
@@ -149,8 +151,6 @@ jobs:
               run: mkdir artifacts
               run: mkdir artifacts
 
 
             - name: Build Windows installer
             - name: Build Windows installer
-              # Temporarily disable desktop builds
-              if: false
               run: |
               run: |
                   flutter config --enable-windows-desktop
                   flutter config --enable-windows-desktop
                   dart pub global activate flutter_distributor
                   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
                   mv dist/**/ente_auth-*-windows-setup.exe artifacts/ente-${{ github.ref_name }}-installer.exe
 
 
             - name: Retain Windows EXE and DLLs
             - 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
               run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows
 
 
             - name: Code sign Windows installer and EXE
             - name: Code sign Windows installer and EXE
-              # Temporarily disable desktop builds
-              if: false
               uses: dlemstra/code-sign-action@v1
               uses: dlemstra/code-sign-action@v1
               with:
               with:
                   certificate: "${{ secrets.WINDOWS_CERTIFICATE }}"
                   certificate: "${{ secrets.WINDOWS_CERTIFICATE }}"
@@ -175,9 +171,10 @@ jobs:
                       auth/ente-${{ github.ref_name }}-windows/auth.exe
                       auth/ente-${{ github.ref_name }}-windows/auth.exe
 
 
             - name: Zip Windows EXE and DLLs
             - 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
             - name: Create a draft GitHub release
               uses: ncipollo/release-action@v1
               uses: ncipollo/release-action@v1
@@ -248,8 +245,6 @@ jobs:
               run: mkdir artifacts
               run: mkdir artifacts
 
 
             - name: Build macOS DMG
             - name: Build macOS DMG
-              # Temporarily disable desktop builds
-              if: false
               run: |
               run: |
                   flutter config --enable-macos-desktop
                   flutter config --enable-macos-desktop
                   dart pub global activate flutter_distributor
                   dart pub global activate flutter_distributor
@@ -257,16 +252,12 @@ jobs:
                   mv dist/**/ente_auth-*-macos.dmg artifacts/ente-${{ github.ref_name }}.dmg
                   mv dist/**/ente_auth-*-macos.dmg artifacts/ente-${{ github.ref_name }}.dmg
 
 
             - name: Code sign DMG
             - name: Code sign DMG
-              # Temporarily disable desktop builds
-              if: false
               run: |
               run: |
                   CERT_NAME=$(security find-identity -v -p codesigning | grep "Developer ID Application" | awk -F'"' '{print $2}' | grep -m1 "")
                   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 --force --timestamp --sign "$CERT_NAME" --options runtime artifacts/ente-${{ github.ref_name }}.dmg
                   codesign --verify --verbose=4 artifacts/ente-${{ github.ref_name }}.dmg
                   codesign --verify --verbose=4 artifacts/ente-${{ github.ref_name }}.dmg
 
 
             - name: Notarize and staple DMG
             - name: Notarize and staple DMG
-              # Temporarily disable desktop builds
-              if: false
               run: |
               run: |
                   xcrun notarytool submit artifacts/ente-${{ github.ref_name }}.dmg \
                   xcrun notarytool submit artifacts/ente-${{ github.ref_name }}.dmg \
                     --wait \
                     --wait \
@@ -279,6 +270,9 @@ jobs:
                   APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
                   APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
                   APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
                   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
             - name: Create a draft GitHub release
               uses: ncipollo/release-action@v1
               uses: ncipollo/release-action@v1
               with:
               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:
 on:
     # Run on every push to a branch other than main that changes docs/
     # Run on every push to a branch other than main that changes docs/
     push:
     push:
-        branches-ignore: [main]
+        branches-ignore: [main, "deploy/**"]
         paths:
         paths:
             - "docs/**"
             - "docs/**"
             - ".github/workflows/docs-verify-build.yml"
             - ".github/workflows/docs-verify-build.yml"

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

@@ -2,15 +2,15 @@ name: "Sync Crowdin translations (mobile)"
 
 
 on:
 on:
     push:
     push:
+        branches: [main]
         paths:
         paths:
             # Run workflow when mobiles's intl_en.arb is changed
             # Run workflow when mobiles's intl_en.arb is changed
             - "mobile/lib/l10n/intl_en.arb"
             - "mobile/lib/l10n/intl_en.arb"
             # Or the workflow itself is changed
             # Or the workflow itself is changed
             - ".github/workflows/mobile-crowdin.yml"
             - ".github/workflows/mobile-crowdin.yml"
-        branches: [main]
     schedule:
     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
     # Also allow manually running the workflow
     workflow_dispatch:
     workflow_dispatch:
 
 
@@ -28,7 +28,7 @@ jobs:
                   base_path: "mobile/"
                   base_path: "mobile/"
                   config: "mobile/crowdin.yml"
                   config: "mobile/crowdin.yml"
                   upload_sources: true
                   upload_sources: true
-                  upload_translations: true
+                  upload_translations: false
                   download_translations: true
                   download_translations: true
                   localization_branch_name: crowdin-translations-mobile
                   localization_branch_name: crowdin-translations-mobile
                   create_pull_request: true
                   create_pull_request: true

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

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

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

@@ -3,7 +3,7 @@ name: "Lint (server)"
 on:
 on:
     # Run on every push to a branch other than main that changes server/
     # Run on every push to a branch other than main that changes server/
     push:
     push:
-        branches-ignore: [main]
+        branches-ignore: [main, "deploy/**"]
         paths:
         paths:
             - "server/**"
             - "server/**"
             - ".github/workflows/server-lint.yml"
             - ".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:
     build:
         runs-on: ubuntu-latest
         runs-on: ubuntu-latest
         steps:
         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:
               with:
                   dockerfile: server/Dockerfile
                   dockerfile: server/Dockerfile
                   directory: server
                   directory: server

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

@@ -2,15 +2,21 @@ name: "Sync Crowdin translations (web)"
 
 
 on:
 on:
     push:
     push:
+        branches: [main]
         paths:
         paths:
             # Run workflow when web's en-US/translation.json is changed
             # Run workflow when web's en-US/translation.json is changed
             - "web/apps/photos/public/locales/en-US/translation.json"
             - "web/apps/photos/public/locales/en-US/translation.json"
             # Or the workflow itself is changed
             # Or the workflow itself is changed
             - ".github/workflows/web-crowdin.yml"
             - ".github/workflows/web-crowdin.yml"
-        branches: [main]
     schedule:
     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
     # Also allow manually running the workflow
     workflow_dispatch:
     workflow_dispatch:
 
 
@@ -28,7 +34,7 @@ jobs:
                   base_path: "web/"
                   base_path: "web/"
                   config: "web/crowdin.yml"
                   config: "web/crowdin.yml"
                   upload_sources: true
                   upload_sources: true
-                  upload_translations: true
+                  upload_translations: false
                   download_translations: true
                   download_translations: true
                   localization_branch_name: crowdin-translations-web
                   localization_branch_name: crowdin-translations-web
                   create_pull_request: true
                   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:
 on:
     # Run on every push to a branch other than main that changes web/
     # Run on every push to a branch other than main that changes web/
     push:
     push:
-        branches-ignore: [main]
+        branches-ignore: [main, "deploy/**"]
         paths:
         paths:
             - "web/**"
             - "web/**"
             - ".github/workflows/web-lint.yml"
             - ".github/workflows/web-lint.yml"

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

@@ -2,7 +2,7 @@ name: "Nightly (web)"
 
 
 on:
 on:
     schedule:
     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
         # 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
         # This runs 23:15 UTC everyday - 1 and 15 are just arbitrary offset to
@@ -78,6 +78,19 @@ jobs:
                   directory: web/apps/cast/out
                   directory: web/apps/cast/out
                   wranglerVersion: "3"
                   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
             - name: Build photos
               run: yarn build:photos
               run: yarn build:photos
               env:
               env:

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

@@ -12,6 +12,7 @@ on:
                     - "accounts"
                     - "accounts"
                     - "auth"
                     - "auth"
                     - "cast"
                     - "cast"
+                    - "payments"
                     - "photos"
                     - "photos"
 
 
 jobs:
 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/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/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/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)
 [<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
 
 
 </div>
 </div>

+ 9 - 0
auth/.gitignore

@@ -9,12 +9,20 @@
 .history
 .history
 .svn/
 .svn/
 
 
+# Editors
+.vscode/
+
 # IntelliJ related
 # IntelliJ related
 *.iml
 *.iml
 *.ipr
 *.ipr
 *.iws
 *.iws
 .idea/
 .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
 # Flutter/Dart/Pub related
 **/doc/api/
 **/doc/api/
 .dart_tool/
 .dart_tool/
@@ -32,3 +40,4 @@ lib/generated_plugin_registrant.dart
 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
 
 
 android/key.properties
 android/key.properties
+dist/

+ 20 - 11
auth/.metadata

@@ -1,11 +1,11 @@
 # This file tracks properties of this Flutter project.
 # This file tracks properties of this Flutter project.
 # Used by Flutter tool to assess capabilities and perform upgrades etc.
 # 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:
 version:
-  revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-  channel: unknown
+  revision: "ba393198430278b6595976de84fe170f553cc728"
+  channel: "[user-branch]"
 
 
 project_type: app
 project_type: app
 
 
@@ -13,17 +13,26 @@ project_type: app
 migration:
 migration:
   platforms:
   platforms:
     - platform: root
     - 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
     - platform: linux
-      create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-      base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
     - platform: macos
     - platform: macos
-      create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-      base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
+    - platform: web
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
     - platform: windows
     - platform: windows
-      create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
-      base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
+      create_revision: ba393198430278b6595976de84fe170f553cc728
+      base_revision: ba393198430278b6595976de84fe170f553cc728
 
 
   # User provided section
   # 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">
   <img height="59" src="../.github/assets/app-store-badge.svg">
 </a>
 </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
 ## 🧑‍💻 Build from source
 
 

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

@@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) {
 }
 }
 
 
 android {
 android {
-    compileSdkVersion 33
+    compileSdkVersion 34
 
 
     sourceSets {
     sourceSets {
         main.java.srcDirs += 'src/main/kotlin'
         main.java.srcDirs += 'src/main/kotlin'
@@ -46,7 +46,7 @@ android {
 
 
     defaultConfig {
     defaultConfig {
         applicationId "io.ente.auth"
         applicationId "io.ente.auth"
-        minSdkVersion 20
+        minSdkVersion 21
         targetSdkVersion 33
         targetSdkVersion 33
         versionCode flutterVersionCode.toInteger()
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
         versionName flutterVersionName
@@ -56,11 +56,11 @@ android {
 
 
     signingConfigs {
     signingConfigs {
        release {
        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"
     flavorDimensions "default"
@@ -109,6 +109,7 @@ dependencies {
     implementation 'io.sentry:sentry-android:2.0.0'
     implementation 'io.sentry:sentry-android:2.0.0'
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
     implementation 'com.android.support:multidex:1.0.3'
     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'
     implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
     testImplementation 'junit:junit:4.12'
     testImplementation 'junit:junit:4.12'
     androidTestImplementation 'androidx.test:runner:1.1.1'
     androidTestImplementation 'androidx.test:runner:1.1.1'

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

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

File diff suppressed because it is too large
+ 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>

File diff suppressed because it is too large
+ 6 - 0
auth/assets/custom-icons/icons/habbo.svg


File diff suppressed because it is too large
+ 6 - 0
auth/assets/custom-icons/icons/mercado_livre.svg


File diff suppressed because it is too large
+ 0 - 0
auth/assets/custom-icons/icons/rockstar_games.svg


File diff suppressed because it is too large
+ 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
 # 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
 ```sh
 git tag auth-v1.2.3
 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.
 * Creates a new release in the internal track on Play Store.
 
 
 Once the workflow completes, go to the draft GitHub release that was created.
 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
 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
 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
 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
 SUPPORT
 
 
 If you need help, please reach out to support@ente.io, and a human will get in touch with you.
 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:
 PODS:
-  - connectivity (0.0.1):
+  - app_links (0.0.1):
     - Flutter
     - Flutter
-    - Reachability
+  - connectivity_plus (0.0.1):
+    - Flutter
+    - ReachabilitySwift
   - device_info_plus (0.0.1):
   - device_info_plus (0.0.1):
     - Flutter
     - Flutter
   - DKImagePickerController/Core (4.3.4):
   - DKImagePickerController/Core (4.3.4):
@@ -45,27 +47,26 @@ PODS:
   - Flutter (1.0.0)
   - Flutter (1.0.0)
   - flutter_email_sender (0.0.1):
   - flutter_email_sender (0.0.1):
     - Flutter
     - Flutter
-  - flutter_inappwebview (0.0.1):
+  - flutter_inappwebview_ios (0.0.1):
     - Flutter
     - Flutter
-    - flutter_inappwebview/Core (= 0.0.1)
+    - flutter_inappwebview_ios/Core (= 0.0.1)
     - OrderedSet (~> 5.0)
     - OrderedSet (~> 5.0)
-  - flutter_inappwebview/Core (0.0.1):
+  - flutter_inappwebview_ios/Core (0.0.1):
     - Flutter
     - Flutter
     - OrderedSet (~> 5.0)
     - OrderedSet (~> 5.0)
+  - flutter_local_authentication (1.2.0):
+    - Flutter
   - flutter_local_notifications (0.0.1):
   - flutter_local_notifications (0.0.1):
     - Flutter
     - Flutter
   - flutter_native_splash (0.0.1):
   - flutter_native_splash (0.0.1):
     - Flutter
     - Flutter
   - flutter_secure_storage (6.0.0):
   - flutter_secure_storage (6.0.0):
     - Flutter
     - Flutter
-  - flutter_sodium (0.0.1):
-    - Flutter
   - fluttertoast (0.0.2):
   - fluttertoast (0.0.2):
     - Flutter
     - Flutter
     - Toast
     - 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):
   - local_auth_ios (0.0.1):
     - Flutter
     - Flutter
   - move_to_background (0.0.1):
   - move_to_background (0.0.1):
@@ -82,46 +83,65 @@ PODS:
   - qr_code_scanner (0.2.0):
   - qr_code_scanner (0.2.0):
     - Flutter
     - Flutter
     - MTBBarcodeScanner
     - 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):
   - sentry_flutter (0.0.1):
     - Flutter
     - Flutter
     - FlutterMacOS
     - FlutterMacOS
-    - Sentry/HybridSDK (= 8.9.1)
-  - SentryPrivate (8.9.1)
+    - Sentry/HybridSDK (= 8.21.0)
+  - SentryPrivate (8.21.0)
   - share_plus (0.0.1):
   - share_plus (0.0.1):
     - Flutter
     - Flutter
   - shared_preferences_foundation (0.0.1):
   - shared_preferences_foundation (0.0.1):
     - Flutter
     - Flutter
     - FlutterMacOS
     - FlutterMacOS
+  - smart_auth (0.0.1):
+    - Flutter
+  - sodium_libs (2.2.1):
+    - Flutter
   - sqflite (0.0.3):
   - sqflite (0.0.3):
     - Flutter
     - 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
     - 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):
   - url_launcher_ios (0.0.1):
     - Flutter
     - Flutter
 
 
 DEPENDENCIES:
 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`)
   - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
   - file_picker (from `.symlinks/plugins/file_picker/ios`)
   - file_picker (from `.symlinks/plugins/file_picker/ios`)
   - file_saver (from `.symlinks/plugins/file_saver/ios`)
   - file_saver (from `.symlinks/plugins/file_saver/ios`)
   - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`)
   - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`)
   - Flutter (from `Flutter`)
   - Flutter (from `Flutter`)
   - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
   - 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_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
   - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
   - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/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`)
   - 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`)
   - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`)
   - move_to_background (from `.symlinks/plugins/move_to_background/ios`)
   - move_to_background (from `.symlinks/plugins/move_to_background/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
@@ -131,27 +151,31 @@ DEPENDENCIES:
   - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
   - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
   - share_plus (from `.symlinks/plugins/share_plus/ios`)
   - share_plus (from `.symlinks/plugins/share_plus/ios`)
   - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
   - 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`)
   - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
 
 
 SPEC REPOS:
 SPEC REPOS:
   trunk:
   trunk:
     - DKImagePickerController
     - DKImagePickerController
     - DKPhotoGallery
     - DKPhotoGallery
-    - FMDB
     - MTBBarcodeScanner
     - MTBBarcodeScanner
     - OrderedSet
     - OrderedSet
-    - Reachability
+    - ReachabilitySwift
     - SDWebImage
     - SDWebImage
     - Sentry
     - Sentry
     - SentryPrivate
     - SentryPrivate
+    - sqlite3
     - SwiftyGif
     - SwiftyGif
     - Toast
     - Toast
 
 
 EXTERNAL SOURCES:
 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:
   device_info_plus:
     :path: ".symlinks/plugins/device_info_plus/ios"
     :path: ".symlinks/plugins/device_info_plus/ios"
   file_picker:
   file_picker:
@@ -164,18 +188,20 @@ EXTERNAL SOURCES:
     :path: Flutter
     :path: Flutter
   flutter_email_sender:
   flutter_email_sender:
     :path: ".symlinks/plugins/flutter_email_sender/ios"
     :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:
   flutter_local_notifications:
     :path: ".symlinks/plugins/flutter_local_notifications/ios"
     :path: ".symlinks/plugins/flutter_local_notifications/ios"
   flutter_native_splash:
   flutter_native_splash:
     :path: ".symlinks/plugins/flutter_native_splash/ios"
     :path: ".symlinks/plugins/flutter_native_splash/ios"
   flutter_secure_storage:
   flutter_secure_storage:
     :path: ".symlinks/plugins/flutter_secure_storage/ios"
     :path: ".symlinks/plugins/flutter_secure_storage/ios"
-  flutter_sodium:
-    :path: ".symlinks/plugins/flutter_sodium/ios"
   fluttertoast:
   fluttertoast:
     :path: ".symlinks/plugins/fluttertoast/ios"
     :path: ".symlinks/plugins/fluttertoast/ios"
+  local_auth_darwin:
+    :path: ".symlinks/plugins/local_auth_darwin/darwin"
   local_auth_ios:
   local_auth_ios:
     :path: ".symlinks/plugins/local_auth_ios/ios"
     :path: ".symlinks/plugins/local_auth_ios/ios"
   move_to_background:
   move_to_background:
@@ -194,50 +220,58 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/share_plus/ios"
     :path: ".symlinks/plugins/share_plus/ios"
   shared_preferences_foundation:
   shared_preferences_foundation:
     :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
     :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
+  smart_auth:
+    :path: ".symlinks/plugins/smart_auth/ios"
+  sodium_libs:
+    :path: ".symlinks/plugins/sodium_libs/ios"
   sqflite:
   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:
   url_launcher_ios:
     :path: ".symlinks/plugins/url_launcher_ios/ios"
     :path: ".symlinks/plugins/url_launcher_ios/ios"
 
 
 SPEC CHECKSUMS:
 SPEC CHECKSUMS:
-  connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467
-  device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
+  app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
+  connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d
+  device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
   DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
   DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
   DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
   DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
-  file_picker: ce3938a0df3cc1ef404671531facef740d03f920
+  file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de
   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
-  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
+  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
   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_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
   flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
   flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
-  flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
   fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
   fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
-  FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
-  local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
+  local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98
+  local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9
   move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
   move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
   MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
   MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
   OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
   OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
-  package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
-  path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
+  package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
+  path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
   privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
   privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
   qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
   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
   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
   SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
-  Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
-  uni_links: d97da20c7701486ba192624d99bffaaffcfc298a
-  url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
+  Toast: ec33c32b8688982cecc6348adeae667c1b9938da
+  url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586
 
 
 PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb
 PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb
 
 

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

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

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

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

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

@@ -1,5 +1,6 @@
-import UIKit
 import Flutter
 import Flutter
+import UIKit
+import app_links
 
 
 @UIApplicationMain
 @UIApplicationMain
 @objc class AppDelegate: FlutterAppDelegate {
 @objc class AppDelegate: FlutterAppDelegate {
@@ -8,6 +9,15 @@ import Flutter
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
   ) -> Bool {
   ) -> Bool {
     GeneratedPluginRegistrant.register(with: self)
     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>
 	</array>
 	<key>UIViewControllerBasedStatusBarAppearance</key>
 	<key>UIViewControllerBasedStatusBarAppearance</key>
 	<false/>
 	<false/>
+	<key>LSSupportsOpeningDocumentsInPlace</key>
+	<true/>
+	<key>UIFileSharingEnabled</key>
+	<true/>
 </dict>
 </dict>
 </plist>
 </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/onboarding/view/onboarding_page.dart";
 import 'package:ente_auth/services/update_service.dart';
 import 'package:ente_auth/services/update_service.dart';
 import 'package:ente_auth/services/user_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/home_page.dart';
 import 'package:ente_auth/ui/settings/app_update_dialog.dart';
 import 'package:ente_auth/ui/settings/app_update_dialog.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 import "package:flutter/material.dart";
 import "package:flutter/material.dart";
 import 'package:flutter_localizations/flutter_localizations.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 {
 class App extends StatefulWidget {
   final Locale locale;
   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) {
   static void setLocale(BuildContext context, Locale newLocale) {
     _AppState state = context.findAncestorStateOfType<_AppState>()!;
     _AppState state = context.findAncestorStateOfType<_AppState>()!;
@@ -31,7 +34,7 @@ class App extends StatefulWidget {
   State<App> createState() => _AppState();
   State<App> createState() => _AppState();
 }
 }
 
 
-class _AppState extends State<App> {
+class _AppState extends State<App> with WindowListener, TrayListener {
   late StreamSubscription<SignedOutEvent> _signedOutEvent;
   late StreamSubscription<SignedOutEvent> _signedOutEvent;
   late StreamSubscription<SignedInEvent> _signedInEvent;
   late StreamSubscription<SignedInEvent> _signedInEvent;
   Locale? locale;
   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
   @override
   void initState() {
   void initState() {
+    initWindowManager();
+    initTrayManager();
+
     _signedOutEvent = Bus.instance.on<SignedOutEvent>().listen((event) {
     _signedOutEvent = Bus.instance.on<SignedOutEvent>().listen((event) {
       if (mounted) {
       if (mounted) {
         setState(() {});
         setState(() {});
@@ -76,6 +90,10 @@ class _AppState extends State<App> {
   @override
   @override
   void dispose() {
   void dispose() {
     super.dispose();
     super.dispose();
+
+    windowManager.removeListener(this);
+    trayManager.removeListener(this);
+
     _signedOutEvent.cancel();
     _signedOutEvent.cancel();
     _signedInEvent.cancel();
     _signedInEvent.cancel();
   }
   }
@@ -134,4 +152,45 @@ class _AppState extends State<App> {
           : const OnboardingPage(),
           : 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:async";
 import "dart:developer";
 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/key_gen_result.dart';
 import 'package:ente_auth/models/private_key_attributes.dart';
 import 'package:ente_auth/models/private_key_attributes.dart';
 import 'package:ente_auth/store/authenticator_db.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_secure_storage/flutter_secure_storage.dart';
-import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 import 'package:tuple/tuple.dart';
 import 'package:tuple/tuple.dart';
 
 
 class Configuration {
 class Configuration {
@@ -72,9 +72,10 @@ class Configuration {
 
 
   Future<void> init() async {
   Future<void> init() async {
     _preferences = await SharedPreferences.getInstance();
     _preferences = await SharedPreferences.getInstance();
+    sqfliteFfiInit();
     _secureStorage = const FlutterSecureStorage();
     _secureStorage = const FlutterSecureStorage();
     _documentsDirectory = (await getApplicationDocumentsDirectory()).path;
     _documentsDirectory = (await getApplicationDocumentsDirectory()).path;
-    _tempDirectory = _documentsDirectory + "/temp/";
+    _tempDirectory = "$_documentsDirectory/temp/";
     final tempDirectory = io.Directory(_tempDirectory);
     final tempDirectory = io.Directory(_tempDirectory);
     try {
     try {
       final currentTime = DateTime.now().microsecondsSinceEpoch;
       final currentTime = DateTime.now().microsecondsSinceEpoch;
@@ -162,7 +163,7 @@ class Configuration {
     // decrypt the master key
     // decrypt the master key
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
     final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
     final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
-      utf8.encode(password) as Uint8List,
+      utf8.encode(password),
       kekSalt,
       kekSalt,
     );
     );
     final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
     final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
@@ -172,28 +173,28 @@ class Configuration {
         CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
         CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
 
 
     // Generate a public-private keypair and encrypt the latter
     // Generate a public-private keypair and encrypt the latter
-    final keyPair = await CryptoUtil.generateKeyPair();
+    final keyPair = CryptoUtil.generateKeyPair();
     final encryptedSecretKeyData =
     final encryptedSecretKeyData =
-        CryptoUtil.encryptSync(keyPair.sk, masterKey);
+        CryptoUtil.encryptSync(keyPair.secretKey.extractBytes(), masterKey);
 
 
     final attributes = KeyAttributes(
     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.memLimit,
       derivedKeyResult.opsLimit,
       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(
     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);
     return KeyGenResult(attributes, privateAttributes, loginKey);
   }
   }
@@ -208,7 +209,7 @@ class Configuration {
     // decrypt the master key
     // decrypt the master key
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
     final kekSalt = CryptoUtil.getSaltToDeriveKey();
     final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
     final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
-      utf8.encode(password) as Uint8List,
+      utf8.encode(password),
       kekSalt,
       kekSalt,
     );
     );
     final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
     final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
@@ -220,9 +221,9 @@ class Configuration {
     final existingAttributes = getKeyAttributes();
     final existingAttributes = getKeyAttributes();
 
 
     final updatedAttributes = existingAttributes!.copyWith(
     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,
       memLimit: derivedKeyResult.memLimit,
       opsLimit: derivedKeyResult.opsLimit,
       opsLimit: derivedKeyResult.opsLimit,
     );
     );
@@ -240,8 +241,8 @@ class Configuration {
   }) async {
   }) async {
     _logger.info('Start decryptAndSaveSecrets');
     _logger.info('Start decryptAndSaveSecrets');
     keyEncryptionKey ??= await CryptoUtil.deriveKey(
     keyEncryptionKey ??= await CryptoUtil.deriveKey(
-      utf8.encode(password) as Uint8List,
-      Sodium.base642bin(attributes.kekSalt),
+      utf8.encode(password),
+      CryptoUtil.base642bin(attributes.kekSalt),
       attributes.memLimit,
       attributes.memLimit,
       attributes.opsLimit,
       attributes.opsLimit,
     );
     );
@@ -250,31 +251,31 @@ class Configuration {
     Uint8List key;
     Uint8List key;
     try {
     try {
       key = CryptoUtil.decryptSync(
       key = CryptoUtil.decryptSync(
-        Sodium.base642bin(attributes.encryptedKey),
+        CryptoUtil.base642bin(attributes.encryptedKey),
         keyEncryptionKey,
         keyEncryptionKey,
-        Sodium.base642bin(attributes.keyDecryptionNonce),
+        CryptoUtil.base642bin(attributes.keyDecryptionNonce),
       );
       );
     } catch (e) {
     } catch (e) {
       _logger.severe('master-key failed, incorrect password?', e);
       _logger.severe('master-key failed, incorrect password?', e);
       throw Exception("Incorrect password");
       throw Exception("Incorrect password");
     }
     }
     _logger.info("master-key done");
     _logger.info("master-key done");
-    await setKey(Sodium.bin2base64(key));
+    await setKey(CryptoUtil.bin2base64(key));
     final secretKey = CryptoUtil.decryptSync(
     final secretKey = CryptoUtil.decryptSync(
-      Sodium.base642bin(attributes.encryptedSecretKey),
+      CryptoUtil.base642bin(attributes.encryptedSecretKey),
       key,
       key,
-      Sodium.base642bin(attributes.secretKeyDecryptionNonce),
+      CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
     );
     );
     _logger.info("secret-key done");
     _logger.info("secret-key done");
-    await setSecretKey(Sodium.bin2base64(secretKey));
+    await setSecretKey(CryptoUtil.bin2base64(secretKey));
     final token = CryptoUtil.openSealSync(
     final token = CryptoUtil.openSealSync(
-      Sodium.base642bin(getEncryptedToken()!),
-      Sodium.base642bin(attributes.publicKey),
+      CryptoUtil.base642bin(getEncryptedToken()!),
+      CryptoUtil.base642bin(attributes.publicKey),
       secretKey,
       secretKey,
     );
     );
     _logger.info('appToken done');
     _logger.info('appToken done');
     await setToken(
     await setToken(
-      Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
+      CryptoUtil.bin2base64(token, urlSafe: true),
     );
     );
     return keyEncryptionKey;
     return keyEncryptionKey;
   }
   }
@@ -293,28 +294,28 @@ class Configuration {
     Uint8List masterKey;
     Uint8List masterKey;
     try {
     try {
       masterKey = await CryptoUtil.decrypt(
       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) {
     } catch (e) {
       _logger.severe(e);
       _logger.severe(e);
       rethrow;
       rethrow;
     }
     }
-    await setKey(Sodium.bin2base64(masterKey));
+    await setKey(CryptoUtil.bin2base64(masterKey));
     final secretKey = CryptoUtil.decryptSync(
     final secretKey = CryptoUtil.decryptSync(
-      Sodium.base642bin(attributes.encryptedSecretKey),
+      CryptoUtil.base642bin(attributes.encryptedSecretKey),
       masterKey,
       masterKey,
-      Sodium.base642bin(attributes.secretKeyDecryptionNonce),
+      CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
     );
     );
-    await setSecretKey(Sodium.bin2base64(secretKey));
+    await setSecretKey(CryptoUtil.bin2base64(secretKey));
     final token = CryptoUtil.openSealSync(
     final token = CryptoUtil.openSealSync(
-      Sodium.base642bin(getEncryptedToken()!),
-      Sodium.base642bin(attributes.publicKey),
+      CryptoUtil.base642bin(getEncryptedToken()!),
+      CryptoUtil.base642bin(attributes.publicKey),
       secretKey,
       secretKey,
     );
     );
     await setToken(
     await setToken(
-      Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe),
+      CryptoUtil.bin2base64(token, urlSafe: true),
     );
     );
   }
   }
 
 
@@ -407,27 +408,31 @@ class Configuration {
   }
   }
 
 
   Uint8List? getKey() {
   Uint8List? getKey() {
-    return _key == null ? null : Sodium.base642bin(_key!);
+    return _key == null ? null : CryptoUtil.base642bin(_key!);
   }
   }
 
 
   Uint8List? getSecretKey() {
   Uint8List? getSecretKey() {
-    return _secretKey == null ? null : Sodium.base642bin(_secretKey!);
+    return _secretKey == null ? null : CryptoUtil.base642bin(_secretKey!);
   }
   }
 
 
   Uint8List? getAuthSecretKey() {
   Uint8List? getAuthSecretKey() {
-    return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!);
+    return _authSecretKey == null
+        ? null
+        : CryptoUtil.base642bin(_authSecretKey!);
   }
   }
 
 
   Uint8List? getOfflineSecretKey() {
   Uint8List? getOfflineSecretKey() {
-    return _offlineAuthKey == null ? null : Sodium.base642bin(_offlineAuthKey!);
+    return _offlineAuthKey == null
+        ? null
+        : CryptoUtil.base642bin(_offlineAuthKey!);
   }
   }
 
 
   Uint8List getRecoveryKey() {
   Uint8List getRecoveryKey() {
     final keyAttributes = getKeyAttributes()!;
     final keyAttributes = getKeyAttributes()!;
     return CryptoUtil.decryptSync(
     return CryptoUtil.decryptSync(
-      Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
+      CryptoUtil.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
       getKey()!,
       getKey()!,
-      Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
+      CryptoUtil.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
     );
     );
   }
   }
 
 
@@ -454,7 +459,7 @@ class Configuration {
         iOptions: _secureStorageOptionsIOS,
         iOptions: _secureStorageOptionsIOS,
       );
       );
     } else {
     } else {
-      _offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey());
+      _offlineAuthKey = CryptoUtil.bin2base64(CryptoUtil.generateKey());
       await _secureStorage.write(
       await _secureStorage.write(
         key: offlineAuthSecretKey,
         key: offlineAuthSecretKey,
         value: _offlineAuthKey,
         value: _offlineAuthKey,

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

@@ -7,7 +7,8 @@ const String sentryDSN =
     "https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
     "https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
 const String sentryTunnel = "https://sentry-reporter.ente.io";
 const String sentryTunnel = "https://sentry-reporter.ente.io";
 const String roadmapURL = "https://roadmap.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 microSecondsInDay = 86400000000;
 const int android11SDKINT = 30;
 const int android11SDKINT = 30;
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748

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

@@ -1,9 +1,9 @@
 class InvalidFileError extends ArgumentError {
 class InvalidFileError extends ArgumentError {
-  InvalidFileError(String message) : super(message);
+  InvalidFileError(String super.message);
 }
 }
 
 
 class InvalidFileUploadState extends AssertionError {
 class InvalidFileUploadState extends AssertionError {
-  InvalidFileUploadState(String message) : super(message);
+  InvalidFileUploadState(String super.message);
 }
 }
 
 
 class SubscriptionAlreadyClaimedError extends Error {}
 class SubscriptionAlreadyClaimedError extends Error {}
@@ -30,19 +30,15 @@ class UnauthorizedError extends Error {}
 class RequestCancelledError extends Error {}
 class RequestCancelledError extends Error {}
 
 
 class InvalidSyncStatusError extends AssertionError {
 class InvalidSyncStatusError extends AssertionError {
-  InvalidSyncStatusError(String message) : super(message);
+  InvalidSyncStatusError(String super.message);
 }
 }
 
 
 class UnauthorizedEditError extends AssertionError {}
 class UnauthorizedEditError extends AssertionError {}
 
 
 class InvalidStateError 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 SrpSetupNotCompleteError extends Error {}
 
 
 class AuthenticatorKeyNotFound extends Error {}
 class AuthenticatorKeyNotFound extends Error {}

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

@@ -235,14 +235,14 @@ class SuperLogging {
       extraLines = null;
       extraLines = null;
     }
     }
 
 
-    final str = (config.prefix) + " " + rec.toPrettyString(extraLines);
+    final str = "${config.prefix} ${rec.toPrettyString(extraLines)}";
 
 
     // write to stdout
     // write to stdout
     printLog(str);
     printLog(str);
 
 
     // push to log queue
     // push to log queue
     if (fileIsEnabled) {
     if (fileIsEnabled) {
-      fileQueueEntries.add(str + '\n');
+      fileQueueEntries.add('$str\n');
       if (fileQueueEntries.length == 1) {
       if (fileQueueEntries.length == 1) {
         flushQueue();
         flushQueue();
       }
       }
@@ -275,7 +275,7 @@ class SuperLogging {
   static var logChunkSize = 800;
   static var logChunkSize = 800;
 
 
   static void printLog(String text) {
   static void printLog(String text) {
-    text.chunked(logChunkSize).forEach(print);
+    text.chunked(logChunkSize).forEach(debugPrint);
   }
   }
 
 
   /// A queue to be consumed by [setupSentry].
   /// A queue to be consumed by [setupSentry].
@@ -354,7 +354,7 @@ class SuperLogging {
         final date = config.dateFmt!.parse(basename(file.path));
         final date = config.dateFmt!.parse(basename(file.path));
         dates[file as File] = date;
         dates[file as File] = date;
         files.add(file);
         files.add(file);
-      } on FormatException {}
+      } on Exception catch (_) {}
     }
     }
     final nowTime = DateTime.now();
     final nowTime = DateTime.now();
 
 
@@ -374,7 +374,7 @@ class SuperLogging {
             "deleting log file ${file.path}",
             "deleting log file ${file.path}",
           );
           );
           await file.delete();
           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(
         _options.logger(
           SentryLevel.error,
           SentryLevel.error,
           'API returned an error, statusCode = ${response.statusCode}, '
           'API returned an error, statusCode = ${response.statusCode}, '
-              'body = ${response.body}',
+          'body = ${response.body}',
         );
         );
       }
       }
       return const SentryId.empty();
       return const SentryId.empty();
@@ -65,8 +65,8 @@ class TunneledTransport implements Transport {
   }
   }
 
 
   Future<StreamedRequest> _createStreamedRequest(
   Future<StreamedRequest> _createStreamedRequest(
-      SentryEnvelope envelope,
-      ) async {
+    SentryEnvelope envelope,
+  ) async {
     final streamedRequest = StreamedRequest('POST', _tunnel);
     final streamedRequest = StreamedRequest('POST', _tunnel);
     envelope
     envelope
         .envelopeStream(_options)
         .envelopeStream(_options)
@@ -91,10 +91,10 @@ class _CredentialBuilder {
         _clock = clock;
         _clock = clock;
 
 
   factory _CredentialBuilder(
   factory _CredentialBuilder(
-      Dsn? dsn,
-      String sdkIdentifier,
-      ClockProvider clock,
-      ) {
+    Dsn? dsn,
+    String sdkIdentifier,
+    ClockProvider clock,
+  ) {
     final authHeader = _buildAuthHeader(
     final authHeader = _buildAuthHeader(
       publicKey: dsn?.publicKey,
       publicKey: dsn?.publicKey,
       secretKey: dsn?.secretKey,
       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/configuration.dart';
 import 'package:ente_auth/core/event_bus.dart';
 import 'package:ente_auth/core/event_bus.dart';
 import 'package:ente_auth/events/endpoint_updated_event.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:fk_user_agent/fk_user_agent.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
-import 'package:package_info_plus/package_info_plus.dart';
 import 'package:uuid/uuid.dart';
 import 'package:uuid/uuid.dart';
 
 
 int kConnectTimeout = 15000;
 int kConnectTimeout = 15000;
@@ -16,34 +17,41 @@ class Network {
   late Dio _enteDio;
   late Dio _enteDio;
 
 
   Future<void> init() async {
   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();
     final endpoint = Configuration.instance.getHttpEndpoint();
-    
+
     _dio = Dio(
     _dio = Dio(
       BaseOptions(
       BaseOptions(
-        connectTimeout: kConnectTimeout,
+        connectTimeout: Duration(milliseconds: kConnectTimeout),
         headers: {
         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(
     _enteDio = Dio(
       BaseOptions(
       BaseOptions(
         baseUrl: endpoint,
         baseUrl: endpoint,
-        connectTimeout: kConnectTimeout,
+        connectTimeout: Duration(milliseconds: kConnectTimeout),
         headers: {
         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);
     _setupInterceptors(endpoint);
-    
+
     Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
     Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
       final endpoint = Configuration.instance.getHttpEndpoint();
       final endpoint = Configuration.instance.getHttpEndpoint();
       _enteDio.options.baseUrl = endpoint;
       _enteDio.options.baseUrl = endpoint;

+ 85 - 37
auth/lib/ente_theme_data.dart

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

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

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

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

@@ -145,6 +145,7 @@
   "lostDeviceTitle": "Gerät verloren?",
   "lostDeviceTitle": "Gerät verloren?",
   "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
   "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung",
   "passkeyAuthTitle": "Passkey Authentifizierung",
   "passkeyAuthTitle": "Passkey Authentifizierung",
+  "verifyPasskey": "Passkey verifizieren",
   "recoverAccount": "Konto wiederherstellen",
   "recoverAccount": "Konto wiederherstellen",
   "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
   "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein",
   "recover": "Wiederherstellen",
   "recover": "Wiederherstellen",
@@ -407,7 +408,7 @@
   "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
   "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!",
   "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...",
   "waitingForBrowserRequest": "Warten auf Browseranfrage...",
-  "launchPasskeyUrlAgain": "Passwort-URL erneut starten",
+  "waitingForVerification": "Warte auf Bestätigung...",
   "passkey": "Passkey",
   "passkey": "Passkey",
   "developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?",
   "developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?",
   "developerSettings": "Entwicklereinstellungen",
   "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.",
   "recoveryKeySaveDescription": "We don't store this key, please save this 24 word key in a safe place.",
   "doThisLater": "Do this later",
   "doThisLater": "Do this later",
   "saveKey": "Save key",
   "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",
   "back": "Back",
   "createAccount": "Create account",
   "createAccount": "Create account",
   "passwordStrength": "Password strength: {passwordStrengthValue}",
   "passwordStrength": "Password strength: {passwordStrengthValue}",
@@ -407,6 +411,7 @@
   "doNotSignOut": "Do not sign out",
   "doNotSignOut": "Do not sign out",
   "hearUsWhereTitle": "How did you hear about Ente? (optional)",
   "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!",
   "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...",
   "waitingForBrowserRequest": "Waiting for browser request...",
   "waitingForVerification": "Waiting for verification...",
   "waitingForVerification": "Waiting for verification...",
   "passkey": "Passkey",
   "passkey": "Passkey",

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

@@ -145,6 +145,7 @@
   "lostDeviceTitle": "デバイスを紛失しましたか?",
   "lostDeviceTitle": "デバイスを紛失しましたか?",
   "twoFactorAuthTitle": "2 要素認証",
   "twoFactorAuthTitle": "2 要素認証",
   "passkeyAuthTitle": "パスキー認証",
   "passkeyAuthTitle": "パスキー認証",
+  "verifyPasskey": "パスキーの認証",
   "recoverAccount": "アカウントを回復",
   "recoverAccount": "アカウントを回復",
   "enterRecoveryKeyHint": "回復キーを入力",
   "enterRecoveryKeyHint": "回復キーを入力",
   "recover": "回復",
   "recover": "回復",
@@ -407,7 +408,7 @@
   "hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
   "hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)",
   "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!",
   "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!",
   "waitingForBrowserRequest": "ブラウザのリクエストを待っています...",
   "waitingForBrowserRequest": "ブラウザのリクエストを待っています...",
-  "launchPasskeyUrlAgain": "パスキーのURLを再度起動する",
+  "waitingForVerification": "認証を待っています...",
   "passkey": "パスキー",
   "passkey": "パスキー",
   "developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?",
   "developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?",
   "developerSettings": "開発者向け設定",
   "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",
   "enterCodeHint": "Voer de 6-cijferige code van je verificatie-app in",
   "lostDeviceTitle": "Apparaat verloren?",
   "lostDeviceTitle": "Apparaat verloren?",
   "twoFactorAuthTitle": "Tweestapsverificatie",
   "twoFactorAuthTitle": "Tweestapsverificatie",
+  "passkeyAuthTitle": "Passkey verificatie",
+  "verifyPasskey": "Bevestig passkey",
   "recoverAccount": "Account herstellen",
   "recoverAccount": "Account herstellen",
   "enterRecoveryKeyHint": "Voer je herstelsleutel in",
   "enterRecoveryKeyHint": "Voer je herstelsleutel in",
   "recover": "Herstellen",
   "recover": "Herstellen",
@@ -404,5 +406,15 @@
   "signOutOtherDevices": "Afmelden bij andere apparaten",
   "signOutOtherDevices": "Afmelden bij andere apparaten",
   "doNotSignOut": "Niet uitloggen",
   "doNotSignOut": "Niet uitloggen",
   "hearUsWhereTitle": "Hoe hoorde je over Ente? (optioneel)",
   "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",
   "deleteCodeAuthMessage": "Uwierzytelnij, aby usunąć kod",
   "showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR",
   "showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR",
   "confirmAccountDeleteTitle": "Potwierdź usunięcie konta",
   "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",
   "copyEmailAction": "Copiar e-mail",
   "exportLogsAction": "Exportar logs",
   "exportLogsAction": "Exportar logs",
-  "reportABug": "Reportar um bug",
+  "reportABug": "Reportar um problema",
   "crashAndErrorReporting": "Reporte de erros e falhas",
   "crashAndErrorReporting": "Reporte de erros e falhas",
-  "reportBug": "Reportar bug",
+  "reportBug": "Reportar problema",
   "emailUsMessage": "Por favor, envie um e-mail para {email}",
   "emailUsMessage": "Por favor, envie um e-mail para {email}",
   "@emailUsMessage": {
   "@emailUsMessage": {
     "placeholders": {
     "placeholders": {
@@ -145,6 +145,7 @@
   "lostDeviceTitle": "Perdeu seu dispositivo?",
   "lostDeviceTitle": "Perdeu seu dispositivo?",
   "twoFactorAuthTitle": "Autenticação de dois fatores",
   "twoFactorAuthTitle": "Autenticação de dois fatores",
   "passkeyAuthTitle": "Autenticação via Chave de acesso",
   "passkeyAuthTitle": "Autenticação via Chave de acesso",
+  "verifyPasskey": "Verificar chave de acesso",
   "recoverAccount": "Recuperar conta",
   "recoverAccount": "Recuperar conta",
   "enterRecoveryKeyHint": "Digite sua chave de recuperação",
   "enterRecoveryKeyHint": "Digite sua chave de recuperação",
   "recover": "Recuperar",
   "recover": "Recuperar",
@@ -162,7 +163,7 @@
   "invalidEmailMessage": "Por favor, insira um endereço de e-mail válido.",
   "invalidEmailMessage": "Por favor, insira um endereço de e-mail válido.",
   "deleteAccount": "Excluir conta",
   "deleteAccount": "Excluir conta",
   "deleteAccountQuery": "Sentiremos muito por vê-lo partir. Você está enfrentando algum problema?",
   "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",
   "noDeleteAccountAction": "Não, excluir conta",
   "initiateAccountDeleteTitle": "Por favor, autentique-se para iniciar a exclusão de conta",
   "initiateAccountDeleteTitle": "Por favor, autentique-se para iniciar a exclusão de conta",
   "sendEmail": "Enviar e-mail",
   "sendEmail": "Enviar e-mail",
@@ -407,7 +408,7 @@
   "hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
   "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!",
   "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!",
   "waitingForBrowserRequest": "Aguardando solicitação do navegador...",
   "waitingForBrowserRequest": "Aguardando solicitação do navegador...",
-  "launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente",
+  "waitingForVerification": "Esperando por verificação...",
   "passkey": "Chave de acesso",
   "passkey": "Chave de acesso",
   "developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?",
   "developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?",
   "developerSettings": "Configurações de desenvolvedor",
   "developerSettings": "Configurações de desenvolvedor",

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

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

+ 63 - 6
auth/lib/main.dart

@@ -2,7 +2,6 @@ import 'dart:async';
 import 'dart:io';
 import 'dart:io';
 
 
 import 'package:adaptive_theme/adaptive_theme.dart';
 import 'package:adaptive_theme/adaptive_theme.dart';
-import 'package:computer/computer.dart';
 import "package:ente_auth/app/view/app.dart";
 import "package:ente_auth/app/view/app.dart";
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/constants.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/update_service.dart';
 import 'package:ente_auth/services/user_remote_flag_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/user_service.dart';
+import 'package:ente_auth/services/window_listener_service.dart';
 import 'package:ente_auth/store/code_store.dart';
 import 'package:ente_auth/store/code_store.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/ui/tools/lock_screen.dart';
 import 'package:ente_auth/ui/tools/lock_screen.dart';
 import 'package:ente_auth/ui/utils/icon_utils.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/foundation.dart';
 import "package:flutter/material.dart";
 import "package:flutter/material.dart";
 import 'package:flutter/scheduler.dart';
 import 'package:flutter/scheduler.dart';
@@ -29,11 +31,52 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:privacy_screen/privacy_screen.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");
 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 {
 void main() async {
   WidgetsFlutterBinding.ensureInitialized();
   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 _runInForeground();
   await _setupPrivacyScreen();
   await _setupPrivacyScreen();
   if (Platform.isAndroid) {
   if (Platform.isAndroid) {
@@ -70,10 +113,14 @@ ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) {
 }
 }
 
 
 Future _runWithLogs(Function() function, {String prefix = ""}) async {
 Future _runWithLogs(Function() function, {String prefix = ""}) async {
+  String dir = "";
+  try {
+    dir = "${(await getApplicationSupportDirectory()).path}/logs";
+  } catch (_) {}
   await SuperLogging.main(
   await SuperLogging.main(
     LogConfig(
     LogConfig(
       body: function,
       body: function,
-      logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
+      logDirPath: dir,
       maxLogFiles: 5,
       maxLogFiles: 5,
       sentryDsn: sentryDSN,
       sentryDsn: sentryDSN,
       enableInDebugMode: true,
       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 {
 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 PreferenceService.instance.init();
   await CodeStore.instance.init();
   await CodeStore.instance.init();
   await Configuration.instance.init();
   await Configuration.instance.init();
@@ -100,6 +156,7 @@ Future<void> _init(bool bool, {String? via}) async {
 }
 }
 
 
 Future<void> _setupPrivacyScreen() async {
 Future<void> _setupPrivacyScreen() async {
+  if (!PlatformUtil.isMobile()) return;
   final brightness =
   final brightness =
       SchedulerBinding.instance.platformDispatcher.platformBrightness;
       SchedulerBinding.instance.platformDispatcher.platformBrightness;
   bool isInDarkMode = brightness == Brightness.dark;
   bool isInDarkMode = brightness == Brightness.dark;

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

@@ -57,14 +57,7 @@ class Code {
       updatedAlgo,
       updatedAlgo,
       updatedType,
       updatedType,
       updatedCounter,
       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,
       generatedID: generatedID,
     );
     );
   }
   }
@@ -83,35 +76,28 @@ class Code {
       Algorithm.sha1,
       Algorithm.sha1,
       Type.totp,
       Type.totp,
       0,
       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) {
   static Code fromRawData(String rawData) {
     Uri uri = Uri.parse(rawData);
     Uri uri = Uri.parse(rawData);
     try {
     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,
       // if account name contains # without encoding,
       // rest of the url are treated as url fragment
       // rest of the url are treated as url fragment
-      if(rawData.contains("#")) {
+      if (rawData.contains("#")) {
         return Code.fromRawData(rawData.replaceAll("#", '%23'));
         return Code.fromRawData(rawData.replaceAll("#", '%23'));
       } else {
       } else {
         rethrow;
         rethrow;
@@ -141,7 +127,7 @@ class Code {
       if (uri.queryParameters.containsKey("issuer")) {
       if (uri.queryParameters.containsKey("issuer")) {
         String issuerName = uri.queryParameters['issuer']!;
         String issuerName = uri.queryParameters['issuer']!;
         // Handle issuer name with period
         // 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=")) {
         if (issuerName.contains("period=")) {
           return issuerName.substring(0, issuerName.indexOf("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:async';
+import 'dart:io';
 
 
 import 'package:ente_auth/app/view/app.dart';
 import 'package:ente_auth/app/view/app.dart';
 import 'package:ente_auth/core/configuration.dart';
 import 'package:ente_auth/core/configuration.dart';
@@ -28,7 +29,7 @@ import "package:flutter/material.dart";
 import 'package:local_auth/local_auth.dart';
 import 'package:local_auth/local_auth.dart';
 
 
 class OnboardingPage extends StatefulWidget {
 class OnboardingPage extends StatefulWidget {
-  const OnboardingPage({Key? key}) : super(key: key);
+  const OnboardingPage({super.key});
 
 
   @override
   @override
   State<OnboardingPage> createState() => _OnboardingPageState();
   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 {
   Future<void> _optForOfflineMode() async {
-    bool canCheckBio = await LocalAuthentication().canCheckBiometrics;
+    bool canCheckBio = Platform.isMacOS || Platform.isLinux
+        ? true
+        : await LocalAuthentication().canCheckBiometrics;
     if (!canCheckBio) {
     if (!canCheckBio) {
       showToast(
       showToast(
         context,
         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 {
 class SetupEnterSecretKeyPage extends StatefulWidget {
   final Code? code;
   final Code? code;
 
 
-  SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key);
+  SetupEnterSecretKeyPage({this.code, super.key});
 
 
   @override
   @override
   State<SetupEnterSecretKeyPage> createState() =>
   State<SetupEnterSecretKeyPage> createState() =>
@@ -32,7 +32,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
           widget.code != null ? safeDecode(widget.code!.account).trim() : null,
           widget.code != null ? safeDecode(widget.code!.account).trim() : null,
     );
     );
     _secretController = TextEditingController(
     _secretController = TextEditingController(
-      text: widget.code != null ? widget.code!.secret : null,
+      text: widget.code?.secret,
     );
     );
     _secretKeyObscured = widget.code != null;
     _secretKeyObscured = widget.code != null;
     super.initState();
     super.initState();
@@ -45,8 +45,8 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
       appBar: AppBar(
       appBar: AppBar(
         title: Text(l10n.importAccountPageTitle),
         title: Text(l10n.importAccountPageTitle),
       ),
       ),
-      body: SafeArea(
-        child: Center(
+      body: Center(
+        child: SingleChildScrollView(
           child: Padding(
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
             padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
             child: Column(
             child: Column(

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

@@ -1,4 +1,3 @@
-
 import 'dart:math';
 import 'dart:math';
 
 
 import "package:ente_auth/l10n/l10n.dart";
 import "package:ente_auth/l10n/l10n.dart";
@@ -10,7 +9,7 @@ import 'package:qr_flutter/qr_flutter.dart';
 class ViewQrPage extends StatelessWidget {
 class ViewQrPage extends StatelessWidget {
   final Code? code;
   final Code? code;
 
 
-  ViewQrPage({this.code, Key? key}) : super(key: key);
+  ViewQrPage({this.code, super.key});
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
@@ -22,15 +21,22 @@ class ViewQrPage extends StatelessWidget {
       appBar: AppBar(
       appBar: AppBar(
         title: Text(l10n.qrCode),
         title: Text(l10n.qrCode),
       ),
       ),
-      body: SafeArea(
-        child: Center(
+      body: Center(
+        child: SingleChildScrollView(
           child: Padding(
           child: Padding(
             padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
             padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
             child: Column(
             child: Column(
               children: [
               children: [
-                QrImage(
+                QrImageView(
                   data: code!.rawData,
                   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,
                   version: QrVersions.auto,
                   size: qrSize,
                   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/models/authenticator/local_auth_entity.dart';
 import 'package:ente_auth/store/authenticator_db.dart';
 import 'package:ente_auth/store/authenticator_db.dart';
 import 'package:ente_auth/store/offline_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/foundation.dart';
-import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
 
@@ -75,10 +74,10 @@ class AuthenticatorService {
     final key = await getOrCreateAuthDataKey(mode);
     final key = await getOrCreateAuthDataKey(mode);
     for (LocalAuthEntity e in result) {
     for (LocalAuthEntity e in result) {
       try {
       try {
-        final decryptedValue = await CryptoUtil.decryptChaCha(
-          Sodium.base642bin(e.encryptedData),
+        final decryptedValue = await CryptoUtil.decryptData(
+          CryptoUtil.base642bin(e.encryptedData),
           key,
           key,
-          Sodium.base642bin(e.header),
+          CryptoUtil.base642bin(e.header),
         );
         );
         final hasSynced = !(e.id == null || e.shouldSync);
         final hasSynced = !(e.id == null || e.shouldSync);
         entities.add(
         entities.add(
@@ -101,12 +100,13 @@ class AuthenticatorService {
     AccountMode accountMode,
     AccountMode accountMode,
   ) async {
   ) async {
     var key = await getOrCreateAuthDataKey(accountMode);
     var key = await getOrCreateAuthDataKey(accountMode);
-    final encryptedKeyData = await CryptoUtil.encryptChaCha(
-      utf8.encode(plainText) as Uint8List,
+    final encryptedKeyData = await CryptoUtil.encryptData(
+      utf8.encode(plainText),
       key,
       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
     final insertedID = accountMode.isOnline
         ? await _db.insert(encryptedData, header)
         ? await _db.insert(encryptedData, header)
         : await _offlineDb.insert(encryptedData, header);
         : await _offlineDb.insert(encryptedData, header);
@@ -123,12 +123,13 @@ class AuthenticatorService {
     AccountMode accountMode,
     AccountMode accountMode,
   ) async {
   ) async {
     var key = await getOrCreateAuthDataKey(accountMode);
     var key = await getOrCreateAuthDataKey(accountMode);
-    final encryptedKeyData = await CryptoUtil.encryptChaCha(
-      utf8.encode(plainText) as Uint8List,
+    final encryptedKeyData = await CryptoUtil.encryptData(
+      utf8.encode(plainText),
       key,
       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
     final int affectedRows = accountMode.isOnline
         ? await _db.updateEntry(generatedID, encryptedData, header)
         ? await _db.updateEntry(generatedID, encryptedData, header)
         : await _offlineDb.updateEntry(generatedID, encryptedData, header);
         : await _offlineDb.updateEntry(generatedID, encryptedData, header);
@@ -191,25 +192,25 @@ class AuthenticatorService {
   Future<void> _remoteToLocalSync() async {
   Future<void> _remoteToLocalSync() async {
     _logger.info('Initiating remote to local sync');
     _logger.info('Initiating remote to local sync');
     final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
     final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
-    _logger.info("Current sync is " + lastSyncTime.toString());
+    _logger.info("Current sync is $lastSyncTime");
     const int fetchLimit = 500;
     const int fetchLimit = 500;
     final List<AuthEntity> result =
     final List<AuthEntity> result =
         await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
         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) {
     if (result.isEmpty) {
       return;
       return;
     }
     }
     final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
     final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
     List<String> deletedIDs =
     List<String> deletedIDs =
         result.where((element) => element.isDeleted).map((e) => e.id).toList();
         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);
     result.removeWhere((element) => element.isDeleted);
     await _db.insertOrReplace(result);
     await _db.insertOrReplace(result);
     if (deletedIDs.isNotEmpty) {
     if (deletedIDs.isNotEmpty) {
       await _db.deleteByIDs(ids: deletedIDs);
       await _db.deleteByIDs(ids: deletedIDs);
     }
     }
     await _prefs.setInt(_lastEntitySyncTime, maxSyncTime);
     await _prefs.setInt(_lastEntitySyncTime, maxSyncTime);
-    _logger.info("Setting synctime to " + maxSyncTime.toString());
+    _logger.info("Setting synctime to $maxSyncTime");
     if (result.length == fetchLimit) {
     if (result.length == fetchLimit) {
       _logger.info("Diff limit reached, pulling again");
       _logger.info("Diff limit reached, pulling again");
       await _remoteToLocalSync();
       await _remoteToLocalSync();
@@ -223,7 +224,7 @@ class AuthenticatorService {
         .where((element) => element.shouldSync || element.id == null)
         .where((element) => element.shouldSync || element.id == null)
         .toList();
         .toList();
     _logger.info(
     _logger.info(
-      pendingUpdate.length.toString() + " entries to be updated at remote",
+      "${pendingUpdate.length} entries to be updated at remote",
     );
     );
     for (LocalAuthEntity entity in pendingUpdate) {
     for (LocalAuthEntity entity in pendingUpdate) {
       if (entity.id == null) {
       if (entity.id == null) {
@@ -262,21 +263,21 @@ class AuthenticatorService {
     try {
     try {
       final AuthKey response = await _gateway.getKey();
       final AuthKey response = await _gateway.getKey();
       final authKey = CryptoUtil.decryptSync(
       final authKey = CryptoUtil.decryptSync(
-        Sodium.base642bin(response.encryptedKey),
+        CryptoUtil.base642bin(response.encryptedKey),
         _config.getKey()!,
         _config.getKey()!,
-        Sodium.base642bin(response.header),
+        CryptoUtil.base642bin(response.header),
       );
       );
-      await _config.setAuthSecretKey(Sodium.bin2base64(authKey));
+      await _config.setAuthSecretKey(CryptoUtil.bin2base64(authKey));
       return authKey;
       return authKey;
     } on AuthenticatorKeyNotFound catch (e) {
     } on AuthenticatorKeyNotFound catch (e) {
       _logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}");
       _logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}");
       final key = CryptoUtil.generateKey();
       final key = CryptoUtil.generateKey();
       final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
       final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!);
       await _gateway.createKey(
       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;
       return key;
     } catch (e, s) {
     } catch (e, s) {
       _logger.severe("Failed to getOrCreateAuthDataKey", 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() {
   Future<Response<dynamic>> _fetchPrivateBillingPlans() {
     return _dio.get(
     return _dio.get(
-      _config.getHttpEndpoint() + "/billing/user-plans/",
+      "${_config.getHttpEndpoint()}/billing/user-plans/",
       options: Options(
       options: Options(
         headers: {
         headers: {
           "X-Auth-Token": _config.getToken(),
           "X-Auth-Token": _config.getToken(),
@@ -60,7 +60,7 @@ class BillingService {
   }
   }
 
 
   Future<Response<dynamic>> _fetchPublicBillingPlans() {
   Future<Response<dynamic>> _fetchPublicBillingPlans() {
-    return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2");
+    return _dio.get("${_config.getHttpEndpoint()}/billing/plans/v2");
   }
   }
 
 
   Future<Subscription> verifySubscription(
   Future<Subscription> verifySubscription(
@@ -70,7 +70,7 @@ class BillingService {
   }) async {
   }) async {
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/billing/verify-subscription",
+        "${_config.getHttpEndpoint()}/billing/verify-subscription",
         data: {
         data: {
           "paymentProvider": paymentProvider ??
           "paymentProvider": paymentProvider ??
               (Platform.isAndroid ? "playstore" : "appstore"),
               (Platform.isAndroid ? "playstore" : "appstore"),
@@ -84,7 +84,7 @@ class BillingService {
         ),
         ),
       );
       );
       return Subscription.fromMap(response.data["subscription"]);
       return Subscription.fromMap(response.data["subscription"]);
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       if (e.response != null && e.response!.statusCode == 409) {
       if (e.response != null && e.response!.statusCode == 409) {
         throw SubscriptionAlreadyClaimedError();
         throw SubscriptionAlreadyClaimedError();
       } else {
       } else {
@@ -100,7 +100,7 @@ class BillingService {
     if (_cachedSubscription == null) {
     if (_cachedSubscription == null) {
       try {
       try {
         final response = await _dio.get(
         final response = await _dio.get(
-          _config.getHttpEndpoint() + "/billing/subscription",
+          "${_config.getHttpEndpoint()}/billing/subscription",
           options: Options(
           options: Options(
             headers: {
             headers: {
               "X-Auth-Token": _config.getToken(),
               "X-Auth-Token": _config.getToken(),
@@ -109,7 +109,7 @@ class BillingService {
         );
         );
         _cachedSubscription =
         _cachedSubscription =
             Subscription.fromMap(response.data["subscription"]);
             Subscription.fromMap(response.data["subscription"]);
-      } on DioError catch (e, s) {
+      } on DioException catch (e, s) {
         _logger.severe(e, s);
         _logger.severe(e, s);
         rethrow;
         rethrow;
       }
       }
@@ -120,7 +120,7 @@ class BillingService {
   Future<Subscription> cancelStripeSubscription() async {
   Future<Subscription> cancelStripeSubscription() async {
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/billing/stripe/cancel-subscription",
+        "${_config.getHttpEndpoint()}/billing/stripe/cancel-subscription",
         options: Options(
         options: Options(
           headers: {
           headers: {
             "X-Auth-Token": _config.getToken(),
             "X-Auth-Token": _config.getToken(),
@@ -129,7 +129,7 @@ class BillingService {
       );
       );
       final subscription = Subscription.fromMap(response.data["subscription"]);
       final subscription = Subscription.fromMap(response.data["subscription"]);
       return subscription;
       return subscription;
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       _logger.severe(e, s);
       _logger.severe(e, s);
       rethrow;
       rethrow;
     }
     }
@@ -138,7 +138,7 @@ class BillingService {
   Future<Subscription> activateStripeSubscription() async {
   Future<Subscription> activateStripeSubscription() async {
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/billing/stripe/activate-subscription",
+        "${_config.getHttpEndpoint()}/billing/stripe/activate-subscription",
         options: Options(
         options: Options(
           headers: {
           headers: {
             "X-Auth-Token": _config.getToken(),
             "X-Auth-Token": _config.getToken(),
@@ -147,7 +147,7 @@ class BillingService {
       );
       );
       final subscription = Subscription.fromMap(response.data["subscription"]);
       final subscription = Subscription.fromMap(response.data["subscription"]);
       return subscription;
       return subscription;
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       _logger.severe(e, s);
       _logger.severe(e, s);
       rethrow;
       rethrow;
     }
     }
@@ -158,7 +158,7 @@ class BillingService {
   }) async {
   }) async {
     try {
     try {
       final response = await _dio.get(
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/billing/stripe/customer-portal",
+        "${_config.getHttpEndpoint()}/billing/stripe/customer-portal",
         queryParameters: {
         queryParameters: {
           "redirectURL": kWebPaymentRedirectUrl,
           "redirectURL": kWebPaymentRedirectUrl,
         },
         },
@@ -169,7 +169,7 @@ class BillingService {
         ),
         ),
       );
       );
       return response.data["url"];
       return response.data["url"];
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       _logger.severe(e, s);
       _logger.severe(e, s);
       rethrow;
       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/core/configuration.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/ui/tools/app_lock.dart';
 import 'package:ente_auth/utils/auth_util.dart';
 import 'package:ente_auth/utils/auth_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.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:local_auth/local_auth.dart';
+import 'package:logging/logging.dart';
 
 
 class LocalAuthenticationService {
 class LocalAuthenticationService {
   LocalAuthenticationService._privateConstructor();
   LocalAuthenticationService._privateConstructor();
   static final LocalAuthenticationService instance =
   static final LocalAuthenticationService instance =
       LocalAuthenticationService._privateConstructor();
       LocalAuthenticationService._privateConstructor();
+  final logger = Logger((LocalAuthenticationService).toString());
 
 
   Future<bool> requestLocalAuthentication(
   Future<bool> requestLocalAuthentication(
     BuildContext context,
     BuildContext context,
@@ -38,7 +44,7 @@ class LocalAuthenticationService {
     String errorDialogContent, [
     String errorDialogContent, [
     String errorDialogTitle = "",
     String errorDialogTitle = "",
   ]) async {
   ]) async {
-    if (await LocalAuthentication().isDeviceSupported()) {
+    if (await _isLocalAuthSupportedOnDevice()) {
       AppLock.of(context)!.disable();
       AppLock.of(context)!.disable();
       final result = await requestAuthentication(
       final result = await requestAuthentication(
         context,
         context,
@@ -65,6 +71,12 @@ class LocalAuthenticationService {
   }
   }
 
 
   Future<bool> _isLocalAuthSupportedOnDevice() async {
   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<
         _flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
             AndroidFlutterLocalNotificationsPlugin>();
             AndroidFlutterLocalNotificationsPlugin>();
     if (implementation != null) {
     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/constants.dart';
 import 'package:ente_auth/core/network.dart';
 import 'package:ente_auth/core/network.dart';
 import 'package:ente_auth/services/notification_service.dart';
 import 'package:ente_auth/services/notification_service.dart';
+import 'package:ente_auth/utils/platform_util.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:shared_preferences/shared_preferences.dart';
@@ -130,7 +131,8 @@ class UpdateService {
 
 
   bool isIndependent() {
   bool isIndependent() {
     return flavor == "independent" ||
     return flavor == "independent" ||
-        _packageInfo.packageName.endsWith("independent");
+        _packageInfo.packageName.endsWith("independent") ||
+        PlatformUtil.isDesktop();
   }
   }
 }
 }
 
 
@@ -141,6 +143,7 @@ class LatestVersionInfo {
   final bool? shouldForceUpdate;
   final bool? shouldForceUpdate;
   final int lastSupportedVersionCode;
   final int lastSupportedVersionCode;
   final String? url;
   final String? url;
+  final String? release;
   final int? size;
   final int? size;
   final bool? shouldNotify;
   final bool? shouldNotify;
 
 
@@ -151,6 +154,7 @@ class LatestVersionInfo {
     this.shouldForceUpdate,
     this.shouldForceUpdate,
     this.lastSupportedVersionCode,
     this.lastSupportedVersionCode,
     this.url,
     this.url,
+    this.release,
     this.size,
     this.size,
     this.shouldNotify,
     this.shouldNotify,
   );
   );
@@ -163,6 +167,7 @@ class LatestVersionInfo {
       map['shouldForceUpdate'],
       map['shouldForceUpdate'],
       map['lastSupportedVersionCode'] ?? 1,
       map['lastSupportedVersionCode'] ?? 1,
       map['url'],
       map['url'],
+      map['release'],
       map['size'],
       map['size'],
       map['shouldNotify'],
       map['shouldNotify'],
     );
     );

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

@@ -96,7 +96,7 @@ class UserRemoteFlagService {
         queryParams["defaultValue"] = defaultValue;
         queryParams["defaultValue"] = defaultValue;
       }
       }
       final response = await _dio.get(
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/remote-store",
+        "${_config.getHttpEndpoint()}/remote-store",
         queryParameters: queryParams,
         queryParameters: queryParams,
         options: Options(
         options: Options(
           headers: {
           headers: {
@@ -119,7 +119,7 @@ class UserRemoteFlagService {
   Future<void> _updateKeyValue(String key, String value) async {
   Future<void> _updateKeyValue(String key, String value) async {
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/remote-store/update",
+        "${_config.getHttpEndpoint()}/remote-store/update",
         data: {
         data: {
           "key": key,
           "key": key,
           "value": value,
           "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/passkey_page.dart';
 import 'package:ente_auth/ui/two_factor_authentication_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/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/dialog_util.dart';
 import 'package:ente_auth/utils/toast_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/foundation.dart";
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
@@ -80,7 +80,7 @@ class UserService {
     await dialog.show();
     await dialog.show();
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/ott",
+        "${_config.getHttpEndpoint()}/users/ott",
         data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
         data: {"email": email, "purpose": isChangeEmail ? "change" : ""},
       );
       );
       await dialog.hide();
       await dialog.hide();
@@ -102,7 +102,7 @@ class UserService {
         return;
         return;
       }
       }
       unawaited(showGenericErrorDialog(context: context));
       unawaited(showGenericErrorDialog(context: context));
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.info(e);
       _logger.info(e);
       if (e.response != null && e.response!.statusCode == 403) {
       if (e.response != null && e.response!.statusCode == 403) {
@@ -129,7 +129,7 @@ class UserService {
     String type = "SubCancellation",
     String type = "SubCancellation",
   }) async {
   }) async {
     await _dio.post(
     await _dio.post(
-      _config.getHttpEndpoint() + "/anonymous/feedback",
+      "${_config.getHttpEndpoint()}/anonymous/feedback",
       data: {"feedback": feedback, "type": "type"},
       data: {"feedback": feedback, "type": "type"},
     );
     );
   }
   }
@@ -173,7 +173,7 @@ class UserService {
     try {
     try {
       final response = await _enteDio.get("/users/sessions");
       final response = await _enteDio.get("/users/sessions");
       return Sessions.fromMap(response.data);
       return Sessions.fromMap(response.data);
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.info(e);
       _logger.info(e);
       rethrow;
       rethrow;
     }
     }
@@ -187,7 +187,7 @@ class UserService {
           "token": token,
           "token": token,
         },
         },
       );
       );
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.info(e);
       _logger.info(e);
       rethrow;
       rethrow;
     }
     }
@@ -196,7 +196,7 @@ class UserService {
   Future<void> leaveFamilyPlan() async {
   Future<void> leaveFamilyPlan() async {
     try {
     try {
       await _enteDio.delete("/family/leave");
       await _enteDio.delete("/family/leave");
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.warning('failed to leave family plan', e);
       _logger.warning('failed to leave family plan', e);
       rethrow;
       rethrow;
     }
     }
@@ -306,11 +306,11 @@ class UserService {
       "ott": ott,
       "ott": ott,
     };
     };
     if (!_config.isLoggedIn()) {
     if (!_config.isLoggedIn()) {
-      verifyData["source"] = 'auth:' + _getRefSource();
+      verifyData["source"] = 'auth:${_getRefSource()}';
     }
     }
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/verify-email",
+        "${_config.getHttpEndpoint()}/users/verify-email",
         data: verifyData,
         data: verifyData,
       );
       );
       await dialog.hide();
       await dialog.hide();
@@ -346,7 +346,7 @@ class UserService {
         // should never reach here
         // should never reach here
         throw Exception("unexpected response during email verification");
         throw Exception("unexpected response during email verification");
       }
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       _logger.info(e);
       _logger.info(e);
       await dialog.hide();
       await dialog.hide();
       if (e.response != null && e.response!.statusCode == 410) {
       if (e.response != null && e.response!.statusCode == 410) {
@@ -410,7 +410,7 @@ class UserService {
         context.l10n.oops,
         context.l10n.oops,
         context.l10n.verificationFailedPleaseTryAgain,
         context.l10n.verificationFailedPleaseTryAgain,
       );
       );
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       await dialog.hide();
       if (e.response != null && e.response!.statusCode == 403) {
       if (e.response != null && e.response!.statusCode == 403) {
         // ignore: unawaited_futures
         // ignore: unawaited_futures
@@ -460,7 +460,7 @@ class UserService {
   Future<SrpAttributes> getSrpAttributes(String email) async {
   Future<SrpAttributes> getSrpAttributes(String email) async {
     try {
     try {
       final response = await _dio.get(
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/users/srp/attributes",
+        "${_config.getHttpEndpoint()}/users/srp/attributes",
         queryParameters: {
         queryParameters: {
           "email": email,
           "email": email,
         },
         },
@@ -470,7 +470,7 @@ class UserService {
       } else {
       } else {
         throw Exception("get-srp-attributes action failed");
         throw Exception("get-srp-attributes action failed");
       }
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       if (e.response != null && e.response!.statusCode == 404) {
       if (e.response != null && e.response!.statusCode == 404) {
         throw SrpSetupNotCompleteError();
         throw SrpSetupNotCompleteError();
       }
       }
@@ -523,7 +523,7 @@ class UserService {
         // ignore: need to calculate secret to get M1, unused_local_variable
         // ignore: need to calculate secret to get M1, unused_local_variable
         final clientS = client.calculateSecret(serverB);
         final clientS = client.calculateSecret(serverB);
         final clientM = client.calculateClientEvidenceMessage();
         final clientM = client.calculateClientEvidenceMessage();
-        // ignore: unused_local_variable
+
         late Response _;
         late Response _;
         if (setKeysRequest == null) {
         if (setKeysRequest == null) {
           _ = await _enteDio.post(
           _ = await _enteDio.post(
@@ -573,7 +573,7 @@ class UserService {
     late Uint8List keyEncryptionKey;
     late Uint8List keyEncryptionKey;
     _logger.finest('Start deriving key');
     _logger.finest('Start deriving key');
     keyEncryptionKey = await CryptoUtil.deriveKey(
     keyEncryptionKey = await CryptoUtil.deriveKey(
-      utf8.encode(userPassword) as Uint8List,
+      utf8.encode(userPassword),
       CryptoUtil.base642bin(srpAttributes.kekSalt),
       CryptoUtil.base642bin(srpAttributes.kekSalt),
       srpAttributes.memLimit,
       srpAttributes.memLimit,
       srpAttributes.opsLimit,
       srpAttributes.opsLimit,
@@ -596,7 +596,7 @@ class UserService {
 
 
     final A = client.generateClientCredentials(salt, identity, password);
     final A = client.generateClientCredentials(salt, identity, password);
     final createSessionResponse = await _dio.post(
     final createSessionResponse = await _dio.post(
-      _config.getHttpEndpoint() + "/users/srp/create-session",
+      "${_config.getHttpEndpoint()}/users/srp/create-session",
       data: {
       data: {
         "srpUserID": srpAttributes.srpUserID,
         "srpUserID": srpAttributes.srpUserID,
         "srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
         "srpA": base64Encode(SRP6Util.encodeBigInt(A!)),
@@ -610,7 +610,7 @@ class UserService {
     final clientS = client.calculateSecret(serverB);
     final clientS = client.calculateSecret(serverB);
     final clientM = client.calculateClientEvidenceMessage();
     final clientM = client.calculateClientEvidenceMessage();
     final response = await _dio.post(
     final response = await _dio.post(
-      _config.getHttpEndpoint() + "/users/srp/verify-session",
+      "${_config.getHttpEndpoint()}/users/srp/verify-session",
       data: {
       data: {
         "sessionID": sessionID,
         "sessionID": sessionID,
         "srpUserID": srpAttributes.srpUserID,
         "srpUserID": srpAttributes.srpUserID,
@@ -709,7 +709,7 @@ class UserService {
     await dialog.show();
     await dialog.show();
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/two-factor/verify",
+        "${_config.getHttpEndpoint()}/users/two-factor/verify",
         data: {
         data: {
           "sessionID": sessionID,
           "sessionID": sessionID,
           "code": code,
           "code": code,
@@ -729,7 +729,7 @@ class UserService {
           (route) => route.isFirst,
           (route) => route.isFirst,
         );
         );
       }
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
       if (e.response != null && e.response!.statusCode == 404) {
@@ -772,7 +772,7 @@ class UserService {
     await dialog.show();
     await dialog.show();
     try {
     try {
       final response = await _dio.get(
       final response = await _dio.get(
-        _config.getHttpEndpoint() + "/users/two-factor/recover",
+        "${_config.getHttpEndpoint()}/users/two-factor/recover",
         queryParameters: {
         queryParameters: {
           "sessionID": sessionID,
           "sessionID": sessionID,
           "twoFactorType": twoFactorTypeToString(type),
           "twoFactorType": twoFactorTypeToString(type),
@@ -794,7 +794,7 @@ class UserService {
           (route) => route.isFirst,
           (route) => route.isFirst,
         );
         );
       }
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
       if (e.response != null && e.response!.statusCode == 404) {
@@ -868,7 +868,7 @@ class UserService {
     }
     }
     try {
     try {
       final response = await _dio.post(
       final response = await _dio.post(
-        _config.getHttpEndpoint() + "/users/two-factor/remove",
+        "${_config.getHttpEndpoint()}/users/two-factor/remove",
         data: {
         data: {
           "sessionID": sessionID,
           "sessionID": sessionID,
           "secret": secret,
           "secret": secret,
@@ -891,7 +891,7 @@ class UserService {
           (route) => route.isFirst,
           (route) => route.isFirst,
         );
         );
       }
       }
-    } on DioError catch (e) {
+    } on DioException catch (e) {
       await dialog.hide();
       await dialog.hide();
       _logger.severe(e);
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
       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/auth_entity.dart';
 import 'package:ente_auth/models/authenticator/local_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:flutter/foundation.dart';
 import 'package:path/path.dart';
 import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:sqflite/sqflite.dart';
 import 'package:sqflite/sqflite.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 
 
 class AuthenticatorDB {
 class AuthenticatorDB {
   static const _databaseName = "ente.authenticator.db";
   static const _databaseName = "ente.authenticator.db";
@@ -25,6 +27,16 @@ class AuthenticatorDB {
   }
   }
 
 
   Future<Database> _initDatabase() async {
   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 =
     final Directory documentsDirectory =
         await getApplicationDocumentsDirectory();
         await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     final String path = join(documentsDirectory.path, _databaseName);
@@ -166,7 +178,7 @@ class AuthenticatorDB {
         batch.delete(entityTable, where: whereID, whereArgs: [id]);
         batch.delete(entityTable, where: whereID, whereArgs: [id]);
       }
       }
     }
     }
-    await batch.commit();
+    final _ = await batch.commit();
     debugPrint("Done");
     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/auth_entity.dart';
 import 'package:ente_auth/models/authenticator/local_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:flutter/foundation.dart';
 import 'package:path/path.dart';
 import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:path_provider/path_provider.dart';
-import 'package:sqflite/sqflite.dart';
+import 'package:sqflite_common_ffi/sqflite_ffi.dart';
 
 
 class OfflineAuthenticatorDB {
 class OfflineAuthenticatorDB {
   static const _databaseName = "ente.offline_authenticator.db";
   static const _databaseName = "ente.offline_authenticator.db";
@@ -26,6 +27,16 @@ class OfflineAuthenticatorDB {
   }
   }
 
 
   Future<Database> _initDatabase() async {
   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 =
     final Directory documentsDirectory =
         await getApplicationDocumentsDirectory();
         await getApplicationDocumentsDirectory();
     final String path = join(documentsDirectory.path, _databaseName);
     final String path = join(documentsDirectory.path, _databaseName);
@@ -152,7 +163,7 @@ class OfflineAuthenticatorDB {
         batch.delete(entityTable, where: whereID, whereArgs: [id]);
         batch.delete(entityTable, where: whereID, whereArgs: [id]);
       }
       }
     }
     }
-    await batch.commit();
+    final _ = await batch.commit();
     debugPrint("Done");
     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 _warning500 = Color.fromRGBO(255, 101, 101, 1);
 const Color _warning800 = Color(0xFFF53434);
 const Color _warning800 = Color(0xFFF53434);
 const Color warning500 = Color.fromRGBO(255, 101, 101, 1);
 const Color warning500 = Color.fromRGBO(255, 101, 101, 1);
+// ignore: unused_element
 const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
 const Color _warning400 = Color.fromRGBO(255, 111, 111, 1);
 
 
 const Color _caution500 = Color.fromRGBO(255, 194, 71, 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';
 import 'package:flutter/material.dart';
 
 
 class ChangeEmailDialog extends StatefulWidget {
 class ChangeEmailDialog extends StatefulWidget {
-  const ChangeEmailDialog({Key? key}) : super(key: key);
+  const ChangeEmailDialog({super.key});
 
 
   @override
   @override
   State<ChangeEmailDialog> createState() => _ChangeEmailDialogState();
   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/services/user_service.dart';
 import 'package:ente_auth/ui/common/dialogs.dart';
 import 'package:ente_auth/ui/common/dialogs.dart';
 import 'package:ente_auth/ui/common/gradient_button.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/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/material.dart';
-import 'package:flutter_sodium/flutter_sodium.dart';
 
 
 class DeleteAccountPage extends StatelessWidget {
 class DeleteAccountPage extends StatelessWidget {
   const DeleteAccountPage({
   const DeleteAccountPage({
-    Key? key,
-  }) : super(key: key);
+    super.key,
+  });
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
@@ -150,6 +150,8 @@ class DeleteAccountPage extends StatelessWidget {
       l10n.initiateAccountDeleteTitle,
       l10n.initiateAccountDeleteTitle,
     );
     );
 
 
+    await PlatformUtil.refocusWindows();
+
     if (hasAuthenticated) {
     if (hasAuthenticated) {
       final choice = await showChoiceDialogOld(
       final choice = await showChoiceDialogOld(
         context,
         context,
@@ -164,8 +166,10 @@ class DeleteAccountPage extends StatelessWidget {
         return;
         return;
       }
       }
       final decryptChallenge = CryptoUtil.openSealSync(
       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()!,
         Configuration.instance.getSecretKey()!,
       );
       );
       final challengeResponseStr = utf8.decode(decryptChallenge);
       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/services/user_service.dart';
 import 'package:ente_auth/theme/ente_theme.dart';
 import 'package:ente_auth/theme/ente_theme.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.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:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.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";
 import "package:styled_text/styled_text.dart";
 
 
 class EmailEntryPage extends StatefulWidget {
 class EmailEntryPage extends StatefulWidget {
-  const EmailEntryPage({Key? key}) : super(key: key);
+  const EmailEntryPage({super.key});
 
 
   @override
   @override
   State<EmailEntryPage> createState() => _EmailEntryPageState();
   State<EmailEntryPage> createState() => _EmailEntryPageState();
@@ -190,6 +190,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                   padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
                   padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
                   child: TextFormField(
                   child: TextFormField(
                     keyboardType: TextInputType.text,
                     keyboardType: TextInputType.text,
+                    textInputAction: TextInputAction.next,
                     controller: _passwordController1,
                     controller: _passwordController1,
                     obscureText: !_password1Visible,
                     obscureText: !_password1Visible,
                     enableSuggestions: true,
                     enableSuggestions: true,
@@ -427,15 +428,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
               tags: {
               tags: {
                 'u-terms': StyledTextActionTag(
                 'u-terms': StyledTextActionTag(
                   (String? text, Map<String?, String?> attrs) =>
                   (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(
                   style: const TextStyle(
                     decoration: TextDecoration.underline,
                     decoration: TextDecoration.underline,
@@ -443,15 +439,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
                 ),
                 ),
                 'u-policy': StyledTextActionTag(
                 'u-policy': StyledTextActionTag(
                   (String? text, Map<String?, String?> attrs) =>
                   (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(
                   style: const TextStyle(
                     decoration: TextDecoration.underline,
                     decoration: TextDecoration.underline,
@@ -494,15 +485,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
               tags: {
               tags: {
                 'underline': StyledTextActionTag(
                 'underline': StyledTextActionTag(
                   (String? text, Map<String?, String?> attrs) =>
                   (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(
                   style: const TextStyle(
                     decoration: TextDecoration.underline,
                     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/services/user_service.dart';
 import 'package:ente_auth/ui/account/login_pwd_verification_page.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/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:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import "package:styled_text/styled_text.dart";
 import "package:styled_text/styled_text.dart";
 
 
 class LoginPage extends StatefulWidget {
 class LoginPage extends StatefulWidget {
-  const LoginPage({Key? key}) : super(key: key);
+  const LoginPage({super.key});
 
 
   @override
   @override
   State<LoginPage> createState() => _LoginPageState();
   State<LoginPage> createState() => _LoginPageState();
@@ -25,6 +25,36 @@ class _LoginPageState extends State<LoginPage> {
   Color? _emailInputFieldColor;
   Color? _emailInputFieldColor;
   final Logger _logger = Logger('_LoginPageState');
   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
   @override
   void initState() {
   void initState() {
     _email = _config.getEmail();
     _email = _config.getEmail();
@@ -60,36 +90,7 @@ class _LoginPageState extends State<LoginPage> {
         isKeypadOpen: isKeypadOpen,
         isKeypadOpen: isKeypadOpen,
         isFormValid: _emailIsValid,
         isFormValid: _emailIsValid,
         buttonText: context.l10n.logInLabel,
         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(),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -116,6 +117,8 @@ class _LoginPageState extends State<LoginPage> {
                   padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
                   padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
                   child: TextFormField(
                   child: TextFormField(
                     autofillHints: const [AutofillHints.email],
                     autofillHints: const [AutofillHints.email],
+                    onFieldSubmitted:
+                        _emailIsValid ? (value) => onPressed() : null,
                     decoration: InputDecoration(
                     decoration: InputDecoration(
                       fillColor: _emailInputFieldColor,
                       fillColor: _emailInputFieldColor,
                       filled: true,
                       filled: true,
@@ -179,15 +182,10 @@ class _LoginPageState extends State<LoginPage> {
                           tags: {
                           tags: {
                             'u-terms': StyledTextActionTag(
                             'u-terms': StyledTextActionTag(
                               (String? text, Map<String?, String?> attrs) =>
                               (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(
                               style: const TextStyle(
                                 decoration: TextDecoration.underline,
                                 decoration: TextDecoration.underline,
@@ -195,15 +193,10 @@ class _LoginPageState extends State<LoginPage> {
                             ),
                             ),
                             'u-policy': StyledTextActionTag(
                             'u-policy': StyledTextActionTag(
                               (String? text, Map<String?, String?> attrs) =>
                               (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(
                               style: const TextStyle(
                                 decoration: TextDecoration.underline,
                                 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:dio/dio.dart";
 import 'package:ente_auth/core/configuration.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/l10n/l10n.dart";
 import "package:ente_auth/models/api/user/srp.dart";
 import "package:ente_auth/models/api/user/srp.dart";
 import "package:ente_auth/services/user_service.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/ui/components/buttons/button_widget.dart";
 import "package:ente_auth/utils/dialog_util.dart";
 import "package:ente_auth/utils/dialog_util.dart";
 import "package:ente_auth/utils/email_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:flutter/material.dart';
 import "package:logging/logging.dart";
 import "package:logging/logging.dart";
 
 
@@ -19,8 +19,7 @@ import "package:logging/logging.dart";
 // volatile password.
 // volatile password.
 class LoginPasswordVerificationPage extends StatefulWidget {
 class LoginPasswordVerificationPage extends StatefulWidget {
   final SrpAttributes srpAttributes;
   final SrpAttributes srpAttributes;
-  const LoginPasswordVerificationPage({Key? key, required this.srpAttributes})
-      : super(key: key);
+  const LoginPasswordVerificationPage({super.key, required this.srpAttributes});
 
 
   @override
   @override
   State<LoginPasswordVerificationPage> createState() =>
   State<LoginPasswordVerificationPage> createState() =>
@@ -36,6 +35,11 @@ class _LoginPasswordVerificationPageState
   bool _passwordInFocus = false;
   bool _passwordInFocus = false;
   bool _passwordVisible = false;
   bool _passwordVisible = false;
 
 
+  Future<void> onPressed() async {
+    FocusScope.of(context).unfocus();
+    await verifyPassword(context, _passwordController.text);
+  }
+
   @override
   @override
   void initState() {
   void initState() {
     super.initState();
     super.initState();
@@ -77,10 +81,7 @@ class _LoginPasswordVerificationPageState
         isKeypadOpen: isKeypadOpen,
         isKeypadOpen: isKeypadOpen,
         isFormValid: _passwordController.text.isNotEmpty,
         isFormValid: _passwordController.text.isNotEmpty,
         buttonText: context.l10n.logInLabel,
         buttonText: context.l10n.logInLabel,
-        onPressedFunction: () async {
-          FocusScope.of(context).unfocus();
-          await verifyPassword(context, _passwordController.text);
-        },
+        onPressedFunction: onPressed,
       ),
       ),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -101,7 +102,7 @@ class _LoginPasswordVerificationPageState
         password,
         password,
         dialog,
         dialog,
       );
       );
-    } on DioError catch (e, s) {
+    } on DioException catch (e, s) {
       await dialog.hide();
       await dialog.hide();
       if (e.response != null && e.response!.statusCode == 401) {
       if (e.response != null && e.response!.statusCode == 401) {
         _logger.severe('server reject, failed verify SRP login', e, s);
         _logger.severe('server reject, failed verify SRP login', e, s);
@@ -112,7 +113,7 @@ class _LoginPasswordVerificationPageState
         );
         );
       } else {
       } else {
         _logger.severe('API failure during SRP login', e, s);
         _logger.severe('API failure during SRP login', e, s);
-        if (e.type == DioErrorType.other) {
+        if (e.type == DioExceptionType.unknown) {
           await _showContactSupportDialog(
           await _showContactSupportDialog(
             context,
             context,
             context.l10n.noInternetConnection,
             context.l10n.noInternetConnection,
@@ -229,6 +230,9 @@ class _LoginPasswordVerificationPageState
                 Padding(
                 Padding(
                   padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
                   padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
                   child: TextFormField(
                   child: TextFormField(
+                    onFieldSubmitted: _passwordController.text.isNotEmpty
+                        ? (_) => onPressed()
+                        : null,
                     key: const ValueKey("passwordInputField"),
                     key: const ValueKey("passwordInputField"),
                     autofillHints: const [AutofillHints.password],
                     autofillHints: const [AutofillHints.password],
                     decoration: InputDecoration(
                     decoration: InputDecoration(

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

@@ -17,8 +17,8 @@ class OTTVerificationPage extends StatefulWidget {
     this.isChangeEmail = false,
     this.isChangeEmail = false,
     this.isCreateAccountScreen = false,
     this.isCreateAccountScreen = false,
     this.isResetPasswordScreen = false,
     this.isResetPasswordScreen = false,
-    Key? key,
-  }) : super(key: key);
+    super.key,
+  });
 
 
   @override
   @override
   State<OTTVerificationPage> createState() => _OTTVerificationPageState();
   State<OTTVerificationPage> createState() => _OTTVerificationPageState();
@@ -27,6 +27,23 @@ class OTTVerificationPage extends StatefulWidget {
 class _OTTVerificationPageState extends State<OTTVerificationPage> {
 class _OTTVerificationPageState extends State<OTTVerificationPage> {
   final _verificationCodeController = TextEditingController();
   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
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final l10n = context.l10n;
     final l10n = context.l10n;
@@ -68,22 +85,9 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
       body: _getBody(),
       body: _getBody(),
       floatingActionButton: DynamicFAB(
       floatingActionButton: DynamicFAB(
         isKeypadOpen: isKeypadOpen,
         isKeypadOpen: isKeypadOpen,
-        isFormValid: !(_verificationCodeController.text.isEmpty),
+        isFormValid: _verificationCodeController.text.isNotEmpty,
         buttonText: l10n.verify,
         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(),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -160,6 +164,9 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
               padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
               padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
               child: TextFormField(
               child: TextFormField(
                 style: Theme.of(context).textTheme.titleMedium,
                 style: Theme.of(context).textTheme.titleMedium,
+                onFieldSubmitted: _verificationCodeController.text.isNotEmpty
+                    ? (_) => onPressed()
+                    : null,
                 decoration: InputDecoration(
                 decoration: InputDecoration(
                   filled: true,
                   filled: true,
                   hintText: l10n.tapToEnterCode,
                   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/services/user_service.dart';
 import 'package:ente_auth/ui/account/recovery_key_page.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/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/components/models/button_type.dart';
 import 'package:ente_auth/ui/home_page.dart';
 import 'package:ente_auth/ui/home_page.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/navigation_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:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
@@ -25,7 +25,7 @@ enum PasswordEntryMode {
 class PasswordEntryPage extends StatefulWidget {
 class PasswordEntryPage extends StatefulWidget {
   final PasswordEntryMode mode;
   final PasswordEntryMode mode;
 
 
-  const PasswordEntryPage({required this.mode, Key? key}) : super(key: key);
+  const PasswordEntryPage({required this.mode, super.key});
 
 
   @override
   @override
   State<PasswordEntryPage> createState() => _PasswordEntryPageState();
   State<PasswordEntryPage> createState() => _PasswordEntryPageState();
@@ -149,227 +149,239 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
       children: [
       children: [
         Expanded(
         Expanded(
           child: AutofillGroup(
           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 ==
                           _passwordsMatch = _passwordInInputBox ==
                               _passwordInInputConfirmationBox;
                               _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);
           showGenericErrorDialog(context: context);
         }
         }
       }
       }
+
       // ignore: unawaited_futures
       // ignore: unawaited_futures
       routeToPage(
       routeToPage(
         context,
         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/common/dynamic_fab.dart';
 import 'package:ente_auth/ui/components/buttons/button_widget.dart';
 import 'package:ente_auth/ui/components/buttons/button_widget.dart';
 import 'package:ente_auth/ui/home_page.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/dialog_util.dart';
 import 'package:ente_auth/utils/email_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:flutter/material.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 
 
 class PasswordReentryPage extends StatefulWidget {
 class PasswordReentryPage extends StatefulWidget {
-  const PasswordReentryPage({Key? key}) : super(key: key);
+  const PasswordReentryPage({super.key});
 
 
   @override
   @override
   State<PasswordReentryPage> createState() => _PasswordReentryPageState();
   State<PasswordReentryPage> createState() => _PasswordReentryPageState();
@@ -261,8 +261,8 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                 ),
                 ),
                 Padding(
                 Padding(
                   padding: const EdgeInsets.symmetric(horizontal: 20),
                   padding: const EdgeInsets.symmetric(horizontal: 20),
-                  child: Wrap(
-                    alignment: WrapAlignment.spaceBetween,
+                  child: Row(
+                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
                     children: [
                     children: [
                       GestureDetector(
                       GestureDetector(
                         behavior: HitTestBehavior.opaque,
                         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(
                       GestureDetector(
@@ -297,13 +301,17 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
                           Navigator.of(context)
                           Navigator.of(context)
                               .popUntil((route) => route.isFirst);
                               .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 'dart:io' as io;
 
 
 import 'package:bip39/bip39.dart' as bip39;
 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/ente_theme_data.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/l10n/l10n.dart';
 import 'package:ente_auth/ui/common/gradient_button.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:ente_auth/utils/toast_util.dart';
+import 'package:file_saver/file_saver.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter/services.dart';
 import 'package:share_plus/share_plus.dart';
 import 'package:share_plus/share_plus.dart';
@@ -27,7 +31,7 @@ class RecoveryKeyPage extends StatefulWidget {
   const RecoveryKeyPage(
   const RecoveryKeyPage(
     this.recoveryKey,
     this.recoveryKey,
     this.doneText, {
     this.doneText, {
-    Key? key,
+    super.key,
     this.showAppBar,
     this.showAppBar,
     this.onDone,
     this.onDone,
     this.isDismissible,
     this.isDismissible,
@@ -35,7 +39,7 @@ class RecoveryKeyPage extends StatefulWidget {
     this.text,
     this.text,
     this.subText,
     this.subText,
     this.showProgressBar = false,
     this.showProgressBar = false,
-  }) : super(key: key);
+  });
 
 
   @override
   @override
   State<RecoveryKeyPage> createState() => _RecoveryKeyPageState();
   State<RecoveryKeyPage> createState() => _RecoveryKeyPageState();
@@ -44,7 +48,7 @@ class RecoveryKeyPage extends StatefulWidget {
 class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
 class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
   bool _hasTriedToSave = false;
   bool _hasTriedToSave = false;
   final _recoveryKeyFile = io.File(
   final _recoveryKeyFile = io.File(
-    Configuration.instance.getTempDirectory() + "ente-recovery-key.txt",
+    "${Configuration.instance.getTempDirectory()}ente-recovery-key.txt",
   );
   );
 
 
   @override
   @override
@@ -61,6 +65,21 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
             ? 32
             ? 32
             : 120;
             : 120;
 
 
+    Future<void> copy() async {
+      await Clipboard.setData(
+        ClipboardData(
+          text: recoveryKey,
+        ),
+      );
+      showShortToast(
+        context,
+        context.l10n.recoveryKeyCopiedToClipboard,
+      );
+      setState(() {
+        _hasTriedToSave = true;
+      });
+    }
+
     return Scaffold(
     return Scaffold(
       appBar: widget.showProgressBar
       appBar: widget.showProgressBar
           ? AppBar(
           ? AppBar(
@@ -113,62 +132,73 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
                         style: Theme.of(context).textTheme.titleMedium,
                         style: Theme.of(context).textTheme.titleMedium,
                       ),
                       ),
                       const Padding(padding: EdgeInsets.only(top: 24)),
                       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 = [];
     final List<Widget> childrens = [];
     if (!_hasTriedToSave) {
     if (!_hasTriedToSave) {
       childrens.add(
       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));
       childrens.add(const SizedBox(height: 10));
@@ -221,19 +254,32 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
     childrens.add(
     childrens.add(
       GradientButton(
       GradientButton(
         onTap: () async {
         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,
         text: context.l10n.saveKey,
       ),
       ),
     );
     );
+
     if (_hasTriedToSave) {
     if (_hasTriedToSave) {
       childrens.add(const SizedBox(height: 10));
       childrens.add(const SizedBox(height: 10));
       childrens.add(
       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;
     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 {
   Future _shareRecoveryKey(String recoveryKey) async {
     if (_recoveryKeyFile.existsSync()) {
     if (_recoveryKeyFile.existsSync()) {
       await _recoveryKeyFile.delete();
       await _recoveryKeyFile.delete();
     }
     }
     _recoveryKeyFile.writeAsStringSync(recoveryKey);
     _recoveryKeyFile.writeAsStringSync(recoveryKey);
+    // ignore: deprecated_member_use
     await Share.shareFiles([_recoveryKeyFile.path]);
     await Share.shareFiles([_recoveryKeyFile.path]);
     Future.delayed(const Duration(milliseconds: 500), () {
     Future.delayed(const Duration(milliseconds: 500), () {
       if (mounted) {
       if (mounted) {
@@ -264,3 +333,24 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
     widget.onDone!();
     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/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/account/password_entry_page.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
 import 'package:ente_auth/ui/common/dynamic_fab.dart';
 import 'package:ente_auth/utils/dialog_util.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';
 import 'package:flutter/material.dart';
 
 
 class RecoveryPage extends StatefulWidget {
 class RecoveryPage extends StatefulWidget {
-  const RecoveryPage({Key? key}) : super(key: key);
+  const RecoveryPage({super.key});
 
 
   @override
   @override
   State<RecoveryPage> createState() => _RecoveryPageState();
   State<RecoveryPage> createState() => _RecoveryPageState();
@@ -19,6 +16,36 @@ class RecoveryPage extends StatefulWidget {
 class _RecoveryPageState extends State<RecoveryPage> {
 class _RecoveryPageState extends State<RecoveryPage> {
   final _recoveryKey = TextEditingController();
   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
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
     final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
@@ -46,37 +73,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
         isKeypadOpen: isKeypadOpen,
         isKeypadOpen: isKeypadOpen,
         isFormValid: _recoveryKey.text.isNotEmpty,
         isFormValid: _recoveryKey.text.isNotEmpty,
         buttonText: 'Recover',
         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(),
       floatingActionButtonLocation: fabLocation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
       floatingActionButtonAnimator: NoScalingAnimation(),
@@ -89,7 +86,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
                   padding:
                   padding:
                       const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
                       const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
                   child: Text(
                   child: Text(
-                    'Forgot password',
+                    context.l10n.forgotPassword,
                     style: Theme.of(context).textTheme.headlineMedium,
                     style: Theme.of(context).textTheme.headlineMedium,
                   ),
                   ),
                 ),
                 ),
@@ -140,12 +137,14 @@ class _RecoveryPageState extends State<RecoveryPage> {
                         padding: const EdgeInsets.symmetric(horizontal: 20),
                         padding: const EdgeInsets.symmetric(horizontal: 20),
                         child: Center(
                         child: Center(
                           child: Text(
                           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/l10n/l10n.dart";
 import "package:ente_auth/theme/ente_theme.dart";
 import "package:ente_auth/theme/ente_theme.dart";
 import 'package:ente_auth/ui/common/dynamic_fab.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_auth/utils/dialog_util.dart";
+import 'package:ente_crypto_dart/ente_crypto_dart.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
-import "package:flutter_sodium/flutter_sodium.dart";
 import "package:logging/logging.dart";
 import "package:logging/logging.dart";
 
 
 typedef OnPasswordVerifiedFn = Future<void> Function(Uint8List bytes);
 typedef OnPasswordVerifiedFn = Future<void> Function(Uint8List bytes);
@@ -17,8 +16,11 @@ class RequestPasswordVerificationPage extends StatefulWidget {
   final OnPasswordVerifiedFn onPasswordVerified;
   final OnPasswordVerifiedFn onPasswordVerified;
   final Function? onPasswordError;
   final Function? onPasswordError;
 
 
-  const RequestPasswordVerificationPage(
-      {super.key, required this.onPasswordVerified, this.onPasswordError,});
+  const RequestPasswordVerificationPage({
+    super.key,
+    required this.onPasswordVerified,
+    this.onPasswordError,
+  });
 
 
   @override
   @override
   State<RequestPasswordVerificationPage> createState() =>
   State<RequestPasswordVerificationPage> createState() =>
@@ -82,15 +84,15 @@ class _RequestPasswordVerificationPageState
           try {
           try {
             final attributes = Configuration.instance.getKeyAttributes()!;
             final attributes = Configuration.instance.getKeyAttributes()!;
             final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey(
             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.memLimit,
               attributes.opsLimit,
               attributes.opsLimit,
             );
             );
             CryptoUtil.decryptSync(
             CryptoUtil.decryptSync(
-              Sodium.base642bin(attributes.encryptedKey),
+              CryptoUtil.base642bin(attributes.encryptedKey),
               keyEncryptionKey,
               keyEncryptionKey,
-              Sodium.base642bin(attributes.keyDecryptionNonce),
+              CryptoUtil.base642bin(attributes.keyDecryptionNonce),
             );
             );
             await dialog.show();
             await dialog.show();
             // pop
             // pop

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

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

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

@@ -6,12 +6,12 @@ class CodeTimerProgress extends StatefulWidget {
   final int period;
   final int period;
 
 
   CodeTimerProgress({
   CodeTimerProgress({
-    Key? key,
+    super.key,
     required this.period,
     required this.period,
-  }) : super(key: key);
+  });
 
 
   @override
   @override
-  _CodeTimerProgressState createState() => _CodeTimerProgressState();
+  State createState() => _CodeTimerProgressState();
 }
 }
 
 
 class _CodeTimerProgressState extends State<CodeTimerProgress>
 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/code_timer_progress.dart';
 import 'package:ente_auth/ui/utils/icon_utils.dart';
 import 'package:ente_auth/ui/utils/icon_utils.dart';
 import 'package:ente_auth/utils/dialog_util.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/toast_util.dart';
 import 'package:ente_auth/utils/totp_util.dart';
 import 'package:ente_auth/utils/totp_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_context_menu/flutter_context_menu.dart';
 import 'package:flutter_slidable/flutter_slidable.dart';
 import 'package:flutter_slidable/flutter_slidable.dart';
 import 'package:logging/logging.dart';
 import 'package:logging/logging.dart';
 import 'package:move_to_background/move_to_background.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 {
 class CodeWidget extends StatefulWidget {
   final Code code;
   final Code code;
 
 
-  const CodeWidget(this.code, {Key? key}) : super(key: key);
+  const CodeWidget(this.code, {super.key});
 
 
   @override
   @override
   State<CodeWidget> createState() => _CodeWidgetState();
   State<CodeWidget> createState() => _CodeWidgetState();
@@ -84,83 +86,121 @@ class _CodeWidgetState extends State<CodeWidget> {
     final l10n = context.l10n;
     final l10n = context.l10n;
     return Container(
     return Container(
       margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
       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 {
   Future<void> _onEditPressed(_) async {
-    bool _isAuthSuccessful = await LocalAuthenticationService.instance
+    bool isAuthSuccessful = await LocalAuthenticationService.instance
         .requestLocalAuthentication(context, context.l10n.editCodeAuthMessage);
         .requestLocalAuthentication(context, context.l10n.editCodeAuthMessage);
-    if (!_isAuthSuccessful) {
+    await PlatformUtil.refocusWindows();
+    if (!isAuthSuccessful) {
       return;
       return;
     }
     }
     final Code? code = await Navigator.of(context).push(
     final Code? code = await Navigator.of(context).push(
@@ -391,9 +432,10 @@ class _CodeWidgetState extends State<CodeWidget> {
   }
   }
 
 
   Future<void> _onShowQrPressed(_) async {
   Future<void> _onShowQrPressed(_) async {
-    bool _isAuthSuccessful = await LocalAuthenticationService.instance
+    bool isAuthSuccessful = await LocalAuthenticationService.instance
         .requestLocalAuthentication(context, context.l10n.showQRAuthMessage);
         .requestLocalAuthentication(context, context.l10n.showQRAuthMessage);
-    if (!_isAuthSuccessful) {
+    await PlatformUtil.refocusWindows();
+    if (!isAuthSuccessful) {
       return;
       return;
     }
     }
     // ignore: unused_local_variable
     // ignore: unused_local_variable
@@ -407,14 +449,15 @@ class _CodeWidgetState extends State<CodeWidget> {
   }
   }
 
 
   void _onDeletePressed(_) async {
   void _onDeletePressed(_) async {
-    bool _isAuthSuccessful =
+    bool isAuthSuccessful =
         await LocalAuthenticationService.instance.requestLocalAuthentication(
         await LocalAuthenticationService.instance.requestLocalAuthentication(
       context,
       context,
       context.l10n.deleteCodeAuthMessage,
       context.l10n.deleteCodeAuthMessage,
     );
     );
-    if (!_isAuthSuccessful) {
+    if (!isAuthSuccessful) {
       return;
       return;
     }
     }
+    FocusScope.of(context).requestFocus();
     final l10n = context.l10n;
     final l10n = context.l10n;
     await showChoiceActionSheet(
     await showChoiceActionSheet(
       context,
       context,
@@ -451,7 +494,7 @@ class _CodeWidgetState extends State<CodeWidget> {
       code = code.replaceAll(RegExp(r'\d'), '•');
       code = code.replaceAll(RegExp(r'\d'), '•');
     }
     }
     if (code.length == 6) {
     if (code.length == 6) {
-      return code.substring(0, 3) + " " + code.substring(3, 6);
+      return "${code.substring(0, 3)} ${code.substring(3, 6)}";
     }
     }
     return code;
     return code;
   }
   }

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

@@ -5,8 +5,7 @@ import 'package:flutter/material.dart';
 class BottomShadowWidget extends StatelessWidget {
 class BottomShadowWidget extends StatelessWidget {
   final double offsetDy;
   final double offsetDy;
   final Color? shadowColor;
   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
   @override
   Widget build(BuildContext context) {
   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';
 import 'package:flutter/material.dart';
 
 
 class DividerWithPadding extends StatelessWidget {
 class DividerWithPadding extends StatelessWidget {
   final double left, top, right, bottom, thinckness;
   final double left, top, right, bottom, thinckness;
   const DividerWithPadding({
   const DividerWithPadding({
-    Key? key,
+    super.key,
     this.left = 0,
     this.left = 0,
     this.top = 0,
     this.top = 0,
     this.right = 0,
     this.right = 0,
     this.bottom = 0,
     this.bottom = 0,
     this.thinckness = 0.5,
     this.thinckness = 0.5,
-  }) : super(key: key);
+  });
 
 
   @override
   @override
   Widget build(BuildContext context) {
   Widget build(BuildContext context) {

Some files were not shown because too many files changed in this diff