Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7207921838 | ||
|
|
56a741b2ac | ||
|
|
e52aa78fae | ||
|
|
9bcd107de8 | ||
|
|
8e5488eaf9 | ||
|
|
b20139f3b8 | ||
|
|
21b139687d | ||
|
|
385731196f | ||
|
|
71c2958078 | ||
|
|
7a25da3927 | ||
|
|
34c9ee76b3 | ||
|
|
3fbad241b1 | ||
|
|
9143634000 | ||
|
|
a573c012e0 | ||
|
|
94debda2c7 | ||
|
|
c9f37b4f5a | ||
|
|
05652656ae | ||
|
|
8e715b4cf9 | ||
|
|
e264d3456b | ||
|
|
56a73ab7ee | ||
|
|
963d18b501 | ||
|
|
b35d3bda16 | ||
|
|
7ad76adcaa | ||
|
|
691eaabf50 | ||
|
|
7ab16df330 | ||
|
|
3c16cfd829 | ||
|
|
8c9c9c53d1 | ||
|
|
aeaaddbe40 | ||
|
|
25782870c7 | ||
|
|
39e28dd63b | ||
|
|
517ce33fd9 | ||
|
|
02d2cb5733 | ||
|
|
2b5f349b2f | ||
|
|
08ad496975 | ||
|
|
f52ee5683b | ||
|
|
332faa9166 | ||
|
|
3d95c789f5 | ||
|
|
244d4d969a | ||
|
|
32605dc708 | ||
|
|
ef0c302c94 | ||
|
|
fba694de68 | ||
|
|
3d4cd106cb | ||
|
|
61e6e0ffaa | ||
|
|
e0b952e516 | ||
|
|
546a9234a4 | ||
|
|
da2083ef08 | ||
|
|
8a1177e7db | ||
|
|
8acd1faf03 | ||
|
|
0c28b83b46 | ||
|
|
fa0ac608f4 | ||
|
|
0965b367cc | ||
|
|
37db940720 | ||
|
|
334800472e | ||
|
|
0da4497857 | ||
|
|
0b05a21dc7 | ||
|
|
2558473399 | ||
|
|
4dd465fdfa | ||
|
|
1f20ba17f4 | ||
|
|
9ce8059212 | ||
|
|
e69a3adf14 | ||
|
|
8baa350056 | ||
|
|
2526b61620 | ||
|
|
ec4df467a0 | ||
|
|
41fd5337a6 | ||
|
|
8fbec8307e | ||
|
|
d902fdbf75 | ||
|
|
932dc53f6c | ||
|
|
745cb54ffd | ||
|
|
8ced5bf32e | ||
|
|
9b1a8fb4ca | ||
|
|
824c6d769b | ||
|
|
92c2247aa1 | ||
|
|
e84e9db70e | ||
|
|
be72db844d | ||
|
|
2386a5a10b | ||
|
|
09ae14a1d6 | ||
|
|
924ce5ff86 | ||
|
|
c47370163d | ||
|
|
3dd408af01 | ||
|
|
408e3bd2b6 | ||
|
|
ebf634ef1e |
4
.github/workflows/ci.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.3.2'
|
||||
flutter-version: '3.3.8'
|
||||
|
||||
# Fetch sub modules
|
||||
- run: git submodule update --init --recursive
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: release-checksum
|
||||
path: build/app/outputs/flutter-apk/checksum
|
||||
path: build/app/outputs/flutter-apk/sha256sum
|
||||
|
||||
# Create a Github release
|
||||
- uses: ncipollo/release-action@v1
|
||||
|
||||
11
.vscode/launch.json
vendored
@@ -26,14 +26,21 @@
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.3:8080"]
|
||||
"args": ["--dart-define", "endpoint=http://192.168.1.30:8080"]
|
||||
},
|
||||
{
|
||||
"name": "Prod",
|
||||
"name": "iOS Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart"]
|
||||
},
|
||||
{
|
||||
"name": "Android Prod",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"program": "lib/main.dart",
|
||||
"args": ["--target", "lib/main.dart", "--flavor", "independent"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
69
README.md
@@ -1,3 +1,68 @@
|
||||
# ente Auth
|
||||
# ente Authenticator
|
||||
|
||||
Open source authenticator app for your 2FA secrets, with end-to-end encrypted backups.
|
||||
ente's Authenticator app helps you generate and store 2 step verification (2FA)
|
||||
tokens on your mobile devices.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
### Secure Backups
|
||||
|
||||
ente provides end-to-end encrypted cloud backups so that you don't have to worry
|
||||
about losing your tokens. We use the same protocols [ente
|
||||
Photos](https://ente.io) uses to encrypt and preserve your data.
|
||||
|
||||
|
||||
### Multi Device Synchronization
|
||||
|
||||
ente will automatically sync the 2FA tokens you add to your account, across all
|
||||
your devices. Every new device you sign into will have access to these tokens.
|
||||
|
||||
|
||||
### Offline Mode
|
||||
|
||||
ente generates 2FA tokens offline, so your network connectivity will not get in
|
||||
the way of your workflow.
|
||||
|
||||
### Import and Export Tokens
|
||||
|
||||
You can add tokens to ente by one of the following methods:
|
||||
1. Scanning a QR code
|
||||
2. Manually entering (copy-pasting) a 2FA secret
|
||||
3. Bulk importing from a file that contains a list of codes in the following
|
||||
format:
|
||||
```
|
||||
otpauth://totp/ACCOUNT?secret=SUPERSECRET&issuer=SERVICE
|
||||
```
|
||||
The codes maybe separated by new lines or commas.
|
||||
|
||||
You can also export the codes you have added to ente, to an **unencrypted** text
|
||||
file, that adheres to the above format.
|
||||
|
||||
|
||||
## 🔩 Architecture
|
||||
|
||||
The architecture that powers end-to-end encrypted storage and sync of your
|
||||
tokens has been documented [here](architecture/index.md).
|
||||
|
||||
|
||||
## 🧑💻 Building from source
|
||||
|
||||
1. [Install Flutter](https://flutter.dev/docs/get-started/install)
|
||||
2. Clone this repository with `git clone git@github.com:ente-io/auth.git`
|
||||
3. Pull in all submodules with `git submodule update --init --recursive`
|
||||
4. For Android, run `flutter build apk --release --flavor independent`
|
||||
5. For iOS, run `flutter build ios`
|
||||
|
||||
|
||||
## 🙋♂️ Support
|
||||
|
||||
If you need help, please reach out to support@ente.io, and a human will get in
|
||||
touch with you.
|
||||
|
||||
On the other hand, if you wish to support us, please
|
||||
[star](https://github.com/ente-io/auth/stargazers) this project.
|
||||
|
||||
|
||||
## 💜 Community
|
||||
- Follow us on [Twitter](https://twitter.com/enteio)
|
||||
- Join us on [Discord](https://ente.io/discord)
|
||||
|
||||
50
SECURITY.md
Normal file
@@ -0,0 +1,50 @@
|
||||
ente believes that working with security researchers across the globe is crucial
|
||||
to keeping our users safe. If you believe you've found a security issue in our
|
||||
product or service, we encourage you to notify us (security@ente.io). We welcome
|
||||
working with you to resolve the issue promptly. Thanks in advance!
|
||||
|
||||
# Disclosure Policy
|
||||
|
||||
- Let us know as soon as possible upon discovery of a potential security issue,
|
||||
and we'll make every effort to quickly resolve the issue.
|
||||
- Provide us a reasonable amount of time to resolve the issue before any
|
||||
disclosure to the public or a third-party. We may publicly disclose the issue
|
||||
before resolving it, if appropriate.
|
||||
- Make a good faith effort to avoid privacy violations, destruction of data, and
|
||||
interruption or degradation of our service. Only interact with accounts you
|
||||
own or with explicit permission of the account holder.
|
||||
- If you would like to encrypt your report, please use the PGP key with long ID
|
||||
`E273695C0403F34F74171932DF6DDDE98EBD2394` (available in the public keyserver
|
||||
pool).
|
||||
|
||||
# In-scope
|
||||
|
||||
- Security issues in any current release of ente. This includes the web app,
|
||||
desktop app, and mobile apps (iOS and Android). Product downloads are
|
||||
available at https://ente.io. Source code is available at
|
||||
https://github.com/ente-io.
|
||||
|
||||
# Exclusions
|
||||
|
||||
The following bug classes are out-of scope:
|
||||
|
||||
- Bugs that are already reported on any of ente's issue trackers
|
||||
(https://github.com/ente-io), or that we already know of. Note that some of
|
||||
our issue tracking is private.
|
||||
- Issues in an upstream software dependency (ex: Flutter, Next.js etc) which are
|
||||
already reported to the upstream maintainer.
|
||||
- Attacks requiring physical access to a user's device.
|
||||
- Self-XSS
|
||||
- Issues related to software or protocols not under ente's control
|
||||
- Vulnerabilities in outdated versions of ente
|
||||
- Missing security best practices that do not directly lead to a vulnerability
|
||||
- Issues that do not have any impact on the general public
|
||||
|
||||
While researching, we'd like to ask you to refrain from:
|
||||
|
||||
- Denial of service
|
||||
- Spamming
|
||||
- Social engineering (including phishing) of ente staff or contractors
|
||||
- Any physical attempts against ente property or data centers
|
||||
|
||||
Thank you for helping keep ente and our users safe!
|
||||
@@ -35,7 +35,7 @@
|
||||
<meta-data android:name="flutterEmbedding" android:value="2"/>
|
||||
|
||||
<meta-data android:name="io.sentry.dsn"
|
||||
android:value="https://8aeb7f013be74f829f8b73b46b3d7a80@sentry.ente.io/8"/>
|
||||
android:value="https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9"/>
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package io.ente.auth
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
|
||||
class MainActivity: FlutterActivity() {
|
||||
class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
|
||||
35
architecture/authentication.svg
Normal file
|
After Width: | Height: | Size: 119 KiB |
11
architecture/e2ee.svg
Normal file
|
After Width: | Height: | Size: 22 KiB |
26
architecture/file-encryption.svg
Normal file
|
After Width: | Height: | Size: 105 KiB |
275
architecture/index.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Architecture
|
||||
|
||||
This is an overview of ente's end-to-end encrypted architecture and
|
||||
specifications of the underlying cryptography.
|
||||
|
||||
## Introduction
|
||||
|
||||
Your data is end-to-end encrypted with **ente**. Meaning, they are encrypted
|
||||
with your `keys` before they leave your device.
|
||||
|
||||
<img src="e2ee.svg" class="architecture-svg" style="max-width: 600px"
|
||||
title="End-to-end encryption in ente" />
|
||||
|
||||
<br/>
|
||||
|
||||
These `keys` are available only to you. Meaning only you can access your data
|
||||
else where.
|
||||
|
||||
What follows is an explanation of how we do what we do.
|
||||
|
||||
## Key Encryption
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Master Key
|
||||
|
||||
When you sign up for **ente**, your client generates a `masterKey` for you. This
|
||||
never leaves your device unencrypted.
|
||||
|
||||
#### Key Encryption Key
|
||||
|
||||
Once you choose a password, a `keyEncryptionKey` is derived from it. This never
|
||||
leaves your device.
|
||||
|
||||
### Flows
|
||||
|
||||
#### Primary Device
|
||||
|
||||
During registration, your `masterKey` is encrypted with your`keyEncryptionKey`,
|
||||
and the resultant `encryptedMasterKey` is then sent to our servers for storage.
|
||||
|
||||
<img src="key-derivation.svg" class="architecture-svg" title="Key derivation" />
|
||||
|
||||
#### <a id="key-encryption-flows-secondary-device"></a> Secondary Device
|
||||
|
||||
When you sign in on a secondary device, after you successfully verify your
|
||||
email, our servers give you back your `encryptedMasterKey` that was sent to us
|
||||
by your primary device.
|
||||
|
||||
You are then prompted to enter your password. Once entered, your
|
||||
`keyEncryptionKey` is derived, and the client decrypts your `encryptedMasterKey`
|
||||
with this, to yield your original `masterKey`.
|
||||
|
||||
If the decryption fails, the client will know that the derived
|
||||
`keyEncryptionKey` was wrong, indicating an incorrect password, and this
|
||||
information will be surfaced to you.
|
||||
|
||||
### Privacy
|
||||
|
||||
- Since only you know your password, only you can derive your
|
||||
`keyEncryptionKey`.
|
||||
- Since only you can derive your `keyEncryptionKey`, only you have access to
|
||||
your `masterKey`.
|
||||
|
||||
> Keep reading to learn about how this `masterKey` is used to encrypt your data.
|
||||
|
||||
---
|
||||
|
||||
## Token Encryption
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Token Key
|
||||
|
||||
Each of your tokens in **ente** are encrypted with a `tokenKey`. These never
|
||||
leave your device unencrypted.
|
||||
|
||||
#### Authenticator Key
|
||||
|
||||
Each of your `tokenKey`s are in turn encrypted with an `authKey`. This never
|
||||
leave your device unencrypted.
|
||||
|
||||
### Flows
|
||||
|
||||
#### Upload
|
||||
|
||||
- Each token and associated metadata is encrypted with randomly generated
|
||||
`tokenKey`s.
|
||||
- Each `tokenKey` is encrypted with your `authKey`. In case your account does
|
||||
not have an `authKey` yet, one is randomly generated and encrypted with your
|
||||
`masterKey`.
|
||||
- All of the above mentioned encrypted data is then pushed to the server for
|
||||
storage.
|
||||
|
||||
<img src="token-encryption.svg" class="architecture-svg" title="Token
|
||||
encryption" />
|
||||
|
||||
#### Download
|
||||
|
||||
- All of the above mentioned encrypted data is pulled from the server.
|
||||
- You first decrypt your `authKey` with your `masterKey`.
|
||||
- You then decrypt each token's `tokenKey` with your `authKey`.
|
||||
- Finally, you decrypt each token and associated metadata with the respective
|
||||
`tokenKey`s.
|
||||
|
||||
### Privacy
|
||||
|
||||
- As explained in the previous section, only you have access to your
|
||||
`masterKey`.
|
||||
- Since only you have access to your `masterKey`, only you can decrypt your
|
||||
`authKey`.
|
||||
- Since only you have access to your `authKey`, only you can decrypt the
|
||||
`tokenKey`s.
|
||||
- Since only you have access to the `tokenKey`s, only you can decrypt the tokens
|
||||
and their associated metadata.
|
||||
|
||||
---
|
||||
|
||||
## Key Recovery
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### Recovery Key
|
||||
|
||||
When you sign up for **ente**, your app generates a `recoveryKey` for you. This
|
||||
never leaves your device unencrypted.
|
||||
|
||||
### Flow
|
||||
|
||||
#### Storage
|
||||
|
||||
Your `recoveryKey` and `masterKey` are encrypted with each other and stored on
|
||||
the server.
|
||||
|
||||
#### Access
|
||||
|
||||
This encrypted `recoveryKey` is downloaded when you sign in on a new device.
|
||||
This is decrypted with your `masterKey` and surfaced to you whenever you request
|
||||
for it.
|
||||
|
||||
#### Recovery
|
||||
|
||||
Post email verification, if you're unable to unlock your account because you
|
||||
have forgotten your password, the client will prompt you to enter your
|
||||
`recoveryKey`.
|
||||
|
||||
The client then pulls the `masterKey` that was earlier encrypted and pushed to
|
||||
the server (as discussed in [Key Encryption](#key-encryption), and decrypts it
|
||||
with the entered `recoveryKey`. If the decryption succeeds, the client will know
|
||||
that you have entered the correct `recoveryKey`.
|
||||
|
||||
<img src="recovery.svg" class="architecture-svg" title="Recovery" />
|
||||
|
||||
Now that you have your `masterKey`, the client will prompt you to set a new
|
||||
password, using which it will derive a new `keyEncryptionKey`. This is then used
|
||||
to encrypt your `masterKey` and this new `encryptedMasterKey` is uploaded to our
|
||||
servers, similar to what was earlier discussed in [Key
|
||||
Encryption](#key-encryption).
|
||||
|
||||
### Privacy
|
||||
|
||||
- Since only you have access to your `masterKey`, only you can access your
|
||||
`recoveryKey`.
|
||||
- Since only you can access your `recoveryKey`, only you can reset your
|
||||
password.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
### Fundamentals
|
||||
|
||||
#### One Time Token
|
||||
|
||||
When you attempt to verify ownership of an email address, our server generates a
|
||||
`oneTimeToken`, that if presented confirms your access to the said email
|
||||
address. This token is valid for a short time and can only be used once.
|
||||
|
||||
#### Authentication Token
|
||||
|
||||
When you successfully authenticate yourself against our server by proving
|
||||
ownership of your email (and in future any other configured vectors), the server
|
||||
generates an `authToken`, that can from there on be used to authenticate against
|
||||
our private APIs.
|
||||
|
||||
#### Encrypted Authentication Token
|
||||
|
||||
A generated `authToken` is returned to your client after being encrypted with
|
||||
your `publicKey`. This `encryptedAuthToken` can only be decrypted with your
|
||||
`privateKey`.
|
||||
|
||||
### Flow
|
||||
|
||||
- You are asked for an email address, to which a `oneTimeToken` is sent.
|
||||
- Once you present this information correctly to our server, an `authToken` is
|
||||
generated and an `encryptedAuthToken` is returned to you, along with your
|
||||
other encrypted keys.
|
||||
- You are then prompted to enter your password, using which your `masterKey` is
|
||||
derived (as discussed [here](#key-encryption-flows-secondary-device)).
|
||||
- Using this `masterKey`, the rest of your keys, including your `privateKey` is
|
||||
decrypted (as discussed [here](#private-key)).
|
||||
- Using your `privateKey`, the client will then decrypt the `encryptedAuthToken`
|
||||
that was earlier encrypted by our server with your `publicKey`.
|
||||
- This decrypted `authToken` can then from there on be used to authenticate all
|
||||
API calls against our servers.
|
||||
|
||||
<img src="authentication.svg" class="architecture-svg" title="Authentication" />
|
||||
|
||||
### Security
|
||||
|
||||
Only by verifying access to your email and knowing your password can you obtain
|
||||
an`authToken` that can be used to authenticate yourself against our servers.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Details
|
||||
|
||||
We rely on the high level APIs exposed by this wonderful library called
|
||||
[libsodium](https://libsodium.gitbook.io/doc/).
|
||||
|
||||
#### Key Generation
|
||||
|
||||
[`crypto_secretbox_keygen`](https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes)
|
||||
is used to generate all random keys within the application. Your `masterKey`,
|
||||
`recoveryKey`, `authKey`, `tokenKey` are all 256-bit keys generated using this
|
||||
API.
|
||||
|
||||
#### Key Derivation
|
||||
|
||||
[`crypto_pwhash`](https://libsodium.gitbook.io/doc/password_hashing/default_phf)
|
||||
is used to derive your `keyEncryptionKey` from your password.
|
||||
|
||||
`crypto_pwhash_OPSLIMIT_SENSITIVE` and `crypto_pwhash_MEMLIMIT_SENSITIVE` are
|
||||
used as the limits for computation and memory respectively. If the operation
|
||||
fails due to insufficient memory, the former is doubled and the latter is halved
|
||||
progressively, until a key can be derived. If during this process the memory
|
||||
limit is reduced to a value less than `crypto_pwhash_MEMLIMIT_MIN`, the client
|
||||
will not let you register from that device.
|
||||
|
||||
Internally, this uses [Argon2
|
||||
v1.3](https://github.com/P-H-C/phc-winner-argon2/raw/master/argon2-specs.pdf),
|
||||
which is regarded as [one of the best hashing
|
||||
algorithms](https://en.wikipedia.org/wiki/Argon2) currently available.
|
||||
|
||||
#### Symmetric Encryption
|
||||
|
||||
[`crypto_secretbox_easy`](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretbox)
|
||||
is used to encrypt your `masterKey`, `recoveryKey`, `authKey` and `tokenKey`s.
|
||||
Internally, this uses
|
||||
[XSalsa20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xsalsa20)
|
||||
stream cipher with [Poly1305
|
||||
MAC](https://datatracker.ietf.org/doc/html/rfc8439#section-2.5) for
|
||||
authentication.
|
||||
|
||||
[`crypto_secretstream_*`](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
|
||||
APIs are used to encrypt your token data. Internally, this uses
|
||||
[XChaCha20](https://libsodium.gitbook.io/doc/advanced/stream_ciphers/xchacha20)
|
||||
stream cipher with [Poly1305
|
||||
MAC](https://datatracker.ietf.org/doc/html/rfc8439#section-2.5) for
|
||||
authentication.
|
||||
|
||||
#### Salt & Nonce Generation
|
||||
|
||||
[`randombytes_buf`](https://libsodium.gitbook.io/doc/generating_random_data) is
|
||||
used to generate a new salt/nonce every time data needs to be hashed/encrypted.
|
||||
|
||||
---
|
||||
|
||||
## Further Details
|
||||
|
||||
Thank you for reading this far! For implementation details, we request you to
|
||||
checkout [our code](https://github.com/ente-io).
|
||||
|
||||
If you'd like to help us improve this document, kindly email
|
||||
[security@ente.io](mailto:security@ente.io).
|
||||
17
architecture/key-derivation.svg
Normal file
|
After Width: | Height: | Size: 76 KiB |
33
architecture/recovery.svg
Normal file
|
After Width: | Height: | Size: 130 KiB |
21
architecture/sharing.svg
Normal file
|
After Width: | Height: | Size: 123 KiB |
26
architecture/token-encryption.svg
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
assets/2.0x/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
assets/2.0x/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
assets/3.0x/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
assets/3.0x/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
assets/loading_photos_background.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
assets/loading_photos_background_dark.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -4,6 +4,40 @@ PODS:
|
||||
- Reachability
|
||||
- device_info (0.0.1):
|
||||
- Flutter
|
||||
- DKImagePickerController/Core (4.3.4):
|
||||
- DKImagePickerController/ImageDataManager
|
||||
- DKImagePickerController/Resource
|
||||
- DKImagePickerController/ImageDataManager (4.3.4)
|
||||
- DKImagePickerController/PhotoGallery (4.3.4):
|
||||
- DKImagePickerController/Core
|
||||
- DKPhotoGallery
|
||||
- DKImagePickerController/Resource (4.3.4)
|
||||
- DKPhotoGallery (0.0.17):
|
||||
- DKPhotoGallery/Core (= 0.0.17)
|
||||
- DKPhotoGallery/Model (= 0.0.17)
|
||||
- DKPhotoGallery/Preview (= 0.0.17)
|
||||
- DKPhotoGallery/Resource (= 0.0.17)
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Core (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Preview
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Model (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Preview (0.0.17):
|
||||
- DKPhotoGallery/Model
|
||||
- DKPhotoGallery/Resource
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- DKPhotoGallery/Resource (0.0.17):
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- fk_user_agent (2.0.0):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
@@ -16,6 +50,8 @@ PODS:
|
||||
- flutter_inappwebview/Core (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (~> 5.0)
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- flutter_native_splash (0.0.1):
|
||||
- Flutter
|
||||
- flutter_secure_storage (3.3.1):
|
||||
@@ -35,6 +71,8 @@ PODS:
|
||||
- move_to_background (0.0.1):
|
||||
- Flutter
|
||||
- MTBBarcodeScanner (5.0.11)
|
||||
- open_file (0.0.1):
|
||||
- Flutter
|
||||
- OrderedSet (5.0.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
@@ -44,6 +82,14 @@ PODS:
|
||||
- Flutter
|
||||
- MTBBarcodeScanner
|
||||
- Reachability (3.2)
|
||||
- SDWebImage (5.13.4):
|
||||
- SDWebImage/Core (= 5.13.4)
|
||||
- SDWebImage/Core (5.13.4)
|
||||
- Sentry/HybridSDK (7.30.2)
|
||||
- sentry_flutter (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- Sentry/HybridSDK (= 7.30.2)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_ios (0.0.1):
|
||||
@@ -51,6 +97,7 @@ PODS:
|
||||
- sqflite (0.0.2):
|
||||
- Flutter
|
||||
- FMDB (>= 2.7.5)
|
||||
- SwiftyGif (5.4.3)
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
@@ -58,10 +105,12 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- connectivity (from `.symlinks/plugins/connectivity/ios`)
|
||||
- device_info (from `.symlinks/plugins/device_info/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`)
|
||||
- flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`)
|
||||
@@ -69,9 +118,11 @@ DEPENDENCIES:
|
||||
- in_app_purchase (from `.symlinks/plugins/in_app_purchase/ios`)
|
||||
- local_auth (from `.symlinks/plugins/local_auth/ios`)
|
||||
- move_to_background (from `.symlinks/plugins/move_to_background/ios`)
|
||||
- open_file (from `.symlinks/plugins/open_file/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||
- qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
|
||||
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/ios`)
|
||||
@@ -79,10 +130,15 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- FMDB
|
||||
- MTBBarcodeScanner
|
||||
- OrderedSet
|
||||
- Reachability
|
||||
- SDWebImage
|
||||
- Sentry
|
||||
- SwiftyGif
|
||||
- Toast
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
@@ -90,6 +146,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/connectivity/ios"
|
||||
device_info:
|
||||
:path: ".symlinks/plugins/device_info/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
fk_user_agent:
|
||||
:path: ".symlinks/plugins/fk_user_agent/ios"
|
||||
Flutter:
|
||||
@@ -98,6 +156,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/flutter_email_sender/ios"
|
||||
flutter_inappwebview:
|
||||
:path: ".symlinks/plugins/flutter_inappwebview/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
flutter_native_splash:
|
||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||
flutter_secure_storage:
|
||||
@@ -112,12 +172,16 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/local_auth/ios"
|
||||
move_to_background:
|
||||
:path: ".symlinks/plugins/move_to_background/ios"
|
||||
open_file:
|
||||
:path: ".symlinks/plugins/open_file/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
path_provider_ios:
|
||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||
qr_code_scanner:
|
||||
:path: ".symlinks/plugins/qr_code_scanner/ios"
|
||||
sentry_flutter:
|
||||
:path: ".symlinks/plugins/sentry_flutter/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_ios:
|
||||
@@ -130,27 +194,36 @@ EXTERNAL SOURCES:
|
||||
SPEC CHECKSUMS:
|
||||
connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467
|
||||
device_info: d7d233b645a32c40dfdc212de5cf646ca482f175
|
||||
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
|
||||
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
|
||||
file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95
|
||||
fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
|
||||
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
|
||||
flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
|
||||
flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721
|
||||
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
|
||||
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
|
||||
flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec
|
||||
flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b
|
||||
fluttertoast: 16fbe6039d06a763f3533670197d01fc73459037
|
||||
fluttertoast: 74526702fea2c060ea55dde75895b7e1bde1c86b
|
||||
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
|
||||
in_app_purchase: 3e2155afa9d03d4fa32d9e62d567885080ce97d6
|
||||
local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c
|
||||
move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
|
||||
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
|
||||
open_file: 02eb5cb6b21264bd3a696876f5afbfb7ca4f4b7d
|
||||
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
|
||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||
qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e
|
||||
Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96
|
||||
SDWebImage: e5cc87bf736e60f49592f307bdf9e157189298a3
|
||||
Sentry: 9be48e341494bc976c963b05aa4a8ca48308c684
|
||||
sentry_flutter: 544e6376e35b00eef9f0864f8bb7f10a0e204993
|
||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
|
||||
|
||||
|
||||
@@ -58,5 +58,7 @@
|
||||
<false/>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Please allow auth to lock itself with FaceID or TouchID</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Please allow auth to pick a file to import data from</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -10,10 +10,11 @@ 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/onboarding/view/onboarding_page.dart";
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/ui/home_page.dart';
|
||||
import 'package:ente_auth/ui/settings/app_update_dialog.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import "package:flutter_localizations/flutter_localizations.dart";
|
||||
|
||||
class App extends StatefulWidget {
|
||||
@@ -39,6 +40,21 @@ class _AppState extends State<App> {
|
||||
setState(() {});
|
||||
}
|
||||
});
|
||||
UpdateService.instance.shouldUpdate().then((shouldUpdate) {
|
||||
if (shouldUpdate) {
|
||||
Future.delayed(Duration.zero, () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AppUpdateDialog(
|
||||
UpdateService.instance.getLatestVersionInfo(),
|
||||
);
|
||||
},
|
||||
barrierColor: Colors.black.withOpacity(0.85),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@@ -62,7 +78,6 @@ class _AppState extends State<App> {
|
||||
theme: lightTheme,
|
||||
darkTheme: dartTheme,
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: EasyLoading.init(),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
@@ -80,7 +95,6 @@ class _AppState extends State<App> {
|
||||
theme: lightThemeData,
|
||||
darkTheme: darkThemeData,
|
||||
debugShowCheckedModeBanner: false,
|
||||
builder: EasyLoading.init(),
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
|
||||
@@ -4,9 +4,9 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
// import 'package:ente_auth/core/error-reporting/super_logging.dart';
|
||||
import 'package:ente_auth/core/errors.dart';
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
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';
|
||||
@@ -14,13 +14,11 @@ import 'package:ente_auth/models/key_gen_result.dart';
|
||||
import 'package:ente_auth/models/private_key_attributes.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
// import 'package:ente_auth/utils/validator_util.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Configuration {
|
||||
Configuration._privateConstructor();
|
||||
@@ -43,7 +41,6 @@ class Configuration {
|
||||
static const userIDKey = "user_id";
|
||||
static const hasMigratedSecureStorageToFirstUnlockKey =
|
||||
"has_migrated_secure_storage_to_first_unlock";
|
||||
static const anonymousUserIDKey = "anonymous_user_id";
|
||||
|
||||
final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds;
|
||||
|
||||
@@ -120,7 +117,6 @@ class Configuration {
|
||||
}
|
||||
await _migrateSecurityStorageToFirstUnlock();
|
||||
}
|
||||
// SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
|
||||
}
|
||||
|
||||
Future<void> logout({bool autoLogout = false}) async {
|
||||
@@ -130,6 +126,7 @@ class Configuration {
|
||||
_key = null;
|
||||
_cachedToken = null;
|
||||
_secretKey = null;
|
||||
_authSecretKey = null;
|
||||
Bus.instance.fire(SignedOutEvent());
|
||||
}
|
||||
|
||||
@@ -494,12 +491,4 @@ class Configuration {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getOrCreateAnonymousUserID() async {
|
||||
if (!_preferences.containsKey(anonymousUserIDKey)) {
|
||||
//ignore: prefer_const_constructors
|
||||
await _preferences.setString(anonymousUserIDKey, Uuid().v4());
|
||||
}
|
||||
return _preferences.getString(anonymousUserIDKey)!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ const int thumbnailLargeSize = 512;
|
||||
const int compressedThumbnailResolution = 1080;
|
||||
const int thumbnailDataLimit = 100 * 1024;
|
||||
const String sentryDSN =
|
||||
"https://2235e5c99219488ea93da34b9ac1cb68@sentry.ente.io/4";
|
||||
const String sentryDebugDSN =
|
||||
"https://ca5e686dd7f149d9bf94e620564cceba@sentry.ente.io/3";
|
||||
"https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9";
|
||||
const String sentryTunnel = "https://sentry-reporter.ente.io";
|
||||
const String roadmapURL = "https://roadmap.ente.io";
|
||||
const int microSecondsInDay = 86400000000;
|
||||
|
||||
@@ -7,13 +7,18 @@ import 'dart:collection';
|
||||
import 'dart:core';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/logging/tunneled_transport.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';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
typedef FutureOrVoidCallback = FutureOr<void> Function();
|
||||
|
||||
@@ -141,65 +146,62 @@ class SuperLogging {
|
||||
/// The current super logging configuration
|
||||
static LogConfig config;
|
||||
|
||||
static SharedPreferences _preferences;
|
||||
|
||||
static const keyShouldReportErrors = "should_report_errors";
|
||||
|
||||
static const keyAnonymousUserID = "anonymous_user_id";
|
||||
|
||||
static Future<void> main([LogConfig config]) async {
|
||||
config ??= LogConfig();
|
||||
|
||||
SuperLogging.config = config;
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
appVersion ??= await getAppVersion();
|
||||
final isFDroidClient = await isFDroidBuild();
|
||||
if (isFDroidClient) {
|
||||
config.sentryDsn = null;
|
||||
config.tunnel = null;
|
||||
}
|
||||
_preferences = await SharedPreferences.getInstance();
|
||||
|
||||
final enable = config.enableInDebugMode || kReleaseMode;
|
||||
sentryIsEnabled = enable && config.sentryDsn != null && !isFDroidClient;
|
||||
fileIsEnabled = enable && config.logDirPath != null;
|
||||
appVersion ??= await getAppVersion();
|
||||
|
||||
final loggingEnabled = config.enableInDebugMode || kReleaseMode;
|
||||
sentryIsEnabled =
|
||||
loggingEnabled && config.sentryDsn != null && shouldReportErrors();
|
||||
fileIsEnabled = loggingEnabled && config.logDirPath != null;
|
||||
|
||||
if (fileIsEnabled) {
|
||||
await setupLogDir();
|
||||
}
|
||||
if (sentryIsEnabled) {
|
||||
setupSentry();
|
||||
}
|
||||
|
||||
Logger.root.level = Level.ALL;
|
||||
Logger.root.onRecord.listen(onLogRecord);
|
||||
|
||||
if (isFDroidClient) {
|
||||
assert(
|
||||
sentryIsEnabled == false,
|
||||
"sentry dsn should be disabled for "
|
||||
"f-droid config ${config.sentryDsn} & ${config.tunnel}",
|
||||
);
|
||||
if (sentryIsEnabled) {
|
||||
setupSentry();
|
||||
} else {
|
||||
$.info("Sentry is disabled");
|
||||
}
|
||||
|
||||
if (!enable) {
|
||||
if (!loggingEnabled) {
|
||||
$.info("detected debug mode; sentry & file logging disabled.");
|
||||
}
|
||||
if (fileIsEnabled) {
|
||||
$.info("log file for today: $logFile with prefix ${config.prefix}");
|
||||
}
|
||||
if (sentryIsEnabled) {
|
||||
$.info("sentry uploader started");
|
||||
}
|
||||
|
||||
if (config.body == null) return;
|
||||
|
||||
if (enable && sentryIsEnabled) {
|
||||
// await SentryFlutter.init(
|
||||
// (options) {
|
||||
// options.dsn = config.sentryDsn;
|
||||
// options.httpClient = http.Client();
|
||||
// if (config.tunnel != null) {
|
||||
// options.transport =
|
||||
// TunneledTransport(Uri.parse(config.tunnel), options);
|
||||
// }
|
||||
// },
|
||||
// appRunner: () => config.body(),
|
||||
// );
|
||||
if (loggingEnabled && sentryIsEnabled) {
|
||||
await SentryFlutter.init(
|
||||
(options) {
|
||||
options.dsn = config.sentryDsn;
|
||||
options.httpClient = http.Client();
|
||||
if (config.tunnel != null) {
|
||||
options.transport =
|
||||
TunneledTransport(Uri.parse(config.tunnel), options);
|
||||
}
|
||||
},
|
||||
appRunner: () => config.body(),
|
||||
);
|
||||
} else {
|
||||
await config.body();
|
||||
}
|
||||
@@ -207,21 +209,21 @@ class SuperLogging {
|
||||
|
||||
static void setUserID(String userID) async {
|
||||
if (config?.sentryDsn != null) {
|
||||
// Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
|
||||
Sentry.configureScope((scope) => scope.user = SentryUser(id: userID));
|
||||
$.info("setting sentry user ID to: $userID");
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _sendErrorToSentry(Object error, StackTrace stack) async {
|
||||
// try {
|
||||
// await Sentry.captureException(
|
||||
// error,
|
||||
// stackTrace: stack,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// $.info('Sending report to sentry.io failed: $e');
|
||||
// $.info('Original error: $error');
|
||||
// }
|
||||
try {
|
||||
await Sentry.captureException(
|
||||
error,
|
||||
stackTrace: stack,
|
||||
);
|
||||
} catch (e) {
|
||||
$.info('Sending report to sentry.io failed: $e');
|
||||
$.info('Original error: $error');
|
||||
}
|
||||
}
|
||||
|
||||
static String _lastExtraLines = '';
|
||||
@@ -249,8 +251,16 @@ class SuperLogging {
|
||||
}
|
||||
|
||||
// add error to sentry queue
|
||||
if (sentryIsEnabled && rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
if (sentryIsEnabled) {
|
||||
if (rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
} else if (rec.level == Level.SEVERE || rec.level == Level.SHOUT) {
|
||||
if (rec.error != null) {
|
||||
_sendErrorToSentry(rec.error, null);
|
||||
} else {
|
||||
_sendErrorToSentry(rec.message, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,17 +295,19 @@ class SuperLogging {
|
||||
static bool sentryIsEnabled;
|
||||
|
||||
static Future<void> setupSentry() async {
|
||||
$.info("Setting up sentry");
|
||||
SuperLogging.setUserID(await _getOrCreateAnonymousUserID());
|
||||
await for (final error in sentryQueueControl.stream.asBroadcastStream()) {
|
||||
// try {
|
||||
// Sentry.captureException(
|
||||
// error,
|
||||
// );
|
||||
// } catch (e) {
|
||||
// $.fine(
|
||||
// "sentry upload failed; will retry after ${config.sentryRetryDelay}",
|
||||
// );
|
||||
// doSentryRetry(error);
|
||||
// }
|
||||
try {
|
||||
Sentry.captureException(
|
||||
error,
|
||||
);
|
||||
} catch (e) {
|
||||
$.fine(
|
||||
"sentry upload failed; will retry after ${config.sentryRetryDelay}",
|
||||
);
|
||||
doSentryRetry(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,6 +316,26 @@ class SuperLogging {
|
||||
sentryQueueControl.add(error);
|
||||
}
|
||||
|
||||
static bool shouldReportErrors() {
|
||||
if (_preferences.containsKey(keyShouldReportErrors)) {
|
||||
return _preferences.getBool(keyShouldReportErrors);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> setShouldReportErrors(bool value) {
|
||||
return _preferences.setBool(keyShouldReportErrors, value);
|
||||
}
|
||||
|
||||
static Future<String> _getOrCreateAnonymousUserID() async {
|
||||
if (!_preferences.containsKey(keyAnonymousUserID)) {
|
||||
//ignore: prefer_const_constructors
|
||||
await _preferences.setString(keyAnonymousUserID, Uuid().v4());
|
||||
}
|
||||
return _preferences.getString(keyAnonymousUserID);
|
||||
}
|
||||
|
||||
/// The log file currently in use.
|
||||
static File logFile;
|
||||
|
||||
@@ -372,6 +404,6 @@ class SuperLogging {
|
||||
return false;
|
||||
}
|
||||
final pkgName = (await PackageInfo.fromPlatform()).packageName;
|
||||
return pkgName.startsWith("io.ente.photos.fdroid");
|
||||
return pkgName.endsWith("fdroid");
|
||||
}
|
||||
}
|
||||
|
||||
141
lib/core/logging/tunneled_transport.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:http/http.dart';
|
||||
import 'package:sentry/sentry.dart';
|
||||
|
||||
/// A transport is in charge of sending the event to the Sentry server.
|
||||
class TunneledTransport implements Transport {
|
||||
final Uri _tunnel;
|
||||
final SentryOptions _options;
|
||||
|
||||
final Dsn _dsn;
|
||||
|
||||
_CredentialBuilder _credentialBuilder;
|
||||
|
||||
final Map<String, String> _headers;
|
||||
|
||||
factory TunneledTransport(Uri tunnel, SentryOptions options) {
|
||||
return TunneledTransport._(tunnel, options);
|
||||
}
|
||||
|
||||
TunneledTransport._(this._tunnel, this._options)
|
||||
: _dsn = Dsn.parse(_options.dsn),
|
||||
_headers = _buildHeaders(
|
||||
_options.platformChecker.isWeb,
|
||||
_options.sdk.identifier,
|
||||
) {
|
||||
_credentialBuilder = _CredentialBuilder(
|
||||
_dsn,
|
||||
_options.sdk.identifier,
|
||||
_options.clock,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SentryId> send(SentryEnvelope envelope) async {
|
||||
final streamedRequest = await _createStreamedRequest(envelope);
|
||||
final response = await _options.httpClient
|
||||
.send(streamedRequest)
|
||||
.then(Response.fromStream);
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
// body guard to not log the error as it has performance impact to allocate
|
||||
// the body String.
|
||||
if (_options.debug) {
|
||||
_options.logger(
|
||||
SentryLevel.error,
|
||||
'API returned an error, statusCode = ${response.statusCode}, '
|
||||
'body = ${response.body}',
|
||||
);
|
||||
}
|
||||
return const SentryId.empty();
|
||||
} else {
|
||||
_options.logger(
|
||||
SentryLevel.debug,
|
||||
'Envelope ${envelope.header.eventId ?? "--"} was sent successfully.',
|
||||
);
|
||||
}
|
||||
|
||||
final eventId = json.decode(response.body)['id'];
|
||||
if (eventId == null) {
|
||||
return null;
|
||||
}
|
||||
return SentryId.fromId(eventId);
|
||||
}
|
||||
|
||||
Future<StreamedRequest> _createStreamedRequest(
|
||||
SentryEnvelope envelope,
|
||||
) async {
|
||||
final streamedRequest = StreamedRequest('POST', _tunnel);
|
||||
envelope
|
||||
.envelopeStream(_options)
|
||||
.listen(streamedRequest.sink.add)
|
||||
.onDone(streamedRequest.sink.close);
|
||||
|
||||
streamedRequest.headers.addAll(_credentialBuilder.configure(_headers));
|
||||
|
||||
return streamedRequest;
|
||||
}
|
||||
}
|
||||
|
||||
class _CredentialBuilder {
|
||||
final String _authHeader;
|
||||
|
||||
final ClockProvider _clock;
|
||||
|
||||
int get timestamp => _clock().millisecondsSinceEpoch;
|
||||
|
||||
_CredentialBuilder._(String authHeader, ClockProvider clock)
|
||||
: _authHeader = authHeader,
|
||||
_clock = clock;
|
||||
|
||||
factory _CredentialBuilder(
|
||||
Dsn dsn,
|
||||
String sdkIdentifier,
|
||||
ClockProvider clock,
|
||||
) {
|
||||
final authHeader = _buildAuthHeader(
|
||||
publicKey: dsn.publicKey,
|
||||
secretKey: dsn.secretKey,
|
||||
sdkIdentifier: sdkIdentifier,
|
||||
);
|
||||
|
||||
return _CredentialBuilder._(authHeader, clock);
|
||||
}
|
||||
|
||||
static String _buildAuthHeader({
|
||||
String publicKey,
|
||||
String secretKey,
|
||||
String sdkIdentifier,
|
||||
}) {
|
||||
var header = 'Sentry sentry_version=7, sentry_client=$sdkIdentifier, '
|
||||
'sentry_key=$publicKey';
|
||||
|
||||
if (secretKey != null) {
|
||||
header += ', sentry_secret=$secretKey';
|
||||
}
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
Map<String, String> configure(Map<String, String> headers) {
|
||||
return headers
|
||||
..addAll(
|
||||
<String, String>{
|
||||
'X-Sentry-Auth': '$_authHeader, sentry_timestamp=$timestamp'
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> _buildHeaders(bool isWeb, String sdkIdentifier) {
|
||||
final headers = {'Content-Type': 'application/x-sentry-envelope'};
|
||||
// NOTE(lejard_h) overriding user agent on VM and Flutter not sure why
|
||||
// for web it use browser user agent
|
||||
if (!isWeb) {
|
||||
headers['User-Agent'] = sdkIdentifier;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
@@ -224,6 +224,14 @@ extension CustomColorScheme on ColorScheme {
|
||||
Color get inverseBackgroundColor =>
|
||||
brightness != Brightness.light ? backgroundBaseLight : backgroundBaseDark;
|
||||
|
||||
Color get fabForegroundColor => brightness == Brightness.light
|
||||
? const Color.fromRGBO(255, 255, 255, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 1);
|
||||
|
||||
Color get fabBackgroundColor => brightness != Brightness.light
|
||||
? const Color.fromRGBO(255, 255, 255, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 1);
|
||||
|
||||
Color get defaultTextColor =>
|
||||
brightness == Brightness.light ? textBaseLight : textBaseDark;
|
||||
|
||||
@@ -344,6 +352,10 @@ extension CustomColorScheme on ColorScheme {
|
||||
? Colors.black.withOpacity(0.32)
|
||||
: Colors.black.withOpacity(0.64);
|
||||
|
||||
Color get codeCardBackgroundColor => brightness == Brightness.light
|
||||
? const Color.fromRGBO(246, 246, 246, 1)
|
||||
: const Color.fromRGBO(40, 40, 40, 0.6);
|
||||
|
||||
EnteTheme get enteTheme =>
|
||||
brightness == Brightness.light ? lightTheme : darkTheme;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@
|
||||
"importScanQrCode": "Scan a QR Code",
|
||||
"importEnterSetupKey": "Enter a setup key",
|
||||
"importAccountPageTitle": "Enter account details",
|
||||
"accountNameHint": "Account name",
|
||||
"accountKeyHint" : "Your key",
|
||||
"codeIssuerHint": "Issuer",
|
||||
"codeSecretKeyHint" : "Secret Key",
|
||||
"codeAccountHint": "Account (you@domain.com)",
|
||||
"accountKeyType": "Type of key",
|
||||
"timeBasedKeyType": "Time based (TOTP)",
|
||||
"counterBasedKeyType": "Counter based (HOTP)",
|
||||
"importAddAction": "Add",
|
||||
"saveAction": "Save",
|
||||
|
||||
"existingUser": "Existing User",
|
||||
"newUser" : "New to ente"
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// @dart=2.9
|
||||
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/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_remote_flag_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
@@ -29,6 +32,7 @@ Future<void> _runInForeground() async {
|
||||
return await _runWithLogs(() async {
|
||||
_logger.info("Starting app in foreground");
|
||||
await _init(false, via: 'mainMethod');
|
||||
UpdateService.instance.showUpdateNotification();
|
||||
runApp(
|
||||
AppLock(
|
||||
builder: (args) => const App(),
|
||||
@@ -47,6 +51,7 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
|
||||
body: function,
|
||||
logDirPath: (await getApplicationSupportDirectory()).path + "/logs",
|
||||
maxLogFiles: 5,
|
||||
sentryDsn: sentryDSN,
|
||||
enableInDebugMode: true,
|
||||
prefix: prefix,
|
||||
),
|
||||
@@ -56,12 +61,14 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async {
|
||||
Future<void> _init(bool bool, {String via}) async {
|
||||
InAppPurchaseConnection.enablePendingPurchases();
|
||||
CryptoUtil.init();
|
||||
await PreferenceService.instance.init();
|
||||
await CodeStore.instance.init();
|
||||
await Configuration.instance.init();
|
||||
await Network.instance.init();
|
||||
await UserService.instance.init();
|
||||
await UserRemoteFlagService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
await AuthenticatorService.instance.init();
|
||||
await BillingService.instance.init();
|
||||
await NotificationService.instance.init();
|
||||
await UpdateService.instance.init();
|
||||
}
|
||||
|
||||
7
lib/models/authenticator/entity_result.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
class EntityResult {
|
||||
final int generatedID;
|
||||
final String rawData;
|
||||
final bool hasSynced;
|
||||
|
||||
EntityResult(this.generatedID, this.rawData, this.hasSynced);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
|
||||
class Code {
|
||||
static const defaultDigits = 6;
|
||||
static const defaultPeriod = 30;
|
||||
|
||||
int? id;
|
||||
int? generatedID;
|
||||
final String account;
|
||||
final String issuer;
|
||||
final int digits;
|
||||
@@ -11,6 +13,7 @@ class Code {
|
||||
final Algorithm algorithm;
|
||||
final Type type;
|
||||
final String rawData;
|
||||
bool? hasSynced;
|
||||
|
||||
Code(
|
||||
this.account,
|
||||
@@ -21,24 +24,28 @@ class Code {
|
||||
this.algorithm,
|
||||
this.type,
|
||||
this.rawData, {
|
||||
this.id,
|
||||
this.generatedID,
|
||||
});
|
||||
|
||||
static Code fromAccountAndSecret(String account, String secret) {
|
||||
static Code fromAccountAndSecret(
|
||||
String account,
|
||||
String issuer,
|
||||
String secret,
|
||||
) {
|
||||
return Code(
|
||||
account,
|
||||
"",
|
||||
issuer,
|
||||
defaultDigits,
|
||||
defaultPeriod,
|
||||
secret,
|
||||
Algorithm.sha1,
|
||||
Type.totp,
|
||||
"otpauth://totp/" +
|
||||
account +
|
||||
issuer +
|
||||
":" +
|
||||
account +
|
||||
"?algorithm=SHA1&digits=6&issuer=" +
|
||||
account +
|
||||
issuer +
|
||||
"period=30&secret=" +
|
||||
secret,
|
||||
);
|
||||
@@ -51,7 +58,7 @@ class Code {
|
||||
_getIssuer(uri),
|
||||
_getDigits(uri),
|
||||
_getPeriod(uri),
|
||||
uri.queryParameters['secret']!,
|
||||
getSanitizedSecret(uri.queryParameters['secret']!),
|
||||
_getAlgorithm(uri),
|
||||
_getType(uri),
|
||||
rawData,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import "package:ente_auth/l10n/l10n.dart";
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
// ignore: import_of_legacy_library_into_null_safe
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
SetupEnterSecretKeyPage({Key? key}) : super(key: key);
|
||||
final Code? code;
|
||||
|
||||
SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SetupEnterSecretKeyPage> createState() =>
|
||||
@@ -14,9 +16,27 @@ class SetupEnterSecretKeyPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
final _accountController = TextEditingController();
|
||||
final _secretController =
|
||||
TextEditingController(text: kDebugMode ? "JBSWY3DPEHPK3PXP" : "");
|
||||
late TextEditingController _issuerController;
|
||||
late TextEditingController _accountController;
|
||||
late TextEditingController _secretController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_issuerController = TextEditingController(
|
||||
text: widget.code != null
|
||||
? Uri.decodeFull(widget.code!.issuer).trim()
|
||||
: null,
|
||||
);
|
||||
_accountController = TextEditingController(
|
||||
text: widget.code != null
|
||||
? Uri.decodeFull(widget.code!.account).trim()
|
||||
: null,
|
||||
);
|
||||
_secretController = TextEditingController(
|
||||
text: widget.code != null ? widget.code!.secret : null,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -40,9 +60,9 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.accountNameHint,
|
||||
hintText: l10n.codeIssuerHint,
|
||||
),
|
||||
controller: _accountController,
|
||||
controller: _issuerController,
|
||||
autofocus: true,
|
||||
),
|
||||
const SizedBox(
|
||||
@@ -57,10 +77,26 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.accountKeyHint,
|
||||
hintText: l10n.codeSecretKeyHint,
|
||||
),
|
||||
controller: _secretController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
TextFormField(
|
||||
// The validator receives the text that the user has entered.
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return "Please enter some text";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.codeAccountHint,
|
||||
),
|
||||
controller: _accountController,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 40,
|
||||
),
|
||||
@@ -76,10 +112,14 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
try {
|
||||
final code = Code.fromAccountAndSecret(
|
||||
_accountController.text.trim(),
|
||||
_issuerController.text.trim(),
|
||||
_secretController.text.trim(),
|
||||
);
|
||||
// Verify the validity of the code
|
||||
getTotp(code);
|
||||
if (widget.code != null) {
|
||||
code.generatedID = widget.code!.generatedID;
|
||||
}
|
||||
Navigator.of(context).pop(code);
|
||||
} catch (e) {
|
||||
_showIncorrectDetailsDialog(context);
|
||||
@@ -90,7 +130,7 @@ class _SetupEnterSecretKeyPageState extends State<SetupEnterSecretKeyPage> {
|
||||
horizontal: 16.0,
|
||||
vertical: 4,
|
||||
),
|
||||
child: Text(l10n.importAddAction),
|
||||
child: Text(l10n.saveAction),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:ente_auth/events/signed_in_event.dart';
|
||||
import 'package:ente_auth/gateway/authenticator.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_entity.dart';
|
||||
import 'package:ente_auth/models/authenticator/auth_key.dart';
|
||||
import 'package:ente_auth/models/authenticator/entity_result.dart';
|
||||
import 'package:ente_auth/models/authenticator/local_auth_entity.dart';
|
||||
import 'package:ente_auth/store/authenticator_db.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
@@ -44,25 +45,36 @@ class AuthenticatorService {
|
||||
});
|
||||
}
|
||||
|
||||
Future<Map<int, String>> getAllIDtoStringMap() async {
|
||||
Future<List<EntityResult>> getEntities() async {
|
||||
final List<LocalAuthEntity> result = await _db.getAll();
|
||||
final Map<int, String> entries = <int, String>{};
|
||||
final List<EntityResult> entities = [];
|
||||
if (result.isEmpty) {
|
||||
return entries;
|
||||
return entities;
|
||||
}
|
||||
final key = await getOrCreateAuthDataKey();
|
||||
for (LocalAuthEntity e in result) {
|
||||
final decryptedValue = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(e.encryptedData),
|
||||
key,
|
||||
Sodium.base642bin(e.header),
|
||||
);
|
||||
entries[e.generatedID] = utf8.decode(decryptedValue);
|
||||
try {
|
||||
final decryptedValue = await CryptoUtil.decryptChaCha(
|
||||
Sodium.base642bin(e.encryptedData),
|
||||
key,
|
||||
Sodium.base642bin(e.header),
|
||||
);
|
||||
final hasSynced = !(e.id == null || e.shouldSync);
|
||||
entities.add(
|
||||
EntityResult(
|
||||
e.generatedID,
|
||||
utf8.decode(decryptedValue),
|
||||
hasSynced,
|
||||
),
|
||||
);
|
||||
} catch (e, s) {
|
||||
_logger.severe(e, s);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
return entities;
|
||||
}
|
||||
|
||||
Future<int> addEntry(String plainText) async {
|
||||
Future<int> addEntry(String plainText, bool shouldSync) async {
|
||||
var key = await getOrCreateAuthDataKey();
|
||||
final encryptedKeyData = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
@@ -71,11 +83,17 @@ class AuthenticatorService {
|
||||
String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!);
|
||||
String header = Sodium.bin2base64(encryptedKeyData.header!);
|
||||
final insertedID = await _db.insert(encryptedData, header);
|
||||
unawaited(sync());
|
||||
if (shouldSync) {
|
||||
unawaited(sync());
|
||||
}
|
||||
return insertedID;
|
||||
}
|
||||
|
||||
Future<void> updateEntry(int generatedID, String plainText) async {
|
||||
Future<void> updateEntry(
|
||||
int generatedID,
|
||||
String plainText,
|
||||
bool shouldSync,
|
||||
) async {
|
||||
var key = await getOrCreateAuthDataKey();
|
||||
final encryptedKeyData = await CryptoUtil.encryptChaCha(
|
||||
utf8.encode(plainText) as Uint8List,
|
||||
@@ -89,7 +107,9 @@ class AuthenticatorService {
|
||||
affectedRows == 1,
|
||||
"updateEntry should have updated exactly one row",
|
||||
);
|
||||
unawaited(sync());
|
||||
if (shouldSync) {
|
||||
unawaited(sync());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteEntry(int genID) async {
|
||||
@@ -107,15 +127,9 @@ class AuthenticatorService {
|
||||
Future<void> sync() async {
|
||||
try {
|
||||
_logger.info("Sync");
|
||||
_logger.info("State of DB before sync");
|
||||
await _printDBState();
|
||||
await _remoteToLocalSync();
|
||||
_logger.info("remote fetch completed");
|
||||
_logger.info("State of DB after remoteToLocal sync");
|
||||
await _printDBState();
|
||||
await _localToRemoteSync();
|
||||
_logger.info("State of DB after localToRemote sync");
|
||||
await _printDBState();
|
||||
_logger.info("local push completed");
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
} catch (e) {
|
||||
@@ -126,24 +140,27 @@ class AuthenticatorService {
|
||||
Future<void> _remoteToLocalSync() async {
|
||||
_logger.info('Initiating remote to local sync');
|
||||
final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0;
|
||||
_logger.info("Current synctime is " + lastSyncTime.toString());
|
||||
const int fetchLimit = 500;
|
||||
final List<AuthEntity> result =
|
||||
await _gateway.getDiff(lastSyncTime, limit: fetchLimit);
|
||||
_logger.info(result.length.toString() + " entries fetched from remote");
|
||||
if (result.isEmpty) {
|
||||
return;
|
||||
} else {
|
||||
_logger.info(result.length.toString() + " entries fetched from remote");
|
||||
}
|
||||
final maxSyncTime = result.map((e) => e.updatedAt).reduce(max);
|
||||
List<String> deletedIDs =
|
||||
result.where((element) => element.isDeleted).map((e) => e.id).toList();
|
||||
_logger.info(deletedIDs.length.toString() + " entries deleted");
|
||||
result.removeWhere((element) => element.isDeleted);
|
||||
await _db.insertOrReplace(result);
|
||||
if (deletedIDs.isNotEmpty) {
|
||||
await _db.deleteByIDs(ids: deletedIDs);
|
||||
}
|
||||
_prefs.setInt(_lastEntitySyncTime, maxSyncTime);
|
||||
_logger.info("Setting synctime to " + maxSyncTime.toString());
|
||||
if (result.length == fetchLimit) {
|
||||
_logger.info("Diff limit reached, pulling again");
|
||||
await _remoteToLocalSync();
|
||||
}
|
||||
}
|
||||
@@ -178,6 +195,10 @@ class AuthenticatorService {
|
||||
await _db.updateLocalEntity(entity.copyWith(shouldSync: false));
|
||||
}
|
||||
}
|
||||
if (pendingUpdate.isNotEmpty) {
|
||||
_logger.info("Initiating remote sync since local entries were pushed");
|
||||
await _remoteToLocalSync();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Uint8List> getOrCreateAuthDataKey() async {
|
||||
@@ -208,13 +229,4 @@ class AuthenticatorService {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _printDBState() async {
|
||||
_logger.info("_____");
|
||||
final entities = await _db.getAll();
|
||||
for (final entity in entities) {
|
||||
_logger.info(entity.id);
|
||||
}
|
||||
_logger.info("_____");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,56 @@
|
||||
// import 'dart:io';
|
||||
//
|
||||
// import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
//
|
||||
// class NotificationService {
|
||||
// static final NotificationService instance =
|
||||
// NotificationService._privateConstructor();
|
||||
//
|
||||
// NotificationService._privateConstructor();
|
||||
// final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
// FlutterLocalNotificationsPlugin();
|
||||
//
|
||||
// Future<void> init() async {
|
||||
// if (!Platform.isAndroid) {
|
||||
// return;
|
||||
// }
|
||||
// const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
// AndroidInitializationSettings('notification_icon');
|
||||
// const InitializationSettings initializationSettings =
|
||||
// InitializationSettings(
|
||||
// android: initializationSettingsAndroid,
|
||||
// );
|
||||
// await _flutterLocalNotificationsPlugin.initialize(
|
||||
// initializationSettings,
|
||||
// onSelectNotification: selectNotification,
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future selectNotification(String? payload) async {}
|
||||
//
|
||||
// Future<void> showNotification(String title, String message) async {
|
||||
// if (!Platform.isAndroid) {
|
||||
// return;
|
||||
// }
|
||||
// const AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||
// AndroidNotificationDetails(
|
||||
// 'io.ente.photos',
|
||||
// 'ente',
|
||||
// channelDescription: 'ente alerts',
|
||||
// importance: Importance.max,
|
||||
// priority: Priority.high,
|
||||
// showWhen: false,
|
||||
// );
|
||||
// const NotificationDetails platformChannelSpecifics =
|
||||
// NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
// await _flutterLocalNotificationsPlugin.show(
|
||||
// 0,
|
||||
// title,
|
||||
// message,
|
||||
// platformChannelSpecifics,
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
class NotificationService {
|
||||
static final NotificationService instance =
|
||||
NotificationService._privateConstructor();
|
||||
|
||||
NotificationService._privateConstructor();
|
||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
Future<void> init() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
||||
AndroidInitializationSettings('notification_icon');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: initializationSettingsAndroid,
|
||||
);
|
||||
await _flutterLocalNotificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
);
|
||||
final implementation =
|
||||
_flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
if (implementation != null) {
|
||||
implementation.requestPermission();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showNotification(String title, String message) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return;
|
||||
}
|
||||
const AndroidNotificationDetails androidPlatformChannelSpecifics =
|
||||
AndroidNotificationDetails(
|
||||
'io.ente.auth',
|
||||
'auth',
|
||||
channelDescription: 'auth alerts',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
showWhen: false,
|
||||
);
|
||||
const NotificationDetails platformChannelSpecifics =
|
||||
NotificationDetails(android: androidPlatformChannelSpecifics);
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
0,
|
||||
title,
|
||||
message,
|
||||
platformChannelSpecifics,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
27
lib/services/preference_service.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class PreferenceService {
|
||||
PreferenceService._privateConstructor();
|
||||
static final PreferenceService instance =
|
||||
PreferenceService._privateConstructor();
|
||||
|
||||
late final SharedPreferences _prefs;
|
||||
|
||||
static const kHasShownCoachMarkKey = "has_shown_coach_mark";
|
||||
|
||||
Future<void> init() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
}
|
||||
|
||||
bool hasShownCoachMark() {
|
||||
if (_prefs.containsKey(kHasShownCoachMarkKey)) {
|
||||
return _prefs.getBool(kHasShownCoachMarkKey)!;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> setHasShownCoachMark(bool value) {
|
||||
return _prefs.setBool(kHasShownCoachMarkKey, value);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
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:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -55,33 +57,33 @@ class UpdateService {
|
||||
return _latestVersion;
|
||||
}
|
||||
|
||||
// Future<void> showUpdateNotification() async {
|
||||
// if (!isIndependent()) {
|
||||
// return;
|
||||
// }
|
||||
// final shouldUpdate = await this.shouldUpdate();
|
||||
// final lastNotificationShownTime =
|
||||
// _prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
|
||||
// final now = DateTime.now().microsecondsSinceEpoch;
|
||||
// final hasBeen3DaysSinceLastNotification =
|
||||
// (now - lastNotificationShownTime) > (3 * microSecondsInDay);
|
||||
// if (shouldUpdate &&
|
||||
// hasBeen3DaysSinceLastNotification &&
|
||||
// _latestVersion.shouldNotify) {
|
||||
// NotificationService.instance.showNotification(
|
||||
// "update available",
|
||||
// "click to install our best version yet",
|
||||
// );
|
||||
// await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
// } else {
|
||||
// _logger.info("Debouncing notification");
|
||||
// }
|
||||
// }
|
||||
Future<void> showUpdateNotification() async {
|
||||
if (!isIndependent()) {
|
||||
return;
|
||||
}
|
||||
final shouldUpdate = await this.shouldUpdate();
|
||||
final lastNotificationShownTime =
|
||||
_prefs.getInt(kUpdateAvailableShownTimeKey) ?? 0;
|
||||
final now = DateTime.now().microsecondsSinceEpoch;
|
||||
final hasBeen3DaysSinceLastNotification =
|
||||
(now - lastNotificationShownTime) > (3 * microSecondsInDay);
|
||||
if (shouldUpdate &&
|
||||
hasBeen3DaysSinceLastNotification &&
|
||||
_latestVersion.shouldNotify) {
|
||||
NotificationService.instance.showNotification(
|
||||
"Update available",
|
||||
"Click to install our best version yet",
|
||||
);
|
||||
await _prefs.setInt(kUpdateAvailableShownTimeKey, now);
|
||||
} else {
|
||||
_logger.info("Debouncing notification");
|
||||
}
|
||||
}
|
||||
|
||||
Future<LatestVersionInfo> _getLatestVersionInfo() async {
|
||||
final response = await Network.instance
|
||||
.getDio()
|
||||
.get("https://ente.io/release-info/independent.json");
|
||||
.get("https://ente.io/release-info/auth-independent.json");
|
||||
return LatestVersionInfo.fromMap(response.data["latestVersion"]);
|
||||
}
|
||||
|
||||
@@ -89,18 +91,7 @@ class UpdateService {
|
||||
if (Platform.isIOS) {
|
||||
return false;
|
||||
}
|
||||
if (!kDebugMode &&
|
||||
_packageInfo.packageName != "io.ente.auth.independent") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isIndependentFlavor() {
|
||||
if (Platform.isIOS) {
|
||||
return false;
|
||||
}
|
||||
return _packageInfo.packageName.startsWith("io.ente.auth.independent");
|
||||
return kDebugMode || _packageInfo.packageName.endsWith("independent");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'package:bip39/bip39.dart' as bip39;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/constants.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';
|
||||
@@ -12,12 +14,17 @@ 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/two_factor_authentication_page.dart';
|
||||
import 'package:ente_auth/ui/two_factor_recovery_page.dart';
|
||||
import 'package:ente_auth/utils/crypto_util.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class UserService {
|
||||
@@ -265,11 +272,16 @@ class UserService {
|
||||
await dialog.hide();
|
||||
if (response != null && response.statusCode == 200) {
|
||||
Widget page;
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
page = const PasswordReentryPage();
|
||||
final String twoFASessionID = response.data["twoFactorSessionID"];
|
||||
if (twoFASessionID != null && twoFASessionID.isNotEmpty) {
|
||||
page = TwoFactorAuthenticationPage(twoFASessionID);
|
||||
} else {
|
||||
page = const PasswordEntryPage();
|
||||
await _saveConfiguration(response);
|
||||
if (Configuration.instance.getEncryptedToken() != null) {
|
||||
page = const PasswordReentryPage();
|
||||
} else {
|
||||
page = const PasswordEntryPage();
|
||||
}
|
||||
}
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
@@ -484,4 +496,204 @@ class UserService {
|
||||
await Configuration.instance.setToken(response.data["token"]);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/recover",
|
||||
queryParameters: {
|
||||
"sessionID": sessionID,
|
||||
},
|
||||
);
|
||||
if (response != null && response.statusCode == 200) {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return TwoFactorRecoveryPage(
|
||||
sessionID,
|
||||
response.data["encryptedSecret"],
|
||||
response.data["secretDecryptionNonce"],
|
||||
);
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String code,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Authenticating...");
|
||||
await dialog.show();
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/verify",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"code": code,
|
||||
},
|
||||
);
|
||||
await dialog.hide();
|
||||
if (response != null && response.statusCode == 200) {
|
||||
showToast(context, "Authentication successful!");
|
||||
await _saveConfiguration(response);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect code",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Authentication failed, please try again",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeTwoFactor(
|
||||
BuildContext context,
|
||||
String sessionID,
|
||||
String recoveryKey,
|
||||
String encryptedSecret,
|
||||
String secretDecryptionNonce,
|
||||
) async {
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
String secret;
|
||||
try {
|
||||
if (recoveryKey.contains(' ')) {
|
||||
if (recoveryKey.split(' ').length != mnemonicKeyWordCount) {
|
||||
throw AssertionError(
|
||||
'recovery code should have $mnemonicKeyWordCount words',
|
||||
);
|
||||
}
|
||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||
}
|
||||
secret = Sodium.bin2base64(
|
||||
await CryptoUtil.decrypt(
|
||||
Sodium.base642bin(encryptedSecret),
|
||||
Sodium.hex2bin(recoveryKey.trim()),
|
||||
Sodium.base642bin(secretDecryptionNonce),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Incorrect recovery key",
|
||||
"The recovery key you entered is incorrect",
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
_config.getHttpEndpoint() + "/users/two-factor/remove",
|
||||
data: {
|
||||
"sessionID": sessionID,
|
||||
"secret": secret,
|
||||
},
|
||||
);
|
||||
if (response != null && response.statusCode == 200) {
|
||||
showShortToast(context, "Two-factor authentication successfully reset");
|
||||
await _saveConfiguration(response);
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const PasswordReentryPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
} on DioError catch (e) {
|
||||
_logger.severe(e);
|
||||
if (e.response != null && e.response.statusCode == 404) {
|
||||
showToast(context, "Session expired");
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const LoginPage();
|
||||
},
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
} else {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.severe(e);
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Oops",
|
||||
"Something went wrong, please try again",
|
||||
);
|
||||
} finally {
|
||||
await dialog.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ import 'dart:convert';
|
||||
|
||||
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:logging/logging.dart';
|
||||
|
||||
class CodeStore {
|
||||
static final CodeStore instance = CodeStore._privateConstructor();
|
||||
@@ -11,33 +13,61 @@ class CodeStore {
|
||||
CodeStore._privateConstructor();
|
||||
|
||||
late AuthenticatorService _authenticatorService;
|
||||
final _logger = Logger("CodeStore");
|
||||
|
||||
Future<void> init() async {
|
||||
_authenticatorService = AuthenticatorService.instance;
|
||||
}
|
||||
|
||||
Future<List<Code>> getAllCodes() async {
|
||||
final Map<int, String> rawCodesMap =
|
||||
await _authenticatorService.getAllIDtoStringMap();
|
||||
final List<EntityResult> entities =
|
||||
await _authenticatorService.getEntities();
|
||||
final List<Code> codes = [];
|
||||
for (final entry in rawCodesMap.entries) {
|
||||
final decodeJson = jsonDecode(entry.value);
|
||||
for (final entity in entities) {
|
||||
final decodeJson = jsonDecode(entity.rawData);
|
||||
final code = Code.fromRawData(decodeJson);
|
||||
code.id = entry.key;
|
||||
code.generatedID = entity.generatedID;
|
||||
code.hasSynced = entity.hasSynced;
|
||||
codes.add(code);
|
||||
}
|
||||
codes.sort((c1, c2) {
|
||||
return c1.issuer.toLowerCase().compareTo(c2.issuer.toLowerCase());
|
||||
});
|
||||
return codes;
|
||||
}
|
||||
|
||||
Future<void> addCode(Code code) async {
|
||||
Future<void> addCode(
|
||||
Code code, {
|
||||
bool shouldSync = true,
|
||||
}) async {
|
||||
final codes = await getAllCodes();
|
||||
code.id = await _authenticatorService.addEntry(jsonEncode(code.rawData));
|
||||
codes.add(code);
|
||||
bool isExistingCode = false;
|
||||
for (final existingCode in codes) {
|
||||
if (existingCode == code) {
|
||||
_logger.info("Found duplicate code, skipping add");
|
||||
return;
|
||||
} else if (existingCode.generatedID == code.generatedID) {
|
||||
isExistingCode = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isExistingCode) {
|
||||
await _authenticatorService.updateEntry(
|
||||
code.generatedID!,
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
);
|
||||
} else {
|
||||
code.generatedID = await _authenticatorService.addEntry(
|
||||
jsonEncode(code.rawData),
|
||||
shouldSync,
|
||||
);
|
||||
}
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
}
|
||||
|
||||
Future<void> removeCode(Code code) async {
|
||||
await _authenticatorService.deleteEntry(code.id!);
|
||||
await _authenticatorService.deleteEntry(code.generatedID!);
|
||||
Bus.instance.fire(CodesUpdatedEvent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class _ChangeEmailDialogState extends State<ChangeEmailDialog> {
|
||||
child: const Text(
|
||||
"Verify",
|
||||
style: TextStyle(
|
||||
color: Colors.green,
|
||||
color: Colors.purple,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
|
||||
@@ -152,9 +152,10 @@ class DeleteAccountPage extends StatelessWidget {
|
||||
if (hasAuthenticated) {
|
||||
final choice = await showChoiceDialog(
|
||||
context,
|
||||
'Are you sure you want to delete your account?',
|
||||
'Your uploaded data will be scheduled for deletion, and your account '
|
||||
'will be permanently deleted. \n\nThis action is not reversible.',
|
||||
'Are you sure you want to delete your ente account?',
|
||||
'Your uploaded data, across all apps '
|
||||
'(Photos and Authenticator both), will be scheduled for deletion,'
|
||||
'and your account will be permanently deleted.',
|
||||
firstAction: 'Cancel',
|
||||
secondAction: 'Delete',
|
||||
firstActionColor: Theme.of(context).colorScheme.onSurface,
|
||||
|
||||
@@ -161,7 +161,7 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
|
||||
),
|
||||
),
|
||||
controller: _verificationCodeController,
|
||||
autofocus: false,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (_) {
|
||||
|
||||
@@ -68,7 +68,7 @@ class _RecoveryPageState extends State<RecoveryPage> {
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
String errMessage = 'the recovery key you entered is incorrect';
|
||||
String errMessage = 'The recovery key you entered is incorrect';
|
||||
if (e is AssertionError) {
|
||||
errMessage = '$errMessage : ${e.message}';
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/onboarding/view/setup_enter_secret_key_page.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/utils/toast_util.dart';
|
||||
import 'package:ente_auth/utils/totp_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
|
||||
// import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
class CodeWidget extends StatefulWidget {
|
||||
@@ -48,92 +49,180 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: 'Delete',
|
||||
),
|
||||
],
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
FlutterClipboard.copy(_getTotp())
|
||||
.then((value) => showToast(context, "Copied to clipboard"));
|
||||
},
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FAProgressBar(
|
||||
currentValue: _timeRemaining / widget.code.period * 100,
|
||||
size: 4,
|
||||
animatedDuration: const Duration(milliseconds: 200),
|
||||
progressColor: Colors.orange,
|
||||
changeColorValue: 40,
|
||||
changeProgressColor: Colors.green,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 10,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Text(
|
||||
Uri.decodeFull(widget.code.issuer),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8),
|
||||
child: Slidable(
|
||||
key: ValueKey(widget.code.hashCode),
|
||||
endActionPane: ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: _onEditPressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor:
|
||||
Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
icon: Icons.edit_outlined,
|
||||
label: 'Edit',
|
||||
padding: const EdgeInsets.only(left: 4, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 4,
|
||||
),
|
||||
SlidableAction(
|
||||
onPressed: _onDeletePressed,
|
||||
backgroundColor: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
foregroundColor: const Color(0xFFFE4A49),
|
||||
icon: Icons.delete,
|
||||
label: 'Delete',
|
||||
padding: const EdgeInsets.only(left: 0, right: 0),
|
||||
spacing: 8,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.codeCardBackgroundColor,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
_copyToClipboard();
|
||||
},
|
||||
onLongPress: () {
|
||||
_copyToClipboard();
|
||||
},
|
||||
child: SizedBox(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
FAProgressBar(
|
||||
currentValue:
|
||||
_timeRemaining / widget.code.period * 100,
|
||||
size: 4,
|
||||
animatedDuration: const Duration(milliseconds: 200),
|
||||
progressColor: Colors.orange,
|
||||
changeColorValue: 40,
|
||||
changeProgressColor: Colors.green,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
Uri.decodeFull(widget.code.issuer).trim(),
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
Uri.decodeFull(
|
||||
widget.code.account,
|
||||
).trim(),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.caption
|
||||
?.copyWith(
|
||||
fontSize: 12,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
widget.code.hasSynced != null &&
|
||||
widget.code.hasSynced!
|
||||
? Container()
|
||||
: const Icon(
|
||||
Icons.sync_disabled,
|
||||
size: 20,
|
||||
color: Colors.amber,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getTotp(),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"next",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
Text(
|
||||
_getNextTotp(),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"next",
|
||||
style: Theme.of(context).textTheme.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_getTotp(),
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_getNextTotp(),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _copyToClipboard() {
|
||||
FlutterClipboard.copy(_getTotp()).then(
|
||||
(value) => showToast(context, "Copied to clipboard"),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onEditPressed(_) async {
|
||||
final Code? code = await Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return SetupEnterSecretKeyPage(code: widget.code);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (code != null) {
|
||||
CodeStore.instance.addCode(code);
|
||||
}
|
||||
}
|
||||
|
||||
void _onDeletePressed(_) {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
@@ -195,16 +284,4 @@ class _CodeWidgetState extends State<CodeWidget> {
|
||||
return "Error";
|
||||
}
|
||||
}
|
||||
|
||||
Color _getProgressColor() {
|
||||
final progress = _timeRemaining / widget.code.period;
|
||||
if (progress > 0.6) {
|
||||
return Colors.green;
|
||||
} else if (progress > 0.4) {
|
||||
return Colors.yellow;
|
||||
} else if (progress > 2) {
|
||||
return Colors.orange;
|
||||
}
|
||||
return Colors.red;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/core/event_bus.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
@@ -9,6 +10,7 @@ import 'package:ente_auth/events/codes_updated_event.dart';
|
||||
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/services/preference_service.dart';
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/ui/code_widget.dart';
|
||||
@@ -41,7 +43,6 @@ class _HomePageState extends State<HomePage> {
|
||||
_loadCodes();
|
||||
_streamSubscription = Bus.instance.on<CodesUpdatedEvent>().listen((event) {
|
||||
_loadCodes();
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -122,7 +123,11 @@ class _HomePageState extends State<HomePage> {
|
||||
appBar: AppBar(
|
||||
title: const Text('ente Authenticator'),
|
||||
),
|
||||
floatingActionButton: !_hasLoaded || _codes.isEmpty ? null : _getFab(),
|
||||
floatingActionButton: !_hasLoaded ||
|
||||
_codes.isEmpty ||
|
||||
!PreferenceService.instance.hasShownCoachMark()
|
||||
? null
|
||||
: _getFab(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -132,12 +137,22 @@ class _HomePageState extends State<HomePage> {
|
||||
if (_codes.isEmpty) {
|
||||
return _getEmptyState();
|
||||
} else {
|
||||
return ListView.builder(
|
||||
final list = ListView.builder(
|
||||
itemBuilder: ((context, index) {
|
||||
return CodeWidget(_codes[index]);
|
||||
}),
|
||||
itemCount: _codes.length,
|
||||
);
|
||||
if (!PreferenceService.instance.hasShownCoachMark()) {
|
||||
return Stack(
|
||||
children: [
|
||||
list,
|
||||
_getCoachMarkWidget(),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return const EnteLoadingWidget();
|
||||
@@ -152,8 +167,8 @@ class _HomePageState extends State<HomePage> {
|
||||
childPadding: const EdgeInsets.all(5),
|
||||
spaceBetweenChildren: 4,
|
||||
tooltip: 'Add Code',
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
overlayOpacity: 0.5,
|
||||
overlayColor: Theme.of(context).colorScheme.background,
|
||||
elevation: 8.0,
|
||||
@@ -161,16 +176,16 @@ class _HomePageState extends State<HomePage> {
|
||||
children: [
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.qr_code),
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
label: 'Scan a QR Code',
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
labelWidget: const SpeedDialLabelWidget("Scan a QR Code"),
|
||||
onTap: _redirectToScannerPage,
|
||||
),
|
||||
SpeedDialChild(
|
||||
child: const Icon(Icons.keyboard),
|
||||
foregroundColor: Theme.of(context).colorScheme.background,
|
||||
backgroundColor: Theme.of(context).colorScheme.inverseBackgroundColor,
|
||||
label: 'Enter details manually',
|
||||
foregroundColor: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
backgroundColor: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
labelWidget: const SpeedDialLabelWidget("Enter details manually"),
|
||||
onTap: _redirectToManualEntryPage,
|
||||
),
|
||||
],
|
||||
@@ -223,4 +238,89 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getCoachMarkWidget() {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await PreferenceService.instance.setHasShownCoachMark(true);
|
||||
setState(() {});
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: Theme.of(context).colorScheme.background.withOpacity(0.1),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.swipe_left,
|
||||
size: 42,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
Text(
|
||||
"Swipe left to edit or remove codes",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 36,
|
||||
),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
child: OutlinedButton(
|
||||
onPressed: () async {
|
||||
await PreferenceService.instance
|
||||
.setHasShownCoachMark(true);
|
||||
setState(() {});
|
||||
},
|
||||
child: const Text("OK"),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SpeedDialLabelWidget extends StatelessWidget {
|
||||
final String label;
|
||||
|
||||
const SpeedDialLabelWidget(
|
||||
this.label, {
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Theme.of(context).colorScheme.fabBackgroundColor,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.fabForegroundColor,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,6 @@ class AboutSectionWidget extends StatelessWidget {
|
||||
Widget _getSectionOptions(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
sectionOptionSpacing,
|
||||
const AboutMenuItemWidget(
|
||||
title: "FAQ",
|
||||
url: "https://ente.io/faq",
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
const AboutMenuItemWidget(
|
||||
title: "Terms",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/account/change_email_dialog.dart';
|
||||
import 'package:ente_auth/ui/account/password_entry_page.dart';
|
||||
@@ -17,13 +14,8 @@ import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:ente_auth/utils/navigation_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_sodium/flutter_sodium.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class AccountSectionWidget extends StatelessWidget {
|
||||
final _codeFile = File(
|
||||
Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt",
|
||||
);
|
||||
|
||||
AccountSectionWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -37,46 +29,42 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
|
||||
Column _getSectionOptions(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
if (Configuration.instance.getRecoveryKey() != null) {
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Recovery key",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to view your recovery key",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey =
|
||||
Sodium.bin2base64(Configuration.instance.getRecoveryKey());
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
}
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
"OK",
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
]);
|
||||
}
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Recovery key",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
final hasAuthenticated = await LocalAuthenticationService.instance
|
||||
.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to view your recovery key",
|
||||
);
|
||||
if (hasAuthenticated) {
|
||||
String recoveryKey;
|
||||
try {
|
||||
recoveryKey =
|
||||
Sodium.bin2hex(Configuration.instance.getRecoveryKey());
|
||||
} catch (e) {
|
||||
showGenericErrorDialog(context);
|
||||
return;
|
||||
}
|
||||
routeToPage(
|
||||
context,
|
||||
RecoveryKeyPage(
|
||||
recoveryKey,
|
||||
"OK",
|
||||
showAppBar: true,
|
||||
onDone: () {},
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
@@ -131,90 +119,9 @@ class AccountSectionWidget extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Export secrets",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
_showWarningDialog(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showWarningDialog(BuildContext context) async {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Warning",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: const Text(
|
||||
"The exported file contains sensitive information. Please store this safely.",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"I understand",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
_exportCodes(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
barrierColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportCodes(BuildContext context) async {
|
||||
final hasAuthenticated =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to export your codes",
|
||||
);
|
||||
if (!hasAuthenticated) {
|
||||
return;
|
||||
}
|
||||
if (_codeFile.existsSync()) {
|
||||
await _codeFile.delete();
|
||||
}
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
String data = "";
|
||||
for (final code in codes) {
|
||||
data += code.rawData + "\n";
|
||||
}
|
||||
_codeFile.writeAsStringSync(data);
|
||||
await Share.shareFiles([_codeFile.path]);
|
||||
Future.delayed(const Duration(seconds: 15), () async {
|
||||
if (_codeFile.existsSync()) {
|
||||
_codeFile.deleteSync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// @dart=2.9
|
||||
|
||||
// import 'package:open_file/open_file.dart';
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/core/network.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
|
||||
class AppUpdateDialog extends StatefulWidget {
|
||||
final LatestVersionInfo latestVersionInfo;
|
||||
@@ -25,7 +25,12 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
|
||||
changelog.add(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 4, 0, 4),
|
||||
child: Text("- " + log, style: Theme.of(context).textTheme.caption),
|
||||
child: Text(
|
||||
"- " + log,
|
||||
style: Theme.of(context).textTheme.caption.copyWith(
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -152,7 +157,7 @@ class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
|
||||
},
|
||||
);
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
// OpenFile.open(_saveUrl);
|
||||
OpenFile.open(_saveUrl);
|
||||
} catch (e) {
|
||||
Logger("ApkDownloader").severe(e);
|
||||
final AlertDialog alert = AlertDialog(
|
||||
|
||||
282
lib/ui/settings/data_section_widget.dart
Normal file
@@ -0,0 +1,282 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/core/configuration.dart';
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:ente_auth/models/code.dart';
|
||||
import 'package:ente_auth/services/authenticator_service.dart';
|
||||
import 'package:ente_auth/services/local_authentication_service.dart';
|
||||
import 'package:ente_auth/store/code_store.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class DataSectionWidget extends StatelessWidget {
|
||||
final _logger = Logger("AccountSectionWidget");
|
||||
|
||||
final _codeFile = File(
|
||||
Configuration.instance.getTempDirectory() + "ente-authenticator-codes.txt",
|
||||
);
|
||||
|
||||
DataSectionWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpandableMenuItemWidget(
|
||||
title: "Data",
|
||||
selectionOptionsWidget: _getSectionOptions(context),
|
||||
leadingIcon: Icons.key_outlined,
|
||||
);
|
||||
}
|
||||
|
||||
Column _getSectionOptions(BuildContext context) {
|
||||
List<Widget> children = [];
|
||||
children.addAll([
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Import codes",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
_showImportInstructionDialog(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Export codes",
|
||||
),
|
||||
pressedColor: getEnteColorScheme(context).fillFaint,
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
_showExportWarningDialog(context);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
]);
|
||||
return Column(
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showImportInstructionDialog(BuildContext context) async {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Import codes",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
const Text(
|
||||
"Please select a file that contains a list of your codes in the following format",
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.gNavBackgroundColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
"otpauth://totp/provider.com:you@email.com?secret=YOUR_SECRET",
|
||||
style: TextStyle(
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
fontFamily: Platform.isIOS ? "Courier" : "monospace",
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 20,
|
||||
),
|
||||
const Text(
|
||||
"The codes can be separated by a comma or a new line",
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Select file",
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
_pickImportFile(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
barrierColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showExportWarningDialog(BuildContext context) async {
|
||||
final AlertDialog alert = AlertDialog(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Warning",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: const Text(
|
||||
"The exported file contains sensitive information. Please store this safely.",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"I understand",
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
_exportCodes(context);
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text(
|
||||
"Cancel",
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return alert;
|
||||
},
|
||||
barrierColor: Colors.black12,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportCodes(BuildContext context) async {
|
||||
final hasAuthenticated =
|
||||
await LocalAuthenticationService.instance.requestLocalAuthentication(
|
||||
context,
|
||||
"Please authenticate to export your codes",
|
||||
);
|
||||
if (!hasAuthenticated) {
|
||||
return;
|
||||
}
|
||||
if (_codeFile.existsSync()) {
|
||||
await _codeFile.delete();
|
||||
}
|
||||
final codes = await CodeStore.instance.getAllCodes();
|
||||
String data = "";
|
||||
for (final code in codes) {
|
||||
data += code.rawData + "\n";
|
||||
}
|
||||
_codeFile.writeAsStringSync(data);
|
||||
await Share.shareFiles([_codeFile.path]);
|
||||
Future.delayed(const Duration(seconds: 15), () async {
|
||||
if (_codeFile.existsSync()) {
|
||||
_codeFile.deleteSync();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _pickImportFile(BuildContext context) async {
|
||||
FilePickerResult result = await FilePicker.platform.pickFiles();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
final dialog = createProgressDialog(context, "Please wait...");
|
||||
await dialog.show();
|
||||
try {
|
||||
File file = File(result.files.single.path);
|
||||
final codes = await file.readAsString();
|
||||
List<String> splitCodes = codes.split(",");
|
||||
if (splitCodes.length == 1) {
|
||||
splitCodes = codes.split("\n");
|
||||
}
|
||||
final parsedCodes = [];
|
||||
for (final code in splitCodes) {
|
||||
try {
|
||||
parsedCodes.add(Code.fromRawData(code));
|
||||
} catch (e) {
|
||||
_logger.severe("Could not parse code", e);
|
||||
}
|
||||
}
|
||||
for (final code in parsedCodes) {
|
||||
await CodeStore.instance.addCode(code, shouldSync: false);
|
||||
}
|
||||
unawaited(AuthenticatorService.instance.sync());
|
||||
await dialog.hide();
|
||||
await showConfettiDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
title: Text(
|
||||
"Yay!",
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
content: Text(
|
||||
"You have imported " + parsedCodes.length.toString() + " codes!",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: Text(
|
||||
"Okay",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context, rootNavigator: true).pop('dialog');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
await dialog.hide();
|
||||
await showErrorDialog(
|
||||
context,
|
||||
"Sorry",
|
||||
"Could not parse the selected file.\nPlease write to support@ente.io if you need help!",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
lib/ui/settings/made_with_love_widget.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class MadeWithLoveWidget extends StatelessWidget {
|
||||
const MadeWithLoveWidget({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
launchUrl(Uri.parse("https://ente.io"));
|
||||
},
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: "made with ❤️ at ",
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: const <TextSpan>[
|
||||
TextSpan(
|
||||
text: 'ente.io',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/services/update_service.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
@@ -33,19 +30,6 @@ class SocialSectionWidget extends StatelessWidget {
|
||||
const SocialsMenuItemWidget("Reddit", "https://reddit.com/r/enteio"),
|
||||
sectionOptionSpacing,
|
||||
];
|
||||
if (!UpdateService.instance.isIndependent()) {
|
||||
options.addAll(
|
||||
[
|
||||
SocialsMenuItemWidget(
|
||||
"Rate us! ✨",
|
||||
Platform.isAndroid
|
||||
? "https://play.google.com/store/apps/details?id=io.ente.photos"
|
||||
: "https://apps.apple.com/in/app/ente-photos/id1542026904",
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
],
|
||||
);
|
||||
}
|
||||
return Column(children: options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/core/constants.dart';
|
||||
import 'package:ente_auth/core/logging/super_logging.dart';
|
||||
import 'package:ente_auth/theme/ente_theme.dart';
|
||||
import 'package:ente_auth/ui/components/captioned_text_widget.dart';
|
||||
import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/menu_item_widget.dart';
|
||||
import 'package:ente_auth/ui/components/toggle_switch_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/common_settings.dart';
|
||||
import 'package:ente_auth/utils/email_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SupportSectionWidget extends StatelessWidget {
|
||||
class SupportSectionWidget extends StatefulWidget {
|
||||
const SupportSectionWidget({Key key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<SupportSectionWidget> createState() => _SupportSectionWidgetState();
|
||||
}
|
||||
|
||||
class _SupportSectionWidgetState extends State<SupportSectionWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExpandableMenuItemWidget(
|
||||
@@ -24,8 +29,6 @@ class SupportSectionWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _getSectionOptions(BuildContext context) {
|
||||
final String bugsEmail =
|
||||
Platform.isAndroid ? "android-bugs@ente.io" : "ios-bugs@ente.io";
|
||||
return Column(
|
||||
children: [
|
||||
sectionOptionSpacing,
|
||||
@@ -49,14 +52,27 @@ class SupportSectionWidget extends StatelessWidget {
|
||||
trailingIcon: Icons.chevron_right_outlined,
|
||||
trailingIconIsMuted: true,
|
||||
onTap: () async {
|
||||
await sendLogs(context, "Report bug", bugsEmail);
|
||||
await sendLogs(context, "Report bug", "auth@ente.io");
|
||||
},
|
||||
onDoubleTap: () async {
|
||||
final zipFilePath = await getZippedLogsFile(context);
|
||||
await shareLogs(context, bugsEmail, zipFilePath);
|
||||
await shareLogs(context, "auth@ente.io", zipFilePath);
|
||||
},
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
MenuItemWidget(
|
||||
captionedTextWidget: const CaptionedTextWidget(
|
||||
title: "Crash & error reporting",
|
||||
),
|
||||
trailingSwitch: ToggleSwitchWidget(
|
||||
value: SuperLogging.shouldReportErrors(),
|
||||
onChanged: (value) async {
|
||||
await SuperLogging.setShouldReportErrors(value);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
sectionOptionSpacing,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:ente_auth/ui/settings/about_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/account_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/app_version_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/danger_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/data_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/made_with_love_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/security_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/social_section_widget.dart';
|
||||
import 'package:ente_auth/ui/settings/support_section_widget.dart';
|
||||
@@ -60,6 +62,8 @@ class SettingsPage extends StatelessWidget {
|
||||
contents.addAll([
|
||||
AccountSectionWidget(),
|
||||
sectionSpacing,
|
||||
DataSectionWidget(),
|
||||
sectionSpacing,
|
||||
const SecuritySectionWidget(),
|
||||
sectionSpacing,
|
||||
]);
|
||||
@@ -80,6 +84,7 @@ class SettingsPage extends StatelessWidget {
|
||||
sectionSpacing,
|
||||
const DangerSectionWidget(),
|
||||
const AppVersionWidget(),
|
||||
const MadeWithLoveWidget(),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 60),
|
||||
),
|
||||
|
||||
@@ -34,7 +34,7 @@ class _LockScreenState extends State<LockScreen> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Opacity(
|
||||
opacity: 0.2,
|
||||
opacity: 0.3,
|
||||
child: Image.asset('assets/loading_photos_background.png'),
|
||||
),
|
||||
SizedBox(
|
||||
|
||||
153
lib/ui/two_factor_authentication_page.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/ui/lifecycle_event_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:pinput/pin_put/pin_put.dart';
|
||||
|
||||
class TwoFactorAuthenticationPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
|
||||
const TwoFactorAuthenticationPage(this.sessionID, {Key key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<TwoFactorAuthenticationPage> createState() =>
|
||||
_TwoFactorAuthenticationPageState();
|
||||
}
|
||||
|
||||
class _TwoFactorAuthenticationPageState
|
||||
extends State<TwoFactorAuthenticationPage> {
|
||||
final _pinController = TextEditingController();
|
||||
String _code = "";
|
||||
LifecycleEventHandler _lifecycleEventHandler;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_lifecycleEventHandler = LifecycleEventHandler(
|
||||
resumeCallBack: () async {
|
||||
if (mounted) {
|
||||
final data = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
if (data != null && data.text != null && data.text.length == 6) {
|
||||
_pinController.text = data.text;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
WidgetsBinding.instance.addObserver(_lifecycleEventHandler);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(_lifecycleEventHandler);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Two-factor authentication",
|
||||
),
|
||||
),
|
||||
body: _getBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getBody() {
|
||||
final pinPutDecoration = BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context)
|
||||
.inputDecorationTheme
|
||||
.focusedBorder
|
||||
.borderSide
|
||||
.color,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
const Text(
|
||||
"Enter the 6-digit code from\nyour authenticator app",
|
||||
style: TextStyle(
|
||||
height: 1.4,
|
||||
fontSize: 16,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(32)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(40, 0, 40, 0),
|
||||
child: PinPut(
|
||||
fieldsCount: 6,
|
||||
onSubmit: (String code) {
|
||||
_verifyTwoFactorCode(code);
|
||||
},
|
||||
onChanged: (String pin) {
|
||||
setState(() {
|
||||
_code = pin;
|
||||
});
|
||||
},
|
||||
controller: _pinController,
|
||||
submittedFieldDecoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(20.0),
|
||||
),
|
||||
selectedFieldDecoration: pinPutDecoration,
|
||||
followingFieldDecoration: pinPutDecoration.copyWith(
|
||||
borderRadius: BorderRadius.circular(5.0),
|
||||
),
|
||||
inputDecoration: const InputDecoration(
|
||||
focusedBorder: InputBorder.none,
|
||||
border: InputBorder.none,
|
||||
counterText: '',
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(24)),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
child: OutlinedButton(
|
||||
onPressed: _code.length == 6
|
||||
? () async {
|
||||
_verifyTwoFactorCode(_code);
|
||||
}
|
||||
: null,
|
||||
child: const Text("Verify"),
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(30)),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
UserService.instance.recoverTwoFactor(context, widget.sessionID);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: const Center(
|
||||
child: Text(
|
||||
"Lost device?",
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _verifyTwoFactorCode(String code) async {
|
||||
await UserService.instance.verifyTwoFactor(context, widget.sessionID, code);
|
||||
}
|
||||
}
|
||||
112
lib/ui/two_factor_recovery_page.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
// @dart=2.9
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:ente_auth/services/user_service.dart';
|
||||
import 'package:ente_auth/utils/dialog_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TwoFactorRecoveryPage extends StatefulWidget {
|
||||
final String sessionID;
|
||||
final String encryptedSecret;
|
||||
final String secretDecryptionNonce;
|
||||
|
||||
const TwoFactorRecoveryPage(
|
||||
this.sessionID,
|
||||
this.encryptedSecret,
|
||||
this.secretDecryptionNonce, {
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<TwoFactorRecoveryPage> createState() => _TwoFactorRecoveryPageState();
|
||||
}
|
||||
|
||||
class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
|
||||
final _recoveryKey = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
"Recover account",
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(60, 0, 60, 0),
|
||||
child: TextFormField(
|
||||
decoration: const InputDecoration(
|
||||
hintText: "Enter your recovery key",
|
||||
contentPadding: EdgeInsets.all(20),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
controller: _recoveryKey,
|
||||
autofocus: false,
|
||||
autocorrect: false,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
onChanged: (_) {
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(24)),
|
||||
Container(
|
||||
padding: const EdgeInsets.fromLTRB(80, 0, 80, 0),
|
||||
width: double.infinity,
|
||||
height: 64,
|
||||
child: OutlinedButton(
|
||||
onPressed: _recoveryKey.text.isNotEmpty
|
||||
? () async {
|
||||
await UserService.instance.removeTwoFactor(
|
||||
context,
|
||||
widget.sessionID,
|
||||
_recoveryKey.text,
|
||||
widget.encryptedSecret,
|
||||
widget.secretDecryptionNonce,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
child: const Text("Recover"),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
showErrorDialog(
|
||||
context,
|
||||
"Contact support",
|
||||
"Please drop an email to support@ente.io from your registered email address",
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(40),
|
||||
child: Center(
|
||||
child: Text(
|
||||
"No recovery key?",
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: 12,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,9 @@ Future<void> sendLogs(
|
||||
content.addAll(
|
||||
[
|
||||
const Text(
|
||||
"This will send across logs to help us debug your issue. Please note that file names will be included to help track issues with specific files.",
|
||||
"This will send across logs to help us debug your issue. "
|
||||
"While we take precautions to ensure that sensitive information is not "
|
||||
"logged, we encourage you to view these logs before sharing them.",
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
fontSize: 16,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ente_auth/ente_theme_data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_easyloading/flutter_easyloading.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
|
||||
Future showToast(
|
||||
@@ -11,31 +8,16 @@ Future showToast(
|
||||
toastLength = Toast.LENGTH_LONG,
|
||||
iOSDismissOnTap = true,
|
||||
}) async {
|
||||
if (Platform.isAndroid) {
|
||||
await Fluttertoast.cancel();
|
||||
return Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: toastLength,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
timeInSecForIosWeb: 1,
|
||||
backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor,
|
||||
textColor: Theme.of(context).colorScheme.toastTextColor,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
} else {
|
||||
EasyLoading.instance
|
||||
..backgroundColor = Theme.of(context).colorScheme.toastBackgroundColor
|
||||
..indicatorColor = Theme.of(context).colorScheme.toastBackgroundColor
|
||||
..textColor = Theme.of(context).colorScheme.toastTextColor
|
||||
..userInteractions = true
|
||||
..loadingStyle = EasyLoadingStyle.custom;
|
||||
return EasyLoading.showToast(
|
||||
message,
|
||||
duration: Duration(seconds: (toastLength == Toast.LENGTH_LONG ? 5 : 2)),
|
||||
toastPosition: EasyLoadingToastPosition.bottom,
|
||||
dismissOnTap: iOSDismissOnTap,
|
||||
);
|
||||
}
|
||||
await Fluttertoast.cancel();
|
||||
return Fluttertoast.showToast(
|
||||
msg: message,
|
||||
toastLength: toastLength,
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
timeInSecForIosWeb: 1,
|
||||
backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor,
|
||||
textColor: Theme.of(context).colorScheme.toastTextColor,
|
||||
fontSize: 16.0,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showShortToast(context, String message) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:otp/otp.dart' as otp;
|
||||
|
||||
String getTotp(Code code) {
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
code.secret,
|
||||
getSanitizedSecret(code.secret),
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
length: code.digits,
|
||||
interval: code.period,
|
||||
@@ -14,7 +14,7 @@ String getTotp(Code code) {
|
||||
|
||||
String getNextTotp(Code code) {
|
||||
return otp.OTP.generateTOTPCodeString(
|
||||
code.secret,
|
||||
getSanitizedSecret(code.secret),
|
||||
DateTime.now().millisecondsSinceEpoch + code.period * 1000,
|
||||
length: code.digits,
|
||||
interval: code.period,
|
||||
@@ -33,3 +33,7 @@ otp.Algorithm _getAlgorithm(Code code) {
|
||||
return otp.Algorithm.SHA1;
|
||||
}
|
||||
}
|
||||
|
||||
String getSanitizedSecret(String secret) {
|
||||
return secret.toUpperCase().trim();
|
||||
}
|
||||
|
||||
@@ -7,12 +7,16 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) sentry_flutter_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin");
|
||||
sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_linux
|
||||
sentry_flutter
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import connectivity_macos
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import package_info_plus_macos
|
||||
import path_provider_macos
|
||||
import sentry_flutter
|
||||
import share_plus_macos
|
||||
import shared_preferences_macos
|
||||
import sqflite
|
||||
@@ -16,9 +18,11 @@ import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin"))
|
||||
FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
||||
79
pubspec.lock
@@ -260,6 +260,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
dbus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dbus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.4"
|
||||
device_info:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -344,6 +351,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.6.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -377,13 +391,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.1"
|
||||
flutter_easyloading:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_easyloading
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
flutter_email_sender:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -405,6 +412,27 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.3"
|
||||
flutter_local_notifications:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_local_notifications
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "12.0.3"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
flutter_local_notifications_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_local_notifications_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -487,13 +515,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.2.0"
|
||||
flutter_spinkit:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_spinkit
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -517,7 +538,7 @@ packages:
|
||||
name: fluttertoast
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "8.0.9"
|
||||
version: "8.1.1"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -707,6 +728,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
open_file:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: open_file
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
otp:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -917,6 +945,20 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
sentry:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.15.1"
|
||||
sentry_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: sentry_flutter
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.15.1"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1167,6 +1209,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: timezone
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
timing:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
75
pubspec.yaml
@@ -1,6 +1,6 @@
|
||||
name: ente_auth
|
||||
description: ente two-factor authenticator
|
||||
version: 1.0.3+4
|
||||
version: 1.0.18+18
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
@@ -8,64 +8,63 @@ environment:
|
||||
|
||||
dependencies:
|
||||
adaptive_theme: ^3.1.0 # done
|
||||
bloc: ^8.0.3 #done
|
||||
bip39: ^1.0.6 #done
|
||||
bloc: ^8.0.3 #done
|
||||
clipboard: ^0.1.3
|
||||
collection: # dart
|
||||
computer: ^2.0.0
|
||||
confetti: ^0.7.0
|
||||
connectivity: ^3.0.3
|
||||
cupertino_icons: ^1.0.0
|
||||
device_info: ^2.0.2
|
||||
dio: ^4.0.6
|
||||
dotted_border: ^2.0.0+2
|
||||
email_validator: ^2.0.1
|
||||
event_bus: ^2.0.0
|
||||
dio: ^4.0.6
|
||||
expandable: ^5.0.1
|
||||
expansion_tile_card: ^2.0.0
|
||||
file_picker: ^4.6.1
|
||||
fk_user_agent: ^2.1.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_animation_progress_bar: ^2.2.1
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_email_sender: ^5.1.0
|
||||
flutter_inappwebview: ^5.7.1
|
||||
flutter_launcher_icons: ^0.9.3
|
||||
dotted_border: ^2.0.0+2
|
||||
in_app_purchase: ^0.5.2
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_animation_progress_bar: ^2.2.1
|
||||
flutter_slidable: ^2.0.0
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.0.1
|
||||
flutter_native_splash: ^2.2.13
|
||||
local_auth: ^1.1.5
|
||||
pinput: ^1.2.2
|
||||
password_strength: ^0.2.0
|
||||
flutter_sodium: ^0.2.0
|
||||
flutter_windowmanager: ^0.2.0
|
||||
flutter_local_notifications: ^12.0.3
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
# sentry:
|
||||
# path: thirdparty/sentry-dart/dart
|
||||
# sentry_flutter:
|
||||
# path: thirdparty/sentry-dart/flutter
|
||||
json_annotation: ^4.5.0
|
||||
fluttertoast: ^8.0.6
|
||||
flutter_native_splash: ^2.2.13
|
||||
flutter_secure_storage: ^6.0.0
|
||||
flutter_slidable: ^2.0.0
|
||||
flutter_sodium: ^0.2.0
|
||||
flutter_speed_dial: ^6.2.0
|
||||
flutter_windowmanager: ^0.2.0
|
||||
fluttertoast: ^8.1.1
|
||||
google_nav_bar: ^5.0.5 #supported
|
||||
http: ^0.13.4
|
||||
move_to_background: ^1.0.2
|
||||
otp: ^3.1.1
|
||||
path_provider: ^2.0.11
|
||||
in_app_purchase: ^0.5.2
|
||||
intl: ^0.17.0
|
||||
qr_code_scanner: ^1.0.1
|
||||
sqflite: ^2.1.0
|
||||
share_plus: ^4.4.0
|
||||
package_info_plus: ^1.0.1
|
||||
shared_preferences: ^2.0.5
|
||||
flutter_easyloading: ^3.0.5
|
||||
uuid: ^3.0.4
|
||||
url_launcher: ^6.1.5
|
||||
json_annotation: ^4.5.0
|
||||
local_auth: ^1.1.5
|
||||
logging: ^1.0.1
|
||||
move_to_background: ^1.0.2
|
||||
open_file: ^3.2.1
|
||||
otp: ^3.1.1
|
||||
package_info_plus: ^1.0.1
|
||||
password_strength: ^0.2.0
|
||||
path_provider: ^2.0.11
|
||||
pinput: ^1.2.2
|
||||
qr_code_scanner: ^1.0.1
|
||||
sentry: ^6.12.1
|
||||
sentry_flutter: ^6.12.1
|
||||
share_plus: ^4.4.0
|
||||
shared_preferences: ^2.0.5
|
||||
sqflite: ^2.1.0
|
||||
step_progress_indicator: ^1.0.2
|
||||
confetti: ^0.7.0
|
||||
clipboard: ^0.1.3
|
||||
flutter_speed_dial: ^6.2.0
|
||||
url_launcher: ^6.1.5
|
||||
uuid: ^3.0.4
|
||||
|
||||
dev_dependencies:
|
||||
bloc_test: ^9.0.3
|
||||
|
||||
@@ -7,11 +7,14 @@
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <sentry_flutter/sentry_flutter_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
SentryFlutterPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SentryFlutterPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
flutter_secure_storage_windows
|
||||
sentry_flutter
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
||||