diff --git a/.github/workflows/l18n-crowdin.yml b/.github/workflows/l18n-crowdin.yml new file mode 100644 index 000000000..bdfd83096 --- /dev/null +++ b/.github/workflows/l18n-crowdin.yml @@ -0,0 +1,35 @@ +name: Sync crowdin translation + +on: + push: + paths: # run action automatically when app_en.arb file is changed + - 'lib/l10n/arb/app_en.arb' + branches: [ main ] + schedule: + - cron: '0 */12 * * *' # Every 12 hours - https://crontab.guru/#0_*/12_*_*_* + workflow_dispatch: # for manually running the action + +jobs: + synchronize-with-crowdin: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: crowdin action + uses: crowdin/github-action@v1 + with: + upload_sources: true + upload_translations: true + download_translations: true + localization_branch_name: l10n_translations + create_pull_request: true + skip_untranslated_strings: true + pull_request_title: 'New Translations' + pull_request_body: 'New translations via [Crowdin GH Action](https://github.com/crowdin/github-action)' + pull_request_base_branch_name: 'main' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index b9917ca2e..60bfb12af 100644 --- a/README.md +++ b/README.md @@ -52,15 +52,20 @@ releases](https://github.com/ente-io/auth/releases/latest/download/ente-auth.apk contains APKs, built straight from source. These builds keep themselves updated, without relying on third party stores. -You can alternatively install the build from PlayStore. +You can alternatively install the build from PlayStore or F-Droid. Get it on Google Play + + Get it on F-Droid + + ### iPhone / Apple Silicon + Download on AppStore @@ -102,6 +107,10 @@ If you have feature requests, please create a [GitHub issue](https://github.com/ If you wish to support us, please ⭐ [star](https://github.com/ente-io/auth/stargazers) this project. +## 🙌 Translation +[![Crowdin](https://badges.crowdin.net/ente-authenticator-app/localized.svg)](https://crowdin.com/project/ente-authenticator-app) + +If you're interested in helping out with translation, please visit our [Crowdin project](https://crowdin.com/project/ente-authenticator-app) to get started. Thank you for your support. ## 💜 Community diff --git a/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml index 5f349f7f4..b26e945b8 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml @@ -2,4 +2,5 @@ + diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..87443717a --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,6 @@ +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_PERSONAL_TOKEN + +files: + - source: /lib/l10n/arb/app_en.arb + translation: /lib/l10n/arb/app_%two_letters_code%.arb \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 21fe7cbe3..b70ce028d 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,30 +1,30 @@ -ente authenticator 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. +ente's Authenticator app helps you generate and store 2 step verification (2FA) +tokens on your mobile devices. -Multi Device Synchronization +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 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. - - Mode - +- 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: +- 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: +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 @@ -32,4 +32,6 @@ file, that adheres to the above format. SUPPORT -We take pride in offering human support. If you are our paid customer, you can reach out to team@ente.io and expect a response from our team within 24 hours. + +If you need help, please reach out to support@ente.io, and a human will get in touch with you. +If you have feature requests, please create an issue @ https://github.com/ente-io/auth diff --git a/lib/core/configuration.dart b/lib/core/configuration.dart index 89ec1e51a..5633caa01 100644 --- a/lib/core/configuration.dart +++ b/lib/core/configuration.dart @@ -38,8 +38,7 @@ class Configuration { static const tokenKey = "token"; static const encryptedTokenKey = "encrypted_token"; static const userIDKey = "user_id"; - static const hasMigratedSecureStorageToFirstUnlockKey = - "has_migrated_secure_storage_to_first_unlock"; + static const hasMigratedSecureStorageKey = "has_migrated_secure_storage"; final kTempFolderDeletionTimeBuffer = const Duration(days: 1).inMicroseconds; @@ -63,8 +62,9 @@ class Configuration { late String _sharedDocumentsMediaDirectory; String? _volatilePassword; - final _secureStorageOptionsIOS = - const IOSOptions(accessibility: KeychainAccessibility.first_unlock); + final _secureStorageOptionsIOS = const IOSOptions( + accessibility: KeychainAccessibility.first_unlock_this_device, + ); // const IOSOptions(accessibility: IOSAccessibility.first_unlock); @@ -462,7 +462,7 @@ class Configuration { Future _migrateSecurityStorageToFirstUnlock() async { final hasMigratedSecureStorageToFirstUnlock = - _preferences.getBool(hasMigratedSecureStorageToFirstUnlockKey) ?? false; + _preferences.getBool(hasMigratedSecureStorageKey) ?? false; if (!hasMigratedSecureStorageToFirstUnlock && _key != null && _secretKey != null) { @@ -477,7 +477,7 @@ class Configuration { iOptions: _secureStorageOptionsIOS, ); await _preferences.setBool( - hasMigratedSecureStorageToFirstUnlockKey, + hasMigratedSecureStorageKey, true, ); } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index ef67987e4..17fced456 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -1,8 +1,8 @@ { - "@@locale": "de", + "@@locale": "en", "counterAppBarTitle": "Zähler", "@counterAppBarTitle": { - "description": "Text, der in der AppBar der Zähler-Seite angezeigt wird" + "description": "Text shown in the AppBar of the Counter Page" }, "onBoardingBody": "Sichern Sie Ihre 2FA-Codes", "onBoardingGetStarted": "Los geht's", @@ -11,7 +11,7 @@ "importEnterSetupKey": "Einen Setup-Schlüssel eingeben", "importAccountPageTitle": "Kontodaten eingeben", "codeIssuerHint": "Aussteller", - "codeSecretKeyHint" : "Geheimer Schlüssel", + "codeSecretKeyHint": "Geheimer Schlüssel", "codeAccountHint": "Konto (you@domain.com)", "accountKeyType": "Art des Schlüssels", "timeBasedKeyType": "Zeitbasiert (TOTP)", @@ -56,21 +56,19 @@ "welcomeBackTitle": "Willkommen zurück!", "madeWithLoveAtPrefix": "gemacht mit ❤️ bei ", "changeEmail": "E-Mail ändern", - "ok": "OK", "cancel": "Abbrechen", "yes": "Ja", "no": "Nein", "email": "E-Mail", - "support": "Support", + "support": "Unterstützung", "settings": "Einstellungen", "copied": "Kopiert", "tryAgainMessage": "Bitte versuchen Sie es erneut", "existingUser": "Bestehender Benutzer", - "newUser" : "Neu bei ente", + "newUser": "Neu bei ente", "delete": "Löschen", "enterYourPasswordHint": "Geben Sie Ihr Passwort ein", "forgotPassword": "Passwort vergessen", - "oops": "Oops", "somethingWentWrongMessage": "Ein Fehler ist aufgetreten, bitte versuchen Sie es erneut", "leaveFamily": "Familie verlassen", "leaveFamilyMessage": "Sind Sie sicher, dass Sie den Familien-Plan verlassen wollen?", @@ -85,7 +83,7 @@ "recoverAccount": "Konto wiederherstellen", "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein", "recover": "Wiederherstellen", - "contactSupportViaEmailMessage":"Bitte senden Sie eine E-Mail an {email} von Ihrer registrierten E-Mail-Adresse", + "contactSupportViaEmailMessage": "Bitte senden Sie eine E-Mail an {email} von Ihrer registrierten E-Mail-Adresse", "@contactSupportViaEmailMessage": { "placeholders": { "email": { @@ -112,5 +110,4 @@ "confirmPassword": "Bestätigen Sie das Passwort", "close": "Schließen", "oopsSomethingWentWrong": "Ups, da ist etwas schief gelaufen." - -} +} \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index eb58fbec5..d696f950d 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -55,6 +55,8 @@ "incorrectPassword": "Incorrect password", "welcomeBackTitle": "Welcome back!", "madeWithLoveAtPrefix": "made with ❤️ at ", + "supportDevs" : "Subscribe to ente to support this project.", + "supportDiscount" : "Use coupon code \"AUTH\" to get 10% off first year", "changeEmail": "change email", "ok": "OK", "cancel": "Cancel", diff --git a/lib/l10n/arb/app_es.arb b/lib/l10n/arb/app_es.arb index f1405f02e..5c9096734 100644 --- a/lib/l10n/arb/app_es.arb +++ b/lib/l10n/arb/app_es.arb @@ -1,7 +1,7 @@ { - "@@locale": "es", - "counterAppBarTitle": "Contador", - "@counterAppBarTitle": { - "description": "Texto mostrado en la AppBar de la página del contador" - } + "@@locale": "en", + "counterAppBarTitle": "Contador", + "@counterAppBarTitle": { + "description": "Text shown in the AppBar of the Counter Page" + } } \ No newline at end of file diff --git a/lib/l10n/arb/app_fr.arb b/lib/l10n/arb/app_fr.arb new file mode 100644 index 000000000..31891dec6 --- /dev/null +++ b/lib/l10n/arb/app_fr.arb @@ -0,0 +1,116 @@ +{ + "@@locale": "en", + "counterAppBarTitle": "Compteur", + "@counterAppBarTitle": { + "description": "Text shown in the AppBar of the Counter Page" + }, + "onBoardingBody": "Sécurisez vos codes 2FA", + "onBoardingGetStarted": "Premiers Pas", + "setupFirstAccount": "Configurez votre premier compte", + "importScanQrCode": "Scannez un QR Code", + "importEnterSetupKey": "Saisir une clé de configuration", + "importAccountPageTitle": "Saisir les détails du compte", + "codeIssuerHint": "Émetteur", + "codeSecretKeyHint": "Clé secrète", + "codeAccountHint": "Compte (vous@domaine.fr)", + "accountKeyType": "Type de clé", + "timeBasedKeyType": "Basé sur l'heure (TOTP)", + "counterBasedKeyType": "Basé sur le compteur (HOTP)", + "saveAction": "Sauvegarder", + "nextTotpTitle": "suivant", + "deleteCodeTitle": "Supprimer le code ?", + "deleteCodeMessage": "Êtes-vous sûr de vouloir supprimer ce code ? Cette action est irréversible.", + "viewLogsAction": "Afficher les journaux", + "sendLogsDescription": "Cela enverra des logs pour nous aider à déboguer votre problème. Bien que nous prenions des précautions pour nous assurer que les informations sensibles ne sont pas enregistrées, nous vous encourageons à consulter ces journaux avant de les partager.", + "preparingLogsTitle": "Préparation des journaux...", + "emailLogsTitle": "Journaux d'email", + "emailLogsMessage": "Envoyez les logs à {email}", + "@emailLogsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "copyEmailAction": "Copier l’e-mail", + "exportLogsAction": "Exporter les journaux", + "reportABug": "Signaler un bug", + "crashAndErrorReporting": "Rapport de plantage et d'erreur", + "reportBug": "Signaler un bug", + "emailUsMessage": "Veuillez nous envoyer un e-mail à {email}", + "@emailUsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "contactSupport": "Contacter le support", + "verifyPassword": "Vérifier le mot de passe", + "pleaseWaitTitle": "Veuillez patienter...", + "generatingEncryptionKeysTitle": "Génération des clés de chiffrement...", + "recreatePassword": "Recréer le mot de passe", + "recreatePasswordMessage": "L'appareil actuel n'est pas assez puissant pour vérifier votre mot de passe, donc nous avons besoin de le régénérer une fois d'une manière qu'il fonctionne avec tous les périphériques.\n\nVeuillez vous connecter en utilisant votre clé de récupération et régénérer votre mot de passe (vous pouvez utiliser le même si vous le souhaitez).", + "useRecoveryKeyAction": "Utiliser la clé de récupération", + "incorrectPassword": "Mot de passe incorrect", + "welcomeBackTitle": "Bon retour parmi nous !", + "madeWithLoveAtPrefix": "fait avec ❤️ à ", + "supportDiscount": "Utilisez le code coupon \"AUTH\" pour obtenir 10% de réduction sur la première année", + "changeEmail": "modifier l'e-mail", + "ok": "Ok", + "cancel": "Annuler", + "yes": "Oui", + "no": "Non", + "email": "E-mail", + "support": "Support", + "settings": "Paramètres", + "copied": "Copié", + "tryAgainMessage": "Veuillez réessayer", + "existingUser": "Utilisateur existant", + "newUser": "Nouveau sur ente", + "delete": "Supprimer", + "enterYourPasswordHint": "Saisir votre mot de passe", + "forgotPassword": "Mot de passe oublié", + "oops": "Oups", + "somethingWentWrongMessage": "Quelque chose s'est mal passé, veuillez recommencer", + "leaveFamily": "Quitter le plan familial", + "leaveFamilyMessage": "Êtes-vous certains de vouloir quitter le plan familial ?", + "inFamilyPlanMessage": "Vous êtes sur un plan familial !", + "swipeHint": "Glisser vers la gauche pour modifier ou supprimer des codes", + "scan": "Analyser", + "scanACode": "Scanner un code", + "verify": "Vérifier", + "enterCodeHint": "Saisir le code à 6 caractères de votre appli d'authentification", + "lostDeviceTitle": "Appareil perdu ?", + "twoFactorAuthTitle": "Authentification à deux facteurs", + "recoverAccount": "Récupérer un compte", + "enterRecoveryKeyHint": "Saisissez votre clé de récupération", + "recover": "Restaurer", + "contactSupportViaEmailMessage": "Veuillez envoyer un e-mail à {} depuis votre adresse enregistrée", + "@contactSupportViaEmailMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "noRecoveryKeyTitle": "Pas de clé de récupération ?", + "enterEmailHint": "Entrez votre adresse e-mail", + "invalidEmailTitle": "Adresse e-mail non valide", + "invalidEmailMessage": "Veuillez saisir une adresse e-mail valide.", + "deleteAccount": "Supprimer le compte", + "deleteAccountQuery": "Nous sommes désolés de vous voir partir. Êtes-vous confronté à un problème?", + "yesSendFeedbackAction": "Oui, envoyer un commentaire", + "noDeleteAccountAction": "Non, supprimer le compte", + "initiateAccountDeleteTitle": "Veuillez vous authentifier pour débuter la suppression du compte", + "confirmAccountDeleteTitle": "Êtes-vous sûr de vouloir supprimer votre compte ente ?", + "confirmAccountDeleteMessage": "Vos données téléchargées, à travers toutes les applications (Photos et Authenticator), seront planifiées pour la suppression, et votre compte sera définitivement supprimé.", + "sendEmail": "Envoyer un e-mail", + "createNewAccount": "Créer un nouveau compte", + "passwordStrengthWeak": "Faible", + "passwordStrengthStrong": "Fort", + "passwordStrengthModerate": "Modéré", + "confirmPassword": "Confirmer le mot de passe", + "close": "Fermer", + "oopsSomethingWentWrong": "Oops ! Une erreur s'est produite." +} \ No newline at end of file diff --git a/lib/l10n/arb/app_it.arb b/lib/l10n/arb/app_it.arb new file mode 100644 index 000000000..8789d2f2a --- /dev/null +++ b/lib/l10n/arb/app_it.arb @@ -0,0 +1,117 @@ +{ + "@@locale": "en", + "counterAppBarTitle": "Contatore", + "@counterAppBarTitle": { + "description": "Text shown in the AppBar of the Counter Page" + }, + "onBoardingBody": "Proteggi i tuoi codici 2FA", + "onBoardingGetStarted": "Per iniziare", + "setupFirstAccount": "Configura il tuo primo account", + "importScanQrCode": "Scansiona un codice QR", + "importEnterSetupKey": "Inserisci il codice segreto", + "importAccountPageTitle": "Inserisci i dettagli del tuo account", + "codeIssuerHint": "Emittente", + "codeSecretKeyHint": "Codice segreto", + "codeAccountHint": "Account (username@dominio.it)", + "accountKeyType": "Tipo di chiave", + "timeBasedKeyType": "Basata sul tempo (TOTP)", + "counterBasedKeyType": "Basata su contatore (HOTP)", + "saveAction": "Salva", + "nextTotpTitle": "successivo", + "deleteCodeTitle": "Eliminare il codice?", + "deleteCodeMessage": "Sei sicuro di voler rimuovere questo codice? L'azione è irreversibile.", + "viewLogsAction": "Visualizza i log", + "sendLogsDescription": "Invierai i tuoi log per aiutarci a risolvere il tuo problema. Prendiamo precauzioni per garantire che le informazioni sensibili non siano registrate, tuttavia ti invitiamo a leggerli prima di condividerli con noi.", + "preparingLogsTitle": "Preparando i log...", + "emailLogsTitle": "Invia i log per email", + "emailLogsMessage": "Invia i log all'indirizzo {email}", + "@emailLogsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "copyEmailAction": "Copia email", + "exportLogsAction": "Esporta log", + "reportABug": "Segnala un problema", + "crashAndErrorReporting": "Segnalazione degli errori e dei crash", + "reportBug": "Segnala un bug", + "emailUsMessage": "Per favore inviaci un'email a {email}", + "@emailUsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "contactSupport": "Contatta il supporto", + "verifyPassword": "Verifica la password", + "pleaseWaitTitle": "Attendere prego...", + "generatingEncryptionKeysTitle": "Generazione delle chiavi di crittografia...", + "recreatePassword": "Crea una nuova password", + "recreatePasswordMessage": "Il tuo dispositivo non è abbastanza potente per verificare la tua password, quindi abbiamo bisogno di rigenerarla in un modo che funziona con tutti i dispositivi. \n\nEffettua il login utilizzando la tua chiave di recupero e rigenera la tua password (puoi utilizzare nuovamente la stessa se vuoi).", + "useRecoveryKeyAction": "Utilizza un codice di recupero", + "incorrectPassword": "Password sbagliata", + "welcomeBackTitle": "Bentornato!", + "madeWithLoveAtPrefix": "realizzato con ❤️ a ", + "supportDevs": "Iscriviti a ente per supportare questo progetto.", + "supportDiscount": "Utilizzare il codice coupon \"AUTH\" per ottenere il 10% di sconto al primo anno", + "changeEmail": "modifica email", + "ok": "OK", + "cancel": "Annulla", + "yes": "Si", + "no": "No", + "email": "Email", + "support": "Supporto", + "settings": "Impostazioni", + "copied": "Copiato", + "tryAgainMessage": "Per favore riprova", + "existingUser": "Accedi", + "newUser": "Nuovo utente", + "delete": "Cancella", + "enterYourPasswordHint": "Inserisci la tua password", + "forgotPassword": "Password dimenticata", + "oops": "Oops", + "somethingWentWrongMessage": "Qualcosa è andato storto, per favore riprova", + "leaveFamily": "Abbandona il piano famiglia", + "leaveFamilyMessage": "Sei sicuro di voler uscire dal piano famiglia?", + "inFamilyPlanMessage": "Sei un utente con piano famiglia!", + "swipeHint": "Scorri a sinistra per modificare o rimuovere i codici", + "scan": "Scansiona", + "scanACode": "Scansiona un codice", + "verify": "Verifica", + "enterCodeHint": "Inserisci il codice di 6 cifre dalla tua app di autenticazione", + "lostDeviceTitle": "Dispositivo perso?", + "twoFactorAuthTitle": "Autenticazione a due fattori", + "recoverAccount": "Recupera account", + "enterRecoveryKeyHint": "Inserisci la tua chiave di recupero", + "recover": "Recupera", + "contactSupportViaEmailMessage": "Per favore invia un'email a {email} dal tuo indirizzo email registrato", + "@contactSupportViaEmailMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "noRecoveryKeyTitle": "Nessuna chiave di recupero?", + "enterEmailHint": "Inserisci il tuo indirizzo email", + "invalidEmailTitle": "Indirizzo email non valido", + "invalidEmailMessage": "Inserisci un indirizzo email valido.", + "deleteAccount": "Elimina account", + "deleteAccountQuery": "Ci dispiace vederti andare via. Stai avendo qualche problema?", + "yesSendFeedbackAction": "Sì, invia un feedback", + "noDeleteAccountAction": "No, elimina l'account", + "initiateAccountDeleteTitle": "Si prega di autenticarsi per avviare l'eliminazione dell'account", + "confirmAccountDeleteTitle": "Sei sicuro di voler eliminare il tuo account?", + "confirmAccountDeleteMessage": "I tuoi dati caricati, in tutte le app (sia foto che autenticatore), verranno pianificati per la cancellazione, e il tuo account sarà eliminato in modo permanente.", + "sendEmail": "Invia email", + "createNewAccount": "Crea un nuovo account", + "passwordStrengthWeak": "Debole", + "passwordStrengthStrong": "Forte", + "passwordStrengthModerate": "Mediocre", + "confirmPassword": "Conferma la password", + "close": "Chiudi", + "oopsSomethingWentWrong": "Oops, qualcosa è andato storto." +} \ No newline at end of file diff --git a/lib/l10n/arb/app_nl.arb b/lib/l10n/arb/app_nl.arb new file mode 100644 index 000000000..da4295f03 --- /dev/null +++ b/lib/l10n/arb/app_nl.arb @@ -0,0 +1,116 @@ +{ + "@@locale": "en", + "counterAppBarTitle": "Teller", + "@counterAppBarTitle": { + "description": "Text shown in the AppBar of the Counter Page" + }, + "onBoardingBody": "Beveilig je 2FA codes", + "onBoardingGetStarted": "Begin", + "setupFirstAccount": "Maak je account aan", + "importScanQrCode": "Scan een QR-code", + "importEnterSetupKey": "Voer een toegangssleutel in", + "importAccountPageTitle": "Accountgegevens invoeren", + "codeIssuerHint": "Uitgever", + "codeSecretKeyHint": "Geheime code", + "codeAccountHint": "Account (jij@domein.nl)", + "accountKeyType": "Type sleutel", + "timeBasedKeyType": "Tijd gebaseerd (TOTP)", + "counterBasedKeyType": "Teller gebaseerd (HOTP)", + "saveAction": "Opslaan", + "nextTotpTitle": "volgende", + "deleteCodeTitle": "Code verwijderen?", + "deleteCodeMessage": "Weet je zeker dat je deze code wilt verwijderen? Deze actie is onomkeerbaar.", + "viewLogsAction": "Bekijk logs", + "sendLogsDescription": "Dit zal logs verzenden om ons te helpen uw probleem op te lossen. Hoewel we voorzorgsmaatregelen nemen om ervoor te zorgen dat gevoelige informatie niet wordt gedeeld, raden we je aan deze logs te bekijken voordat je ze deelt.", + "preparingLogsTitle": "Klaarmaken...", + "emailLogsTitle": "E-mail logs", + "emailLogsMessage": "Verstuur de logs alsjeblieft naar {email}", + "@emailLogsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "copyEmailAction": "E-mail kopiëren", + "exportLogsAction": "Logs exporteren", + "reportABug": "Een fout melden", + "crashAndErrorReporting": "Crash- en foutrapportage", + "reportBug": "Een fout melden", + "emailUsMessage": "Stuur ons een e-mail op {email}", + "@emailUsMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "contactSupport": "Klantenservice", + "verifyPassword": "Bevestig wachtwoord", + "pleaseWaitTitle": "Een ogenblik geduld...", + "generatingEncryptionKeysTitle": "Encryptiesleutels genereren...", + "recreatePassword": "Wachtwoord opnieuw instellen", + "recreatePasswordMessage": "Het huidige apparaat is niet krachtig genoeg om je wachtwoord te verifiëren, dus moeten we de code een keer opnieuw genereren op een manier die met alle apparaten werkt.\n\nLog in met behulp van uw herstelcode en genereer opnieuw uw wachtwoord (je kunt dezelfde indien gewenst opnieuw gebruiken).", + "useRecoveryKeyAction": "Herstelcode gebruiken", + "incorrectPassword": "Onjuist wachtwoord", + "welcomeBackTitle": "Welkom terug!", + "madeWithLoveAtPrefix": "gemaakt met ❤️ door ", + "supportDiscount": "Gebruik couponcode \"AUTH\" om 10% korting te krijgen op het eerste jaar", + "changeEmail": "e-mailadres wijzigen", + "ok": "Oké", + "cancel": "Annuleer", + "yes": "Ja", + "no": "Nee", + "email": "E-mail", + "support": "Ondersteuning", + "settings": "Instellingen", + "copied": "Gekopieerd", + "tryAgainMessage": "Probeer het nog eens", + "existingUser": "Bestaande gebruiker", + "newUser": "Nieuw bij ente", + "delete": "Verwijderen", + "enterYourPasswordHint": "Voer je wachtwoord in", + "forgotPassword": "Wachtwoord vergeten", + "oops": "Oeps", + "somethingWentWrongMessage": "Er is iets fout gegaan, probeer het opnieuw", + "leaveFamily": "Familie verlaten", + "leaveFamilyMessage": "Weet je zeker dat je het familie-plan wil verlaten?", + "inFamilyPlanMessage": "Je hebt een familie-plan!", + "swipeHint": "Veeg naar links om codes te bewerken of te verwijderen", + "scan": "Scannen", + "scanACode": "Scan een code", + "verify": "Verifiëren", + "enterCodeHint": "Voer de 6-cijferige code van je verificatie-app in", + "lostDeviceTitle": "Apparaat verloren?", + "twoFactorAuthTitle": "Tweestapsverificatie", + "recoverAccount": "Account herstellen", + "enterRecoveryKeyHint": "Voer je herstelcode in", + "recover": "Herstellen", + "contactSupportViaEmailMessage": "Verstuur a.u.b. een e-mail naar {email} van uw geregistreerde e-mailadres", + "@contactSupportViaEmailMessage": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "noRecoveryKeyTitle": "Geen herstelcode?", + "enterEmailHint": "Voer je e-mailadres in", + "invalidEmailTitle": "Ongeldig e-mailadres", + "invalidEmailMessage": "Voer een geldig e-mailadres in.", + "deleteAccount": "Account verwijderen", + "deleteAccountQuery": "We zullen het vervelend vinden om je te zien vertrekken. Zijn er problemen?", + "yesSendFeedbackAction": "Ja, geef feedback", + "noDeleteAccountAction": "Nee, verwijder account", + "initiateAccountDeleteTitle": "Gelieve te verifiëren om het account te verwijderen", + "confirmAccountDeleteTitle": "Weet je zeker dat je je ente account wil verwijderen?", + "confirmAccountDeleteMessage": "Uw geüploade gegevens, in alle apps (Photos en Authenticator), worden ingepland voor verwijdering en uw account zal permanent worden verwijderd.", + "sendEmail": "E-mail versturen", + "createNewAccount": "Nieuw account aanmaken", + "passwordStrengthWeak": "Zwak", + "passwordStrengthStrong": "Sterk", + "passwordStrengthModerate": "Matig", + "confirmPassword": "Wachtwoord bevestigen", + "close": "Sluiten", + "oopsSomethingWentWrong": "Oeps, er is iets fout gegaan." +} \ No newline at end of file diff --git a/lib/l10n/arb/app_pt.arb b/lib/l10n/arb/app_pt.arb new file mode 100644 index 000000000..c8d329e48 --- /dev/null +++ b/lib/l10n/arb/app_pt.arb @@ -0,0 +1,3 @@ +{ + "@@locale": "en" +} \ No newline at end of file diff --git a/lib/l10n/arb/app_zh.arb b/lib/l10n/arb/app_zh.arb new file mode 100644 index 000000000..c8d329e48 --- /dev/null +++ b/lib/l10n/arb/app_zh.arb @@ -0,0 +1,3 @@ +{ + "@@locale": "en" +} \ No newline at end of file diff --git a/lib/models/code.dart b/lib/models/code.dart index 36a564d80..e3913daa8 100644 --- a/lib/models/code.dart +++ b/lib/models/code.dart @@ -46,7 +46,7 @@ class Code { account + "?algorithm=SHA1&digits=6&issuer=" + issuer + - "period=30&secret=" + + "&period=30&secret=" + secret, ); } @@ -76,6 +76,9 @@ class Code { static String _getIssuer(Uri uri) { try { + if (uri.queryParameters.containsKey("issuer")) { + return uri.queryParameters['issuer']!; + } final String path = Uri.decodeComponent(uri.path); return path.split(':')[0].substring(1); } catch (e) { diff --git a/lib/services/billing_service.dart b/lib/services/billing_service.dart index 36febf96c..62d299d49 100644 --- a/lib/services/billing_service.dart +++ b/lib/services/billing_service.dart @@ -32,10 +32,11 @@ class BillingService { bool _isOnSubscriptionPage = false; + Subscription _cachedSubscription; + Future _future; - Future init() async { - } + Future init() async {} void clearCache() { _future = null; @@ -99,22 +100,25 @@ class BillingService { } } - Future fetchSubscription() async { - try { - final response = await _dio.get( - _config.getHttpEndpoint() + "/billing/subscription", - options: Options( - headers: { - "X-Auth-Token": _config.getToken(), - }, - ), - ); - final subscription = Subscription.fromMap(response.data["subscription"]); - return subscription; - } on DioError catch (e, s) { - _logger.severe(e, s); - rethrow; + Future getSubscription() async { + if (_cachedSubscription == null) { + try { + final response = await _dio.get( + _config.getHttpEndpoint() + "/billing/subscription", + options: Options( + headers: { + "X-Auth-Token": _config.getToken(), + }, + ), + ); + _cachedSubscription = + Subscription.fromMap(response.data["subscription"]); + } on DioError catch (e, s) { + _logger.severe(e, s); + rethrow; + } } + return _cachedSubscription; } Future cancelStripeSubscription() async { diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index d00ac7d4b..b7de982f0 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -28,6 +28,7 @@ class EnteColorScheme { final Color strokeFaint; // Fixed Colors + final Color primaryGreen; final Color primary700; final Color primary500; final Color primary400; @@ -53,6 +54,7 @@ class EnteColorScheme { this.strokeBase, this.strokeMuted, this.strokeFaint, { + this.primaryGreen = _primaryGreen, this.primary700 = _primary700, this.primary500 = _primary500, this.primary400 = _primary400, @@ -143,6 +145,8 @@ const Color strokeFaintDark = Color.fromRGBO(255, 255, 255, 0.16); // Fixed Colors +const Color _primaryGreen = Color.fromRGBO(29, 185, 84, 1); + const Color _primary700 = Color.fromARGB(255, 164, 0, 182); const Color _primary500 = Color.fromARGB(255, 204, 10, 101); const Color _primary400 = Color.fromARGB(255, 122, 41, 193); diff --git a/lib/ui/code_timer_progress.dart b/lib/ui/code_timer_progress.dart index e34c56a3f..1dbb4358e 100644 --- a/lib/ui/code_timer_progress.dart +++ b/lib/ui/code_timer_progress.dart @@ -1,7 +1,6 @@ -import 'dart:async'; - +import 'package:ente_auth/ui/linear_progress_widget.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animation_progress_bar/flutter_animation_progress_bar.dart'; +import 'package:flutter/scheduler.dart'; class CodeTimerProgress extends StatefulWidget { final int period; @@ -15,46 +14,42 @@ class CodeTimerProgress extends StatefulWidget { _CodeTimerProgressState createState() => _CodeTimerProgressState(); } -class _CodeTimerProgressState extends State { - Timer? _everySecondTimer; - late int _timeRemaining; +class _CodeTimerProgressState extends State + with SingleTickerProviderStateMixin { + late final Ticker _ticker; + double _progress = 0.0; + late final int _microSecondsInPeriod; @override void initState() { super.initState(); - _timeRemaining = widget.period; - _updateTimeRemaining(); - _everySecondTimer = - Timer.periodic(const Duration(milliseconds: 200), (Timer t) { + _microSecondsInPeriod = widget.period * 1000000; + _ticker = createTicker((elapsed) { _updateTimeRemaining(); }); + _ticker.start(); + _updateTimeRemaining(); } void _updateTimeRemaining() { - int newTimeRemaining = - widget.period - (DateTime.now().second % widget.period); - if (newTimeRemaining != _timeRemaining) { - setState(() { - _timeRemaining = newTimeRemaining; - }); - } + int timeRemaining = (_microSecondsInPeriod) - + (DateTime.now().microsecondsSinceEpoch % _microSecondsInPeriod); + setState(() { + _progress = (timeRemaining / _microSecondsInPeriod); + }); } @override void dispose() { - _everySecondTimer?.cancel(); + _ticker.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return FAProgressBar( - currentValue: _timeRemaining / widget.period * 100, - size: 4, - animatedDuration: const Duration(milliseconds: 200), - progressColor: Colors.orange, - changeColorValue: 40, - changeProgressColor: Colors.green, + return LinearProgressWidget( + color: _progress > 0.4 ? Colors.green : Colors.orange, + fractionOfStorage: _progress, ); } } diff --git a/lib/ui/linear_progress_widget.dart b/lib/ui/linear_progress_widget.dart new file mode 100644 index 000000000..6296e5d4a --- /dev/null +++ b/lib/ui/linear_progress_widget.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class LinearProgressWidget extends StatelessWidget { + final Color color; + final double fractionOfStorage; + const LinearProgressWidget({ + required this.color, + required this.fractionOfStorage, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constrains) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: color, + ), + width: constrains.maxWidth * fractionOfStorage, + height: 4, + ); + }, + ); + } +} diff --git a/lib/ui/settings/support_dev_widget.dart b/lib/ui/settings/support_dev_widget.dart new file mode 100644 index 000000000..52885f62f --- /dev/null +++ b/lib/ui/settings/support_dev_widget.dart @@ -0,0 +1,68 @@ +import 'dart:io'; + +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/subscription.dart'; +// ignore: import_of_legacy_library_into_null_safe +import 'package:ente_auth/services/billing_service.dart'; +import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:styled_text/styled_text.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SupportDevWidget extends StatelessWidget { + const SupportDevWidget({ + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + + // fetch + return FutureBuilder( + future: BillingService.instance.getSubscription(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final subscription = snapshot.data; + if (subscription != null && subscription.productID != "free") { + return GestureDetector( + onTap: () { + launchUrl(Uri.parse("https://ente.io")); + }, + child: Padding( + padding: + const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6), + child: Column( + children: [ + StyledText( + text: l10n.supportDevs, + tags: { + 'bold-green': StyledTextTag( + style: TextStyle( + fontWeight: FontWeight.bold, + color: getEnteColorScheme(context).primaryGreen, + ), + ), + }, + ), + const Padding(padding: EdgeInsets.all(6)), + Platform.isAndroid + ? Text( + l10n.supportDiscount, + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.grey, + ), + ) + : const SizedBox.shrink(), + ], + ), + ), + ); + } + } + return const SizedBox.shrink(); + }, + ); + } +} diff --git a/lib/ui/settings_page.dart b/lib/ui/settings_page.dart index 22e4a472d..c7a983534 100644 --- a/lib/ui/settings_page.dart +++ b/lib/ui/settings_page.dart @@ -9,9 +9,9 @@ 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_dev_widget.dart'; import 'package:ente_auth/ui/settings/support_section_widget.dart'; import 'package:ente_auth/ui/settings/theme_switch_widget.dart'; import 'package:ente_auth/ui/settings/title_bar_widget.dart'; @@ -84,7 +84,7 @@ class SettingsPage extends StatelessWidget { sectionSpacing, const DangerSectionWidget(), const AppVersionWidget(), - const MadeWithLoveWidget(), + const SupportDevWidget(), const Padding( padding: EdgeInsets.only(bottom: 60), ), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 66302605c..00d26b6af 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -21,7 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) - FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin")) + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) diff --git a/pubspec.lock b/pubspec.lock index ac52ab3a2..c46f62503 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -438,14 +438,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_animation_progress_bar: - dependency: "direct main" - description: - name: flutter_animation_progress_bar - sha256: "85f05404faeef7141b1c5b53571293fc19359c120c4aea4430b2b7d808461d09" - url: "https://pub.dev" - source: hosted - version: "2.3.1" flutter_bloc: dependency: "direct main" description: @@ -527,26 +519,26 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: de957362e046bc68da8dcf6c1d922cb8bdad8dd4979ec69480cf1a3c481abe8e + sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "8.0.0" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "736436adaf91552433823f51ce22e098c2f0551db06b6596f58597a25b8ea797" + sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: "388f76fd0f093e7415a39ec4c169ae7cceeee6d9f9ba529d788a13f2be4de7bd" + sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -567,10 +559,10 @@ packages: dependency: transitive description: name: flutter_secure_storage_windows - sha256: ca89c8059cf439985aa83c59619b3674c7ef6cc2e86943d169a7369d6a69cab5 + sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.0" flutter_slidable: dependency: "direct main" description: @@ -1315,6 +1307,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + styled_text: + dependency: "direct main" + description: + name: styled_text + sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c + url: "https://pub.dev" + source: hosted + version: "7.0.0" synchronized: dependency: transitive description: @@ -1523,6 +1523,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.0" + xmlstream: + dependency: transitive + description: + name: xmlstream + sha256: "2d10c69a9d5fc46f71798b80ee6db15bc0d5bf560fdbdd264776cbeee0c83631" + url: "https://pub.dev" + source: hosted + version: "1.0.0" yaml: dependency: transitive description: @@ -1533,4 +1541,4 @@ packages: version: "3.1.1" sdks: dart: ">=2.18.0 <3.0.0" - flutter: ">=3.3.0" + flutter: ">=3.7.0" diff --git a/pubspec.yaml b/pubspec.yaml index dd15856bb..8a565b7c0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 1.0.32+32 +version: 1.0.36+36 publish_to: none environment: @@ -27,7 +27,6 @@ dependencies: 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 @@ -36,7 +35,7 @@ dependencies: flutter_localizations: sdk: flutter flutter_native_splash: ^2.2.13 - flutter_secure_storage: ^6.0.0 + flutter_secure_storage: ^8.0.0 flutter_slidable: ^2.0.0 flutter_sodium: git: @@ -63,6 +62,7 @@ dependencies: shared_preferences: ^2.0.5 sqflite: ^2.1.0 step_progress_indicator: ^1.0.2 + styled_text: ^7.0.0 url_launcher: ^6.1.5 uuid: ^3.0.4