Merge branch 'main' into f-droid

This commit is contained in:
ashilkn 2024-04-02 17:22:34 +05:30
commit 07808d6139
805 changed files with 35573 additions and 20193 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

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

View file

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

View file

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

View file

@ -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 }}

View file

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

View file

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

View file

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

View file

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

38
.github/workflows/server-publish.yml vendored Normal file
View file

@ -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 }}

View file

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

View file

@ -2,15 +2,21 @@ name: "Sync Crowdin translations (web)"
on:
push:
branches: [main]
paths:
# Run workflow when web's en-US/translation.json is changed
- "web/apps/photos/public/locales/en-US/translation.json"
# Or the workflow itself is changed
- ".github/workflows/web-crowdin.yml"
branches: [main]
schedule:
# See: [Note: Run every 24 hours]
- cron: "20 1 * * *"
# [Note: Run workflow on specific days of the week]
#
# The last (5th) component of the cron syntax denotes the day of the
# week, with 0 == SUN and 6 == SAT. So, for example, to run on every TUE
# and FRI, this can be set to `2,5`.
#
# See also: [Note: Run workflow every 24 hours]
- cron: "20 1 * * 2,5"
# Also allow manually running the workflow
workflow_dispatch:
@ -28,7 +34,7 @@ jobs:
base_path: "web/"
config: "web/crowdin.yml"
upload_sources: true
upload_translations: true
upload_translations: false
download_translations: true
localization_branch_name: crowdin-translations-web
create_pull_request: true

View file

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

View file

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

View file

@ -2,7 +2,7 @@ name: "Nightly (web)"
on:
schedule:
# [Note: Run every 24 hours]
# [Note: Run workflow every 24 hours]
#
# Run every 24 hours - First field is minute, second is hour of the day
# This runs 23:15 UTC everyday - 1 and 15 are just arbitrary offset to
@ -78,6 +78,19 @@ jobs:
directory: web/apps/cast/out
wranglerVersion: "3"
- name: Build payments
run: yarn build:payments
- name: Publish payments
uses: cloudflare/pages-action@1
with:
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
projectName: ente
branch: n-payments
directory: web/apps/payments/out
wranglerVersion: "3"
- name: Build photos
run: yarn build:photos
env:

View file

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

View file

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

9
auth/.gitignore vendored
View file

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

View file

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

View file

@ -31,14 +31,16 @@ You can alternatively install the build from PlayStore or F-Droid.
<img height="59" src="../.github/assets/app-store-badge.svg">
</a>
### Desktop
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.
### Web
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.
### Desktop
A native desktop app is coming soon!
or managing your secrets, please use our mobile or desktop app.
## 🧑‍💻 Build from source

View file

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

View file

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

View file

@ -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>

After

Width:  |  Height:  |  Size: 4.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -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>

After

Width:  |  Height:  |  Size: 1.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 57 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="6.525 7.459 339.266 319.582"><path fill="orange" d="M71.598 11.25H280.72c33.844 0 61.282 25.782 61.282 57.586v196.512c0 31.804-27.437 57.586-61.282 57.586H71.598c-33.845 0-61.28-25.782-61.28-57.586V68.836c0-31.804 27.435-57.586 61.28-57.586z"/><path d="M280.719 326.725H71.598c-35.881 0-65.072-27.533-65.072-61.377V68.836c0-33.844 29.19-61.377 65.072-61.377H280.72c35.88 0 65.072 27.533 65.072 61.377v196.512c0 33.844-29.192 61.377-65.073 61.377zM71.598 15.042c-31.7 0-57.49 24.131-57.49 53.794v196.512c0 29.662 25.79 53.794 57.49 53.794H280.72c31.7 0 57.49-24.132 57.49-53.794V68.836c0-29.662-25.79-53.794-57.49-53.794H71.598z"/><path d="M127.423 64.013l62.975.149c13.161-.099 22.989 2.002 29.48 6.303 7.928 5.272 11.89 14.343 11.89 27.213 0 21.19-9.828 33.195-29.482 36.012v.297c8.667 2.159 13.048 9.335 13.146 21.528 0 6.245-.233 14.245-.7 24 0 6.542 1.384 12.202 4.156 16.98H184.38c-1.611-1.747-2.416-5.305-2.416-10.677.66-8.98.99-16.347.99-22.098 0-11.617-5.582-17.425-16.746-17.425h-22.329l-9.738 46.987h-33.613l26.895-129.269zm53.236 26.941h-24.744l-6.453 31.192h26.775c14.967 0 22.497-5.906 22.595-17.721.001-8.98-6.058-13.47-18.173-13.47z"/><path d="M223.456 196.346l24.915-43.18 6.717 43.478h42.506l-38.349 27.534 6.14 43.18-33.204-26.05-44.633 27.089 20.878-45.973-24.333-26.2 39.363.122zm113.568 113.887c1.38 0 2.726.362 4.04 1.086 1.315.723 2.339 1.76 3.07 3.108.735 1.348 1.101 2.753 1.101 4.216 0 1.449-.36 2.841-1.084 4.177a7.735 7.735 0 01-3.039 3.114c-1.302.74-2.665 1.108-4.09 1.108-1.421 0-2.784-.37-4.089-1.108a7.762 7.762 0 01-3.044-3.114c-.725-1.336-1.089-2.73-1.089-4.177 0-1.463.369-2.868 1.106-4.216.738-1.348 1.763-2.384 3.077-3.108 1.316-.725 2.662-1.086 4.04-1.086zm0 1.391c-1.154 0-2.278.303-3.37.909a6.451 6.451 0 00-2.566 2.594c-.617 1.126-.926 2.297-.926 3.514 0 1.211.304 2.372.91 3.482a6.542 6.542 0 002.542 2.596c1.089.619 2.224.93 3.409.93 1.183 0 2.32-.311 3.41-.93a6.498 6.498 0 002.536-2.596c.603-1.11.904-2.27.904-3.482 0-1.218-.308-2.388-.92-3.514a6.39 6.39 0 00-2.565-2.594c-1.094-.606-2.216-.909-3.364-.909zm-3.604 11.664v-9.045h3.038c1.039 0 1.79.083 2.254.25.466.167.835.459 1.11.875.277.416.415.857.415 1.325 0 .661-.23 1.237-.692 1.726-.46.49-1.072.764-1.835.824.312.135.563.294.75.48.358.356.792.954 1.308 1.792l1.078 1.772h-1.742l-.783-1.426c-.617-1.12-1.115-1.823-1.492-2.105-.262-.209-.643-.313-1.145-.313h-.838v3.844h-1.426zm1.426-5.092h1.732c.829 0 1.393-.126 1.694-.38.3-.25.451-.586.451-1.002a1.24 1.24 0 00-.218-.718 1.302 1.302 0 00-.604-.473c-.258-.103-.734-.157-1.431-.157h-1.624v2.73z"/><path fill="#FFF" d="M252.503 221.088l25.47-18.142h-28.177l-4.881-31.464-17.882 31.168h-28.467l17.302 18.587-14.305 31.193 31.052-18.735 24.48 19.18-4.592-31.787z"/></svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -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>

After

Width:  |  Height:  |  Size: 523 B

View file

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View file

@ -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

View file

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

View file

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

View file

@ -1 +1 @@
ente Authenticator
Ente Authenticator

View file

@ -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 @@
Subproject commit 41456452f29d64e8deb623a3c927524bcf9f111b
Subproject commit ba393198430278b6595976de84fe170f553cc728

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
),
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
{}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
}

View file

@ -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,
});
}

View file

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:ente_auth/app/view/app.dart';
import 'package:ente_auth/core/configuration.dart';
@ -28,7 +29,7 @@ import "package:flutter/material.dart";
import 'package:local_auth/local_auth.dart';
class OnboardingPage extends StatefulWidget {
const OnboardingPage({Key? key}) : super(key: key);
const OnboardingPage({super.key});
@override
State<OnboardingPage> createState() => _OnboardingPageState();
@ -86,118 +87,128 @@ class _OnboardingPageState extends State<OnboardingPage> {
}
}
},
child: Center(
child: SingleChildScrollView(
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40),
child: Column(
children: [
Column(
children: [
kDebugMode
? GestureDetector(
child: const Align(
alignment: Alignment.topRight,
child: Text("Lang"),
),
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,
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"),
),
),
],
),
const SizedBox(height: 100),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: GradientButton(
onTap: _navigateToSignUpPage,
text: l10n.newUser,
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,
),
),
],
),
),
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: 100),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: GradientButton(
onTap: _navigateToSignUpPage,
text: l10n.newUser,
),
),
const SizedBox(height: 16),
Container(
height: 56,
width: double.infinity,
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: Hero(
tag: "log_in",
child: ElevatedButton(
style: Theme.of(context)
.colorScheme
.optionalActionButtonStyle,
onPressed: _navigateToSignInPage,
child: Text(
l10n.existingUser,
style: const TextStyle(
color: Colors.black, // same for both themes
),
),
),
),
),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: GestureDetector(
onTap: _optForOfflineMode,
child: Center(
child: Text(
l10n.useOffline,
style: body.copyWith(
color:
Theme.of(context).colorScheme.mutedTextColor,
const SizedBox(height: 4),
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 20, bottom: 20),
child: GestureDetector(
onTap: _optForOfflineMode,
child: Center(
child: Text(
l10n.useOffline,
style: body.copyWith(
color: Theme.of(context)
.colorScheme
.mutedTextColor,
),
),
),
),
),
),
const DeveloperSettingsWidget(),
],
const DeveloperSettingsWidget(),
],
),
),
),
),
@ -208,7 +219,9 @@ class _OnboardingPageState extends State<OnboardingPage> {
}
Future<void> _optForOfflineMode() async {
bool canCheckBio = await LocalAuthentication().canCheckBiometrics;
bool canCheckBio = Platform.isMacOS || Platform.isLinux
? true
: await LocalAuthentication().canCheckBiometrics;
if (!canCheckBio) {
showToast(
context,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,11 +4,11 @@ import 'package:ente_auth/models/key_gen_result.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/account/recovery_key_page.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/ui/common/web_page.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/home_page.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -25,7 +25,7 @@ enum PasswordEntryMode {
class PasswordEntryPage extends StatefulWidget {
final PasswordEntryMode mode;
const PasswordEntryPage({required this.mode, Key? key}) : super(key: key);
const PasswordEntryPage({required this.mode, super.key});
@override
State<PasswordEntryPage> createState() => _PasswordEntryPageState();
@ -149,227 +149,239 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
children: [
Expanded(
child: AutofillGroup(
child: ListView(
children: [
Padding(
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
buttonTextAndHeading,
style: Theme.of(context).textTheme.headlineMedium,
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,
),
),
),
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),
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(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,
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,
),
),
},
),
),
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,
),
),
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,
),
},
),
),
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,
),
),
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,
),
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),
),
),
focusNode: _password2FocusNode,
onChanged: (cnfPassword) {
setState(() {
_passwordInInputConfirmationBox = cnfPassword;
if (_passwordInInputBox != '') {
obscureText: !_password1Visible,
controller: _passwordController1,
autofocus: false,
autocorrect: false,
keyboardType: TextInputType.visiblePassword,
onChanged: (password) {
setState(() {
_passwordInInputBox = password;
_passwordStrength =
estimatePasswordStrength(password);
_isPasswordValid = _passwordStrength >=
kMildPasswordStrengthThreshold;
_passwordsMatch = _passwordInInputBox ==
_passwordInInputConfirmationBox;
}
});
},
),
),
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,
),
});
},
textInputAction: TextInputAction.next,
focusNode: _password1FocusNode,
),
),
),
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",
);
},
),
);
},
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 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;
}
});
},
),
),
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 Padding(padding: EdgeInsets.all(20)),
],
const SizedBox(height: 8),
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {
PlatformUtil.openWebView(
context,
context.l10n.howItWorks,
"https://ente.io/architecture",
);
},
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: RichText(
text: TextSpan(
text: context.l10n.howItWorks,
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,
),
),
),
),
),
const Padding(padding: EdgeInsets.all(20)),
],
),
),
),
),
@ -463,6 +475,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
showGenericErrorDialog(context: context);
}
}
// ignore: unawaited_futures
routeToPage(
context,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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