Compare commits

..

1 Commits

Author SHA1 Message Date
Neeraj
5f1f1a5871 Update internal_changes.txt 2025-08-15 07:46:10 +05:30
1034 changed files with 73528 additions and 103519 deletions

View File

@@ -8,7 +8,7 @@ on:
env:
FLUTTER_VERSION: "3.32.8"
RUST_VERSION: "1.86.0"
RUST_VERSION: "1.85.1"
permissions:
contents: read
@@ -47,9 +47,6 @@ jobs:
- name: Install Flutter Rust Bridge
run: cargo install flutter_rust_bridge_codegen
- name: Generate Rust bindings
run: flutter_rust_bridge_codegen generate
- name: Increment version code for build
run: |
CURRENT_VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //')

View File

@@ -9,7 +9,6 @@ on:
env:
FLUTTER_VERSION: "3.32.8"
RUST_VERSION: "1.86.0"
permissions:
contents: read
@@ -32,18 +31,7 @@ jobs:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- run: flutter pub get
- name: Install Rust ${{ env.RUST_VERSION }}
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.RUST_VERSION }}
- name: Install Flutter Rust Bridge
run: cargo install flutter_rust_bridge_codegen
- name: Generate Rust bindings
run: flutter_rust_bridge_codegen generate
- run: flutter analyze --no-fatal-infos

View File

@@ -142,22 +142,6 @@ var _updateFreeUserStorage = &cobra.Command{
},
}
var _sendMail = &cobra.Command{
Use: "send-mail <to-email> <from-email> <from-name>",
Args: cobra.ExactArgs(3),
Short: "Sends a test mail via the admin api",
RunE: func(cmd *cobra.Command, args []string) error {
recoverWithLog()
var flags = &model.AdminActionForUser{}
cmd.Flags().VisitAll(func(f *pflag.Flag) {
if f.Name == "admin-user" {
flags.AdminEmail = f.Value.String()
}
})
return ctrl.SendTestMail(context.Background(), *flags, args[0], args[1], args[2])
},
}
func init() {
rootCmd.AddCommand(_adminCmd)
_ = _userDetailsCmd.MarkFlagRequired("admin-user")
@@ -175,6 +159,5 @@ func init() {
_updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)")
// add a flag with no value --no-limit
_updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years")
_sendMail.Flags().StringP("admin-user", "a", "", "The email of the admin user. ")
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser, _sendMail)
_adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _disablePasskeyCmd, _updateFreeUserStorage, _listUsers, _deleteUser)
}

View File

@@ -2,11 +2,10 @@ package cmd
import (
"fmt"
"os"
"runtime"
"github.com/ente-io/cli/pkg"
"github.com/spf13/cobra/doc"
"os"
"runtime"
"github.com/spf13/viper"
@@ -21,6 +20,11 @@ var ctrl *pkg.ClICtrl
var rootCmd = &cobra.Command{
Use: "ente",
Short: "CLI tool for exporting your photos from ente.io",
// Uncomment the following line if your bare application
// has an action associated with it:
Run: func(cmd *cobra.Command, args []string) {
fmt.Sprintf("Hello World")
},
}
func GenerateDocs() error {

View File

@@ -139,28 +139,5 @@ func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.User
}
}
return nil
}
func (c *Client) SendTestMail(ctx context.Context, toEmail, fromEmail, fromName string) error {
body := map[string]interface{}{
"to": []string{toEmail},
"fromName": fromName,
"fromEmail": fromEmail,
"subject": "Test mail from Ente",
"body": "This is a test mail from Ente",
}
r, err := c.restClient.R().
SetContext(ctx).
SetBody(body).
Post("/admin/mail")
if err != nil {
return err
}
if r.IsError() {
return &ApiError{
StatusCode: r.StatusCode(),
Message: r.String(),
}
}
return nil
}

View File

@@ -156,23 +156,6 @@ func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActio
return nil
}
func (c *ClICtrl) SendTestMail(ctx context.Context, params model.AdminActionForUser, to, from, fromName string) error {
accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail)
if err != nil {
return err
}
err = c.Client.SendTestMail(accountCtx, to, from, fromName)
if err != nil {
if apiErr, ok := err.(*api.ApiError); ok && apiErr.StatusCode == 400 && strings.Contains(apiErr.Message, "Token is too old") {
fmt.Printf("Error: old admin token, please re-authenticate using `ente account add` \n")
return nil
}
return err
}
fmt.Printf("Successfully sent test email to %s\n", to)
return nil
}
func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) {
accounts, err := c.GetAccounts(ctx)
if err != nil {

View File

@@ -2,8 +2,6 @@
## v1.7.15 (Unreleased)
- Custom domains.
- Support Czech translations.
- .
## v1.7.14

View File

View File

@@ -8,12 +8,6 @@ description: Guide to configuring Ente CLI for Self Hosted Instance
If you are self-hosting, you can configure Ente CLI to export data & perform
basic administrative actions.
::: tip Installing Ente CLI
For instructions on installing the Ente CLI, see the [README available on Github](https://github.com/ente-io/ente/tree/main/cli/README.md).
:::
## Step 1: Configure endpoint
To do this, first configure the CLI to use your server's endpoint.

View File

@@ -63,20 +63,11 @@ It has no relation to Backblaze, Wasabi or Scaleway.
Each bucket's endpoint, region, key and secret should be configured accordingly
if using an external bucket.
If a bucket has SSL support enabled, set `s3.are_local_buckets` to `false`. Enable path-style URL by setting `s3.use_path_style_urls` to `true`.
::: note
You can configure this for individual buckets over defining top-level configuration if you are using the latest server image (August 2025)
:::
A sample configuration for `b2-eu-cen` is provided, which can be used for other 2 buckets as well:
A sample configuration for `b2-eu-cen` is provided, which can be used for other
2 buckets as well:
```yaml
b2-eu-cen:
are_local_buckets: true
use_path_style_urls: true
key: <key>
secret: <secret>
endpoint: localhost:3200

View File

@@ -22,7 +22,8 @@ can achieve this the following steps:
# Change the DB name and DB user name if you use different
# values.
# If using Docker
docker exec -it <postgres-ente-container-name> sh
docker exec -it <postgres-ente-container-name>
psql -U pguser -d ente_db
# Or when using psql directly

View File

@@ -96,8 +96,8 @@ provide correct credentials for proper connectivity within Museum.
The `s3` section within `museum.yaml` is by default configured to use local
MinIO buckets when using `quickstart.sh` or Docker Compose.
If you wish to use an external S3 provider with SSL, you can edit the configuration with
your provider's credentials, and set `s3.are_local_buckets` to `false`. Additionally, you can configure this for specific buckets in the corresponding bucket sections in the Compose file.
If you wish to use an external S3 provider, you can edit the configuration with
your provider's credentials, and set `s3.are_local_buckets` to `false`.
If you are using default MinIO, it is accessible at port `3200`. Web Console can
be accessed by enabling port `3201` in the Compose file.
@@ -111,11 +111,11 @@ and [troubleshooting](/self-hosting/troubleshooting/uploads) sections.
| Variable | Description | Default |
| -------------------------------------- | -------------------------------------------- | ------- |
| `s3.b2-eu-cen` | Primary hot storage bucket configuration | |
| `s3.b2-eu-cen` | Primary hot storage S3 config | |
| `s3.wasabi-eu-central-2-v3.compliance` | Whether to disable compliance lock on delete | `true` |
| `s3.scw-eu-fr-v3` | Cold storage bucket configuration | |
| `s3.wasabi-eu-central-2-v3` | Secondary hot storage configuration | |
| `s3.are_local_buckets` | | `true` |
| `s3.scw-eu-fr-v3` | Optional secondary S3 config | |
| `s3.wasabi-eu-central-2-derived` | Derived data storage | |
| `s3.are_local_buckets` | Use local MinIO-compatible storage | `false` |
| `s3.use_path_style_urls` | Enable path-style URLs for MinIO | `false` |
### Encryption Keys
@@ -171,8 +171,6 @@ smtp:
email:
# Optional name for sender
sender-name:
# Optional encryption
encryption:
```
| Variable | Description | Default |
@@ -183,7 +181,6 @@ smtp:
| `smtp.password` | SMTP auth password | |
| `smtp.email` | Sender email address | |
| `smtp.sender-name` | Custom name for email sender | |
| `smtp.encryption` | Encryption method (tls, ssl) | |
| `transmail.key` | Zeptomail API key | |
### WebAuthn Passkey Support

View File

@@ -46,7 +46,7 @@ If running Museum without Docker, the code should be visible in the terminal
# Change the DB name and DB user name if you use different
# values.
# If using Docker docker exec -it <postgres-ente-container-name> sh
# If using Docker docker exec -it <postgres-ente-container-name>
psql -U pguser -d ente_db
# Or when using psql directly

45
mobile/.gitignore vendored
View File

@@ -1,45 +0,0 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
melos_*.iml
# 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/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

View File

@@ -1,72 +0,0 @@
# For more linters, we can check https://dart-lang.github.io/linter/lints/index.html
# or https://pub.dev/packages/lint (Effective dart)
# use "flutter analyze ." or "dart analyze ." for running lint checks
include: package:flutter_lints/flutter.yaml
linter:
rules:
# Ref https://github.com/flutter/packages/blob/master/packages/flutter_lints/lib/flutter.yaml
# Ref https://dart-lang.github.io/linter/lints/
- avoid_print
- avoid_unnecessary_containers
- avoid_web_libraries_in_flutter
- no_logic_in_create_state
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_final_locals
- require_trailing_commas
- sized_box_for_whitespace
- use_full_hex_values_for_flutter_colors
- use_key_in_widget_constructors
- cancel_subscriptions
- avoid_empty_else
- exhaustive_cases
# just style suggestions
- sort_pub_dependencies
- use_rethrow_when_possible
- prefer_double_quotes
- directives_ordering
- always_use_package_imports
- sort_child_properties_last
- unawaited_futures
analyzer:
errors:
avoid_empty_else: error
exhaustive_cases: error
curly_braces_in_flow_control_structures: error
directives_ordering: error
require_trailing_commas: error
always_use_package_imports: warning
prefer_final_fields: error
unused_import: error
camel_case_types: error
prefer_is_empty: warning
use_rethrow_when_possible: info
unused_field: warning
use_key_in_widget_constructors: warning
sort_child_properties_last: warning
sort_pub_dependencies: warning
library_private_types_in_public_api: warning
constant_identifier_names: ignore
prefer_const_constructors: warning
prefer_const_declarations: warning
prefer_const_constructors_in_immutables: warning
prefer_final_locals: warning
unnecessary_const: error
cancel_subscriptions: error
unrelated_type_equality_checks: error
unnecessary_cast: info
unawaited_futures: warning # convert to warning after fixing existing issues
invalid_dependency: info
use_build_context_synchronously: ignore # experimental lint, requires many changes
prefer_interpolation_to_compose_strings: ignore # later too many warnings
prefer_double_quotes: ignore # too many warnings
avoid_renaming_method_parameters: ignore # incorrect warnings for `equals` overrides

View File

@@ -33,8 +33,6 @@
.pub/
/build/
macos/build/
.gradle/
settings.local.json
# Web related
lib/generated_plugin_registrant.dart

View File

@@ -34,9 +34,6 @@ android {
ndkVersion flutter.ndkVersion
compileOptions {
// Flag to enable support for the new language APIs
coreLibraryDesugaringEnabled true
// Sets Java compatibility to Java 8
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
@@ -59,7 +56,7 @@ android {
applicationId "io.ente.auth"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 22
minSdkVersion 21
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -118,7 +115,4 @@ flutter {
source '../..'
}
dependencies {
// For AGP 7.4+
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
dependencies {}

View File

@@ -1,6 +0,0 @@
# Please add these rules to your existing keep rules in order to suppress warnings.
# This is generated automatically by the Android Gradle plugin.
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
-dontwarn com.google.errorprone.annotations.CheckReturnValue
-dontwarn com.google.errorprone.annotations.Immutable
-dontwarn com.google.errorprone.annotations.RestrictedApi

View File

@@ -6,19 +6,6 @@ allprojects {
}
rootProject.buildDir = '../build'
subprojects {
afterEvaluate { project ->
if (project.hasProperty('android')) {
project.android {
if (namespace == null) {
namespace project.group
}
}
}
}
}
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

View File

@@ -19,8 +19,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "2.1.10" apply false
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

View File

@@ -382,11 +382,6 @@
{
"title": "CoinDCX"
},
{
"title": "CoinTracking",
"slug": "cointracking",
"altNames": ["cointracking.info", "Coin Tracking"]
},
{
"title": "colorado",
"altNames": [
@@ -740,11 +735,6 @@
{
"title": "Hivelocity"
},
{
"title": "HRDocumentBox",
"slug": "hrdocumentbox",
"altNames": ["HRDocumentBox", "HR Document Box"]
},
{
"title": "HSA Bank",
"slug": "hsa_bank",
@@ -1050,13 +1040,6 @@
"MistralAI"
]
},
{
"title": "Mobile01",
"slug": "mobile01",
"altNames": [
"M01"
]
},
{
"title": "Mozilla"
},
@@ -1790,11 +1773,6 @@
"uollet.com.br"
]
},
{
"title": "VHV",
"slug": "vhv",
"altNames": ["VHV", "VHV Versicherung"]
},
{
"title": "Vikunja"
},

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 22.0.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 1220.8 227.9" style="enable-background:new 0 0 1220.8 227.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:#0D253E;}
.st1{fill:#008AFB;}
</style>
<title>CoinTracking light</title>
<g id="Layer_2_1_">
<g id="Layer_1-2">
<path class="st0" d="M198.1,167c-30.2,0-54.7-24.5-54.7-54.7s24.5-54.7,54.7-54.7s54.7,24.5,54.7,54.7l0,0 C252.8,142.5,228.3,167,198.1,167z M198.1,81.6c-17,0-30.7,13.7-30.7,30.7s13.7,30.7,30.7,30.7s30.7-13.7,30.7-30.7l0,0 C228.8,95.4,215,81.6,198.1,81.6z"/>
<path class="st0" d="M292.2,59.5h-23.6v107.9h23.6V59.5z"/>
<path class="st0" d="M339.5,167.4h-23.8V59.5h23.8v16.2c6.2-12.5,21-18.5,33-18.5c26.1,0,41.1,16.9,41.1,47.3v62.8h-23.8v-60.1 c0-17.1-8.8-26.8-22.6-26.8c-14.1,0-27.7,7.6-27.7,28.9V167.4z"/>
<path class="st1" d="M390.6,8.2h151.7v22.9h-65.9v136.3h-25V31.1h-60.9V8.2H390.6z"/>
<path class="st1" d="M540.3,167.4h-23.8V59.5h23.8V79c7.4-13.5,15.4-19.5,29.6-21.8c7.5-1.2,15.7,1.8,19.2,4.2l-3.9,22 c-4.9-2.5-10.4-3.8-15.9-3.7c-20.3,0-28.9,20.3-28.9,49L540.3,167.4L540.3,167.4z"/>
<path class="st1" d="M680.7,151.9c-7.2,11.8-22.9,17.8-36.3,17.8c-29.1,0-54.8-21.9-54.8-56.4s25.6-56.1,54.8-56.1 c12.9,0,28.9,5.3,36.3,17.5V59.5h23.6v107.9h-23.6V151.9z M647.2,146.6c17.6,0,33.3-12.2,33.3-33.5s-17.1-32.8-33.3-32.8 c-18,0-33,12.9-33,32.8S629.2,146.6,647.2,146.6L647.2,146.6z"/>
<path class="st1" d="M778.4,57.2c17.1,0,32.6,6.7,42.7,18.7l-18.5,14.8c-5.8-6.7-14.8-10.4-24.3-10.4c-18,0-34,12.7-34,32.8 s15.9,33.7,34,33.7c9.5,0,18.5-3.9,24.3-10.6l18.7,14.6c-10.2,12-25.6,18.9-43,18.9c-31,0-57.5-22.4-57.5-56.6 S747.4,57.2,778.4,57.2z"/>
<path class="st1" d="M860.3,117.2v50.1h-23.6V0.8h23.6V95l34.2-35.6h31.9l-46,46.9l56.4,61h-30.5L860.3,117.2z"/>
<path class="st1" d="M967.4,59.5h-23.6v107.9h23.6V59.5z"/>
<path class="st1" d="M1014.7,167.4h-23.8V59.5h23.8v16.2c6.2-12.5,21-18.5,33-18.5c26.1,0,41.1,16.9,41.1,47.3v62.8H1065v-60.1 c0-17.1-8.8-26.8-22.6-26.8c-14.1,0-27.7,7.6-27.7,28.9V167.4z"/>
<path class="st1" d="M1220.2,111.5c0-12.3-4.5-24.2-12.5-33.6l13-17.1l-19.6-13l-11.9,15.7c-8.3-4.1-17.5-6.2-26.8-6.2 c-31.9,0-57.8,24.4-57.8,54.3s25.9,54.3,57.8,54.3c20.2,0,34.2,10.2,34.2,19.3s-14.1,19.3-34.2,19.3s-34.2-10.2-34.2-19.3h-23.6 c0,24,25.4,42.9,57.8,42.9s57.8-18.8,57.8-42.9c0-13.2-7.7-24.8-19.9-32.6C1212.5,142.5,1220.2,127.8,1220.2,111.5z M1128.2,111.5 c0-16.9,15.4-30.7,34.2-30.7s34.2,13.8,34.2,30.7s-15.4,30.7-34.2,30.7S1128.2,128.4,1128.2,111.5L1128.2,111.5z"/>
<path class="st0" d="M81.4,170.5C36.4,170.5,0,134,0,89c0-21.6,8.6-42.3,23.9-57.6c31.8-31.8,83.4-31.8,115.2,0l0,0l-17.7,17.7 C99.3,27,63.6,27,41.5,49.1s-22.1,57.8,0,79.9s57.8,22.1,79.9,0l17.7,17.7C123.8,162,103.1,170.6,81.4,170.5z"/>
<circle class="st0" cx="280.7" cy="15.5" r="15.5"/>
<circle class="st1" cx="954.7" cy="15.5" r="15.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,26 +0,0 @@
<?xml version="1.0"?>
<svg width="320" height="280" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg" version="1.1">
<g class="layer">
<title>Layer 1</title>
<g id="Layer1000">
<g id="Layer1002">
<g id="Layer1003">
<path d="m123.08,34.29c-66.43,0 -120.27,53.85 -120.27,120.27c0,66.43 53.85,120.27 120.27,120.27c66.47,0 120.32,-53.85 120.32,-120.27c0,-66.43 -53.85,-120.27 -120.32,-120.27zm0,215.67c-52.67,0 -95.36,-42.73 -95.36,-95.4c0,-52.67 42.68,-95.4 95.36,-95.4c52.72,0 95.4,42.73 95.4,95.4c0,52.67 -42.68,95.4 -95.4,95.4z" fill="#2a5e00" fill-rule="evenodd" id="path7"/>
<g id="Layer1004">
<g id="Layer1005">
<path d="m138.72,146.29l59.61,-41.47l7.78,33.7l-67.39,7.78z" fill="#2a5e00" fill-rule="evenodd" id="path8"/>
<path d="m110.88,146.29l-59.61,-41.47l-7.78,33.7l67.39,7.78z" fill="#2a5e00" fill-rule="evenodd" id="path9"/>
</g>
<path d="m43.95,192.02l74.62,49.75l87.12,-78.8l-161.75,29.05z" fill="#2a5e00" fill-rule="evenodd" id="path10"/>
</g>
<path d="m94.24,59.29l-30.48,-55.1l54.26,33.24l-23.79,21.86z" fill="#2a5e00" fill-rule="evenodd" id="path11"/>
<path d="m202.64,78.1l30.43,-55.1l-54.22,33.24l23.79,21.86z" fill="#2a5e00" fill-rule="evenodd" id="path12"/>
</g>
<path d="m275.63,274.67l29.35,0l0,-240.76l-29.35,0l0,240.76z" fill="#2a5e00" fill-rule="evenodd" id="path13"/>
<path d="m317.94,125.93c0,15.3 -12.33,27.63 -27.63,27.63c-15.26,0 -27.63,-12.33 -27.63,-27.63c0,-15.26 12.37,-27.63 27.63,-27.63c15.3,0 27.63,12.37 27.63,27.63z" fill="#2a5e00" fill-rule="evenodd" id="path14"/>
<path d="m288.84,33.91l-41.76,0l16.76,45.99l23.58,0l1.42,-45.99z" fill="#2a5e00" fill-rule="evenodd" id="path15"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,27 +0,0 @@
<?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">
<!-- Created with Inkscape (http://www.inkscape.org/) by Marsupilami -->
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024" height="357" viewBox="-1.98252 -1.98252 201.02104 70.04904" id="svg3349">
<defs id="defs3351"/>
<path d="m 0,0 11.76,0 4.455,31.962 0.12,0 L 20.789,0 32.55,0 23.402,42.417 9.147,42.417 0,0" id="path3131" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 34.039,0 11.05,0 0,15.567 6.771,0 0,-15.567 11.05,0 0,42.417 -11.05,0 0,-17.465 -6.771,0 0,17.465 -11.05,0 0,-42.417" id="path3133" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 64.395,0 11.757,0 4.457,31.962 0.121,0 L 85.185,0 96.944,0 87.797,42.417 73.54,42.417 64.395,0" id="path3135" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 97.576,42.414 8.907,0 9.222,-42.41 -8.912,0 -9.217,42.41" id="path3137" style="fill:#f0ab00;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 111.026,42.424 8.908,-0.01 9.218,-42.41 -8.913,0 -9.213,42.42" id="path3139" style="fill:#f0ab00;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 124.564,42.414 8.906,0 9.217,-42.41 -8.906,0 -9.217,42.41" id="path3141" style="fill:#f0ab00;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 4.169,46.784 5.251,0 1.985,14.261 0.05,0 1.99,-14.261 5.247,0 -4.082,18.928 -6.363,0 -4.082,-18.928" id="path3143" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 19.356,46.784 11.216,0 0,4.031 -6.282,0 0,3.234 5.882,0 0,3.87 -5.882,0 0,3.763 6.517,0 0,4.03 -11.451,0 0,-18.928" id="path3145" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 54.93,52.247 0,-0.45 c 0,-1.117 -0.449,-2.043 -1.405,-2.043 -1.06,0 -1.538,0.823 -1.538,1.67 0,3.739 8.059,1.91 8.059,8.828 0,4.027 -2.357,5.832 -6.705,5.832 -4.082,0 -6.362,-1.404 -6.362,-5.331 l 0,-0.663 4.772,0 0,0.453 c 0,1.615 0.663,2.202 1.614,2.202 1.007,0 1.594,-0.798 1.594,-1.831 0,-3.739 -7.743,-1.88 -7.743,-8.586 0,-3.819 2.043,-5.913 6.205,-5.913 4.294,0 6.12,1.775 6.12,5.832 l -4.611,0" id="path3147" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 61.476,65.712 4.932,0 0,-18.928 -4.932,0 0,18.928 z" id="path3149" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 32.477,46.784 7.714,0 c 3.766,0 5.195,2.124 5.195,5.039 0,2.516 -0.979,4.16 -3.232,4.506 2.385,0.265 3.103,1.776 3.103,4.294 l 0,1.616 c 0,0.981 0,2.251 0.237,2.626 0.132,0.209 0.237,0.422 0.558,0.582 l 0,0.265 -5.25,0 C 40.325,64.705 40.325,62.9 40.325,62.109 l 0,-1.275 c 0,-2.147 -0.423,-2.704 -1.619,-2.704 l -1.296,0 0,7.582 -4.933,0 0,-18.928 z m 4.933,8.008 0.98,0 c 1.405,0 2.066,-0.9 2.066,-2.252 0,-1.541 -0.609,-2.202 -2.094,-2.202 l -0.952,0 0,4.454" id="path3151" style="fill:#1e1e1e;fill-opacity:1;fill-rule:evenodd;stroke:none"/>
<path d="m 75.975,52.54 c 0,-2.147 -0.396,-2.786 -1.35,-2.786 -1.513,0 -1.67,1.381 -1.67,6.494 0,5.117 0.157,6.497 1.67,6.497 1.22,0 1.485,-1.063 1.485,-4.64 l 4.771,0 0,1.405 c 0,5.3 -3.101,6.574 -6.256,6.574 -5.54,0 -6.76,-2.784 -6.76,-9.836 0,-7.236 1.642,-9.833 6.76,-9.833 4.451,0 6.12,2.334 6.12,5.992 l 0,1.19 -4.77,0 0,-1.057" id="path3153" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 82.524,46.784 4.928,0 0,6.948 3.02,0 0,-6.948 4.932,0 0,18.928 -4.932,0 0,-7.793 -3.02,0 0,7.793 -4.928,0 0,-18.928" id="path3155" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 97.606,46.784 11.21,0 0,4.031 -6.282,0 0,3.234 5.886,0 0,3.87 -5.886,0 0,3.763 6.522,0 0,4.03 -11.45,0 0,-18.928" id="path3157" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 130.526,46.784 0,13.655 c 0,1.824 0.559,2.306 1.485,2.306 0.928,0 1.485,-0.482 1.485,-2.306 l 0,-13.655 4.929,0 0,12.408 c 0,5.298 -2.28,6.892 -6.414,6.892 -4.135,0 -6.412,-1.594 -6.412,-6.892 l 0,-12.408 4.927,0" id="path3159" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 140.415,46.784 5.51,0 3.185,11.665 0.05,0 0,-11.665 4.614,0 0,18.928 -5.407,0 -3.288,-11.689 -0.05,0 0,11.689 -4.613,0 0,-18.928" id="path3161" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 110.726,46.784 7.716,0 c 3.76,0 5.192,2.124 5.192,5.039 0,2.516 -0.979,4.16 -3.234,4.506 2.383,0.265 3.101,1.776 3.101,4.294 l 0,1.616 c 0,0.981 0,2.251 0.238,2.626 0.131,0.209 0.24,0.422 0.558,0.582 l 0,0.265 -5.251,0 c -0.477,-1.007 -0.477,-2.812 -0.477,-3.603 l 0,-1.275 c 0,-2.147 -0.422,-2.704 -1.612,-2.704 l -1.301,0 0,7.582 -4.93,0 0,-18.928 z m 4.93,8.008 0.979,0 c 1.404,0 2.07,-0.9 2.07,-2.252 0,-1.541 -0.61,-2.202 -2.095,-2.202 l -0.954,0 0,4.454" id="path3163" style="fill:#1e1e1e;fill-opacity:1;fill-rule:evenodd;stroke:none"/>
<path d="m 162.334,55.428 6.361,0 0,10.284 -3.34,0 -0.113,-1.669 -0.05,0 c -0.662,1.617 -2.412,2.041 -4.082,2.041 -5.01,0 -5.459,-3.58 -5.459,-9.836 0,-6.336 1.22,-9.833 7.047,-9.833 3.502,0 5.993,1.775 5.993,6.36 l -4.771,0 c 0,-0.952 -0.07,-1.695 -0.266,-2.198 -0.185,-0.53 -0.554,-0.823 -1.139,-0.823 -1.612,0 -1.774,1.381 -1.774,6.494 0,5.117 0.161,6.497 1.67,6.497 1.034,0 1.639,-0.666 1.669,-3.978 l -1.75,0 0,-3.339" id="path3165" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 170.577,46.784 11.213,0 0,4.031 -6.28,0 0,3.234 5.885,0 0,3.87 -5.885,0 0,3.763 6.519,0 0,4.03 -11.452,0 0,-18.928" id="path3167" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
<path d="m 183.697,46.784 5.514,0 3.18,11.665 0.05,0 0,-11.665 4.615,0 0,18.928 -5.408,0 -3.284,-11.689 -0.05,0 0,11.689 -4.614,0 0,-18.928" id="path3169" style="fill:#1e1e1e;fill-opacity:1;fill-rule:nonzero;stroke:none"/>
</svg>
<!-- version: 20110311, original size: 197.056 66.084, border: 3% -->

Before

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -6,14 +6,14 @@
"request": "launch",
"type": "dart",
"flutterMode": "debug",
"program": "mobile/apps/auth/lib/main.dart",
"program": "auth/lib/main.dart",
"args": ["--dart-define", "endpoint=http://localhost:8080"]
},
{
"name": "Auth Android Dev",
"request": "launch",
"type": "dart",
"program": "mobile/apps/auth/lib/main.dart",
"program": "auth/lib/main.dart",
"args": [
"--dart-define",
"endpoint=http://192.168.1.3:8080",
@@ -25,21 +25,21 @@
"name": "Auth iOS Dev",
"request": "launch",
"type": "dart",
"program": "mobile/apps/auth/lib/main.dart",
"program": "auth/lib/main.dart",
"args": ["--dart-define", "endpoint=http://192.168.1.30:8080"]
},
{
"name": "Auth iOS Prod",
"request": "launch",
"type": "dart",
"program": "mobile/apps/auth/lib/main.dart",
"program": "auth/lib/main.dart",
"args": ["--target", "lib/main.dart"]
},
{
"name": "Auth Android Prod",
"request": "launch",
"type": "dart",
"program": "mobile/apps/auth/lib/main.dart",
"program": "auth/lib/main.dart",
"args": ["--target", "lib/main.dart", "--flavor", "independent"]
}
]

View File

@@ -1,576 +0,0 @@
import 'package:ente_auth/app/view/app.dart';
import 'package:ente_auth/bootstrap.dart';
import 'package:ente_auth/main.dart';
import 'package:ente_auth/onboarding/view/common/add_chip.dart';
import 'package:ente_auth/services/update_service.dart';
import 'package:ente_lock_screen/local_authentication_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('Authentication Flow Integration Test with Persistence', () {
testWidgets(
'Complete auth flow: Use without backup -> Enter setup key -> Verify entry persists after restart',
(WidgetTester tester) async {
// Bootstrap the app
await bootstrap(App.new);
await init(false, via: 'integrationTest');
await UpdateService.instance.init();
await tester.pumpAndSettle();
// Step 1: Click on "Use without backup" option
final useOfflineText = find.text('Use without backups');
expect(
useOfflineText,
findsOneWidget,
reason:
'ERROR: "Use without backups" button not found on initial screen. Check if app loaded correctly or text changed.',
);
await tester.tap(useOfflineText);
await tester.pumpAndSettle();
// Step 2: Click OK button on the warning dialog
final okButton = find.text('Ok');
expect(
okButton,
findsOneWidget,
reason:
'ERROR: "Ok" button not found in warning dialog. Check if dialog appeared or button text changed.',
);
await tester.tap(okButton);
await tester.pumpAndSettle();
// Wait for navigation to complete
await tester.pumpAndSettle(const Duration(seconds: 2));
// Step 3: Navigate to manual entry screen
bool foundManualEntry = false;
// Try FloatingActionButton approach first
final fabFinder = find.byType(FloatingActionButton);
if (fabFinder.evaluate().isNotEmpty) {
await tester.tap(fabFinder);
await tester.pumpAndSettle();
final manualEntryFinder = find.text('Enter a setup key');
if (manualEntryFinder.evaluate().isNotEmpty) {
await tester.tap(manualEntryFinder);
await tester.pumpAndSettle();
foundManualEntry = true;
}
}
// Alternative approaches if FAB didn't work
if (!foundManualEntry) {
final alternatives = [
'Enter details manually',
'Enter a setup key',
];
for (final text in alternatives) {
final finder = find.text(text);
if (finder.evaluate().isNotEmpty) {
await tester.tap(finder.first);
await tester.pumpAndSettle();
foundManualEntry = true;
break;
}
}
}
expect(
foundManualEntry,
isTrue,
reason:
'ERROR: Could not find manual entry option. Tried FAB + "Enter details manually", "Enter a setup key", etc. Check UI navigation.',
);
// Step 4: Fill in the form with test data
final textFields = find.byType(TextFormField);
expect(
textFields.evaluate().length,
greaterThanOrEqualTo(3),
reason:
'ERROR: Expected at least 3 text fields (issuer, secret, account) but found ${textFields.evaluate().length}. Check manual entry form.',
);
// Fill issuer field
await tester.tap(textFields.first);
await tester.enterText(textFields.first, 'testIssuer');
await tester.pumpAndSettle();
// Fill secret field
await tester.tap(textFields.at(1));
await tester.enterText(
textFields.at(1),
'JBSWY3DPEHPK3PXP',
); // Valid base32 secret
await tester.pumpAndSettle();
// Fill account field
await tester.tap(textFields.at(2));
await tester.enterText(textFields.at(2), 'testAccount');
await tester.pumpAndSettle();
// Step 5: Save the entry
final saveButton = find.text('Save');
expect(
saveButton,
findsOneWidget,
reason:
'ERROR: "Save" button not found on manual entry form. Check if button text changed or form layout changed.',
);
await tester.tap(saveButton);
await tester.pumpAndSettle();
// Step 6: Verify entry was created successfully
await tester.pumpAndSettle(const Duration(seconds: 1));
// Check if coach mark overlay is present and dismiss it
final coachMarkOverlay = find.text('Ok');
if (coachMarkOverlay.evaluate().isNotEmpty) {
print('🎯 Dismissing coach mark overlay...');
await tester.tap(coachMarkOverlay);
await tester.pumpAndSettle();
}
// Look for the created entry
final issuerEntryFinder = find.textContaining('testIssuer');
expect(
issuerEntryFinder,
findsAtLeastNWidgets(1),
reason:
'ERROR: testIssuer entry not found after saving. Entry creation may have failed or navigation issue occurred.',
);
final accountEntryFinder = find.textContaining('testAccount');
expect(
accountEntryFinder,
findsAtLeastNWidgets(1),
reason:
'ERROR: testAccount not found after saving. Account field may not have been saved properly.',
);
print('✅ Step 1 completed: Entry created successfully');
print('- testIssuer entry is visible');
print('- testAccount is visible');
// warning about clearing
// Step 8: Add second code entry
print('🔄 Adding second code entry...');
// Wait a moment before adding second entry
await tester.pumpAndSettle(const Duration(seconds: 1));
// Click FAB to add second entry
final fabFinder2 = find.byType(FloatingActionButton);
expect(
fabFinder2,
findsOneWidget,
reason: 'FAB not found for second entry',
);
await tester.tap(fabFinder2);
await tester.pumpAndSettle();
// Click "Enter details manually" (coach mark won't show second time)
final manualEntryFinder = find.text('Enter details manually');
expect(
manualEntryFinder,
findsOneWidget,
reason: 'Manual entry option not found',
);
await tester.tap(manualEntryFinder);
await tester.pumpAndSettle();
// Fill second entry form
final textFields2 = find.byType(TextFormField);
expect(textFields2.evaluate().length, greaterThanOrEqualTo(3));
// Fill second issuer field
await tester.tap(textFields2.first);
await tester.enterText(textFields2.first, 'testIssuer2');
await tester.pumpAndSettle();
// Verify issuer field was filled
final issuerField = tester.widget<TextFormField>(textFields2.first);
print(
'✓ Issuer field controller text: "${issuerField.controller?.text ?? "null"}"');
// Fill second secret field
await tester.tap(textFields2.at(1));
await tester.enterText(textFields2.at(1), 'JBSWY3DPEHPK3PXP');
await tester.pumpAndSettle();
// Verify secret field was filled
final secretField = tester.widget<TextFormField>(textFields2.at(1));
print(
'✓ Secret field controller text: "${secretField.controller?.text ?? "null"}"');
// Fill second account field
await tester.tap(textFields2.at(2));
await tester.enterText(textFields2.at(2), 'testAccount2');
await tester.pumpAndSettle();
// Save second entry
final saveButton2 = find.text('Save');
await tester.tap(saveButton2);
await tester.pumpAndSettle();
await tester.pumpAndSettle(const Duration(seconds: 2));
// Verify both entries exist
final issuer1Finder = find.textContaining('testIssuer');
final issuer2Finder = find.textContaining('testIssuer2');
final account1Finder = find.textContaining('testAccount');
final account2Finder = find.textContaining('testAccount2');
expect(issuer1Finder, findsAtLeastNWidgets(1),
reason: 'First issuer not found');
expect(issuer2Finder, findsAtLeastNWidgets(1),
reason: 'Second issuer not found');
expect(
account1Finder,
findsAtLeastNWidgets(1),
reason: 'First account not found',
);
expect(
account2Finder,
findsAtLeastNWidgets(1),
reason: 'Second account not found',
);
print('✅ Step 2 completed: Both entries created successfully');
print('- testIssuer and testIssuer2 entries are visible');
print('- testAccount and testAccount2 are visible');
// Step 9: Test search functionality
print('🔍 Testing search functionality...');
// Click on search icon to activate search
final searchIcon = find.byIcon(Icons.search);
expect(searchIcon, findsOneWidget, reason: 'Search icon not found');
await tester.tap(searchIcon);
await tester.pumpAndSettle();
// Find the search text field
final searchField = find.byType(TextField);
expect(searchField, findsOneWidget,
reason: 'Search text field not found');
// Enter search term "issuer2"
await tester.tap(searchField);
await tester.enterText(searchField, 'issuer2');
await tester.pumpAndSettle();
// Verify only one result is shown (testIssuer2)
final searchResults = find.textContaining('testIssuer');
final issuer2Results = find.textContaining('testIssuer2');
// Should find testIssuer2 but not testIssuer when searching for "issuer2"
expect(issuer2Results, findsAtLeastNWidgets(1),
reason: 'testIssuer2 not found in search results');
// Verify total results - should only show the matching entry
final allVisibleIssuers = find.textContaining('testIssuer');
expect(allVisibleIssuers.evaluate().length, equals(1),
reason: 'Search should show only one result for "issuer2"');
print('✅ Search results verified: only testIssuer2 is visible');
// Clear search bar
final clearIcon = find.byIcon(Icons.clear);
expect(clearIcon, findsOneWidget,
reason: 'Clear search icon not found');
await tester.tap(clearIcon);
await tester.pumpAndSettle();
// Verify both entries are visible again after clearing search
final allIssuer1 = find.textContaining('testIssuer');
final allIssuer2 = find.textContaining('testIssuer2');
expect(allIssuer1, findsAtLeastNWidgets(1),
reason: 'testIssuer not visible after clearing search');
expect(allIssuer2, findsAtLeastNWidgets(1),
reason: 'testIssuer2 not visible after clearing search');
print('✅ Search cleared: both entries visible again');
print('✅ Step 3 completed: Search functionality working correctly');
// Step 10: Long press on issuer2 to edit and add tags
print('🏷️ Testing tag functionality...');
// Long press on testIssuer2 entry to bring up edit menu
final issuer2Entry = find.textContaining('testIssuer2');
expect(issuer2Entry, findsOneWidget,
reason: 'testIssuer2 entry not found for long press');
await tester.longPress(issuer2Entry);
await tester.pumpAndSettle();
LocalAuthenticationService.instance.lastAuthTime = DateTime.now()
.add(const Duration(minutes: 10))
.millisecondsSinceEpoch;
// Look for edit option and tap it
final editOption = find.text('Edit');
expect(editOption, findsOneWidget, reason: 'Edit option not found');
await tester.tap(editOption);
await tester.pumpAndSettle();
// Wait for edit page to load
await tester.pumpAndSettle(const Duration(seconds: 2));
// Look for AddChip widget to add first tag
final addChip = find.byType(AddChip);
expect(addChip, findsOneWidget, reason: 'AddChip widget not found');
await tester.tap(addChip);
await tester.pumpAndSettle();
// Enter first tag name "tag1"
final tagInputField = find.byType(TextField).last;
await tester.tap(tagInputField);
await tester.enterText(tagInputField, 'tag1');
await tester.pumpAndSettle();
// Tap create/save button for first tag
final createButton = find.text('Create');
expect(createButton, findsOneWidget,
reason: 'Create button not found for first tag');
await tester.tap(createButton);
await tester.pumpAndSettle();
// Add second tag
final addChip2 = find.byType(AddChip);
await tester.tap(addChip2);
await tester.pumpAndSettle();
// Enter second tag name "tag2"
final tagInputField2 = find.byType(TextField).last;
await tester.tap(tagInputField2);
await tester.enterText(tagInputField2, 'tag2');
await tester.pumpAndSettle();
// Tap create button for second tag
final createButton2 = find.text('Create');
await tester.tap(createButton2);
await tester.pumpAndSettle();
// Verify tags are selected/visible
final tag1Chip = find.text('tag1');
final tag2Chip = find.text('tag2');
expect(tag1Chip, findsOneWidget,
reason: 'tag1 not found after creation');
expect(tag2Chip, findsOneWidget,
reason: 'tag2 not found after creation');
print('✅ Tags created: tag1 and tag2 are visible');
// Save the edited entry
final saveEditButton = find.text('Save');
expect(saveEditButton, findsOneWidget,
reason: 'Save button not found on edit page');
await tester.tap(saveEditButton);
await tester.pumpAndSettle();
// Wait for navigation back to home
await tester.pumpAndSettle(const Duration(seconds: 2));
print('✅ Entry saved with tags');
// Step 11: Test tag filtering functionality
print('🏷️ Testing tag filtering...');
// Click on tag1 to filter entries
final tag1Filter = find.textContaining('tag1');
if (tag1Filter.evaluate().isNotEmpty) {
await tester.tap(tag1Filter.first);
await tester.pumpAndSettle();
// Verify only testIssuer2 is visible (the one with tag1)
final filteredIssuer2 = find.textContaining('testIssuer2');
final filteredIssuer1 = find.textContaining('testIssuer').evaluate().where(
(element) => !element.widget.toString().contains('testIssuer2')
).length;
expect(filteredIssuer2, findsAtLeastNWidgets(1),
reason: 'testIssuer2 not visible when filtering by tag1');
expect(filteredIssuer1, equals(0),
reason: 'testIssuer should not be visible when filtering by tag1');
print('✅ Tag1 filtering verified: only testIssuer2 is visible');
// Click "All" to clear tag filter
final allFilter = find.text('All');
if (allFilter.evaluate().isNotEmpty) {
await tester.tap(allFilter);
await tester.pumpAndSettle();
// Verify both entries are visible again
final allEntriesIssuer1 = find.textContaining('testIssuer');
final allEntriesIssuer2 = find.textContaining('testIssuer2');
expect(allEntriesIssuer1, findsAtLeastNWidgets(1));
expect(allEntriesIssuer2, findsAtLeastNWidgets(1));
print('✅ Tag filter cleared: both entries visible again');
}
}
print('✅ Step 4 completed: Tag functionality working correctly');
// Step 12: Test trash functionality
print('🗑️ Testing trash functionality...');
// Long press on testIssuer2 entry to bring up context menu
final issuer2EntryForTrash = find.textContaining('testIssuer2').first;
await tester.longPress(issuer2EntryForTrash);
await tester.pumpAndSettle();
// Look for trash/delete option and tap it
final trashOption = find.text('Trash');
if (trashOption.evaluate().isEmpty) {
// Try alternative delete options
final deleteOption = find.text('Delete');
expect(deleteOption, findsOneWidget, reason: 'Delete/Trash option not found');
await tester.tap(deleteOption);
} else {
await tester.tap(trashOption);
}
await tester.pumpAndSettle();
// Confirm deletion if dialog appears
final confirmButtons = [
find.text('Yes'),
find.text('OK'),
find.text('Confirm'),
find.text('Delete'),
find.text('Trash'),
];
for (final button in confirmButtons) {
if (button.evaluate().isNotEmpty) {
await tester.tap(button);
await tester.pumpAndSettle();
break;
}
}
print('✅ Issuer2 entry trashed');
// Step 13: Verify tags are no longer visible and trash tag appears
print('🏷️ Verifying tag visibility after trash...');
// Wait for UI to update
await tester.pumpAndSettle(const Duration(seconds: 1));
// Verify tag1 and tag2 are no longer visible (since issuer2 was the only entry with these tags)
final tag1Visible = find.textContaining('tag1');
final tag2Visible = find.textContaining('tag2');
// Tags should not be visible anymore since the only entry with these tags was trashed
expect(tag1Visible.evaluate().isEmpty, isTrue, reason: 'tag1 should not be visible after trashing issuer2');
expect(tag2Visible.evaluate().isEmpty, isTrue, reason: 'tag2 should not be visible after trashing issuer2');
// Verify trash tag is now visible
final trashTag = find.text('Trash');
expect(trashTag, findsOneWidget, reason: 'Trash tag not visible after trashing entry');
print('✅ Tags hidden and Trash tag visible');
// Step 14: Test trash filtering
print('🗑️ Testing trash tag filtering...');
// Click on Trash tag to show trashed items
await tester.tap(trashTag);
await tester.pumpAndSettle();
// Verify issuer2 is visible in trash
final trashedIssuer2 = find.textContaining('testIssuer2');
expect(trashedIssuer2, findsOneWidget, reason: 'testIssuer2 not visible in trash');
// Verify issuer1 is not visible (should be filtered out)
final issuer1InTrash = find.textContaining('testIssuer').evaluate().where(
(element) => !element.widget.toString().contains('testIssuer2')
).length;
expect(issuer1InTrash, equals(0), reason: 'testIssuer should not be visible in trash filter');
print('✅ Trash filtering working: only trashed items visible');
// Step 15: Test All filter (should not show trashed items)
print('📋 Testing All filter excludes trash...');
// Click on "All" to show all non-trashed items
final allTag = find.text('All');
await tester.tap(allTag);
await tester.pumpAndSettle();
// Verify issuer1 is visible
final allFilterIssuer1 = find.textContaining('testIssuer').evaluate().where(
(element) => !element.widget.toString().contains('testIssuer2')
).length;
expect(allFilterIssuer1, greaterThan(0), reason: 'testIssuer should be visible in All filter');
// Verify issuer2 is NOT visible in All
final allFilterIssuer2 = find.textContaining('testIssuer2');
expect(allFilterIssuer2.evaluate().isEmpty, isTrue, reason: 'testIssuer2 should not be visible in All filter');
print('✅ All filter working: trashed items excluded');
// Step 16: Test restore functionality
print('♻️ Testing restore functionality...');
// Go back to trash view
await tester.tap(trashTag);
await tester.pumpAndSettle();
// Long press on trashed issuer2 entry
final trashedEntryForRestore = find.textContaining('testIssuer2');
await tester.longPress(trashedEntryForRestore);
await tester.pumpAndSettle();
// Look for restore option
final restoreOption = find.text('Restore');
expect(restoreOption, findsOneWidget, reason: 'Restore option not found');
await tester.tap(restoreOption);
await tester.pumpAndSettle();
print('✅ Restore option tapped');
// Step 17: Verify restoration worked
print('✅ Verifying restoration...');
// Wait for restoration to complete
await tester.pumpAndSettle(const Duration(seconds: 1));
// Go to All view to check if issuer2 is restored
await tester.tap(allTag);
await tester.pumpAndSettle();
// Verify both entries are now visible in All
final restoredIssuer1 = find.textContaining('testIssuer');
final restoredIssuer2 = find.textContaining('testIssuer2');
expect(restoredIssuer1, findsAtLeastNWidgets(1), reason: 'testIssuer not visible after restore');
expect(restoredIssuer2, findsAtLeastNWidgets(1), reason: 'testIssuer2 not visible after restore');
// Verify tags are visible again
final restoredTag1 = find.textContaining('tag1');
final restoredTag2 = find.textContaining('tag2');
if (restoredTag1.evaluate().isNotEmpty && restoredTag2.evaluate().isNotEmpty) {
print('✅ Tags restored and visible again');
}
print('✅ Step 5 completed: Trash and restore functionality working correctly');
print('✅ Integration test completed successfully!');
print('- Both entries created and verified');
print('- Search functionality tested and working');
print('- Tag functionality tested and working');
print('- Trash functionality tested and working');
print('- Restore functionality tested and working');
print('- Multiple TOTP codes are being generated');
print('- Data persistence is working correctly');
},
timeout: const Timeout(Duration(minutes: 3)),
);
});
}

View File

@@ -3,6 +3,7 @@ PODS:
- Flutter
- connectivity_plus (0.0.1):
- Flutter
- FlutterMacOS
- cupertino_http (0.0.1):
- Flutter
- FlutterMacOS
@@ -39,8 +40,6 @@ PODS:
- DKPhotoGallery/Resource (0.0.19):
- SDWebImage
- SwiftyGif
- ente_qr (0.0.1):
- Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
@@ -62,14 +61,13 @@ PODS:
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- flutter_native_splash (0.0.1):
- Flutter
- flutter_secure_storage (6.0.0):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- integration_test (0.0.1):
- Flutter
- Toast
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -89,9 +87,9 @@ PODS:
- qr_code_scanner (0.2.0):
- Flutter
- MTBBarcodeScanner
- SDWebImage (5.21.1):
- SDWebImage/Core (= 5.21.1)
- SDWebImage/Core (5.21.1)
- SDWebImage (5.21.0):
- SDWebImage/Core (= 5.21.0)
- SDWebImage/Core (5.21.0)
- Sentry/HybridSDK (8.46.0)
- sentry_flutter (8.14.2):
- Flutter
@@ -104,43 +102,37 @@ PODS:
- FlutterMacOS
- sodium_libs (2.2.1):
- Flutter
- sqflite_darwin (0.0.4):
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- sqlite3 (3.50.2):
- sqlite3/common (= 3.50.2)
- sqlite3/common (3.50.2)
- sqlite3/dbstatvtab (3.50.2):
- "sqlite3 (3.46.1+1)":
- "sqlite3/common (= 3.46.1+1)"
- "sqlite3/common (3.46.1+1)"
- "sqlite3/dbstatvtab (3.46.1+1)":
- sqlite3/common
- sqlite3/fts5 (3.50.2):
- "sqlite3/fts5 (3.46.1+1)":
- sqlite3/common
- sqlite3/math (3.50.2):
- "sqlite3/perf-threadsafe (3.46.1+1)":
- sqlite3/common
- sqlite3/perf-threadsafe (3.50.2):
- sqlite3/common
- sqlite3/rtree (3.50.2):
- "sqlite3/rtree (3.46.1+1)":
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.50.1)
- "sqlite3 (~> 3.46.0+1)"
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
- sqlite3/perf-threadsafe
- sqlite3/rtree
- SwiftyGif (5.4.5)
- ua_client_hints (1.4.1):
- Flutter
- Toast (4.1.1)
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- app_links (from `.symlinks/plugins/app_links/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`)
- cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- ente_qr (from `.symlinks/plugins/ente_qr/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`)
@@ -152,7 +144,6 @@ DEPENDENCIES:
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
@@ -164,9 +155,8 @@ DEPENDENCIES:
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sodium_libs (from `.symlinks/plugins/sodium_libs/ios`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- ua_client_hints (from `.symlinks/plugins/ua_client_hints/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:
@@ -179,18 +169,17 @@ SPEC REPOS:
- Sentry
- sqlite3
- SwiftyGif
- Toast
EXTERNAL SOURCES:
app_links:
:path: ".symlinks/plugins/app_links/ios"
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
:path: ".symlinks/plugins/connectivity_plus/darwin"
cupertino_http:
:path: ".symlinks/plugins/cupertino_http/darwin"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
ente_qr:
:path: ".symlinks/plugins/ente_qr/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
file_saver:
@@ -213,8 +202,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
move_to_background:
@@ -237,55 +224,51 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sodium_libs:
:path: ".symlinks/plugins/sodium_libs/ios"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
ua_client_hints:
:path: ".symlinks/plugins/ua_client_hints/ios"
:path: ".symlinks/plugins/sqlite3_flutter_libs/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
app_links: f3e17e4ee5e357b39d8b95290a9b2c299fca71c6
connectivity_plus: 2a701ffec2c0ae28a48cf7540e279787e77c447d
app_links: e7a6750a915a9e161c58d91bc610e8cd1d4d0ad0
connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db
cupertino_http: 947a233f40cfea55167a49f2facc18434ea117ba
device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
ente_qr: f39434aa69ea0e71047b49316365b2737f8a20aa
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_email_sender: e03bdda7637bcd3539bfe718fddd980e9508efaa
flutter_email_sender: 10a22605f92809a11ef52b2f412db806c6082d40
flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4
flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb
flutter_local_notifications: df98d66e515e1ca797af436137b4459b160ad8c9
flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
fluttertoast: 21eecd6935e7064cc1fcb733a4c5a428f3f24f0f
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
local_auth_darwin: fa4b06454df7df8e97c18d7ee55151c57e7af0de
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
objective_c: 77e887b5ba1827970907e10e832eec1683f3431d
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
SDWebImage: f29024626962457f3470184232766516dee8dfea
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
Sentry: da60d980b197a46db0b35ea12cb8f39af48d8854
sentry_flutter: 2df8b0aab7e4aba81261c230cbea31c82a62dd1b
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 3e82a2daae39ba3b41ae6ee84a130494585460fc
sqlite3_flutter_libs: 2c48c4ee7217fd653251975e43412250d5bcbbe2
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb
sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
ua_client_hints: aeabd123262c087f0ce151ef96fa3ab77bfc8b38
Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
PODFILE CHECKSUM: 78f002751f1a8f65042b8da97902ba4124271c5a

View File

@@ -2,21 +2,20 @@ import 'dart:async';
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/events/signed_in_event.dart';
import 'package:ente_auth/events/signed_out_event.dart';
import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/locale.dart';
import "package:ente_auth/onboarding/view/onboarding_page.dart";
import 'package:ente_auth/services/authenticator_service.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:ente_events/event_bus.dart';
import 'package:ente_events/models/signed_in_event.dart';
import 'package:ente_events/models/signed_out_event.dart';
import 'package:ente_strings/l10n/strings_localizations.dart';
import 'package:flutter/foundation.dart';
import "package:flutter/material.dart";
import 'package:flutter_localizations/flutter_localizations.dart';
@@ -132,7 +131,6 @@ class _AppState extends State<App>
localeListResolutionCallback: localResolutionCallBack,
localizationsDelegates: const [
AppLocalizations.delegate,
StringsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
@@ -152,7 +150,6 @@ class _AppState extends State<App>
localeListResolutionCallback: localResolutionCallBack,
localizationsDelegates: const [
AppLocalizations.delegate,
StringsLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,

View File

@@ -1,36 +1,95 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:ente_base/models/database.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:bip39/bip39.dart' as bip39;
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/endpoint_updated_event.dart';
import 'package:ente_auth/events/signed_in_event.dart';
import 'package:ente_auth/events/signed_out_event.dart';
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/directory_utils.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'package:tuple/tuple.dart';
class Configuration extends BaseConfiguration {
class Configuration {
Configuration._privateConstructor();
static final Configuration instance = Configuration._privateConstructor();
static const endpoint = String.fromEnvironment(
"endpoint",
defaultValue: kDefaultProductionEndpoint,
);
static const emailKey = "email";
static const keyAttributesKey = "key_attributes";
static const lastTempFolderClearTimeKey = "last_temp_folder_clear_time";
static const keyKey = "key";
static const secretKeyKey = "secret_key";
static const authSecretKeyKey = "auth_secret_key";
static const offlineAuthSecretKey = "offline_auth_secret_key";
static const tokenKey = "token";
static const encryptedTokenKey = "encrypted_token";
static const userIDKey = "user_id";
static const hasMigratedSecureStorageKey = "has_migrated_secure_storage";
static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode";
static const endPointKey = "endpoint";
final List<String> onlineSecureKeys = [
keyKey,
secretKeyKey,
authSecretKeyKey,
];
final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds;
static final _logger = Logger("Configuration");
String? _cachedToken;
late SharedPreferences _preferences;
String? _key;
String? _secretKey;
String? _authSecretKey;
String? _offlineAuthKey;
late FlutterSecureStorage _secureStorage;
late String _tempDirectory;
@override
Future<void> init(List<EnteBaseDatabase> dbs) async {
await super.init(dbs);
String? _volatilePassword;
Future<void> init() async {
_preferences = await SharedPreferences.getInstance();
sqfliteFfiInit();
_secureStorage = const FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
sqfliteFfiInit();
_tempDirectory = (await DirectoryUtils.getDirectoryForInit()).path;
final tempDirectory = io.Directory(_tempDirectory);
try {
final currentTime = DateTime.now().microsecondsSinceEpoch;
if (tempDirectory.existsSync() &&
(_preferences.getInt(lastTempFolderClearTimeKey) ?? 0) <
(currentTime - kTempFolderDeletionTimeBuffer)) {
await tempDirectory.delete(recursive: true);
await _preferences.setInt(lastTempFolderClearTimeKey, currentTime);
_logger.info("Cleared temp folder");
} else {
_logger.info("Skipping temp folder clear");
}
} catch (e) {
_logger.warning(e);
}
tempDirectory.createSync(recursive: true);
await _initOnlineAccount();
await _initOfflineAccount();
}
@@ -40,10 +99,303 @@ class Configuration extends BaseConfiguration {
);
}
@override
Future<void> _initOnlineAccount() async {
if (!_preferences.containsKey(tokenKey)) {
for (final key in onlineSecureKeys) {
unawaited(
_secureStorage.delete(
key: key,
),
);
}
} else {
_key = await _secureStorage.read(
key: keyKey,
);
_secretKey = await _secureStorage.read(
key: secretKeyKey,
);
_authSecretKey = await _secureStorage.read(
key: authSecretKeyKey,
);
if (_key == null) {
await logout(autoLogout: true);
}
}
}
Future<void> logout({bool autoLogout = false}) async {
await _preferences.clear();
for (String key in onlineSecureKeys) {
await _secureStorage.delete(
key: key,
);
}
await AuthenticatorDB.instance.clearTable();
_key = null;
_cachedToken = null;
_secretKey = null;
_authSecretKey = null;
await super.logout();
Bus.instance.fire(SignedOutEvent());
}
Future<KeyGenResult> generateKey(String password) async {
// Create a master key
final masterKey = CryptoUtil.generateKey();
// Create a recovery key
final recoveryKey = CryptoUtil.generateKey();
// Encrypt master key and recovery key with each other
final encryptedMasterKey = CryptoUtil.encryptSync(masterKey, recoveryKey);
final encryptedRecoveryKey = CryptoUtil.encryptSync(recoveryKey, masterKey);
// Derive a key from the password that will be used to encrypt and
// decrypt the master key
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
utf8.encode(password),
kekSalt,
);
final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
// Encrypt the key with this derived key
final encryptedKeyData =
CryptoUtil.encryptSync(masterKey, derivedKeyResult.key);
// Generate a public-private keypair and encrypt the latter
final keyPair = CryptoUtil.generateKeyPair();
final encryptedSecretKeyData =
CryptoUtil.encryptSync(keyPair.secretKey.extractBytes(), masterKey);
final attributes = KeyAttributes(
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,
CryptoUtil.bin2base64(encryptedMasterKey.encryptedData!),
CryptoUtil.bin2base64(encryptedMasterKey.nonce!),
CryptoUtil.bin2base64(encryptedRecoveryKey.encryptedData!),
CryptoUtil.bin2base64(encryptedRecoveryKey.nonce!),
);
final privateAttributes = PrivateKeyAttributes(
CryptoUtil.bin2base64(masterKey),
CryptoUtil.bin2hex(recoveryKey),
CryptoUtil.bin2base64(keyPair.secretKey.extractBytes()),
);
return KeyGenResult(attributes, privateAttributes, loginKey);
}
Future<Tuple2<KeyAttributes, Uint8List>> getAttributesForNewPassword(
String password,
) async {
// Get master key
final masterKey = getKey();
// Derive a key from the password that will be used to encrypt and
// decrypt the master key
final kekSalt = CryptoUtil.getSaltToDeriveKey();
final derivedKeyResult = await CryptoUtil.deriveSensitiveKey(
utf8.encode(password),
kekSalt,
);
final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key);
// Encrypt the key with this derived key
final encryptedKeyData =
CryptoUtil.encryptSync(masterKey!, derivedKeyResult.key);
final existingAttributes = getKeyAttributes();
final updatedAttributes = existingAttributes!.copyWith(
kekSalt: CryptoUtil.bin2base64(kekSalt),
encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!),
keyDecryptionNonce: CryptoUtil.bin2base64(encryptedKeyData.nonce!),
memLimit: derivedKeyResult.memLimit,
opsLimit: derivedKeyResult.opsLimit,
);
return Tuple2(updatedAttributes, loginKey);
}
// decryptSecretsAndGetLoginKey decrypts the master key and recovery key
// with the given password and save them in local secure storage.
// This method also returns the keyEncKey that can be used for performing
// SRP setup for existing users.
Future<Uint8List> decryptSecretsAndGetKeyEncKey(
String password,
KeyAttributes attributes, {
Uint8List? keyEncryptionKey,
}) async {
_logger.info('Start decryptAndSaveSecrets');
keyEncryptionKey ??= await CryptoUtil.deriveKey(
utf8.encode(password),
CryptoUtil.base642bin(attributes.kekSalt),
attributes.memLimit,
attributes.opsLimit,
);
_logger.info('user-key done');
Uint8List key;
try {
key = CryptoUtil.decryptSync(
CryptoUtil.base642bin(attributes.encryptedKey),
keyEncryptionKey,
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(CryptoUtil.bin2base64(key));
final secretKey = CryptoUtil.decryptSync(
CryptoUtil.base642bin(attributes.encryptedSecretKey),
key,
CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
);
_logger.info("secret-key done");
await setSecretKey(CryptoUtil.bin2base64(secretKey));
final token = CryptoUtil.openSealSync(
CryptoUtil.base642bin(getEncryptedToken()!),
CryptoUtil.base642bin(attributes.publicKey),
secretKey,
);
_logger.info('appToken done');
await setToken(
CryptoUtil.bin2base64(token, urlSafe: true),
);
return keyEncryptionKey;
}
Future<void> recover(String recoveryKey) async {
// check if user has entered mnemonic code
if (recoveryKey.contains(' ')) {
final split = recoveryKey.split(' ');
if (split.length != mnemonicKeyWordCount) {
String wordThatIsFollowedByEmptySpaceInSplit = '';
for (int i = 0; i < split.length; i++) {
String word = split[i];
if (word.isEmpty) {
wordThatIsFollowedByEmptySpaceInSplit =
'\n\nExtra space after word at position $i';
break;
}
}
throw AssertionError(
'\nRecovery code should have $mnemonicKeyWordCount words, '
'found ${split.length} words instead.$wordThatIsFollowedByEmptySpaceInSplit',
);
}
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
}
final attributes = getKeyAttributes();
Uint8List masterKey;
try {
masterKey = await CryptoUtil.decrypt(
CryptoUtil.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey),
CryptoUtil.hex2bin(recoveryKey),
CryptoUtil.base642bin(attributes.masterKeyDecryptionNonce),
);
} catch (e) {
_logger.severe(e);
rethrow;
}
await setKey(CryptoUtil.bin2base64(masterKey));
final secretKey = CryptoUtil.decryptSync(
CryptoUtil.base642bin(attributes.encryptedSecretKey),
masterKey,
CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce),
);
await setSecretKey(CryptoUtil.bin2base64(secretKey));
final token = CryptoUtil.openSealSync(
CryptoUtil.base642bin(getEncryptedToken()!),
CryptoUtil.base642bin(attributes.publicKey),
secretKey,
);
await setToken(
CryptoUtil.bin2base64(token, urlSafe: true),
);
}
String getHttpEndpoint() {
return _preferences.getString(endPointKey) ?? endpoint;
}
Future<void> setHttpEndpoint(String endpoint) async {
await _preferences.setString(endPointKey, endpoint);
Bus.instance.fire(EndpointUpdatedEvent());
}
String? getToken() {
_cachedToken ??= _preferences.getString(tokenKey);
return _cachedToken;
}
bool isLoggedIn() {
return getToken() != null;
}
Future<void> setToken(String token) async {
_cachedToken = token;
await _preferences.setString(tokenKey, token);
Bus.instance.fire(SignedInEvent());
}
Future<void> setEncryptedToken(String encryptedToken) async {
await _preferences.setString(encryptedTokenKey, encryptedToken);
}
String? getEncryptedToken() {
return _preferences.getString(encryptedTokenKey);
}
String? getEmail() {
return _preferences.getString(emailKey);
}
Future<void> setEmail(String email) async {
await _preferences.setString(emailKey, email);
}
int? getUserID() {
return _preferences.getInt(userIDKey);
}
Future<void> setUserID(int userID) async {
await _preferences.setInt(userIDKey, userID);
}
Future<void> setKeyAttributes(KeyAttributes attributes) async {
await _preferences.setString(keyAttributesKey, attributes.toJson());
}
KeyAttributes? getKeyAttributes() {
final jsonValue = _preferences.getString(keyAttributesKey);
if (jsonValue == null) {
return null;
} else {
return KeyAttributes.fromJson(jsonValue);
}
}
Future<void> setKey(String key) async {
_key = key;
await _secureStorage.write(
key: keyKey,
value: key,
);
}
Future<void> setSecretKey(String? secretKey) async {
_secretKey = secretKey;
await _secureStorage.write(
key: secretKeyKey,
value: secretKey,
);
}
Future<void> setAuthSecretKey(String? authSecretKey) async {
@@ -54,6 +406,14 @@ class Configuration extends BaseConfiguration {
);
}
Uint8List? getKey() {
return _key == null ? null : CryptoUtil.base642bin(_key!);
}
Uint8List? getSecretKey() {
return _secretKey == null ? null : CryptoUtil.base642bin(_secretKey!);
}
Uint8List? getAuthSecretKey() {
return _authSecretKey == null
? null
@@ -66,6 +426,24 @@ class Configuration extends BaseConfiguration {
: CryptoUtil.base642bin(_offlineAuthKey!);
}
Uint8List getRecoveryKey() {
final keyAttributes = getKeyAttributes()!;
return CryptoUtil.decryptSync(
CryptoUtil.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey),
getKey()!,
CryptoUtil.base642bin(keyAttributes.recoveryKeyDecryptionNonce),
);
}
// Caution: This directory is cleared on app start
String getTempDirectory() {
return _tempDirectory;
}
bool hasConfiguredAccount() {
return getToken() != null && _key != null;
}
bool hasOptedForOfflineMode() {
return _preferences.getBool(hasOptedForOfflineModeKey) ?? false;
}
@@ -86,4 +464,16 @@ class Configuration extends BaseConfiguration {
}
await _preferences.setBool(hasOptedForOfflineModeKey, true);
}
void setVolatilePassword(String volatilePassword) {
_volatilePassword = volatilePassword;
}
void resetVolatilePassword() {
_volatilePassword = null;
}
String? getVolatilePassword() {
return _volatilePassword;
}
}

View File

@@ -3,12 +3,13 @@ import 'dart:collection';
import 'dart:core';
import 'dart:io';
import 'package:ente_logging/tunneled_transport.dart';
import 'package:ente_auth/core/logging/tunneled_transport.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:logging/logging.dart' as log;
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
@@ -16,9 +17,6 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
/// Type definition for functions that may return a value or Future
typedef FutureOrVoidCallback = dynamic Function();
extension SuperString on String {
Iterable<String> chunked(int chunkSize) sync* {
var start = 0;
@@ -36,7 +34,7 @@ extension SuperString on String {
}
}
extension SuperLogRecord on log.LogRecord {
extension SuperLogRecord on LogRecord {
String toPrettyString([String? extraLines]) {
final header = "[$loggerName] [$level] [$time]";
@@ -138,7 +136,7 @@ class LogConfig {
class SuperLogging {
/// The logger for SuperLogging
static final $ = log.Logger('ente_logging');
static final $ = Logger('ente_logging');
/// The current super logging configuration
static late LogConfig config;
@@ -170,8 +168,8 @@ class SuperLogging {
setupSentry().ignore();
}
log.Logger.root.level = log.Level.ALL;
log.Logger.root.onRecord.listen(onLogRecord);
Logger.root.level = Level.ALL;
Logger.root.onRecord.listen(onLogRecord);
if (!enable) {
$.info("detected debug mode; sentry & file logging disabled.");
@@ -226,7 +224,7 @@ class SuperLogging {
static String _lastExtraLines = '';
static Future onLogRecord(log.LogRecord rec) async {
static Future onLogRecord(LogRecord rec) async {
// log misc info if it changed
String? extraLines = "app version: '$appVersion'\n";
if (extraLines != _lastExtraLines) {

View File

@@ -1,13 +1,14 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_events/event_bus.dart';
import 'package:ente_events/models/endpoint_updated_event.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:native_dio_adapter/native_dio_adapter.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:ua_client_hints/ua_client_hints.dart';
import 'package:uuid/uuid.dart';
int kConnectTimeout = 15000;
@@ -16,19 +17,20 @@ class Network {
late Dio _dio;
late Dio _enteDio;
Future<void> init(BaseConfiguration configuration) async {
final String ua = await userAgent();
final packageInfo = await PackageInfo.fromPlatform();
final version = packageInfo.version;
final packageName = packageInfo.packageName;
final endpoint = configuration.getHttpEndpoint();
final isMobile = Platform.isAndroid || Platform.isIOS;
Future<void> init() async {
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: Duration(milliseconds: kConnectTimeout),
headers: {
HttpHeaders.userAgentHeader: isMobile ? ua : Platform.operatingSystem,
HttpHeaders.userAgentHeader: PlatformUtil.isMobile()
? FkUserAgent.userAgent
: Platform.operatingSystem,
'X-Client-Version': version,
'X-Client-Package': packageName,
},
@@ -40,8 +42,8 @@ class Network {
baseUrl: endpoint,
connectTimeout: Duration(milliseconds: kConnectTimeout),
headers: {
if (isMobile)
HttpHeaders.userAgentHeader: ua
if (PlatformUtil.isMobile())
HttpHeaders.userAgentHeader: FkUserAgent.userAgent
else
HttpHeaders.userAgentHeader: Platform.operatingSystem,
'X-Client-Version': version,
@@ -53,12 +55,12 @@ class Network {
_dio.httpClientAdapter = NativeAdapter();
_enteDio.httpClientAdapter = NativeAdapter();
_setupInterceptors(configuration);
_setupInterceptors(endpoint);
Bus.instance.on<EndpointUpdatedEvent>().listen((event) {
final endpoint = configuration.getHttpEndpoint();
final endpoint = Configuration.instance.getHttpEndpoint();
_enteDio.options.baseUrl = endpoint;
_setupInterceptors(configuration);
_setupInterceptors(endpoint);
});
}
@@ -69,12 +71,12 @@ class Network {
Dio getDio() => _dio;
Dio get enteDio => _enteDio;
void _setupInterceptors(BaseConfiguration configuration) {
void _setupInterceptors(String endpoint) {
_dio.interceptors.clear();
_dio.interceptors.add(RequestIdInterceptor());
_enteDio.interceptors.clear();
_enteDio.interceptors.add(EnteRequestInterceptor(configuration));
_enteDio.interceptors.add(EnteRequestInterceptor(endpoint));
}
}
@@ -88,21 +90,21 @@ class RequestIdInterceptor extends Interceptor {
}
class EnteRequestInterceptor extends Interceptor {
final BaseConfiguration configuration;
final String endpoint;
EnteRequestInterceptor(this.configuration);
EnteRequestInterceptor(this.endpoint);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (kDebugMode) {
assert(
options.baseUrl == configuration.getHttpEndpoint(),
options.baseUrl == endpoint,
"interceptor should only be used for API endpoint",
);
}
options.headers
.putIfAbsent("x-request-id", () => const Uuid().v4().toString());
final String? tokenValue = configuration.getToken();
final String? tokenValue = Configuration.instance.getToken();
if (tokenValue != null) {
options.headers.putIfAbsent("X-Auth-Token", () => tokenValue);
}

View File

@@ -1,3 +1,3 @@
import 'package:ente_events/models/event.dart';
import 'package:ente_auth/events/event.dart';
class CodesUpdatedEvent extends Event {}

View File

@@ -0,0 +1,3 @@
import 'package:ente_auth/events/event.dart';
class EndpointUpdatedEvent extends Event {}

View File

@@ -1,3 +1,3 @@
import 'package:ente_events/models/event.dart';
import 'package:ente_auth/events/event.dart';
class IconsChangedEvent extends Event {}

View File

@@ -0,0 +1,5 @@
import 'package:ente_auth/events/event.dart';
// NotificationEvent event is used to re-fresh the UI to show latest notification
// (if any)
class NotificationEvent extends Event {}

View File

@@ -0,0 +1,3 @@
import 'package:ente_auth/events/event.dart';
class SignedInEvent extends Event {}

View File

@@ -1 +1,3 @@
// TODO Implement this library.
import 'package:ente_auth/events/event.dart';
class SignedOutEvent extends Event {}

View File

@@ -1,3 +1,3 @@
import 'package:ente_events/models/event.dart';
import 'package:ente_auth/events/event.dart';
class TriggerLogoutEvent extends Event {}

View File

@@ -1,3 +1,3 @@
import 'package:ente_events/models/event.dart';
import 'package:ente_auth/events/event.dart';
class UserDetailsChangedEvent extends Event {}

View File

@@ -1,8 +1,8 @@
import 'package:dio/dio.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/models/authenticator/auth_entity.dart';
import 'package:ente_auth/models/authenticator/auth_key.dart';
import 'package:ente_network/network.dart';
class AuthenticatorGateway {
late Dio _enteDio;

View File

@@ -111,7 +111,6 @@
"importAegisGuide": "Použijte možnost \"Export the vault\" v nastavení aplikace Aegis.",
"import2FasGuide": "Použijte možnost \"Settings->Backup -Export\" v 2FA.\n\nPokud je Vaše záloha šifrovaná, budete muset zadat heslo pro její odemčení",
"importLastpassGuide": "V nastavení aplikace Lastpass Authenticator vyberte možnost \"Transfer accounts\" a poté \"Export accounts to file\". Vygenerovaný soubor JSON následně nahrajte sem.",
"importProtonAuthGuide": "K exportu kódů použijte možnost „Exportovat“ v nastavení aplikace Proton Authenticator.",
"exportCodes": "Exportovat kódy",
"importLabel": "Importovat",
"importInstruction": "Vyberte, prosím, soubor obsahující seznam Vašich kódů v následujícím formátu",
@@ -125,7 +124,6 @@
"authToChangeYourEmail": "Pro změnu svého e-mailu se, prosím, ověřte",
"authToChangeYourPassword": "Pro změnu svého hesla se, prosím, ověřte",
"authToViewSecrets": "Pro zobrazení svých tajných údajů se musíte ověřit",
"authToInitiateSignIn": "Proveďte ověření a přihlaste se k zálohování.",
"ok": "Ok",
"cancel": "Zrušit",
"yes": "Ano",
@@ -173,7 +171,6 @@
"invalidQRCode": "Neplatný QR kód",
"noRecoveryKeyTitle": "Nemáte obnovovací klíč?",
"enterEmailHint": "Zadejte svou e-mailovou adresu",
"enterNewEmailHint": "Zadejte svou novou e-mailovou adresu",
"invalidEmailTitle": "Neplatná e-mailová adresa",
"invalidEmailMessage": "Prosím, zadejte platnou e-mailovou adresu.",
"deleteAccount": "Odstranit účet",
@@ -512,19 +509,6 @@
"supportEnte": "Podpořte <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Dejte nám hvězdu na Githubu",
"free5GB": "5GB zdarma na <bold-green>ente</bold-green> Fotky",
"loginWithAuthAccount": "Přihlaste se pomocí svého účtu Auth",
"freeStorageOffer": "10% sleva na <bold-green>ente</bold-green> fotky",
"freeStorageOfferDescription": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok",
"advanced": "Pokročilé",
"algorithm": "Algoritmus",
"type": "Typ",
"period": "Období",
"digits": "Digitální",
"importFromGallery": "Importovat z galerie",
"errorCouldNotReadImage": "Nelze přečíst vybraný obrazový soubor.",
"errorInvalidQRCode": "Neplatný QR kód",
"errorInvalidQRCodeBody": "Naskenovaný QR kód není platným účtem 2FA.",
"errorNoQRCode": "Nenalezen žádný QR kód",
"errorGenericTitle": "Došlo k chybě",
"errorGenericBody": "Při importu došlo k neočekávané chybě."
"freeStorageOfferDescription": "Použijte kód \"AUTH\" pro získání 10% slevy na první rok"
}

View File

@@ -111,7 +111,6 @@
"importAegisGuide": "Verwenden Sie die Option \"Tresor exportieren\" in den Einstellungen von Aegis.\n\nFalls Ihr Tresor verschlüsselt ist, müssen Sie das Passwort für den Tresor eingeben, um ihn zu entschlüsseln.",
"import2FasGuide": "Verwenden Sie unter \"Einstellungen → Backup\" die Option \"Exportieren\" in 2FAS.\n\nFalls Ihr Backup verschlüsselt ist, müssen Sie das Passwort eingeben, um das Backup zu entschlüsseln.",
"importLastpassGuide": "Verwenden Sie die Option \"Konten übertragen → Konten in Datei exportieren\" in den Lastpass Authenticator Einstellungen. \nImportieren Sie anschließend die heruntergeladene JSON-Datei.",
"importProtonAuthGuide": "Verwenden Sie die Option \"Exportieren\" in den Proton Authenticator Settings um Ihre Codes zu exportieren.",
"exportCodes": "Codes exportieren",
"importLabel": "Importieren",
"importInstruction": "Bitte wählen Sie eine Datei die Codes in folgendem Format beinhaltet",
@@ -520,12 +519,5 @@
"algorithm": "Algorithmus",
"type": "Typ",
"period": "Periode",
"digits": "Ziffern",
"importFromGallery": "Aus Galerie importieren",
"errorCouldNotReadImage": "Die ausgewählte Bild-Datei konnte nicht verarbeitet werden.",
"errorInvalidQRCode": "Ungültiger QR-Code",
"errorInvalidQRCodeBody": "Der gescannte QR-Code ist kein gültiges 2FA-Konto.",
"errorNoQRCode": "Kein QR-Code gefunden",
"errorGenericTitle": "Ein Fehler ist aufgetreten",
"errorGenericBody": "Beim Importieren ist ein unerwarteter Fehler aufgetreten."
"digits": "Ziffern"
}

View File

@@ -88,8 +88,6 @@
"useRecoveryKey": "Χρήση κλειδιού ανάκτησης",
"incorrectPasswordTitle": "Λάθος κωδικός πρόσβασης",
"welcomeBack": "Καλωσορίσατε και πάλι!",
"emailAlreadyRegistered": "Το email είναι ήδη καταχωρημένο.",
"emailNotRegistered": "Το email δεν έχει καταχωρηθεί.",
"madeWithLoveAtPrefix": "φτιαγμένη με ❤️ στο ",
"supportDevs": "Εγγραφείτε στο <bold-green>ente</bold-green> για να μας υποστηρίξετε",
"supportDiscount": "Χρησιμοποιήστε τον κωδικό κουπονιού \"AUTH\" για να λάβετε 10% έκπτωση για τον πρώτο χρόνο",
@@ -173,7 +171,6 @@
"invalidQRCode": "Μη έγκυρος κωδικός QR",
"noRecoveryKeyTitle": "Χωρίς κλειδί ανάκτησης;",
"enterEmailHint": "Εισάγετε τη διεύθυνση email σας",
"enterNewEmailHint": "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας",
"invalidEmailTitle": "Μη έγκυρη διεύθυνση email",
"invalidEmailMessage": "Παρακαλούμε εισάγετε μια έγκυρη διεύθυνση email.",
"deleteAccount": "Διαγραφή λογαριασμού",
@@ -261,10 +258,6 @@
"areYouSureYouWantToLogout": "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;",
"yesLogout": "Ναι, αποσύνδεση",
"exit": "Εξοδος",
"theme": "Θέμα",
"lightTheme": "Φωτεινό",
"darkTheme": "Σκοτεινό",
"systemTheme": "Σύστημα",
"verifyingRecoveryKey": "Επαλήθευση κλειδιού ανάκτησης...",
"recoveryKeyVerified": "Το κλειδί ανάκτησης επαληθεύτηκε",
"recoveryKeySuccessBody": "Τέλεια! Το κλειδί ανάκτησης σας είναι έγκυρο. Σας ευχαριστούμε για την επαλήθευση.\n\nΠαρακαλώ θυμηθείτε να κρατήσετε το κλειδί ανάκτησης σας και σε αντίγραφο ασφαλείας.",
@@ -497,24 +490,5 @@
"appLockNotEnabled": "Το κλείδωμα εφαρμογής δεν είναι ενεργοποιημένο",
"appLockNotEnabledDescription": "Παρακαλώ ενεργοποιήστε το κλείδωμα εφαρμογής μέσω της επιλογής Ασφάλεια > Κλείδωμα εφαρμογής",
"authToViewPasskey": "Παρακαλώ πιστοποιηθείτε για να δείτε το κλειδί πρόσβασης",
"appLockOfflineModeWarning": "Έχετε επιλέξει να προχωρήσετε χωρίς αντίγραφα ασφαλείας. Αν ξεχάσετε τον κωδικό της εφαρμογής, θα κλειδωθείτε από την πρόσβαση στα δεδομένα σας.",
"duplicateCodes": "Διπλότυποι κωδικοί",
"noDuplicates": "✨ Δεν υπάρχουν διπλότυπα",
"youveNoDuplicateCodesThatCanBeCleared": "Δεν υπάρχουν διπλότυπα αρχεία που μπορούν να εκκαθαριστούν",
"deduplicateCodes": "Διπλότυποι κωδικοί",
"deselectAll": "Αποεπιλογή όλων",
"selectAll": "Επιλογή όλων",
"deleteDuplicates": "Διαγραφή διπλότυπων",
"plainHTML": "Απλό HTML",
"dropReviewiOS": "Αφήστε μια κριτική στο App Store",
"dropReviewAndroid": "Αφήστε μια κριτική στο Play Store",
"giveUsAStarOnGithub": "Δώστε μας ένα αστέρι στο Github",
"free5GB": "5GB δωρεάν στο <bold-green>ente</bold-green> Photos",
"freeStorageOffer": "10% έκπτωση στο <bold-green>ente</bold-green> photos",
"freeStorageOfferDescription": "Χρησιμοποιήστε τον κωδικό \"AUTH\" για να λάβετε 10% έκπτωση για τον πρώτο χρόνο",
"advanced": "Για προχωρημένους",
"algorithm": "Αλγόριθμος",
"type": "Τύπος",
"period": "Περίοδος",
"digits": "Ψηφία"
"appLockOfflineModeWarning": "Έχετε επιλέξει να προχωρήσετε χωρίς αντίγραφα ασφαλείας. Αν ξεχάσετε τον κωδικό της εφαρμογής, θα κλειδωθείτε από την πρόσβαση στα δεδομένα σας."
}

View File

@@ -111,7 +111,6 @@
"importAegisGuide": "Use the \"Export the vault\" option in Aegis's Settings.\n\nIf your vault is encrypted, you will need to enter vault password to decrypt the vault.",
"import2FasGuide": "Use the \"Settings->Backup -Export\" option in 2FAS.\n\nIf your backup is encrypted, you will need to enter the password to decrypt the backup",
"importLastpassGuide": "Use the \"Transfer accounts\" option within Lastpass Authenticator Settings and press \"Export accounts to file\". Import the JSON downloaded.",
"importProtonAuthGuide": "Use the \"Export\" option in Proton Authenticator Settings to export your codes.",
"exportCodes": "Export codes",
"importLabel": "Import",
"importInstruction": "Please select a file that contains a list of your codes in the following format",
@@ -520,12 +519,5 @@
"algorithm": "Algorithm",
"type": "Type",
"period": "Period",
"digits": "Digits",
"importFromGallery": "Import from gallery",
"errorCouldNotReadImage": "Could not read the selected image file.",
"errorInvalidQRCode": "Invalid QR Code",
"errorInvalidQRCodeBody": "The scanned QR code is not a valid 2FA account.",
"errorNoQRCode": "No QR code found",
"errorGenericTitle": "An Error Occurred",
"errorGenericBody": "An unexpected error occurred while importing."
"digits": "Digits"
}

View File

@@ -111,7 +111,6 @@
"importAegisGuide": "Utilisez l'option \"Exporter le coffre-fort\" dans les paramètres d'Aegis.\n\nSi votre coffre-fort est crypté, vous devrez saisir le mot de passe du coffre-fort pour déchiffrer le coffre-fort.",
"import2FasGuide": "Utilisez l'option \"Paramètres->Sauvegarde -Export\" dans 2FAS.\n\nSi votre sauvegarde est chiffrée, vous devrez entrer le mot de passe pour déchiffrer la sauvegarde",
"importLastpassGuide": "Utilisez l'option \"Transférer des comptes\" dans les paramètres de l'authentificateur Lastpass et appuyez sur \"Exporter des comptes vers un fichier\". Importez le JSON téléchargé.",
"importProtonAuthGuide": "Utilisez l'option \"Export\" dans les paramètres de Proton Authenticator pour exporter vos codes.",
"exportCodes": "Exporter les codes",
"importLabel": "Importer",
"importInstruction": "Veuillez sélectionner un fichier qui contient une liste de vos codes dans le format suivant",
@@ -520,12 +519,5 @@
"algorithm": "Algorithme",
"type": "Type",
"period": "Période",
"digits": "Chiffres",
"importFromGallery": "Importer depuis la galerie",
"errorCouldNotReadImage": "Impossible de lire le fichier sélectionné.",
"errorInvalidQRCode": "QR Code invalide",
"errorInvalidQRCodeBody": "Le code QR scanné n'est pas un compte 2FA valide.",
"errorNoQRCode": "Aucun code QR trouvé",
"errorGenericTitle": "Une erreur s'est produite",
"errorGenericBody": "Une erreur inattendue est survenue lors de l'importation."
"digits": "Chiffres"
}

View File

@@ -111,7 +111,6 @@
"importAegisGuide": "Naudokite „Aegis“ nustatymuose esančią parinktį Eksportuoti slėptuvę.\n\nJei jūsų saugykla užšifruota, turėsite įvesti saugyklos slaptažodį, kad iššifruotumėte saugyklą.",
"import2FasGuide": "Naudokite programoje 2FAS esančią parinktį „Settings->2FAS Backup->Export to file“.\n\nJei atsarginė kopija užšifruota, turėsite įvesti slaptažodį, kad iššifruotumėte atsarginę kopiją.",
"importLastpassGuide": "Naudokite „Lastpass Authenticator“ nustatymuose esančią parinktį „Transfer accounts“ (perkelti paskyras) ir paspauskite „Export accounts to file“ (eksportuoti paskyras į failą). Importuokite atsisiųstą JSON failą.",
"importProtonAuthGuide": "Naudokite „Proton Authenticator“ nustatymuose esančią parinktį „Export“ (eksportuoti), kad eksportuotumėte savo kodus.",
"exportCodes": "Eksportuoti kodus",
"importLabel": "Importuoti",
"importInstruction": "Pasirinkite failą, kuriame yra tokio formato jūsų kodų sąrašas",
@@ -520,12 +519,5 @@
"algorithm": "Algoritmas",
"type": "Tipas",
"period": "Laikotarpis",
"digits": "Skaitmenys",
"importFromGallery": "Importuoti iš galerijos",
"errorCouldNotReadImage": "Nepavyko perskaityti pasirinkto vaizdo failo.",
"errorInvalidQRCode": "Netinkamas QR kodas",
"errorInvalidQRCodeBody": "Nuskenuotas QR kodas nėra tinkama 2FA paskyra.",
"errorNoQRCode": "QR kodas nerastas.",
"errorGenericTitle": "Įvyko klaida",
"errorGenericBody": "Importuojant įvyko netikėta klaida."
"digits": "Skaitmenys"
}

View File

@@ -45,7 +45,7 @@
"timeBasedKeyType": "Oparte na czasie (TOTP)",
"counterBasedKeyType": "Oparte na liczniku (HOTP)",
"saveAction": "Zapisz",
"nextTotpTitle": "dalej",
"nextTotpTitle": "następny",
"deleteCodeTitle": "Usunąć kod?",
"deleteCodeMessage": "Czy na pewno chcesz usunąć ten kod? Ta akcja jest nieodwracalna.",
"trashCode": "Przenieść kod do kosza?",
@@ -111,7 +111,6 @@
"importAegisGuide": "Użyj opcji \"Eksportuj sejf\" w ustawieniach Aegis.\n\nJeśli twój sejf jest zaszyfrowany, musisz wprowadzić hasło sejfu, aby odszyfrować sejf.",
"import2FasGuide": "Użyj opcji \"Ustawienia->Kopia Zapasowa-Eksport\" w 2FAS.\n\nJeśli twoja kopia zapasowa jest zaszyfrowana, musisz wprowadzić hasło, aby odszyfrować kopię zapasową",
"importLastpassGuide": "Użyj opcji \"Przenieś konta\" w Ustawieniach Lastpass Authenticator i naciśnij \"Eksportuj konta do pliku\". Zaimportuj pobrany plik JSON.",
"importProtonAuthGuide": "Użyj opcji „Eksportuj” w ustawieniach Proton Authenticator, aby wyeksportować kody.",
"exportCodes": "Eksportuj kody",
"importLabel": "Importuj",
"importInstruction": "Wybierz plik, który zawiera listę twoich kodów w następującym formacie",
@@ -520,12 +519,5 @@
"algorithm": "Algorytm",
"type": "Rodzaj",
"period": "Okres",
"digits": "Cyfry",
"importFromGallery": "Importuj z galerii",
"errorCouldNotReadImage": "Nie można odczytać wybranego pliku obrazu.",
"errorInvalidQRCode": "Nieprawidłowy kod QR",
"errorInvalidQRCodeBody": "Zeskanowany kod QR nie wskazuje na prawidłowe konto 2FA.",
"errorNoQRCode": "Nie znaleziono kodu QR",
"errorGenericTitle": "Wystąpił błąd",
"errorGenericBody": "Podczas importowania wystąpił nieoczekiwany błąd."
"digits": "Cyfry"
}

View File

@@ -18,7 +18,7 @@
"incorrectDetails": "Felaktiga uppgifter",
"pleaseVerifyDetails": "Kontrollera dina detaljer och försök igen",
"codeIssuerHint": "Utfärdare",
"codeSecretKeyHint": "Hemlig nyckel",
"codeSecretKeyHint": "Secret Key",
"secret": "Säkerhetsnyckel",
"all": "Alla",
"notes": "Anteckningar",
@@ -33,7 +33,7 @@
}
}
},
"codeAccountHint": "Konto (du@domain.com)",
"codeAccountHint": "Konto (du@domän.com)",
"codeTagHint": "Tagg",
"accountKeyType": "Typ av nyckel",
"sessionExpired": "Sessionen har gått ut",
@@ -68,7 +68,7 @@
"reportABug": "Rapportera en bugg",
"crashAndErrorReporting": "Krasch och felrapportering",
"reportBug": "Rapportera bugg",
"emailUsMessage": "Skicka e-post till {email}",
"emailUsMessage": "Skicka e-mail till {email}",
"@emailUsMessage": {
"placeholders": {
"email": {
@@ -79,7 +79,7 @@
"contactSupport": "Kontakta support",
"rateUsOnStore": "Betygsätt på {storeName}",
"blog": "Blogg",
"merchandise": "Produkter",
"merchandise": "Merchandise",
"verifyPassword": "Bekräfta lösenord",
"pleaseWait": "Vänligen vänta...",
"generatingEncryptionKeysTitle": "Skapar krypteringsnycklar...",
@@ -104,14 +104,13 @@
"importFromApp": "Importera koder från {appName}",
"importGoogleAuthGuide": "Exportera dina konton från Google Authenticator till en QR-kod med alternativet \"Överföra konton\". Använd sedan en annan enhet och skanna QR-koden.\n\nTips: Du kan använda din bärbara dators webbkamera för att ta en bild av QR-koden.",
"importSelectJsonFile": "Välj JSON-fil",
"importSelectAppExport": "Välj {appName} exporteringsfil",
"importSelectAppExport": "Välj {appName} exportfil",
"importEnteEncGuide": "Välj den krypterade JSON-filen som exporteras från Ente",
"importRaivoGuide": "Använd alternativet \"Exportera OTPs till zip-arkiv\" i Raivos inställningar.\n\nExtrahera zip-filen och importera JSON-filen.",
"importBitwardenGuide": "Använd alternativet \"Exportera valv\" inom Bitwarden Tools och importera den okrypterade JSON-filen.",
"importAegisGuide": "Använd alternativet \"Exportera valvet\" i Aegis inställningar.\n\nOm ditt valv är krypterat måste du ange valvlösenordet för att dekryptera valvet.",
"import2FasGuide": "Använd alternativet \"Inställningar->Säkerhetskopiera -Exportera\" i 2FAS.\n\nOm din säkerhetskopia är krypterad måste du ange lösenordet för att dekryptera säkerhetskopian.",
"importLastpassGuide": "Använd alternativet \"Överför konton\" i LastPass Authenticators inställningar och tryck på \"Exportera konton till fil\". Importera JSON-filen som laddas ner.",
"importProtonAuthGuide": "Använd alternativet \"Exportera\" i Proton Authenticator-inställningarna för att exportera koder.",
"exportCodes": "Exportera koder",
"importLabel": "Importera",
"importInstruction": "Vänligen välj en fil som innehåller en lista över dina koder i följande format",
@@ -120,11 +119,11 @@
"emailVerificationToggle": "E-postverifiering",
"emailVerificationEnableWarning": "För att undvika att bli låst från ditt konto, se till att spara en kopia av din e-post 2FA utanför Ente Auth innan du aktiverar e-postverifiering.",
"authToChangeEmailVerificationSetting": "Autentisera för att ändra din e-postadress",
"authenticateGeneric": "Vänligen autentisera",
"authenticateGeneric": "Var god autentisera",
"authToViewYourRecoveryKey": "Autentisera för att visa din återställningsnyckel",
"authToChangeYourEmail": "Autentisera för att ändra din e-postadress",
"authToChangeYourPassword": "Autentisera för att ändra ditt lösenord",
"authToViewSecrets": "Vänligen autentisera för att visa din återställningsnyckel",
"authToViewSecrets": "Autentisera för att visa din återställningsnyckel",
"authToInitiateSignIn": "Vänligen autentisera för att initiera inloggning för säkerhetskopiering.",
"ok": "OK",
"cancel": "Avbryt",
@@ -148,7 +147,7 @@
"leaveFamily": "Lämna familjen",
"leaveFamilyMessage": "Är du säker på att du vill lämna familjeplanen?",
"inFamilyPlanMessage": "Du är på en familjeplan!",
"hintForMobile": "Tryck länge på en kod för att redigera eller ta bort.",
"hintForMobile": "Håll i på en kod för att redigera eller ta bort.",
"hintForDesktop": "Högerklicka på en kod för att redigera eller ta bort.",
"scan": "Skanna",
"scanACode": "Skanna kod",
@@ -192,7 +191,7 @@
"oopsSomethingWentWrong": "Hoppsan! Något gick fel.",
"selectLanguage": "Välj språk",
"language": "Språk",
"social": "Socialt",
"social": "Social",
"security": "Säkerhet",
"lockscreen": "Låsskärm",
"authToChangeLockscreenSetting": "Vänligen autentisera för att ändra låsskärms inställningar",
@@ -201,7 +200,7 @@
"authToViewYourActiveSessions": "Autentisera för att visa dina aktiva sessioner",
"searchHint": "Sök...",
"search": "Sök",
"sorryUnableToGenCode": "Tyvärr, kunde inte generera en kod för {issuerName}",
"sorryUnableToGenCode": "Tyvärr, det gick inte att generera en kod för {issuerName}",
"noResult": "Inga resultat",
"addCode": "Lägg till kod",
"scanAQrCode": "Skanna en QR-kod",
@@ -216,7 +215,7 @@
"error": "Fel",
"recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp",
"recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.",
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ordsnyckeln på en säker plats.",
"recoveryKeySaveDescription": "Vi lagrar inte och har därför inte åtkomst till denna nyckel, vänligen spara denna 24 ords nyckel på en säker plats.",
"doThisLater": "Gör detta senare",
"saveKey": "Spara nyckel",
"save": "Spara",
@@ -255,7 +254,7 @@
"insecureDevice": "Osäker enhet",
"sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": "Tyvärr, kunde vi inte generera säkra nycklar på den här enheten.\n\nvänligen registrera dig från en annan enhet.",
"howItWorks": "Så här fungerar det",
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>totalsträckskrypterad</underline>.",
"ackPasswordLostWarning": "Jag förstår att om jag förlorar mitt lösenord kan jag förlora mina data eftersom min data är <underline>end-to-end-krypterad</underline>.",
"loginTerms": "Jag samtycker till <u-terms>användarvillkoren</u-terms> och <u-policy>integritetspolicyn</u-policy>",
"logInLabel": "Logga in",
"logout": "Logga ut",
@@ -279,7 +278,7 @@
"recoveryKeyVerifyReason": "Din återställningsnyckel är det enda sättet att återställa dina foton om du glömmer ditt lösenord. Du hittar din återställningsnyckel i Inställningar > Säkerhet.\n\nAnge din återställningsnyckel här för att verifiera att du har sparat den ordentligt.",
"confirmYourRecoveryKey": "Bekräfta din återställningsnyckel",
"confirm": "Bekräfta",
"emailYourLogs": "E-posta dina loggar",
"emailYourLogs": "Maila dina loggar",
"pleaseSendTheLogsTo": "Vänligen skicka loggarna till \n{toEmail}",
"copyEmailAddress": "Kopiera e-postadress",
"exportLogs": "Exportera loggar",
@@ -298,7 +297,7 @@
"criticalUpdateAvailable": "Kritisk uppdatering tillgänglig",
"updateAvailable": "Uppdatering tillgänglig",
"update": "Uppdatera",
"checking": "Kontrollerar...",
"checking": "Kontrollerar ...",
"youAreOnTheLatestVersion": "Du är på den senaste versionen",
"warning": "Varning",
"exportWarningDesc": "Den exporterade filen innehåller känslig information. Förvara den på ett säkert sätt.",
@@ -307,7 +306,7 @@
"description": "Text for the button to confirm the user understands the warning"
},
"authToExportCodes": "Autentisera för att exportera dina koder",
"importSuccessTitle": "Hurra!",
"importSuccessTitle": "Jippi!",
"importSuccessDesc": "Du har importerat {count} koder!",
"@importSuccessDesc": {
"placeholders": {
@@ -325,7 +324,7 @@
"checkInboxAndSpamFolder": "Vänligen kontrollera din inkorg (och skräppost) för att slutföra verifieringen",
"tapToEnterCode": "Tryck för att ange kod",
"resendEmail": "Skicka e-post igen",
"weHaveSendEmailTo": "Vi har skickat ett e-postmeddelande till <green>{email}</green>",
"weHaveSendEmailTo": "Vi har skickat ett mail till <green>{email}</green>",
"@weHaveSendEmailTo": {
"description": "Text to indicate that we have sent a mail to the user",
"placeholders": {
@@ -363,7 +362,7 @@
"selectExportFormat": "Välj exportformat",
"exportDialogDesc": "Krypterad export skyddas av ett lösenord som du väljer.",
"encrypted": "Krypterad",
"plainText": "Oformaterad text",
"plainText": "Enkel text",
"passwordToEncryptExport": "Lösenord för att kryptera export",
"export": "Exportera",
"useOffline": "Använd utan säkerhetskopior",
@@ -375,14 +374,14 @@
"compactMode": "Kompakt läge",
"shouldHideCode": "Dölj koder",
"doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden",
"focusOnSearchBar": "Fokusera på sök vid uppstart av app",
"focusOnSearchBar": "Fokusera på sök vid appstart",
"confirmUpdatingkey": "Är du säker på att du vill uppdatera den hemliga nyckeln?",
"minimizeAppOnCopy": "Minimera appen vid kopiering",
"editCodeAuthMessage": "Autentisera för att redigera kod",
"deleteCodeAuthMessage": "Autentisera för att radera kod",
"showQRAuthMessage": "Autentisera för att visa QR-kod",
"confirmAccountDeleteTitle": "Bekräfta radering av kontot",
"confirmAccountDeleteMessage": "Detta konto är kopplat till andra Ente applikationer, om du använder någon.\n\nDina uppladdade data, över alla Ente applikationer, kommer att schemaläggas för radering och ditt konto kommer att raderas permanent.",
"confirmAccountDeleteMessage": "Detta konto är kopplat till andra Ente apps, om du använder någon.\n\nDina uppladdade data, över alla Ente appar, kommer att schemaläggas för radering och ditt konto kommer att raderas permanent.",
"androidBiometricHint": "Verifiera identitet",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."
@@ -391,7 +390,7 @@
"@androidBiometricNotRecognized": {
"description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters."
},
"androidBiometricSuccess": "Lyckades",
"androidBiometricSuccess": "Slutförd",
"@androidBiometricSuccess": {
"description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters."
},
@@ -442,7 +441,7 @@
"signOutOtherDevices": "Logga ut andra enheter",
"doNotSignOut": "Logga inte ut",
"hearUsWhereTitle": "Hur hörde du talas om Ente? (valfritt)",
"hearUsExplanation": "Vi spårar inte installationer. Det skulle hjälpa oss om du berättade hur du hittade oss!",
"hearUsExplanation": "Vi spårar inte appinstallationer, Det skulle hjälpa oss om du berättade var du hittade oss!",
"recoveryKeySaved": "Återställningsnyckel sparad i nedladdningsmappen!",
"waitingForBrowserRequest": "Väntar på webbläsarbegäran...",
"waitingForVerification": "Väntar på verifiering...",
@@ -489,8 +488,6 @@
"hideContent": "Dölj innehåll",
"hideContentDescriptionAndroid": "Döljer appinnehåll i app-växlaren och inaktiverar skärmdumpar",
"hideContentDescriptioniOS": "Döljer appinnehåll i app-växlaren",
"autoLockFeatureDescription": "Tid efter vilken appen låses efter att ha satts i bakgrunden",
"appLockDescription": "Välj mellan enhetens förvalda låsskärm och en anpassad låsskärm med en PIN-kod eller lösenord.",
"pinLock": "Pinkodslås",
"enterPin": "Ange PIN-kod",
"setNewPin": "Ställ in ny PIN-kod",
@@ -501,31 +498,9 @@
"appLockOfflineModeWarning": "Du har valt att fortsätta utan säkerhetskopior. Om du glömmer ditt applås, kommer du att bli utelåst från att komma åt dina data.",
"duplicateCodes": "Dubblettkoder",
"noDuplicates": "✨ Inga dubbletter",
"youveNoDuplicateCodesThatCanBeCleared": "Du har inga dubbla koder som kan rensas",
"deduplicateCodes": "Deduplicera koder",
"deselectAll": "Avmarkera alla",
"selectAll": "Markera alla",
"deleteDuplicates": "Radera dubbletter",
"plainHTML": "Ren HTML",
"tellUsWhatYouThink": "Berätta vad du tycker",
"dropReviewiOS": "Skriv en recension på App Store",
"dropReviewAndroid": "Skriv en recension på Play Store",
"supportEnte": "Stöd <bold-green>ente</bold-green>",
"giveUsAStarOnGithub": "Ge oss en stjärna på Github",
"free5GB": "5 GB gratis på <bold-green>ente</bold-green> Foton",
"loginWithAuthAccount": "Logga in med ditt Auth-konto",
"freeStorageOffer": "10% rabatt på <bold-green>ente</bold-green> foton",
"freeStorageOfferDescription": "Använd koden \"AUTH\" för att få 10% rabatt första året",
"advanced": "Avancerad",
"algorithm": "Algoritm",
"type": "Typ",
"period": "Tidsperiod",
"digits": "Siffror",
"importFromGallery": "Importera från galleri",
"errorCouldNotReadImage": "Kunde inte läsa den valda bildfilen.",
"errorInvalidQRCode": "Ogiltig QR-kod",
"errorInvalidQRCodeBody": "Den skannade QR-koden är inte ett giltigt 2FA konto.",
"errorNoQRCode": "Ingen QR-kod hittades",
"errorGenericTitle": "Ett fel inträffade",
"errorGenericBody": "Ett oväntat fel inträffade vid import."
"plainHTML": "Ren HTML"
}

View File

@@ -6,7 +6,7 @@
"@counterAppBarTitle": {
"description": "Text shown in the AppBar of the Counter Page"
},
"onBoardingBody": "妥善保管您的双重认证代码",
"onBoardingBody": "妥善保管您的两步验证码",
"onBoardingGetStarted": "开始",
"setupFirstAccount": "设置您的第一个账户",
"importScanQrCode": "扫描二维码",
@@ -111,14 +111,13 @@
"importAegisGuide": "使用 Aegis 设置中的“导出密码库”选项。\n\n如果您的密码库已加密则需要输入密码库密码才能解密密码库。",
"import2FasGuide": "使用 2FAS 中的“设置 -> 备份 -> 导出”选项。\n\n如果您的备份已加密则需要输入密码来解密备份",
"importLastpassGuide": "使用 Lastpass Authenticator 设置中的“转移账户”选项,然后按“将账户导出到文件”。导入下载的 JSON。",
"importProtonAuthGuide": "使用 Proton Authenticator 设置中的“导出”选项导出您的代码。",
"exportCodes": "导出代码",
"importLabel": "导入",
"importInstruction": "请选择一个包含以下格式的代码列表的文件",
"importCodeDelimiterInfo": "代码可以用逗号或换行符分隔",
"selectFile": "选择文件",
"emailVerificationToggle": "电子邮件验证",
"emailVerificationEnableWarning": "为避免被锁在您的账户之外,请在启用电子邮件验证之前确保在 Ente Auth 之外存储电子邮件双重认证的副本。",
"emailVerificationEnableWarning": "为避免被锁在您的账户之外,请在启用电子邮件验证之前确保在 Ente Auth 之外存储电子邮件两步验证的副本。",
"authToChangeEmailVerificationSetting": "请进行身份验证以更改电子邮件验证",
"authenticateGeneric": "请验证",
"authToViewYourRecoveryKey": "请验证以查看您的恢复密钥",
@@ -156,7 +155,7 @@
"verifyEmail": "验证电子邮件",
"enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码",
"lostDeviceTitle": "丢失了设备吗?",
"twoFactorAuthTitle": "双重认证",
"twoFactorAuthTitle": "两步验证",
"passkeyAuthTitle": "通行密钥验证",
"verifyPasskey": "验证通行密钥",
"loginWithTOTP": "使用 TOTP 登录",
@@ -520,12 +519,5 @@
"algorithm": "算法",
"type": "类型",
"period": "周期",
"digits": "数字",
"importFromGallery": "从图库导入",
"errorCouldNotReadImage": "无法读取所选图片文件。",
"errorInvalidQRCode": "二维码无效",
"errorInvalidQRCodeBody": "扫描的二维码不是有效的双重认证账户。",
"errorNoQRCode": "未找到二维码",
"errorGenericTitle": "出错了",
"errorGenericBody": "导入时发生意外错误。"
"digits": "数字"
}

View File

@@ -6,4 +6,4 @@ export "package:ente_auth/l10n/arb/app_localizations.dart"
extension AppLocalizationsX on BuildContext {
AppLocalizations get l10n => AppLocalizations.of(this);
}
}

View File

@@ -2,38 +2,34 @@ import 'dart:async';
import 'dart:io';
import 'package:adaptive_theme/adaptive_theme.dart';
import 'package:ente_accounts/services/user_service.dart';
import "package:ente_auth/app/view/app.dart";
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/core/logging/super_logging.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/locale.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/services/billing_service.dart';
import 'package:ente_auth/services/notification_service.dart';
import 'package:ente_auth/services/preference_service.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/store/authenticator_db.dart';
import 'package:ente_auth/store/code_display_store.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/home_page.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/directory_utils.dart';
import 'package:ente_auth/utils/lock_screen_settings.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:ente_lock_screen/lock_screen_settings.dart';
import 'package:ente_lock_screen/ui/app_lock.dart';
import 'package:ente_lock_screen/ui/lock_screen.dart';
import 'package:ente_logging/logging.dart';
import 'package:ente_network/network.dart';
import 'package:ente_strings/l10n/strings_localizations.dart';
import 'package:ente_ui/theme/theme_config.dart';
import 'package:flutter/foundation.dart';
import "package:flutter/material.dart";
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';
@@ -93,13 +89,11 @@ void main() async {
}
Future<void> _runInForeground() async {
AppThemeConfig.initialize(EnteApp.auth);
final savedThemeMode = _themeMode(await AdaptiveTheme.getThemeMode());
final configuration = Configuration.instance;
return await _runWithLogs(() async {
_logger.info("Starting app in foreground");
try {
await init(false, via: 'mainMethod');
await _init(false, via: 'mainMethod');
} catch (e, s) {
_logger.severe("Failed to init", e, s);
rethrow;
@@ -109,19 +103,12 @@ Future<void> _runInForeground() async {
runApp(
AppLock(
builder: (args) => App(locale: locale),
lockScreen: LockScreen(configuration),
lockScreen: const LockScreen(),
enabled: await LockScreenSettings.instance.shouldShowLockScreen(),
locale: locale,
lightTheme: lightThemeData,
darkTheme: darkThemeData,
savedThemeMode: savedThemeMode,
localeListResolutionCallback: localResolutionCallBack,
localizationsDelegates: const [
...StringsLocalizations.localizationsDelegates,
...AppLocalizations.localizationsDelegates,
],
supportedLocales: appSupportedLocales,
backgroundLockLatency: const Duration(seconds: 0),
),
);
});
@@ -160,20 +147,20 @@ void _registerWindowsProtocol() {
}
}
Future<void> init(bool bool, {String? via}) async {
Future<void> _init(bool bool, {String? via}) async {
_registerWindowsProtocol();
await CryptoUtil.init();
await PreferenceService.instance.init();
await CodeStore.instance.init();
await CodeDisplayStore.instance.init();
await Configuration.instance.init([AuthenticatorDB.instance]);
await Network.instance.init(Configuration.instance);
await UserService.instance.init(Configuration.instance, const HomePage());
await Configuration.instance.init();
await Network.instance.init();
await UserService.instance.init();
await AuthenticatorService.instance.init();
await BillingService.instance.init();
await NotificationService.instance.init();
await UpdateService.instance.init();
await IconUtils.instance.init();
await LockScreenSettings.instance.init(Configuration.instance);
await LockScreenSettings.instance.init();
}

View File

@@ -81,7 +81,6 @@ class CompleteSRPSetupRequest {
);
}
}
class SrpAttributes {
final String srpUserID;
final String srpSalt;

View File

@@ -1,7 +1,7 @@
import "dart:typed_data";
import 'dart:typed_data';
import 'package:ente_base/models/key_attributes.dart';
import 'package:ente_base/models/private_key_attributes.dart';
import 'package:ente_auth/models/key_attributes.dart';
import 'package:ente_auth/models/private_key_attributes.dart';
class KeyGenResult {
final KeyAttributes keyAttributes;

View File

@@ -0,0 +1,65 @@
const freeProductID = "free";
const stripe = "stripe";
const appStore = "appstore";
const playStore = "playstore";
class Subscription {
final String productID;
final int storage;
final String originalTransactionID;
final String paymentProvider;
final int expiryTime;
final String price;
final String period;
final Attributes? attributes;
Subscription({
required this.productID,
required this.storage,
required this.originalTransactionID,
required this.paymentProvider,
required this.expiryTime,
required this.price,
required this.period,
this.attributes,
});
bool isValid() {
return expiryTime > DateTime.now().microsecondsSinceEpoch;
}
bool isYearlyPlan() {
return 'year' == period;
}
static fromMap(Map<String, dynamic>? map) {
if (map == null) return null;
return Subscription(
productID: map['productID'],
storage: map['storage'],
originalTransactionID: map['originalTransactionID'],
paymentProvider: map['paymentProvider'],
expiryTime: map['expiryTime'],
price: map['price'],
period: map['period'],
attributes: map["attributes"] != null
? Attributes.fromJson(map["attributes"])
: null,
);
}
}
class Attributes {
bool? isCancelled;
String? customerID;
Attributes({
this.isCancelled,
this.customerID,
});
Attributes.fromJson(dynamic json) {
isCancelled = json["isCancelled"];
customerID = json["customerID"];
}
}

View File

@@ -0,0 +1,146 @@
import 'dart:convert';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:ente_auth/models/subscription.dart';
class UserDetails {
final String email;
final int usage;
final int fileCount;
final int sharedCollectionsCount;
final Subscription subscription;
final FamilyData? familyData;
final ProfileData? profileData;
UserDetails(
this.email,
this.usage,
this.fileCount,
this.sharedCollectionsCount,
this.subscription,
this.familyData,
this.profileData,
);
bool isPartOfFamily() {
return familyData?.members?.isNotEmpty ?? false;
}
bool isFamilyAdmin() {
assert(isPartOfFamily(), "verify user is part of family before calling");
final FamilyMember currentUserMember = familyData!.members!
.firstWhere((element) => element.email.trim() == email.trim());
return currentUserMember.isAdmin;
}
// getFamilyOrPersonalUsage will return total usage for family if user
// belong to family group. Otherwise, it will return storage consumed by
// current user
int getFamilyOrPersonalUsage() {
return isPartOfFamily() ? familyData!.getTotalUsage() : usage;
}
int getFreeStorage() {
return max(
isPartOfFamily()
? (familyData!.storage - familyData!.getTotalUsage())
: (subscription.storage - (usage)),
0,
);
}
int getTotalStorage() {
return isPartOfFamily() ? familyData!.storage : subscription.storage;
}
factory UserDetails.fromMap(Map<String, dynamic> map) {
return UserDetails(
map['email'] as String,
map['usage'] as int,
(map['fileCount'] ?? 0) as int,
(map['sharedCollectionsCount'] ?? 0) as int,
Subscription.fromMap(map['subscription']),
FamilyData.fromMap(map['familyData']),
ProfileData.fromJson(map['profileData']),
);
}
}
class FamilyMember {
final String email;
final int usage;
final String id;
final bool isAdmin;
FamilyMember(this.email, this.usage, this.id, this.isAdmin);
factory FamilyMember.fromMap(Map<String, dynamic> map) {
return FamilyMember(
(map['email'] ?? '') as String,
map['usage'] as int,
map['id'] as String,
map['isAdmin'] as bool,
);
}
}
class ProfileData {
bool canDisableEmailMFA;
bool isEmailMFAEnabled;
bool isTwoFactorEnabled;
// Constructor with default values
ProfileData({
this.canDisableEmailMFA = false,
this.isEmailMFAEnabled = false,
this.isTwoFactorEnabled = false,
});
// Factory method to create ProfileData instance from JSON
factory ProfileData.fromJson(Map<String, dynamic>? json) {
if (json == null) null;
return ProfileData(
canDisableEmailMFA: json!['canDisableEmailMFA'] ?? false,
isEmailMFAEnabled: json['isEmailMFAEnabled'] ?? false,
isTwoFactorEnabled: json['isTwoFactorEnabled'] ?? false,
);
}
// Method to convert ProfileData instance to JSON
Map<String, dynamic> toJson() {
return {
'canDisableEmailMFA': canDisableEmailMFA,
'isEmailMFAEnabled': isEmailMFAEnabled,
'isTwoFactorEnabled': isTwoFactorEnabled,
};
}
String toJsonString() => json.encode(toJson());
}
class FamilyData {
final List<FamilyMember>? members;
// Storage available based on the family plan
final int storage;
final int expiryTime;
FamilyData(this.members, this.storage, this.expiryTime);
int getTotalUsage() {
return members!.map((e) => e.usage).toList().sum;
}
static fromMap(Map<String, dynamic>? map) {
if (map == null) return null;
assert(map['members'] != null && map['members'].length >= 0);
final members = List<FamilyMember>.from(
map['members'].map((x) => FamilyMember.fromMap(x)),
);
return FamilyData(
members,
map['storage'] as int,
map['expiryTime'] as int,
);
}
}

View File

@@ -1,18 +1,19 @@
import 'dart:async';
import 'dart:io';
import 'package:ente_accounts/pages/email_entry_page.dart';
import 'package:ente_accounts/pages/login_page.dart';
import 'package:ente_accounts/pages/password_entry_page.dart';
import 'package:ente_accounts/pages/password_reentry_page.dart';
import 'package:ente_auth/app/view/app.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/events/trigger_logout_event.dart';
import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/locale.dart';
import 'package:ente_auth/theme/text_style.dart';
import 'package:ente_auth/ui/account/email_entry_page.dart';
import 'package:ente_auth/ui/account/login_page.dart';
import 'package:ente_auth/ui/account/logout_dialog.dart';
import 'package:ente_auth/ui/account/password_entry_page.dart';
import 'package:ente_auth/ui/account/password_reentry_page.dart';
import 'package:ente_auth/ui/common/gradient_button.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.dart';
@@ -23,7 +24,6 @@ import 'package:ente_auth/ui/settings/language_picker.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/navigation_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_events/event_bus.dart';
import 'package:flutter/foundation.dart';
import "package:flutter/material.dart";
import 'package:local_auth/local_auth.dart';
@@ -260,22 +260,17 @@ class _OnboardingPageState extends State<OnboardingPage> {
void _navigateToSignUpPage() {
Widget page;
if (Configuration.instance.getEncryptedToken() == null) {
page = EmailEntryPage(Configuration.instance);
page = const EmailEntryPage();
} else {
// No key
if (Configuration.instance.getKeyAttributes() == null) {
// Never had a key
page = PasswordEntryPage(
Configuration.instance,
PasswordEntryMode.set,
const HomePage(),
page = const PasswordEntryPage(
mode: PasswordEntryMode.set,
);
} else if (Configuration.instance.getKey() == null) {
// Yet to decrypt the key
page = PasswordReentryPage(
Configuration.instance,
const HomePage(),
);
page = const PasswordReentryPage();
} else {
// All is well, user just has not subscribed
page = const HomePage();
@@ -293,22 +288,17 @@ class _OnboardingPageState extends State<OnboardingPage> {
void _navigateToSignInPage() {
Widget page;
if (Configuration.instance.getEncryptedToken() == null) {
page = LoginPage(Configuration.instance);
page = const LoginPage();
} else {
// No key
if (Configuration.instance.getKeyAttributes() == null) {
// Never had a key
page = PasswordEntryPage(
Configuration.instance,
PasswordEntryMode.set,
const HomePage(),
page = const PasswordEntryPage(
mode: PasswordEntryMode.set,
);
} else if (Configuration.instance.getKey() == null) {
// Yet to decrypt the key
page = PasswordReentryPage(
Configuration.instance,
const HomePage(),
);
page = const PasswordReentryPage();
} else {
// All is well, user just has not subscribed
// page = getSubscriptionPage(isOnBoarding: true);

View File

@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
import "package:ente_auth/l10n/l10n.dart";
import 'package:ente_auth/models/all_icon_data.dart';
@@ -22,7 +23,6 @@ import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:ente_events/event_bus.dart';
import "package:flutter/material.dart";
import 'package:logging/logging.dart';

View File

@@ -4,7 +4,9 @@ import 'dart:math';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
import 'package:ente_auth/events/signed_in_event.dart';
import 'package:ente_auth/events/trigger_logout_event.dart';
import 'package:ente_auth/gateway/authenticator.dart';
import 'package:ente_auth/models/authenticator/auth_entity.dart';
@@ -15,8 +17,6 @@ import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/store/authenticator_db.dart';
import 'package:ente_auth/store/offline_authenticator_db.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:ente_events/event_bus.dart';
import 'package:ente_events/models/signed_in_event.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';

View File

@@ -1,11 +1,11 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/models/billing_plan.dart';
import 'package:ente_network/network.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/models/billing_plan.dart';
import 'package:ente_auth/models/subscription.dart';
import 'package:logging/logging.dart';
const kWebPaymentRedirectUrl = "https://payments.ente.io/frameRedirect";

View File

@@ -1,12 +1,12 @@
import 'dart:io';
import 'package:ente_lock_screen/auth_util.dart';
import 'package:ente_lock_screen/lock_screen_settings.dart';
import 'package:ente_lock_screen/ui/app_lock.dart';
import 'package:ente_lock_screen/ui/lock_screen_password.dart';
import 'package:ente_lock_screen/ui/lock_screen_pin.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_ui/utils/toast_util.dart';
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_password.dart';
import 'package:ente_auth/ui/settings/lock_screen/lock_screen_pin.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/lock_screen_settings.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

View File

@@ -1,6 +1,6 @@
import 'package:ente_accounts/services/user_service.dart';
import 'package:ente_network/network.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:url_launcher/url_launcher_string.dart';

View File

@@ -1,5 +1,5 @@
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/icons_changed_event.dart';
import 'package:ente_events/event_bus.dart';
import 'package:shared_preferences/shared_preferences.dart';
enum CodeSortKey {

View File

@@ -2,9 +2,9 @@ import 'dart:async';
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:ente_network/network.dart';
import 'package:logging/logging.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';

View File

@@ -1,39 +1,38 @@
import 'dart:async';
import "dart:convert";
import "dart:io";
import "dart:math";
import 'package:bip39/bip39.dart' as bip39;
import 'package:dio/dio.dart';
import 'package:ente_accounts/models/delete_account.dart';
import 'package:ente_accounts/models/errors.dart';
import 'package:ente_accounts/models/sessions.dart';
import 'package:ente_accounts/models/set_keys_request.dart';
import 'package:ente_accounts/models/set_recovery_key_request.dart';
import 'package:ente_accounts/models/srp.dart';
import 'package:ente_accounts/models/two_factor.dart';
import 'package:ente_accounts/models/user_details.dart';
import 'package:ente_accounts/pages/login_page.dart';
import 'package:ente_accounts/pages/ott_verification_page.dart';
import 'package:ente_accounts/pages/passkey_page.dart';
import 'package:ente_accounts/pages/password_entry_page.dart';
import 'package:ente_accounts/pages/password_reentry_page.dart';
import 'package:ente_accounts/pages/recovery_page.dart';
import 'package:ente_accounts/pages/two_factor_authentication_page.dart';
import 'package:ente_accounts/pages/two_factor_recovery_page.dart';
import 'package:ente_base/models/key_attributes.dart';
import 'package:ente_base/models/key_gen_result.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_configuration/constants.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/constants.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/core/network.dart';
import 'package:ente_auth/events/user_details_changed_event.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/account/two_factor.dart';
import 'package:ente_auth/models/api/user/srp.dart';
import 'package:ente_auth/models/delete_account.dart';
import 'package:ente_auth/models/key_attributes.dart';
import 'package:ente_auth/models/key_gen_result.dart';
import 'package:ente_auth/models/sessions.dart';
import 'package:ente_auth/models/set_keys_request.dart';
import 'package:ente_auth/models/set_recovery_key_request.dart';
import 'package:ente_auth/models/user_details.dart';
import 'package:ente_auth/ui/account/login_page.dart';
import 'package:ente_auth/ui/account/ott_verification_page.dart';
import 'package:ente_auth/ui/account/password_entry_page.dart';
import 'package:ente_auth/ui/account/password_reentry_page.dart';
import 'package:ente_auth/ui/account/recovery_page.dart';
import 'package:ente_auth/ui/common/progress_dialog.dart';
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/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:ente_events/event_bus.dart';
import 'package:ente_events/models/user_details_changed_event.dart';
import 'package:ente_network/network.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/progress_dialog.dart';
import 'package:ente_ui/pages/base_home_page.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_ui/utils/toast_util.dart';
import "package:flutter/foundation.dart";
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
@@ -45,8 +44,6 @@ import "package:pointycastle/srp/srp6_verifier_generator.dart";
import 'package:shared_preferences/shared_preferences.dart';
import "package:uuid/uuid.dart";
const String kAccountsUrl = "https://accounts.ente.io";
class UserService {
static const keyHasEnabledTwoFactor = "has_enabled_two_factor";
static const keyUserDetails = "user_details";
@@ -57,19 +54,18 @@ class UserService {
final _dio = Network.instance.getDio();
final _enteDio = Network.instance.enteDio;
final _logger = Logger((UserService).toString());
final _config = Configuration.instance;
late SharedPreferences _preferences;
late ValueNotifier<String?> emailValueNotifier;
late BaseConfiguration _config;
late BaseHomePage _homePage;
UserService._privateConstructor();
static final UserService instance = UserService._privateConstructor();
Future<void> init(BaseConfiguration config, BaseHomePage homePage) async {
_config = config;
_homePage = homePage;
emailValueNotifier = ValueNotifier<String?>(config.getEmail());
Future<void> init() async {
emailValueNotifier =
ValueNotifier<String?>(Configuration.instance.getEmail());
_preferences = await SharedPreferences.getInstance();
}
@@ -81,7 +77,7 @@ class UserService {
bool isResetPasswordScreen = false,
String? purpose,
}) async {
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final response = await _dio.post(
@@ -89,7 +85,6 @@ class UserService {
data: {
"email": email,
"purpose": isChangeEmail ? "change" : purpose ?? "",
"mobile": Platform.isIOS || Platform.isAndroid,
},
);
await dialog.hide();
@@ -119,24 +114,24 @@ class UserService {
unawaited(
showErrorDialog(
context,
context.strings.oops,
context.strings.emailAlreadyRegistered,
context.l10n.oops,
context.l10n.emailAlreadyRegistered,
),
);
} else if (enteErrCode != null && enteErrCode == "USER_NOT_REGISTERED") {
unawaited(
showErrorDialog(
context,
context.strings.oops,
context.strings.emailNotRegistered,
context.l10n.oops,
context.l10n.emailNotRegistered,
),
);
} else if (e.response != null && e.response!.statusCode == 403) {
unawaited(
showErrorDialog(
context,
context.strings.oops,
context.strings.thisEmailIsAlreadyInUse,
context.l10n.oops,
context.l10n.thisEmailIsAlreadyInUse,
),
);
} else {
@@ -199,13 +194,6 @@ class UserService {
}
}
UserDetails? getCachedUserDetails() {
if (_preferences.containsKey(keyUserDetails)) {
return UserDetails.fromJson(_preferences.getString(keyUserDetails)!);
}
return null;
}
Future<Sessions> getActiveSessions() async {
try {
final response = await _enteDio.get("/users/sessions");
@@ -243,7 +231,7 @@ class UserService {
try {
final response = await _enteDio.post("/users/logout");
if (response.statusCode == 200) {
await _config.logout();
await Configuration.instance.logout();
Navigator.of(context).popUntil((route) => route.isFirst);
} else {
throw Exception("Log out action failed");
@@ -252,7 +240,7 @@ class UserService {
_logger.severe(e);
// check if token is already invalid
if (e is DioException && e.response?.statusCode == 401) {
await _config.logout();
await Configuration.instance.logout();
Navigator.of(context).popUntil((route) => route.isFirst);
return;
}
@@ -302,7 +290,7 @@ class UserService {
);
if (response.statusCode == 200) {
// clear data
await _config.logout();
await Configuration.instance.logout();
} else {
throw Exception("delete action failed");
}
@@ -339,7 +327,7 @@ class UserService {
Future<void> onPassKeyVerified(BuildContext context, Map response) async {
final ProgressDialog dialog =
createProgressDialog(context, context.strings.pleaseWait);
createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final userPassword = _config.getVolatilePassword();
@@ -350,10 +338,7 @@ class UserService {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return PasswordReentryPage(
_config,
_homePage,
);
return const PasswordReentryPage();
},
),
(route) => route.isFirst,
@@ -366,7 +351,7 @@ class UserService {
_config.getKeyAttributes()!,
);
_config.resetVolatilePassword();
page = _homePage;
page = const HomePage();
} else {
throw Exception("unexpected response during passkey verification");
}
@@ -394,7 +379,7 @@ class UserService {
String ott, {
bool isResettingPasswordScreen = false,
}) async {
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
final verifyData = {
"email": _config.getEmail(),
@@ -420,7 +405,6 @@ class UserService {
}
if (passkeySessionID.isNotEmpty) {
page = PasskeyPage(
_config,
passkeySessionID,
totp2FASessionID: twoFASessionID,
accountsUrl: accountsUrl,
@@ -429,23 +413,15 @@ class UserService {
page = TwoFactorAuthenticationPage(twoFASessionID);
} else {
await _saveConfiguration(response);
if (_config.getEncryptedToken() != null) {
if (Configuration.instance.getEncryptedToken() != null) {
if (isResettingPasswordScreen) {
page = RecoveryPage(
_config,
_homePage,
);
page = const RecoveryPage();
} else {
page = PasswordReentryPage(
_config,
_homePage,
);
page = const PasswordReentryPage();
}
} else {
page = PasswordEntryPage(
_config,
PasswordEntryMode.set,
_homePage,
page = const PasswordEntryPage(
mode: PasswordEntryMode.set,
);
}
}
@@ -468,16 +444,16 @@ class UserService {
if (e.response != null && e.response!.statusCode == 410) {
await showErrorDialog(
context,
context.strings.oops,
context.strings.yourVerificationCodeHasExpired,
context.l10n.oops,
context.l10n.yourVerificationCodeHasExpired,
);
Navigator.of(context).pop();
} else {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.incorrectCode,
context.strings.sorryTheCodeYouveEnteredIsIncorrect,
context.l10n.incorrectCode,
context.l10n.sorryTheCodeYouveEnteredIsIncorrect,
);
}
} catch (e) {
@@ -486,8 +462,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.verificationFailedPleaseTryAgain,
context.l10n.oops,
context.l10n.verificationFailedPleaseTryAgain,
);
}
}
@@ -502,7 +478,7 @@ class UserService {
String email,
String ott,
) async {
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final response = await _enteDio.post(
@@ -514,7 +490,7 @@ class UserService {
);
await dialog.hide();
if (response.statusCode == 200) {
showShortToast(context, context.strings.emailChangedTo(email));
showShortToast(context, context.l10n.emailChangedTo(email));
await setEmail(email);
Navigator.of(context).popUntil((route) => route.isFirst);
Bus.instance.fire(UserDetailsChangedEvent());
@@ -523,8 +499,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.verificationFailedPleaseTryAgain,
context.l10n.oops,
context.l10n.verificationFailedPleaseTryAgain,
);
} on DioException catch (e) {
await dialog.hide();
@@ -532,15 +508,15 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.thisEmailIsAlreadyInUse,
context.l10n.oops,
context.l10n.thisEmailIsAlreadyInUse,
);
} else {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.incorrectCode,
context.strings.authenticationFailedPleaseTryAgain,
context.l10n.incorrectCode,
context.l10n.authenticationFailedPleaseTryAgain,
);
}
} catch (e) {
@@ -549,8 +525,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.verificationFailedPleaseTryAgain,
context.l10n.oops,
context.l10n.verificationFailedPleaseTryAgain,
);
}
}
@@ -742,10 +718,9 @@ class UserService {
response.data["twoFactorSessionIDV2"] != null) {
twoFASessionID = response.data["twoFactorSessionIDV2"];
}
_config.setVolatilePassword(userPassword);
Configuration.instance.setVolatilePassword(userPassword);
if (passkeySessionID.isNotEmpty) {
page = PasskeyPage(
_config,
passkeySessionID,
totp2FASessionID: twoFASessionID,
accountsUrl: accountsUrl,
@@ -754,13 +729,13 @@ class UserService {
page = TwoFactorAuthenticationPage(twoFASessionID);
} else {
await _saveConfiguration(response);
if (_config.getEncryptedToken() != null) {
await _config.decryptSecretsAndGetKeyEncKey(
if (Configuration.instance.getEncryptedToken() != null) {
await Configuration.instance.decryptSecretsAndGetKeyEncKey(
userPassword,
_config.getKeyAttributes()!,
Configuration.instance.getKeyAttributes()!,
keyEncryptionKey: keyEncryptionKey,
);
page = _homePage;
page = const HomePage();
} else {
throw Exception("unexpected response during email verification");
}
@@ -830,7 +805,7 @@ class UserService {
String sessionID,
String code,
) async {
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final response = await _dio.post(
@@ -842,16 +817,13 @@ class UserService {
);
await dialog.hide();
if (response.statusCode == 200) {
showShortToast(context, context.strings.authenticationSuccessful);
showShortToast(context, context.l10n.authenticationSuccessful);
await _saveConfiguration(response);
// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return PasswordReentryPage(
_config,
_homePage,
);
return const PasswordReentryPage();
},
),
(route) => route.isFirst,
@@ -866,7 +838,7 @@ class UserService {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPage(_config);
return const LoginPage();
},
),
(route) => route.isFirst,
@@ -875,8 +847,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.incorrectCode,
context.strings.authenticationFailedPleaseTryAgain,
context.l10n.incorrectCode,
context.l10n.authenticationFailedPleaseTryAgain,
);
}
} catch (e) {
@@ -885,8 +857,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.authenticationFailedPleaseTryAgain,
context.l10n.oops,
context.l10n.authenticationFailedPleaseTryAgain,
);
}
}
@@ -896,7 +868,7 @@ class UserService {
String sessionID,
TwoFactorType type,
) async {
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final response = await _dio.get(
@@ -927,12 +899,12 @@ class UserService {
await dialog.hide();
_logger.severe(e);
if (e.response != null && e.response!.statusCode == 404) {
showToast(context, context.strings.sessionExpired);
showToast(context, context.l10n.sessionExpired);
// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPage(_config);
return const LoginPage();
},
),
(route) => route.isFirst,
@@ -941,8 +913,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.somethingWentWrongPleaseTryAgain,
context.l10n.oops,
context.l10n.somethingWentWrongPleaseTryAgain,
);
}
} catch (e) {
@@ -951,8 +923,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.somethingWentWrongPleaseTryAgain,
context.l10n.oops,
context.l10n.somethingWentWrongPleaseTryAgain,
);
} finally {
await dialog.hide();
@@ -967,7 +939,7 @@ class UserService {
String encryptedSecret,
String secretDecryptionNonce,
) async {
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
String secret;
try {
@@ -990,8 +962,8 @@ class UserService {
await dialog.hide();
await showErrorDialog(
context,
context.strings.incorrectRecoveryKey,
context.strings.theRecoveryKeyYouEnteredIsIncorrect,
context.l10n.incorrectRecoveryKey,
context.l10n.theRecoveryKeyYouEnteredIsIncorrect,
);
return;
}
@@ -1008,17 +980,14 @@ class UserService {
if (response.statusCode == 200) {
showShortToast(
context,
context.strings.twofactorAuthenticationSuccessfullyReset,
context.l10n.twofactorAuthenticationSuccessfullyReset,
);
await _saveConfiguration(response);
// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return PasswordReentryPage(
_config,
_homePage,
);
return const PasswordReentryPage();
},
),
(route) => route.isFirst,
@@ -1033,7 +1002,7 @@ class UserService {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPage(_config);
return const LoginPage();
},
),
(route) => route.isFirst,
@@ -1042,8 +1011,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.somethingWentWrongPleaseTryAgain,
context.l10n.oops,
context.l10n.somethingWentWrongPleaseTryAgain,
);
}
} catch (e) {
@@ -1052,8 +1021,8 @@ class UserService {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.somethingWentWrongPleaseTryAgain,
context.l10n.oops,
context.l10n.somethingWentWrongPleaseTryAgain,
);
} finally {
await dialog.hide();
@@ -1064,14 +1033,15 @@ class UserService {
final responseData = response is Map ? response : response.data as Map?;
if (responseData == null) return;
await _config.setUserID(responseData["id"]);
await Configuration.instance.setUserID(responseData["id"]);
if (responseData["encryptedToken"] != null) {
await _config.setEncryptedToken(responseData["encryptedToken"]);
await _config.setKeyAttributes(
await Configuration.instance
.setEncryptedToken(responseData["encryptedToken"]);
await Configuration.instance.setKeyAttributes(
KeyAttributes.fromMap(responseData["keyAttributes"]),
);
} else {
await _config.setToken(responseData["token"]);
await Configuration.instance.setToken(responseData["token"]);
}
}

View File

@@ -4,14 +4,13 @@ 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:ente_base/ente_base.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 extends EnteBaseDatabase {
class AuthenticatorDB {
static const _databaseName = "ente.authenticator.db";
static const _databaseVersion = 1;

View File

@@ -3,12 +3,12 @@ import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
import 'package:ente_auth/models/authenticator/entity_result.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/store/offline_authenticator_db.dart';
import 'package:ente_events/event_bus.dart';
import 'package:logging/logging.dart';
class CodeStore {

View File

@@ -1,7 +1,7 @@
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_utils/email_util.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/email_util.dart';
import 'package:flutter/material.dart';
class ChangeEmailDialog extends StatefulWidget {
@@ -16,8 +16,9 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return AlertDialog(
title: Text(context.strings.enterNewEmailHint),
title: Text(l10n.enterNewEmailHint),
content: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
@@ -25,7 +26,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
children: [
TextFormField(
decoration: InputDecoration(
hintText: context.strings.email,
hintText: l10n.email,
hintStyle: const TextStyle(
color: Colors.white30,
),
@@ -47,7 +48,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
actions: [
TextButton(
child: Text(
context.strings.cancel,
l10n.cancel,
style: const TextStyle(
color: Colors.redAccent,
),
@@ -58,7 +59,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
),
TextButton(
child: Text(
context.strings.verify,
l10n.verify,
style: const TextStyle(
color: Colors.purple,
),
@@ -67,8 +68,8 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
if (!isValidEmail(_email)) {
showErrorDialog(
context,
context.strings.invalidEmailTitle,
context.strings.invalidEmailMessage,
l10n.invalidEmailTitle,
l10n.invalidEmailMessage,
);
return;
}

View File

@@ -1,31 +1,29 @@
import 'dart:convert';
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/delete_account.dart';
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/email_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:ente_lock_screen/local_authentication_service.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/buttons/gradient_button.dart';
import 'package:ente_ui/components/dialogs.dart';
import 'package:ente_ui/theme/ente_theme.dart';
import 'package:ente_utils/email_util.dart';
import 'package:ente_utils/platform_util.dart';
import 'package:flutter/material.dart';
class DeleteAccountPage extends StatelessWidget {
final BaseConfiguration config;
const DeleteAccountPage(
this.config, {
const DeleteAccountPage({
super.key,
});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(context.strings.deleteAccount),
title: Text(l10n.deleteAccount),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
color: Theme.of(context).iconTheme.color,
@@ -49,7 +47,7 @@ class DeleteAccountPage extends StatelessWidget {
),
Center(
child: Text(
context.strings.deleteAccountQuery,
l10n.deleteAccountQuery,
style: Theme.of(context).textTheme.titleMedium,
),
),
@@ -76,7 +74,7 @@ class DeleteAccountPage extends StatelessWidget {
height: 24,
),
GradientButton(
text: context.strings.yesSendFeedbackAction,
text: l10n.yesSendFeedbackAction,
iconData: Icons.check,
onTap: () async {
await sendEmail(
@@ -107,7 +105,7 @@ class DeleteAccountPage extends StatelessWidget {
backgroundColor: Colors.white,
),
label: Text(
context.strings.noDeleteAccountAction,
l10n.noDeleteAccountAction,
style: const TextStyle(
color: Colors.redAccent, // same for both themes
),
@@ -145,10 +143,11 @@ class DeleteAccountPage extends StatelessWidget {
BuildContext context,
DeleteChallengeResponse response,
) async {
final l10n = context.l10n;
final hasAuthenticated =
await LocalAuthenticationService.instance.requestLocalAuthentication(
context,
context.strings.initiateAccountDeleteTitle,
l10n.initiateAccountDeleteTitle,
);
await PlatformUtil.refocusWindows();
@@ -156,11 +155,11 @@ class DeleteAccountPage extends StatelessWidget {
if (hasAuthenticated) {
final choice = await showChoiceDialogOld(
context,
context.strings.confirmAccountDeleteTitle,
context.strings.confirmAccountDeleteMessage,
firstAction: context.strings.cancel,
secondAction: context.strings.delete,
firstActionColor: getEnteColorScheme(context).surface,
l10n.confirmAccountDeleteTitle,
l10n.confirmAccountDeleteMessage,
firstAction: l10n.cancel,
secondAction: l10n.delete,
firstActionColor: Theme.of(context).colorScheme.onSurface,
secondActionColor: Colors.red,
);
if (choice != DialogUserChoice.secondChoice) {
@@ -169,9 +168,9 @@ class DeleteAccountPage extends StatelessWidget {
final decryptChallenge = CryptoUtil.openSealSync(
CryptoUtil.base642bin(response.encryptedChallenge),
CryptoUtil.base642bin(
config.getKeyAttributes()!.publicKey,
Configuration.instance.getKeyAttributes()!.publicKey,
),
config.getSecretKey()!,
Configuration.instance.getSecretKey()!,
);
final challengeResponseStr = utf8.decode(decryptChallenge);
await UserService.instance.deleteAccount(context, challengeResponseStr);
@@ -179,9 +178,10 @@ class DeleteAccountPage extends StatelessWidget {
}
Future<void> _requestEmailForDeletion(BuildContext context) async {
final l10n = context.l10n;
final AlertDialog alert = AlertDialog(
title: Text(
context.strings.deleteAccount,
l10n.deleteAccount,
style: const TextStyle(
color: Colors.red,
),
@@ -204,7 +204,7 @@ class DeleteAccountPage extends StatelessWidget {
),
],
style: TextStyle(
color: getEnteColorScheme(context).surface,
color: Theme.of(context).colorScheme.onSurface,
height: 1.5,
fontSize: 16,
),
@@ -213,7 +213,7 @@ class DeleteAccountPage extends StatelessWidget {
actions: [
TextButton(
child: Text(
context.strings.sendEmail,
l10n.sendEmail,
style: const TextStyle(
color: Colors.red,
),
@@ -229,9 +229,9 @@ class DeleteAccountPage extends StatelessWidget {
),
TextButton(
child: Text(
context.strings.ok,
l10n.ok,
style: TextStyle(
color: getEnteColorScheme(context).surface,
color: Theme.of(context).colorScheme.onSurface,
),
),
onPressed: () {

View File

@@ -1,20 +1,20 @@
import 'package:email_validator/email_validator.dart';
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/buttons/dynamic_fab.dart';
import 'package:ente_ui/theme/ente_theme.dart';
import 'package:ente_utils/platform_util.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
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/utils/platform_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:password_strength/password_strength.dart';
import "package:step_progress_indicator/step_progress_indicator.dart";
import 'package:step_progress_indicator/step_progress_indicator.dart';
import "package:styled_text/styled_text.dart";
class EmailEntryPage extends StatefulWidget {
final BaseConfiguration config;
const EmailEntryPage(this.config, {super.key});
const EmailEntryPage({super.key});
@override
State<EmailEntryPage> createState() => _EmailEntryPageState();
@@ -24,9 +24,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
static const kMildPasswordStrengthThreshold = 0.4;
static const kStrongPasswordStrengthThreshold = 0.7;
final _config = Configuration.instance;
final _passwordController1 = TextEditingController();
final _passwordController2 = TextEditingController();
Color? _validFieldValueColor;
final Color _validFieldValueColor = const Color.fromARGB(51, 157, 45, 194);
String? _email;
String? _password;
@@ -48,7 +49,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
@override
void initState() {
_email = widget.config.getEmail();
_email = _config.getEmail();
_password1FocusNode.addListener(() {
setState(() {
_password1InFocus = _password1FocusNode.hasFocus;
@@ -66,10 +67,6 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
// Initialize theme-aware color
final colorScheme = getEnteColorScheme(context);
_validFieldValueColor = colorScheme.primary300.withOpacity(0.2);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
return null;
@@ -92,10 +89,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 1,
selectedColor: getEnteColorScheme(context).alternativeColor,
selectedColor: Theme.of(context).colorScheme.alternativeColor,
roundedEdges: const Radius.circular(10),
unselectedColor:
getEnteColorScheme(context).stepProgressUnselectedColor,
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
),
);
@@ -106,10 +103,10 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
floatingActionButton: DynamicFAB(
isKeypadOpen: isKeypadOpen,
isFormValid: _isFormValid(),
buttonText: context.strings.createAccount,
buttonText: context.l10n.createAccount,
onPressedFunction: () {
UserService.instance.setEmail(_email!);
widget.config.setVolatilePassword(_passwordController1.text);
_config.setVolatilePassword(_passwordController1.text);
UserService.instance.setRefSource(_referralSource);
UserService.instance.sendOtt(
context,
@@ -126,13 +123,13 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
}
Widget _getBody() {
var passwordStrengthText = context.strings.weakStrength;
var passwordStrengthText = context.l10n.weakStrength;
var passwordStrengthColor = Colors.redAccent;
if (_passwordStrength > kStrongPasswordStrengthThreshold) {
passwordStrengthText = context.strings.strongStrength;
passwordStrengthText = context.l10n.strongStrength;
passwordStrengthColor = Colors.greenAccent;
} else if (_passwordStrength > kMildPasswordStrengthThreshold) {
passwordStrengthText = context.strings.moderateStrength;
passwordStrengthText = context.l10n.moderateStrength;
passwordStrengthColor = Colors.orangeAccent;
}
return Column(
@@ -145,7 +142,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
context.strings.createNewAccount,
context.l10n.createNewAccount,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@@ -157,7 +154,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
decoration: InputDecoration(
fillColor: _emailIsValid ? _validFieldValueColor : null,
filled: true,
hintText: context.strings.email,
hintText: context.l10n.email,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
@@ -170,7 +167,11 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
? Icon(
Icons.check,
size: 20,
color: getEnteColorScheme(context).primary300,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
),
@@ -202,7 +203,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
fillColor:
_passwordIsValid ? _validFieldValueColor : null,
filled: true,
hintText: context.strings.password,
hintText: context.l10n.password,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
@@ -225,7 +226,11 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
: _passwordIsValid
? Icon(
Icons.check,
color: getEnteColorScheme(context).primary300,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
border: UnderlineInputBorder(
@@ -267,7 +272,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
? _validFieldValueColor
: null,
filled: true,
hintText: context.strings.confirmPassword,
hintText: context.l10n.confirmPassword,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
@@ -290,7 +295,11 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
: _passwordsMatch
? Icon(
Icons.check,
color: getEnteColorScheme(context).primary300,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
border: UnderlineInputBorder(
@@ -315,7 +324,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text(
context.strings.passwordStrength(passwordStrengthText),
context.l10n.passwordStrength(passwordStrengthText),
style: TextStyle(
color: passwordStrengthColor,
fontWeight: FontWeight.w500,
@@ -329,7 +338,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
padding:
const EdgeInsets.symmetric(vertical: 0, horizontal: 20),
child: Text(
context.strings.hearUsWhereTitle,
context.l10n.hearUsWhereTitle,
style: getEnteTextTheme(context).smallFaint,
),
),
@@ -353,7 +362,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
onTap: () {
showToast(
context,
context.strings.hearUsExplanation,
context.l10n.hearUsExplanation,
);
},
child: Icon(
@@ -415,7 +424,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
),
Expanded(
child: StyledText(
text: context.strings.signUpTerms,
text: context.l10n.signUpTerms,
style: Theme.of(context)
.textTheme
.titleMedium!
@@ -425,7 +434,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
(String? text, Map<String?, String?> attrs) =>
PlatformUtil.openWebView(
context,
context.strings.termsOfServicesTitle,
context.l10n.termsOfServicesTitle,
"https://ente.io/terms",
),
style: const TextStyle(
@@ -436,7 +445,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
(String? text, Map<String?, String?> attrs) =>
PlatformUtil.openWebView(
context,
context.strings.privacyPolicyTitle,
context.l10n.privacyPolicyTitle,
"https://ente.io/privacy",
),
style: const TextStyle(
@@ -472,7 +481,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
),
Expanded(
child: StyledText(
text: context.strings.ackPasswordLostWarning,
text: context.l10n.ackPasswordLostWarning,
style: Theme.of(context)
.textTheme
.titleMedium!
@@ -482,7 +491,7 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
(String? text, Map<String?, String?> attrs) =>
PlatformUtil.openWebView(
context,
context.strings.encryption,
context.l10n.encryption,
"https://ente.io/architecture",
),
style: const TextStyle(
@@ -504,6 +513,4 @@ class _EmailEntryPageState extends State<EmailEntryPage> {
_hasAgreedToE2E &&
_passwordIsValid;
}
void showToast(BuildContext context, String hearUsExplanation) {}
}

View File

@@ -1,26 +1,25 @@
import "package:email_validator/email_validator.dart";
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_accounts/models/errors.dart';
import 'package:ente_configuration/base_configuration.dart';
import "package:ente_strings/ente_strings.dart";
import 'package:ente_ui/components/buttons/dynamic_fab.dart';
import 'package:ente_ui/theme/ente_theme.dart';
import 'package:ente_utils/platform_util.dart';
import 'package:email_validator/email_validator.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';
import 'package:ente_auth/ui/account/login_pwd_verification_page.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import "package:styled_text/tags/styled_text_tag_action.dart";
import "package:styled_text/widgets/styled_text.dart";
import "package:styled_text/styled_text.dart";
class LoginPage extends StatefulWidget {
final BaseConfiguration config;
const LoginPage(this.config, {super.key});
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _config = Configuration.instance;
bool _emailIsValid = false;
String? _email;
Color? _emailInputFieldColor;
@@ -28,7 +27,7 @@ class _LoginPageState extends State<LoginPage> {
Future<void> onPressed() async {
await UserService.instance.setEmail(_email!);
widget.config.resetVolatilePassword();
Configuration.instance.resetVolatilePassword();
SrpAttributes? attr;
bool isEmailVerificationEnabled = true;
try {
@@ -44,8 +43,7 @@ class _LoginPageState extends State<LoginPage> {
MaterialPageRoute(
builder: (BuildContext context) {
return LoginPasswordVerificationPage(
widget.config,
attr!,
srpAttributes: attr!,
);
},
),
@@ -63,7 +61,7 @@ class _LoginPageState extends State<LoginPage> {
@override
void initState() {
_email = widget.config.getEmail();
_email = _config.getEmail();
super.initState();
}
@@ -95,7 +93,7 @@ class _LoginPageState extends State<LoginPage> {
floatingActionButton: DynamicFAB(
isKeypadOpen: isKeypadOpen,
isFormValid: _emailIsValid,
buttonText: context.strings.logInLabel,
buttonText: context.l10n.logInLabel,
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
@@ -104,6 +102,7 @@ class _LoginPageState extends State<LoginPage> {
}
Widget _getBody() {
final l10n = context.l10n;
return Column(
children: [
Expanded(
@@ -114,7 +113,7 @@ class _LoginPageState extends State<LoginPage> {
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
context.strings.welcomeBack,
l10n.welcomeBack,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@@ -127,7 +126,7 @@ class _LoginPageState extends State<LoginPage> {
decoration: InputDecoration(
fillColor: _emailInputFieldColor,
filled: true,
hintText: context.strings.email,
hintText: l10n.email,
contentPadding: const EdgeInsets.symmetric(
horizontal: 15,
vertical: 15,
@@ -140,7 +139,11 @@ class _LoginPageState extends State<LoginPage> {
? Icon(
Icons.check,
size: 20,
color: getEnteColorScheme(context).primary300,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
),
@@ -149,9 +152,8 @@ class _LoginPageState extends State<LoginPage> {
_email = value.trim();
_emailIsValid = EmailValidator.validate(_email!);
if (_emailIsValid) {
_emailInputFieldColor = getEnteColorScheme(context)
.primary300
.withOpacity(0.2);
_emailInputFieldColor =
const Color.fromARGB(51, 157, 45, 194);
} else {
_emailInputFieldColor = null;
}
@@ -176,14 +178,17 @@ class _LoginPageState extends State<LoginPage> {
Expanded(
flex: 5,
child: StyledText(
text: context.strings.loginTerms,
style: getEnteTextTheme(context).small,
text: context.l10n.loginTerms,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 12),
tags: {
'u-terms': StyledTextActionTag(
(String? text, Map<String?, String?> attrs) =>
PlatformUtil.openWebView(
context,
context.strings.termsOfServicesTitle,
context.l10n.termsOfServicesTitle,
"https://ente.io/terms",
),
style: const TextStyle(
@@ -194,7 +199,7 @@ class _LoginPageState extends State<LoginPage> {
(String? text, Map<String?, String?> attrs) =>
PlatformUtil.openWebView(
context,
context.strings.privacyPolicyTitle,
context.l10n.privacyPolicyTitle,
"https://ente.io/privacy",
),
style: const TextStyle(

View File

@@ -1,13 +1,14 @@
import "package:dio/dio.dart";
import "package:ente_accounts/ente_accounts.dart";
import "package:ente_configuration/base_configuration.dart";
import 'package:ente_auth/core/configuration.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";
import "package:ente_auth/theme/ente_theme.dart";
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:ente_strings/ente_strings.dart";
import "package:ente_ui/components/buttons/button_widget.dart";
import "package:ente_ui/components/buttons/dynamic_fab.dart";
import "package:ente_ui/theme/ente_theme.dart";
import "package:ente_ui/utils/dialog_util.dart";
import "package:ente_utils/email_util.dart";
import 'package:flutter/material.dart';
import "package:logging/logging.dart";
@@ -17,13 +18,8 @@ import "package:logging/logging.dart";
// In the PasswordReentryPage, the password is auto-filled based on the
// volatile password.
class LoginPasswordVerificationPage extends StatefulWidget {
final BaseConfiguration config;
final SrpAttributes srpAttributes;
const LoginPasswordVerificationPage(
this.config,
this.srpAttributes, {
super.key,
});
const LoginPasswordVerificationPage({super.key, required this.srpAttributes});
@override
State<LoginPasswordVerificationPage> createState() =>
@@ -47,7 +43,7 @@ class _LoginPasswordVerificationPageState
@override
void initState() {
super.initState();
email = widget.config.getEmail();
email = Configuration.instance.getEmail();
_passwordFocusNode.addListener(() {
setState(() {
_passwordInFocus = _passwordFocusNode.hasFocus;
@@ -84,7 +80,7 @@ class _LoginPasswordVerificationPageState
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
isFormValid: _passwordController.text.isNotEmpty,
buttonText: context.strings.logInLabel,
buttonText: context.l10n.logInLabel,
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
@@ -95,7 +91,7 @@ class _LoginPasswordVerificationPageState
Future<void> verifyPassword(BuildContext context, String password) async {
final dialog = createProgressDialog(
context,
context.strings.pleaseWait,
context.l10n.pleaseWait,
isDismissible: true,
);
await dialog.show();
@@ -112,22 +108,22 @@ class _LoginPasswordVerificationPageState
_logger.severe('server reject, failed verify SRP login', e, s);
await _showContactSupportDialog(
context,
context.strings.incorrectPasswordTitle,
context.strings.pleaseTryAgain,
context.l10n.incorrectPasswordTitle,
context.l10n.pleaseTryAgain,
);
} else {
_logger.severe('API failure during SRP login', e, s);
if (e.type == DioExceptionType.connectionError) {
await _showContactSupportDialog(
context,
context.strings.noInternetConnection,
context.strings.pleaseCheckYourInternetConnectionAndTryAgain,
context.l10n.noInternetConnection,
context.l10n.pleaseCheckYourInternetConnectionAndTryAgain,
);
} else {
await _showContactSupportDialog(
context,
context.strings.oops,
context.strings.verificationFailedPleaseTryAgain,
context.l10n.oops,
context.l10n.verificationFailedPleaseTryAgain,
);
}
}
@@ -147,9 +143,9 @@ class _LoginPasswordVerificationPageState
// device is not powerful enough to perform derive key
final dialogChoice = await showChoiceDialog(
context,
title: context.strings.recreatePasswordTitle,
body: context.strings.recreatePasswordBody,
firstButtonLabel: context.strings.useRecoveryKey,
title: context.l10n.recreatePasswordTitle,
body: context.l10n.recreatePasswordBody,
firstButtonLabel: context.l10n.useRecoveryKey,
);
if (dialogChoice!.action == ButtonAction.first) {
await UserService.instance.sendOtt(
@@ -163,8 +159,8 @@ class _LoginPasswordVerificationPageState
_logger.severe('unexpected error while verifying password', e, s);
await _showContactSupportDialog(
context,
context.strings.oops,
context.strings.verificationFailedPleaseTryAgain,
context.l10n.oops,
context.l10n.verificationFailedPleaseTryAgain,
);
}
}
@@ -179,13 +175,13 @@ class _LoginPasswordVerificationPageState
context,
title: title,
body: message,
firstButtonLabel: context.strings.contactSupport,
secondButtonLabel: context.strings.ok,
firstButtonLabel: context.l10n.contactSupport,
secondButtonLabel: context.l10n.ok,
);
if (dialogChoice!.action == ButtonAction.first) {
await sendLogs(
context,
context.strings.contactSupport,
context.l10n.contactSupport,
postShare: () {},
);
}
@@ -201,7 +197,7 @@ class _LoginPasswordVerificationPageState
Padding(
padding: const EdgeInsets.only(top: 30, left: 20, right: 20),
child: Text(
context.strings.enterPassword,
context.l10n.enterPassword,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@@ -239,7 +235,7 @@ class _LoginPasswordVerificationPageState
key: const ValueKey("passwordInputField"),
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(
hintText: context.strings.enterYourPasswordHint,
hintText: context.l10n.enterYourPasswordHint,
filled: true,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
@@ -299,7 +295,7 @@ class _LoginPasswordVerificationPageState
},
child: Center(
child: Text(
context.strings.forgotPassword,
context.l10n.forgotPassword,
style: Theme.of(context)
.textTheme
.titleMedium!
@@ -315,17 +311,17 @@ class _LoginPasswordVerificationPageState
onTap: () async {
final dialog = createProgressDialog(
context,
context.strings.pleaseWait,
context.l10n.pleaseWait,
);
await dialog.show();
await widget.config.logout();
await Configuration.instance.logout();
await dialog.hide();
Navigator.of(context)
.popUntil((route) => route.isFirst);
},
child: Center(
child: Text(
context.strings.changeEmail,
context.l10n.changeEmail,
style: Theme.of(context)
.textTheme
.titleMedium!

View File

@@ -1,7 +1,7 @@
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/store/authenticator_db.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';

View File

@@ -1,7 +1,7 @@
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/buttons/dynamic_fab.dart';
import 'package:ente_ui/theme/ente_theme.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:flutter/material.dart';
import 'package:step_progress_indicator/step_progress_indicator.dart';
import 'package:styled_text/styled_text.dart';
@@ -46,6 +46,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
FloatingActionButtonLocation? fabLocation() {
@@ -73,10 +74,10 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 2,
selectedColor: getEnteColorScheme(context).alternativeColor,
selectedColor: Theme.of(context).colorScheme.alternativeColor,
roundedEdges: const Radius.circular(10),
unselectedColor:
getEnteColorScheme(context).stepProgressUnselectedColor,
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
)
: null,
@@ -85,7 +86,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
floatingActionButton: DynamicFAB(
isKeypadOpen: isKeypadOpen,
isFormValid: _verificationCodeController.text.isNotEmpty,
buttonText: context.strings.verify,
buttonText: l10n.verify,
onPressedFunction: onPressed,
),
floatingActionButtonLocation: fabLocation(),
@@ -94,6 +95,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
}
Widget _getBody() {
final l10n = context.l10n;
return ListView(
children: [
Column(
@@ -102,7 +104,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
Padding(
padding: const EdgeInsets.fromLTRB(20, 30, 20, 15),
child: Text(
context.strings.verifyEmail,
l10n.verifyEmail,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@@ -117,8 +119,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
child: StyledText(
text:
context.strings.weHaveSendEmailTo(widget.email),
text: l10n.weHaveSendEmailTo(widget.email),
style: Theme.of(context)
.textTheme
.titleMedium!
@@ -126,7 +127,8 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
tags: {
'green': StyledTextTag(
style: TextStyle(
color: getEnteColorScheme(context)
color: Theme.of(context)
.colorScheme
.alternativeColor,
),
),
@@ -135,14 +137,14 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
),
widget.isResetPasswordScreen
? Text(
context.strings.toResetVerifyEmail,
l10n.toResetVerifyEmail,
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontSize: 14),
)
: Text(
context.strings.checkInboxAndSpamFolder,
l10n.checkInboxAndSpamFolder,
style: Theme.of(context)
.textTheme
.titleMedium!
@@ -167,7 +169,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
: null,
decoration: InputDecoration(
filled: true,
hintText: context.strings.tapToEnterCode,
hintText: l10n.tapToEnterCode,
contentPadding: const EdgeInsets.all(15),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
@@ -202,7 +204,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
);
},
child: Text(
context.strings.resendEmail,
l10n.resendEmail,
style: Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
decoration: TextDecoration.underline,

View File

@@ -1,14 +1,15 @@
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/buttons/dynamic_fab.dart';
import 'package:ente_ui/components/buttons/models/button_type.dart';
import 'package:ente_ui/pages/base_home_page.dart';
import 'package:ente_ui/theme/ente_theme.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_ui/utils/toast_util.dart';
import 'package:ente_utils/navigation_util.dart';
import 'package:ente_utils/platform_util.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
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/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';
import 'package:logging/logging.dart';
@@ -22,16 +23,9 @@ enum PasswordEntryMode {
}
class PasswordEntryPage extends StatefulWidget {
final BaseConfiguration config;
final PasswordEntryMode mode;
final BaseHomePage homePage;
const PasswordEntryPage(
this.config,
this.mode,
this.homePage, {
super.key,
});
const PasswordEntryPage({required this.mode, super.key});
@override
State<PasswordEntryPage> createState() => _PasswordEntryPageState();
@@ -44,7 +38,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
final _logger = Logger((_PasswordEntryPageState).toString());
final _passwordController1 = TextEditingController(),
_passwordController2 = TextEditingController();
Color? _validFieldValueColor;
final Color _validFieldValueColor = const Color.fromRGBO(45, 194, 98, 0.2);
String? _volatilePassword;
String _passwordInInputBox = '';
String _passwordInInputConfirmationBox = '';
@@ -62,7 +56,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
@override
void initState() {
super.initState();
_volatilePassword = widget.config.getVolatilePassword();
_volatilePassword = Configuration.instance.getVolatilePassword();
if (_volatilePassword != null) {
Future.delayed(
Duration.zero,
@@ -87,7 +81,6 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
@override
Widget build(BuildContext context) {
final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100;
_validFieldValueColor = const Color.fromRGBO(45, 194, 98, 0.2);
FloatingActionButtonLocation? fabLocation() {
if (isKeypadOpen) {
@@ -97,13 +90,13 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
}
}
String title = context.strings.setPasswordTitle;
String title = context.l10n.setPasswordTitle;
if (widget.mode == PasswordEntryMode.update) {
title = context.strings.changePasswordTitle;
title = context.l10n.changePasswordTitle;
} else if (widget.mode == PasswordEntryMode.reset) {
title = context.strings.resetPasswordTitle;
title = context.l10n.resetPasswordTitle;
} else if (_volatilePassword != null) {
title = context.strings.encryptionKeys;
title = context.l10n.encryptionKeys;
}
return Scaffold(
resizeToAvoidBottomInset: isKeypadOpen,
@@ -139,14 +132,14 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
}
Widget _getBody(String buttonTextAndHeading) {
final email = widget.config.getEmail();
var passwordStrengthText = context.strings.weakStrength;
final email = Configuration.instance.getEmail();
var passwordStrengthText = context.l10n.weakStrength;
var passwordStrengthColor = Colors.redAccent;
if (_passwordStrength > kStrongPasswordStrengthThreshold) {
passwordStrengthText = context.strings.strongStrength;
passwordStrengthText = context.l10n.strongStrength;
passwordStrengthColor = Colors.greenAccent;
} else if (_passwordStrength > kMildPasswordStrengthThreshold) {
passwordStrengthText = context.strings.moderateStrength;
passwordStrengthText = context.l10n.moderateStrength;
passwordStrengthColor = Colors.orangeAccent;
}
if (_volatilePassword != null) {
@@ -174,8 +167,8 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Text(
widget.mode == PasswordEntryMode.set
? context.strings.enterPasswordToEncrypt
: context.strings.enterNewPasswordToEncrypt,
? context.l10n.enterPasswordToEncrypt
: context.l10n.enterNewPasswordToEncrypt,
textAlign: TextAlign.start,
style: Theme.of(context)
.textTheme
@@ -187,7 +180,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: StyledText(
text: context.strings.passwordWarning,
text: context.l10n.passwordWarning,
style: Theme.of(context)
.textTheme
.titleMedium!
@@ -229,11 +222,10 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
null);
},
decoration: InputDecoration(
fillColor: _isPasswordValid
? _validFieldValueColor
: getEnteColorScheme(context).fillFaint,
fillColor:
_isPasswordValid ? _validFieldValueColor : null,
filled: true,
hintText: context.strings.password,
hintText: context.l10n.password,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
@@ -257,8 +249,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
: _isPasswordValid
? Icon(
Icons.check,
color:
getEnteColorScheme(context).primary300,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
),
@@ -293,11 +288,10 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
onEditingComplete: () =>
TextInput.finishAutofillContext(),
decoration: InputDecoration(
fillColor: _passwordsMatch
? _validFieldValueColor
: getEnteColorScheme(context).fillFaint,
fillColor:
_passwordsMatch ? _validFieldValueColor : null,
filled: true,
hintText: context.strings.confirmPassword,
hintText: context.l10n.confirmPassword,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 20,
@@ -320,8 +314,11 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
: _passwordsMatch
? Icon(
Icons.check,
color:
getEnteColorScheme(context).primary300,
color: Theme.of(context)
.inputDecorationTheme
.focusedBorder!
.borderSide
.color,
)
: null,
border: UnderlineInputBorder(
@@ -351,7 +348,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
vertical: 8,
),
child: Text(
context.strings.passwordStrength(passwordStrengthText),
context.l10n.passwordStrength(passwordStrengthText),
style: TextStyle(
color: passwordStrengthColor,
),
@@ -364,7 +361,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
onTap: () {
PlatformUtil.openWebView(
context,
context.strings.howItWorks,
context.l10n.howItWorks,
"https://ente.io/architecture",
);
},
@@ -372,7 +369,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: RichText(
text: TextSpan(
text: context.strings.howItWorks,
text: context.l10n.howItWorks,
style:
Theme.of(context).textTheme.titleMedium!.copyWith(
fontSize: 14,
@@ -395,10 +392,10 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
void _updatePassword() async {
final logOutFromOthers = await logOutFromOtherDevices(context);
final dialog =
createProgressDialog(context, context.strings.generatingEncryptionKeys);
createProgressDialog(context, context.l10n.generatingEncryptionKeys);
await dialog.show();
try {
final result = await widget.config
final result = await Configuration.instance
.getAttributesForNewPassword(_passwordController1.text);
await UserService.instance.updateKeyAttributes(
result.item1,
@@ -406,7 +403,7 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
logoutOtherDevices: logOutFromOthers,
);
await dialog.hide();
showShortToast(context, context.strings.passwordChangedSuccessfully);
showShortToast(context, context.l10n.passwordChangedSuccessfully);
Navigator.of(context).pop();
if (widget.mode == PasswordEntryMode.reset) {
Navigator.of(context).popUntil((route) => route.isFirst);
@@ -426,15 +423,15 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
bool logOutFromOther = true;
await showChoiceDialog(
context,
title: context.strings.signOutFromOtherDevices,
body: context.strings.signOutOtherBody,
title: context.l10n.signOutFromOtherDevices,
body: context.l10n.signOutOtherBody,
isDismissible: false,
firstButtonLabel: context.strings.signOutOtherDevices,
firstButtonLabel: context.l10n.signOutOtherDevices,
firstButtonType: ButtonType.critical,
firstButtonOnTap: () async {
logOutFromOther = true;
},
secondButtonLabel: context.strings.doNotSignOut,
secondButtonLabel: context.l10n.doNotSignOut,
secondButtonOnTap: () async {
logOutFromOther = false;
},
@@ -446,31 +443,30 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
String password, {
bool usingVolatilePassword = false,
}) async {
final dialog = createProgressDialog(
context,
context.strings.generatingEncryptionKeysTitle,
);
final l10n = context.l10n;
final dialog =
createProgressDialog(context, l10n.generatingEncryptionKeysTitle);
await dialog.show();
try {
if (usingVolatilePassword) {
_logger.info('Using volatile password');
}
final result = await widget.config.generateKey(password);
widget.config.resetVolatilePassword();
final KeyGenResult result =
await Configuration.instance.generateKey(password);
Configuration.instance.resetVolatilePassword();
await dialog.hide();
onDone() async {
final dialog =
createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, l10n.pleaseWait);
await dialog.show();
try {
await UserService.instance.setAttributes(result);
await dialog.hide();
widget.config.resetVolatilePassword();
Configuration.instance.resetVolatilePassword();
// ignore: unawaited_futures
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return widget.homePage;
return const HomePage();
},
),
(route) => route.isFirst,
@@ -490,9 +486,8 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
routeToPage(
context,
RecoveryKeyPage(
widget.config,
result.privateKeyAttributes.recoveryKey,
context.strings.continueLabel,
context.l10n.continueLabel,
showAppBar: false,
isDismissible: false,
onDone: onDone,
@@ -506,8 +501,8 @@ class _PasswordEntryPageState extends State<PasswordEntryPage> {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.insecureDevice,
context.strings.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease,
context.l10n.insecureDevice,
context.l10n.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease,
);
} else {
// ignore: unawaited_futures

View File

@@ -1,28 +1,22 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_accounts/models/errors.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/core/errors.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/user_service.dart';
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/dialog_util.dart';
import 'package:ente_auth/utils/email_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/buttons/button_widget.dart';
import 'package:ente_ui/components/buttons/dynamic_fab.dart';
import 'package:ente_ui/pages/base_home_page.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_utils/email_util.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
class PasswordReentryPage extends StatefulWidget {
final BaseConfiguration config;
final BaseHomePage homePage;
const PasswordReentryPage(
this.config,
this.homePage, {
super.key,
});
const PasswordReentryPage({super.key});
@override
State<PasswordReentryPage> createState() => _PasswordReentryPageState();
@@ -40,8 +34,8 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
@override
void initState() {
super.initState();
email = widget.config.getEmail();
_volatilePassword = widget.config.getVolatilePassword();
email = Configuration.instance.getEmail();
_volatilePassword = Configuration.instance.getVolatilePassword();
if (_volatilePassword != null) {
_passwordController.text = _volatilePassword!;
Future.delayed(
@@ -85,7 +79,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
isFormValid: _passwordController.text.isNotEmpty,
buttonText: context.strings.verifyPassword,
buttonText: context.l10n.verifyPassword,
onPressedFunction: () async {
FocusScope.of(context).unfocus();
await verifyPassword(_passwordController.text);
@@ -101,15 +95,15 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
bool usingVolatilePassword = false,
}) async {
FocusScope.of(context).unfocus();
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
if (usingVolatilePassword) {
_logger.info("Using volatile password");
}
try {
final kek = await widget.config.decryptSecretsAndGetKeyEncKey(
final kek = await Configuration.instance.decryptSecretsAndGetKeyEncKey(
password,
widget.config.getKeyAttributes()!,
Configuration.instance.getKeyAttributes()!,
);
_registerSRPForExistingUsers(kek).ignore();
} on KeyDerivationError catch (e, s) {
@@ -117,19 +111,16 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
await dialog.hide();
final dialogChoice = await showChoiceDialog(
context,
title: context.strings.recreatePasswordTitle,
body: context.strings.recreatePasswordBody,
firstButtonLabel: context.strings.useRecoveryKey,
title: context.l10n.recreatePasswordTitle,
body: context.l10n.recreatePasswordBody,
firstButtonLabel: context.l10n.useRecoveryKey,
);
if (dialogChoice!.action == ButtonAction.first) {
// ignore: unawaited_futures
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return RecoveryPage(
widget.config,
widget.homePage,
);
return const RecoveryPage();
},
),
);
@@ -140,27 +131,27 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
await dialog.hide();
final dialogChoice = await showChoiceDialog(
context,
title: context.strings.incorrectPasswordTitle,
body: context.strings.pleaseTryAgain,
firstButtonLabel: context.strings.contactSupport,
secondButtonLabel: context.strings.ok,
title: context.l10n.incorrectPasswordTitle,
body: context.l10n.pleaseTryAgain,
firstButtonLabel: context.l10n.contactSupport,
secondButtonLabel: context.l10n.ok,
);
if (dialogChoice!.action == ButtonAction.first) {
await sendLogs(
context,
context.strings.contactSupport,
context.l10n.contactSupport,
postShare: () {},
);
}
return;
}
widget.config.resetVolatilePassword();
Configuration.instance.resetVolatilePassword();
await dialog.hide();
unawaited(
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (BuildContext context) {
return widget.homePage;
return const HomePage();
},
),
(route) => false,
@@ -199,7 +190,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
context.strings.welcomeBack,
context.l10n.welcomeBack,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@@ -223,7 +214,7 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
key: const ValueKey("passwordInputField"),
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(
hintText: context.strings.enterYourPasswordHint,
hintText: context.l10n.enterYourPasswordHint,
filled: true,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(
@@ -278,17 +269,14 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return RecoveryPage(
widget.config,
widget.homePage,
);
return const RecoveryPage();
},
),
);
},
child: Center(
child: Text(
context.strings.forgotPassword,
context.l10n.forgotPassword,
style: Theme.of(context)
.textTheme
.titleMedium!
@@ -304,17 +292,17 @@ class _PasswordReentryPageState extends State<PasswordReentryPage> {
onTap: () async {
final dialog = createProgressDialog(
context,
context.strings.pleaseWait,
context.l10n.pleaseWait,
);
await dialog.show();
await widget.config.logout();
await Configuration.instance.logout();
await dialog.hide();
Navigator.of(context)
.popUntil((route) => route.isFirst);
},
child: Center(
child: Text(
context.strings.changeEmail,
context.l10n.changeEmail,
style: Theme.of(context)
.textTheme
.titleMedium!

View File

@@ -3,15 +3,14 @@ import 'dart:io' as io;
import 'package:bip39/bip39.dart' as bip39;
import 'package:dotted_border/dotted_border.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_configuration/constants.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/buttons/gradient_button.dart';
import 'package:ente_ui/theme/ente_theme.dart';
import "package:ente_ui/theme/ente_theme_data.dart";
import 'package:ente_ui/utils/toast_util.dart';
import 'package:ente_utils/platform_util.dart';
import 'package:ente_utils/share_utils.dart';
import 'package:ente_auth/core/configuration.dart';
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';
@@ -19,7 +18,6 @@ import 'package:share_plus/share_plus.dart';
import 'package:step_progress_indicator/step_progress_indicator.dart';
class RecoveryKeyPage extends StatefulWidget {
final BaseConfiguration config;
final bool? showAppBar;
final String recoveryKey;
final String doneText;
@@ -31,7 +29,6 @@ class RecoveryKeyPage extends StatefulWidget {
final bool showProgressBar;
const RecoveryKeyPage(
this.config,
this.recoveryKey,
this.doneText, {
super.key,
@@ -50,15 +47,9 @@ class RecoveryKeyPage extends StatefulWidget {
class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
bool _hasTriedToSave = false;
late final io.File _recoveryKeyFile;
@override
void initState() {
super.initState();
_recoveryKeyFile = io.File(
"${widget.config.getTempDirectory()}ente-recovery-key.txt",
);
}
final _recoveryKeyFile = io.File(
"${Configuration.instance.getTempDirectory()}ente-recovery-key.txt",
);
@override
Widget build(BuildContext context) {
@@ -82,7 +73,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
);
showShortToast(
context,
context.strings.recoveryKeyCopiedToClipboard,
context.l10n.recoveryKeyCopiedToClipboard,
);
setState(() {
_hasTriedToSave = true;
@@ -99,17 +90,17 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
child: StepProgressIndicator(
totalSteps: 4,
currentStep: 3,
selectedColor: getEnteColorScheme(context).alternativeColor,
selectedColor: Theme.of(context).colorScheme.alternativeColor,
roundedEdges: const Radius.circular(10),
unselectedColor:
getEnteColorScheme(context).stepProgressUnselectedColor,
Theme.of(context).colorScheme.stepProgressUnselectedColor,
),
),
)
: widget.showAppBar!
? AppBar(
elevation: 0,
title: Text(widget.title ?? context.strings.recoveryKey),
title: Text(widget.title ?? context.l10n.recoveryKey),
)
: null,
body: Padding(
@@ -130,15 +121,14 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
widget.showAppBar!
? const SizedBox.shrink()
: Text(
widget.title ?? context.strings.recoveryKey,
widget.title ?? context.l10n.recoveryKey,
style: Theme.of(context).textTheme.headlineMedium,
),
Padding(
padding: EdgeInsets.all(widget.showAppBar! ? 0 : 12),
),
Text(
widget.text ??
context.strings.recoveryKeyOnForgotPassword,
widget.text ?? context.l10n.recoveryKeyOnForgotPassword,
style: Theme.of(context).textTheme.titleMedium,
),
const Padding(padding: EdgeInsets.only(top: 24)),
@@ -146,14 +136,14 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
padding: const EdgeInsets.all(1),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
getEnteColorScheme(context).primary700,
getEnteColorScheme(context).primary300,
Color(0x8E9610D6),
Color(0x8E9F4FC6),
],
stops: const [0.0, 0.9753],
stops: [0.0, 0.9753],
),
),
child: DottedBorder(
@@ -217,7 +207,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
padding: const EdgeInsets.symmetric(vertical: 20),
child: Text(
widget.subText ??
context.strings.recoveryKeySaveDescription,
context.l10n.recoveryKeySaveDescription,
style: Theme.of(context).textTheme.bodyLarge,
),
),
@@ -255,7 +245,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
onPressed: () async {
await _saveKeys();
},
child: Text(context.strings.doThisLater),
child: Text(context.l10n.doThisLater),
),
),
);
@@ -267,7 +257,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
onTap: () async {
await shareDialog(
context,
context.strings.recoveryKey,
context.l10n.recoveryKey,
saveAction: () async {
await _saveRecoveryKey(recoveryKey);
},
@@ -276,7 +266,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
},
);
},
text: context.strings.saveKey,
text: context.l10n.saveKey,
),
);
@@ -312,7 +302,7 @@ class _RecoveryKeyPageState extends State<RecoveryKeyPage> {
if (mounted) {
showToast(
context,
context.strings.recoveryKeySaved,
context.l10n.recoveryKeySaved,
);
setState(() {
_hasTriedToSave = true;

View File

@@ -1,17 +1,13 @@
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/buttons/dynamic_fab.dart';
import 'package:ente_ui/pages/base_home_page.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_ui/utils/toast_util.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/ui/account/password_entry_page.dart';
import 'package:ente_auth/ui/common/dynamic_fab.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
class RecoveryPage extends StatefulWidget {
final BaseConfiguration config;
final BaseHomePage homePage;
const RecoveryPage(this.config, this.homePage, {super.key});
const RecoveryPage({super.key});
@override
State<RecoveryPage> createState() => _RecoveryPageState();
@@ -25,18 +21,16 @@ class _RecoveryPageState extends State<RecoveryPage> {
final dialog = createProgressDialog(context, "Decrypting...");
await dialog.show();
try {
await widget.config.recover(_recoveryKey.text.trim());
await Configuration.instance.recover(_recoveryKey.text.trim());
await dialog.hide();
showToast(context, "Recovery successful!");
await Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (BuildContext context) {
return PopScope(
return const PopScope(
canPop: false,
child: PasswordEntryPage(
widget.config,
PasswordEntryMode.reset,
widget.homePage,
mode: PasswordEntryMode.reset,
),
);
},
@@ -92,7 +86,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
padding:
const EdgeInsets.symmetric(vertical: 30, horizontal: 20),
child: Text(
context.strings.forgotPassword,
context.l10n.forgotPassword,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@@ -143,7 +137,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: Text(
context.strings.noRecoveryKeyTitle,
context.l10n.noRecoveryKeyTitle,
style: Theme.of(context)
.textTheme
.titleMedium!

View File

@@ -1,24 +1,22 @@
import "dart:convert";
import "dart:typed_data";
import "package:ente_configuration/base_configuration.dart";
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/dialog_util.dart";
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import "package:ente_strings/ente_strings.dart";
import "package:ente_ui/components/buttons/dynamic_fab.dart";
import "package:ente_ui/theme/ente_theme.dart";
import "package:ente_ui/utils/dialog_util.dart";
import 'package:flutter/material.dart';
import "package:logging/logging.dart";
typedef OnPasswordVerifiedFn = Future<void> Function(Uint8List bytes);
class RequestPasswordVerificationPage extends StatefulWidget {
final BaseConfiguration config;
final OnPasswordVerifiedFn onPasswordVerified;
final Function? onPasswordError;
const RequestPasswordVerificationPage(
this.config, {
const RequestPasswordVerificationPage({
super.key,
required this.onPasswordVerified,
this.onPasswordError,
@@ -41,7 +39,7 @@ class _RequestPasswordVerificationPageState
@override
void initState() {
super.initState();
email = widget.config.getEmail();
email = Configuration.instance.getEmail();
_passwordFocusNode.addListener(() {
setState(() {
_passwordInFocus = _passwordFocusNode.hasFocus;
@@ -78,14 +76,13 @@ class _RequestPasswordVerificationPageState
key: const ValueKey("verifyPasswordButton"),
isKeypadOpen: isKeypadOpen,
isFormValid: _passwordController.text.isNotEmpty,
buttonText: context.strings.verifyPassword,
buttonText: context.l10n.verifyPassword,
onPressedFunction: () async {
FocusScope.of(context).unfocus();
final dialog =
createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
final attributes = widget.config.getKeyAttributes()!;
final attributes = Configuration.instance.getKeyAttributes()!;
final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey(
utf8.encode(_passwordController.text),
CryptoUtil.base642bin(attributes.kekSalt),
@@ -111,8 +108,8 @@ class _RequestPasswordVerificationPageState
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.incorrectPasswordTitle,
context.strings.pleaseTryAgain,
context.l10n.incorrectPasswordTitle,
context.l10n.pleaseTryAgain,
);
}
}
@@ -133,7 +130,7 @@ class _RequestPasswordVerificationPageState
Padding(
padding: const EdgeInsets.only(top: 30, left: 20, right: 20),
child: Text(
context.strings.enterPassword,
context.l10n.enterPassword,
style: Theme.of(context).textTheme.headlineMedium,
),
),
@@ -168,7 +165,7 @@ class _RequestPasswordVerificationPageState
key: const ValueKey("passwordInputField"),
autofillHints: const [AutofillHints.password],
decoration: InputDecoration(
hintText: context.strings.enterYourPasswordHint,
hintText: context.l10n.enterYourPasswordHint,
filled: true,
contentPadding: const EdgeInsets.all(20),
border: UnderlineInputBorder(

View File

@@ -1,20 +1,17 @@
import 'package:ente_accounts/ente_accounts.dart';
import 'package:ente_configuration/base_configuration.dart';
import 'package:ente_strings/ente_strings.dart';
import 'package:ente_ui/components/loading_widget.dart';
import 'package:ente_ui/theme/ente_theme.dart';
import 'package:ente_ui/utils/dialog_util.dart';
import 'package:ente_ui/utils/toast_util.dart';
import 'package:ente_utils/date_time_util.dart';
import 'package:ente_auth/core/configuration.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/sessions.dart';
import 'package:ente_auth/services/user_service.dart';
import 'package:ente_auth/ui/common/loading_widget.dart';
import 'package:ente_auth/utils/date_time_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:logging/logging.dart';
class SessionsPage extends StatefulWidget {
final BaseConfiguration config;
const SessionsPage(
this.config, {
super.key,
});
const SessionsPage({super.key});
@override
State<SessionsPage> createState() => _SessionsPageState();
@@ -37,7 +34,7 @@ class _SessionsPageState extends State<SessionsPage> {
return Scaffold(
appBar: AppBar(
elevation: 0,
title: Text(context.strings.activeSessions),
title: Text(context.l10n.activeSessions),
),
body: _getBody(),
);
@@ -115,7 +112,7 @@ class _SessionsPageState extends State<SessionsPage> {
}
Future<void> _terminateSession(Session session) async {
final dialog = createProgressDialog(context, context.strings.pleaseWait);
final dialog = createProgressDialog(context, context.l10n.pleaseWait);
await dialog.show();
try {
await UserService.instance.terminateSession(session.token);
@@ -127,8 +124,8 @@ class _SessionsPageState extends State<SessionsPage> {
// ignore: unawaited_futures
showErrorDialog(
context,
context.strings.oops,
context.strings.somethingWentWrongPleaseTryAgain,
context.l10n.oops,
context.l10n.somethingWentWrongPleaseTryAgain,
);
}
}
@@ -150,18 +147,18 @@ class _SessionsPageState extends State<SessionsPage> {
void _showSessionTerminationDialog(Session session) {
final isLoggingOutFromThisDevice =
session.token == widget.config.getToken();
session.token == Configuration.instance.getToken();
Widget text;
if (isLoggingOutFromThisDevice) {
text = Text(
context.strings.thisWillLogYouOutOfThisDevice,
context.l10n.thisWillLogYouOutOfThisDevice,
);
} else {
text = SingleChildScrollView(
child: Column(
children: [
Text(
context.strings.thisWillLogYouOutOfTheFollowingDevice,
context.l10n.thisWillLogYouOutOfTheFollowingDevice,
),
const Padding(padding: EdgeInsets.all(8)),
Text(
@@ -173,12 +170,12 @@ class _SessionsPageState extends State<SessionsPage> {
);
}
final AlertDialog alert = AlertDialog(
title: Text(context.strings.terminateSession),
title: Text(context.l10n.terminateSession),
content: text,
actions: [
TextButton(
child: Text(
context.strings.terminate,
context.l10n.terminate,
style: const TextStyle(
color: Colors.red,
),
@@ -194,11 +191,11 @@ class _SessionsPageState extends State<SessionsPage> {
),
TextButton(
child: Text(
context.strings.cancel,
context.l10n.cancel,
style: TextStyle(
color: isLoggingOutFromThisDevice
? getEnteColorScheme(context).alternativeColor
: getEnteColorScheme(context).textBase,
? Theme.of(context).colorScheme.alternativeColor
: Theme.of(context).colorScheme.defaultTextColor,
),
),
onPressed: () {
@@ -217,12 +214,12 @@ class _SessionsPageState extends State<SessionsPage> {
}
Widget _getUAWidget(Session session) {
if (session.token == widget.config.getToken()) {
if (session.token == Configuration.instance.getToken()) {
return Text(
context.strings.thisDevice,
context.l10n.thisDevice,
style: TextStyle(
fontWeight: FontWeight.bold,
color: getEnteColorScheme(context).alternativeColor,
color: Theme.of(context).colorScheme.alternativeColor,
),
);
}

View File

@@ -9,6 +9,7 @@ import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
import 'package:ente_auth/onboarding/view/view_qr_page.dart';
import 'package:ente_auth/services/local_authentication_service.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';
@@ -21,7 +22,6 @@ 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:ente_lock_screen/local_authentication_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_context_menu/flutter_context_menu.dart';

View File

@@ -1,9 +1,9 @@
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_result.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_base/typedefs.dart';
import 'package:flutter/material.dart';
enum DialogUserChoice { firstChoice, secondChoice }

View File

@@ -1,4 +1,5 @@
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:ente_auth/theme/colors.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/theme/text_style.dart';
@@ -8,7 +9,6 @@ import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/components/models/custom_button_style.dart';
import 'package:ente_auth/utils/debouncer.dart';
import "package:ente_auth/utils/dialog_util.dart";
import 'package:ente_base/typedefs.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:ente_auth/theme/colors.dart';
import 'package:ente_auth/theme/effects.dart';
import 'package:ente_auth/theme/ente_theme.dart';
@@ -10,7 +11,6 @@ import 'package:ente_auth/ui/components/models/button_result.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/components/separators.dart';
import 'package:ente_auth/ui/components/text_input_widget.dart';
import 'package:ente_base/typedefs.dart';
import 'package:flutter/material.dart';
///Will return null if dismissed by tapping outside

View File

@@ -1,8 +1,8 @@
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/components/menu_item_child_widgets.dart';
import 'package:ente_auth/utils/debouncer.dart';
import 'package:ente_base/typedefs.dart';
import 'package:expandable/expandable.dart';
import 'package:flutter/material.dart';

View File

@@ -1,9 +1,9 @@
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/common/loading_widget.dart';
import 'package:ente_auth/ui/components/separators.dart';
import 'package:ente_auth/utils/debouncer.dart';
import 'package:ente_base/typedefs.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:logging/logging.dart';

View File

@@ -1,9 +1,9 @@
import 'package:ente_auth/ente_theme_data.dart';
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/theme/colors.dart';
import 'package:ente_auth/models/execution_states.dart';
import 'package:ente_auth/models/typedefs.dart';
import 'package:ente_auth/theme/colors.dart';
import 'package:ente_auth/ui/common/loading_widget.dart';
import 'package:ente_auth/utils/debouncer.dart';
import 'package:ente_base/typedefs.dart';
import 'package:flutter/material.dart';
typedef OnChangedCallBack = void Function(bool);

View File

@@ -1,10 +1,10 @@
import 'dart:ui';
import 'package:ente_auth/core/event_bus.dart';
import 'package:ente_auth/events/codes_updated_event.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/utils/platform_util.dart';
import 'package:ente_events/event_bus.dart';
import 'package:flutter/material.dart';
class CoachMarkWidget extends StatelessWidget {

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