Merge remote-tracking branch 'origin/mobile_face' into mobile_face

This commit is contained in:
laurenspriem 2024-04-03 17:02:39 +05:30
commit b21466bf13
781 changed files with 35143 additions and 20132 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

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

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

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

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

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

6
auth/.gitignore vendored
View file

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

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

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

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,6 +408,7 @@
"hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)",
"hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!",
"waitingForBrowserRequest": "Warten auf Browseranfrage...",
"waitingForVerification": "Warte auf Bestätigung...",
"passkey": "Passkey",
"developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?",
"developerSettings": "Entwicklereinstellungen",

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

@ -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,6 +408,7 @@
"hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)",
"hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!",
"waitingForBrowserRequest": "Aguardando solicitação do navegador...",
"waitingForVerification": "Esperando por verificação...",
"passkey": "Chave de acesso",
"developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?",
"developerSettings": "Configurações de desenvolvedor",

View file

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

View file

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

View file

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

View file

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

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