From 0caf424a298d41e3b086b4d1deaddec6f404bf78 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:20:08 +0100 Subject: [PATCH 01/10] feat(ui_localizations): add email link confirmation and device error labels --- .../lib/l10n/firebase_ui_ar.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_de.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_en.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_es.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_es_419.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_fi.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_fr.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_he.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_hi.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_hu.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_id.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_it.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_ja.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_ko.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_nb.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_nl.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_pl.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_pt.arb | 19 +++++++++++++++++-- .../lib/l10n/firebase_ui_ro.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_ru.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_th.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_tr.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_uk.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_zh.arb | 17 ++++++++++++++++- .../lib/l10n/firebase_ui_zh_TW.arb | 17 ++++++++++++++++- .../lib/src/default_localizations.dart | 14 +++++++++++++- .../lib/src/lang/ar.dart | 13 ++++++++++++- .../lib/src/lang/de.dart | 13 ++++++++++++- .../lib/src/lang/en.dart | 13 ++++++++++++- .../lib/src/lang/es.dart | 13 ++++++++++++- .../lib/src/lang/es_419.dart | 13 ++++++++++++- .../lib/src/lang/fi.dart | 13 ++++++++++++- .../lib/src/lang/fr.dart | 13 ++++++++++++- .../lib/src/lang/he.dart | 13 ++++++++++++- .../lib/src/lang/hi.dart | 13 ++++++++++++- .../lib/src/lang/hu.dart | 13 ++++++++++++- .../lib/src/lang/id.dart | 13 ++++++++++++- .../lib/src/lang/it.dart | 13 ++++++++++++- .../lib/src/lang/ja.dart | 13 ++++++++++++- .../lib/src/lang/ko.dart | 13 ++++++++++++- .../lib/src/lang/nb.dart | 13 ++++++++++++- .../lib/src/lang/nl.dart | 13 ++++++++++++- .../lib/src/lang/pl.dart | 13 ++++++++++++- .../lib/src/lang/pt.dart | 13 ++++++++++++- .../lib/src/lang/ro.dart | 13 ++++++++++++- .../lib/src/lang/ru.dart | 13 ++++++++++++- .../lib/src/lang/th.dart | 13 ++++++++++++- .../lib/src/lang/tr.dart | 13 ++++++++++++- .../lib/src/lang/uk.dart | 13 ++++++++++++- .../lib/src/lang/zh.dart | 13 ++++++++++++- .../lib/src/lang/zh_tw.dart | 13 ++++++++++++- 51 files changed, 714 insertions(+), 52 deletions(-) diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb index d1b22c69..4c0e0aa3 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb @@ -1,6 +1,6 @@ { "@@locale": "ar", - "@@last_modified": "2024-02-04T10:25:05.090612", + "@@last_modified": "2026-09-23T14:42:53.126548", "accessDisabledErrorText": "تم إيقاف إذن الوصول إلى هذا الحساب مؤقتًا.", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb index 096585a4..1f127407 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb @@ -1,6 +1,6 @@ { "@@locale": "de", - "@@last_modified": "2024-02-04T10:25:05.099040", + "@@last_modified": "2026-09-23T14:42:53.123190", "accessDisabledErrorText": "Der Zugriff auf dieses Konto wurde vorübergehend gesperrt", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_en.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_en.arb index 7a351962..ec732e0b 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_en.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_en.arb @@ -1,6 +1,6 @@ { "@@locale": "en", - "@@last_modified": "2024-02-04T10:25:05.107668", + "@@last_modified": "2026-09-23T14:42:53.128214", "accessDisabledErrorText": "Access to this account has been temporarily disabled", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb index ea19977d..a8190658 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb @@ -1,6 +1,6 @@ { "@@locale": "es", - "@@last_modified": "2024-02-04T10:25:05.103038", + "@@last_modified": "2026-09-23T14:42:53.111668", "accessDisabledErrorText": "Se ha inhabilitado temporalmente al acceso a esta cuenta", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb index 2ece068b..c3c81c7d 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb @@ -1,6 +1,6 @@ { "@@locale": "es_419", - "@@last_modified": "2024-02-04T10:25:05.104661", + "@@last_modified": "2026-09-23T14:42:53.126941", "accessDisabledErrorText": "Se inhabilitó temporalmente el acceso a la cuenta", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb index 67a7e8f0..bad55133 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb @@ -1,6 +1,6 @@ { "@@locale": "fi", - "@@last_modified": "2025-04-17T14:19:00.000000", + "@@last_modified": "2026-09-23T14:42:53.120513", "accessDisabledErrorText": "Pääsy tähän tiliin on väliaikaisesti estetty", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb index 1a14e4d1..518a10ed 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb @@ -1,6 +1,6 @@ { "@@locale": "fr", - "@@last_modified": "2024-02-04T10:25:05.105667", + "@@last_modified": "2026-09-23T14:42:53.129938", "accessDisabledErrorText": "L'accès à ce compte a été temporairement désactivé", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb index 37be21d0..f135c176 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb @@ -1,6 +1,6 @@ { "@@locale": "he", - "@@last_modified": "2024-02-04T10:25:05.109669", + "@@last_modified": "2026-09-23T14:42:53.121719", "accessDisabledErrorText": "הגישה לחשבון זה הושבתה באופן זמני", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb index 31943d14..3ea76e3f 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb @@ -1,6 +1,6 @@ { "@@locale": "hi", - "@@last_modified": "2024-02-04T10:25:05.110670", + "@@last_modified": "2026-09-23T14:42:53.129243", "accessDisabledErrorText": "इस खाते के ऐक्सेस पर, कुछ समय के लिए रोक लगा दी गई है", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb index 4bef0346..45744950 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb @@ -1,6 +1,6 @@ { "@@locale": "hu", - "@@last_modified": "2024-02-04T10:25:05.112669", + "@@last_modified": "2026-09-23T14:42:53.117852", "accessDisabledErrorText": "A fiók átmenetileg le van tiltva", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb index 8b0a538b..55f84604 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb @@ -1,6 +1,6 @@ { "@@locale": "id", - "@@last_modified": "2024-02-04T10:25:05.111669", + "@@last_modified": "2026-09-23T14:42:53.119625", "accessDisabledErrorText": "Akses ke akun ini telah dinonaktifkan untuk sementara waktu", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb index 954e04b4..447f4884 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb @@ -1,6 +1,6 @@ { "@@locale": "it", - "@@last_modified": "2024-02-04T10:25:05.112669", + "@@last_modified": "2026-09-23T14:42:53.125090", "accessDisabledErrorText": "L'accesso a questo account è stato temporaneamente disabilitato", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb index 75edac0c..532ca0be 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb @@ -1,6 +1,6 @@ { "@@locale": "ja", - "@@last_modified": "2024-02-04T10:25:05.113670", + "@@last_modified": "2026-09-23T14:42:53.129717", "accessDisabledErrorText": "このアカウントへのアクセスが一時的に無効になっています", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb index 2d2d6266..bc93d656 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb @@ -1,6 +1,6 @@ { "@@locale": "ko", - "@@last_modified": "2024-02-04T10:25:05.115670", + "@@last_modified": "2026-09-23T14:42:53.116132", "accessDisabledErrorText": "이 계정의 액세스가 일시적으로 중지되었습니다.", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb index a4cdae19..9e066f48 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb @@ -1,6 +1,6 @@ { "@@locale": "nb", - "@@last_modified": "2024-02-04T10:25:05.116670", + "@@last_modified": "2026-09-23T14:42:53.124604", "accessDisabledErrorText": "Tilgang til denne kontoen er midlertidig sperret", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb index 26a94100..98a4667c 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb @@ -1,6 +1,6 @@ { "@@locale": "nl", - "@@last_modified": "2024-02-04T10:25:05.114670", + "@@last_modified": "2026-09-23T14:42:53.128915", "accessDisabledErrorText": "De toegang tot dit account is tijdelijk uitgezet", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb index dff09d94..7c87714e 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb @@ -1,6 +1,6 @@ { "@@locale": "pl", - "@@last_modified": "2024-02-04T10:25:05.115670", + "@@last_modified": "2026-09-23T14:42:53.127948", "accessDisabledErrorText": "Dostęp do tego konta został tymczasowo wyłączony", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb index 6e37c6c6..7c8bbd0d 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb @@ -1,6 +1,6 @@ { "@@locale": "pt", - "@@last_modified": "2024-02-04T10:25:05.117671", + "@@last_modified": "2026-09-23T14:42:53.126078", "accessDisabledErrorText": "O acesso a esta conta foi desativado temporariamente", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -434,7 +434,7 @@ "description": "EmailVerificationScreen title", "placeholders": {} }, - "verificationEmailSentText": "Um email de verificação foi enviado para seu email. Confira sua caixa de entrada e clique no link para verificar o seu endereço de email.", + "verificationEmailSentText": "Um email de verificação foi enviado para seu email. Confira sua caixa de entrada e clique no link para verificar o seu endereço de email.", "@verificationEmailSentText": { "description": "Hint text indicating that verification email has been sent", "placeholders": {} @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb index e62dc767..04c95984 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb @@ -1,6 +1,6 @@ { "@@locale": "ro", - "@@last_modified": "2024-02-04T10:25:05.117671", + "@@last_modified": "2026-09-23T14:42:53.122395", "accessDisabledErrorText": "Accesul la acest cont a fost dezactivat temporar", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb index ce34c350..81ca40be 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb @@ -1,6 +1,6 @@ { "@@locale": "ru", - "@@last_modified": "2024-02-04T10:25:05.118670", + "@@last_modified": "2026-09-23T14:42:53.129486", "accessDisabledErrorText": "Доступ к этому аккаунту временно заблокирован.", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb index 584044cf..83c6f775 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb @@ -1,6 +1,6 @@ { "@@locale": "th", - "@@last_modified": "2024-02-04T10:25:05.118670", + "@@last_modified": "2026-09-23T14:42:53.125609", "accessDisabledErrorText": "มีการปิดใช้สิทธิ์เข้าถึงบัญชีนี้ชั่วคราว", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb index c083497b..93da272d 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb @@ -1,6 +1,6 @@ { "@@locale": "tr", - "@@last_modified": "2024-02-04T10:25:05.122670", + "@@last_modified": "2026-09-23T14:42:53.127297", "accessDisabledErrorText": "Bu hesaba erişim geçici olarak devre dışı bırakıldı", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb index 056bdbc1..7ec45b2b 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb @@ -1,6 +1,6 @@ { "@@locale": "uk", - "@@last_modified": "2024-02-04T10:25:05.119669", + "@@last_modified": "2026-09-23T14:42:53.124155", "accessDisabledErrorText": "Доступ до цього облікового запису тимчасово вимкнено", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb index 3a9d6000..69b4e028 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb @@ -1,6 +1,6 @@ { "@@locale": "zh", - "@@last_modified": "2024-02-04T10:25:05.120670", + "@@last_modified": "2026-09-23T14:42:53.127659", "accessDisabledErrorText": "对此帐号的访问权限已被临时停用", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb index f4af67f2..ca6a83b6 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb @@ -1,6 +1,6 @@ { "@@locale": "zh_TW", - "@@last_modified": "2024-02-04T10:25:05.121670", + "@@last_modified": "2026-09-23T14:42:53.123659", "accessDisabledErrorText": "這個帳戶暫時無法存取", "@accessDisabledErrorText": { "description": "Used as an error message when account is blocked and user tries to perform some actions with the account (e.g. unlinking a credential).", @@ -533,5 +533,20 @@ "@sendVerificationEmailLabel": { "description": "Used as a button label to send a verification email", "placeholders": {} + }, + "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "@emailLinkConfirmEmailText": { + "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", + "placeholders": {} + }, + "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "@emailLinkWrongDeviceErrorText": { + "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", + "placeholders": {} + }, + "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "@emailLinkDifferentAnonymousUserErrorText": { + "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", + "placeholders": {} } } diff --git a/packages/firebase_ui_localizations/lib/src/default_localizations.dart b/packages/firebase_ui_localizations/lib/src/default_localizations.dart index 540c4006..87446608 100644 --- a/packages/firebase_ui_localizations/lib/src/default_localizations.dart +++ b/packages/firebase_ui_localizations/lib/src/default_localizations.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. @@ -121,9 +121,21 @@ abstract class FirebaseUILocalizationLabels { /// Used as an error text of the EmailInput when the email is empty. String get emailIsRequiredErrorText; + /// Shown on EmailLinkSignInView when a sign-in link was opened on a different + /// device and the user must confirm their email. + String get emailLinkConfirmEmailText; + + /// Used as an error message when an email sign-in link was requested by an + /// anonymous user who is no longer signed in. + String get emailLinkDifferentAnonymousUserErrorText; + /// Used as a label of the EmailLinkSignInButton. String get emailLinkSignInButtonLabel; + /// Used as an error message when an email sign-in link that must be completed + /// on the device that requested it is opened elsewhere. + String get emailLinkWrongDeviceErrorText; + /// Used as an error message when the user tries to sign up with an email that /// is already used. String get emailTakenErrorText; diff --git a/packages/firebase_ui_localizations/lib/src/lang/ar.dart b/packages/firebase_ui_localizations/lib/src/lang/ar.dart index 909bb60a..a18bb790 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ar.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ar.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -342,4 +342,15 @@ class ArLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/de.dart b/packages/firebase_ui_localizations/lib/src/lang/de.dart index 42176bb0..471a69f2 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/de.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/de.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -339,4 +339,15 @@ class DeLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Verifizierungs-E-Mail senden"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/en.dart b/packages/firebase_ui_localizations/lib/src/lang/en.dart index e7c3866a..f3de756b 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/en.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/en.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -336,4 +336,15 @@ class EnLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/es.dart b/packages/firebase_ui_localizations/lib/src/lang/es.dart index d512b86b..87a867ee 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/es.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/es.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -345,4 +345,15 @@ class EsLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/es_419.dart b/packages/firebase_ui_localizations/lib/src/lang/es_419.dart index 3a9502e5..fbe7dee0 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/es_419.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/es_419.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -342,4 +342,15 @@ class Es419Localizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/fi.dart b/packages/firebase_ui_localizations/lib/src/lang/fi.dart index 6261abce..e117c0e4 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/fi.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/fi.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -339,4 +339,15 @@ class FiLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Lähetä vahvistussähköposti"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/fr.dart b/packages/firebase_ui_localizations/lib/src/lang/fr.dart index eeb975c2..99fc5491 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/fr.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/fr.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -348,4 +348,15 @@ class FrLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Envoyer un e-mail de vérification"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/he.dart b/packages/firebase_ui_localizations/lib/src/lang/he.dart index d4d20e2b..c0f38950 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/he.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/he.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -336,4 +336,15 @@ class HeLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/hi.dart b/packages/firebase_ui_localizations/lib/src/lang/hi.dart index 74ffdc49..aca223de 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/hi.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/hi.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -339,4 +339,15 @@ class HiLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/hu.dart b/packages/firebase_ui_localizations/lib/src/lang/hu.dart index 3e641b52..8ab1b361 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/hu.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/hu.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -337,4 +337,15 @@ class HuLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/id.dart b/packages/firebase_ui_localizations/lib/src/lang/id.dart index ed67e92a..daba890c 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/id.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/id.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -337,4 +337,15 @@ class IdLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/it.dart b/packages/firebase_ui_localizations/lib/src/lang/it.dart index 5adaa83c..4e555e44 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/it.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/it.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -340,4 +340,15 @@ class ItLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ja.dart b/packages/firebase_ui_localizations/lib/src/lang/ja.dart index fd8f160c..494521c0 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ja.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ja.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -326,4 +326,15 @@ class JaLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ko.dart b/packages/firebase_ui_localizations/lib/src/lang/ko.dart index 7df3034a..603cd2c2 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ko.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ko.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -331,4 +331,15 @@ class KoLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/nb.dart b/packages/firebase_ui_localizations/lib/src/lang/nb.dart index 61d886e8..df0eca9a 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/nb.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/nb.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -336,4 +336,15 @@ class NbLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send e-postbekreftelse"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/nl.dart b/packages/firebase_ui_localizations/lib/src/lang/nl.dart index f7d5f73a..cb7142bb 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/nl.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/nl.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -339,4 +339,15 @@ class NlLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/pl.dart b/packages/firebase_ui_localizations/lib/src/lang/pl.dart index d3532f32..fd10800a 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/pl.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/pl.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -339,4 +339,15 @@ class PlLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/pt.dart b/packages/firebase_ui_localizations/lib/src/lang/pt.dart index 8f39f3fe..b872f24e 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/pt.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/pt.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -340,4 +340,15 @@ class PtLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Enviar email de verificação"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ro.dart b/packages/firebase_ui_localizations/lib/src/lang/ro.dart index 956792d1..9fd54c61 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ro.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ro.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -341,4 +341,15 @@ class RoLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ru.dart b/packages/firebase_ui_localizations/lib/src/lang/ru.dart index ee16d1b3..2ee96853 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ru.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ru.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -339,4 +339,15 @@ class RuLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/th.dart b/packages/firebase_ui_localizations/lib/src/lang/th.dart index 5c47cc5a..5e10a6a0 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/th.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/th.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -336,4 +336,15 @@ class ThLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/tr.dart b/packages/firebase_ui_localizations/lib/src/lang/tr.dart index 98e46787..f490ee78 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/tr.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/tr.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -339,4 +339,15 @@ class TrLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/uk.dart b/packages/firebase_ui_localizations/lib/src/lang/uk.dart index c01910b6..c40332d6 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/uk.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/uk.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -341,4 +341,15 @@ class UkLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/zh.dart b/packages/firebase_ui_localizations/lib/src/lang/zh.dart index 58cd6005..25c5fcb0 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/zh.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/zh.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -329,4 +329,15 @@ class ZhLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart b/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart index 50449c36..ada285bd 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart @@ -1,4 +1,4 @@ -// Copyright 2025, the Chromium project authors. Please see the AUTHORS file +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import '../default_localizations.dart'; @@ -329,4 +329,15 @@ class ZhTWLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + + @override + String get emailLinkWrongDeviceErrorText => + "Try opening the link using the same device or browser where you started the sign-in process."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "The session associated with this sign-in request has either expired or was cleared."; } From 5dffe641306bcf6d025930e0811bd9f891ca9654 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:20:08 +0100 Subject: [PATCH 02/10] feat(ui_auth)!: complete email link sign in after cold start and on other devices Keeps a single app link subscription so a second link is no longer dropped, persists the email and a session id so a link that launches the app completes sign in, asks the user to confirm their email when the link is opened on another device, and upgrades anonymous users instead of replacing them. BREAKING CHANGE: EmailLinkAuthListener gains onEmailRequired and EmailLinkAuthController gains confirmEmail, so classes implementing either must add them. --- docs/firebase-ui-auth/providers/email-link.md | 64 ++- .../android/app/src/main/AndroidManifest.xml | 4 +- .../firebase_ui_auth/example/lib/main.dart | 23 +- .../lib/src/flows/email_link_flow.dart | 33 +- .../providers/email_link_auth_provider.dart | 186 +++++++- .../lib/src/providers/email_link_session.dart | 103 +++++ .../screens/email_link_sign_in_screen.dart | 7 +- .../src/views/email_link_sign_in_view.dart | 31 +- .../lib/src/widgets/error_text.dart | 4 + packages/firebase_ui_auth/pubspec.yaml | 1 + .../test/flows/email_link_flow_test.dart | 406 +++++++++++++++++- .../views/email_link_sign_in_view_test.dart | 36 ++ 12 files changed, 841 insertions(+), 57 deletions(-) create mode 100644 packages/firebase_ui_auth/lib/src/providers/email_link_session.dart diff --git a/docs/firebase-ui-auth/providers/email-link.md b/docs/firebase-ui-auth/providers/email-link.md index bbd63339..7c43452c 100644 --- a/docs/firebase-ui-auth/providers/email-link.md +++ b/docs/firebase-ui-auth/providers/email-link.md @@ -56,8 +56,10 @@ MaterialApp( }, '/email-link-sign-in': (context) => EmailLinkSignInScreen( actions: [ - AuthStateChangeAction((context, state) { - Navigator.pushReplacementNamed(context, '/profile'); + AuthStateChangeAction((context, state) { + if (state is SignedIn || state is UserCreated) { + Navigator.pushReplacementNamed(context, '/profile'); + } }), ], ), @@ -68,9 +70,31 @@ MaterialApp( > Notes: > +> - a user signing in with an email link for the first time emits `UserCreated` rather than `SignedIn`, so handle both. > - see [navigation guide](../navigation.md) to learn how navigation works with Firebase UI. > - explore [FirebaseUIActions API docs](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/FirebaseUIAction-class.html). +## Handling the sign in link + +When the link is sent, the email is stored on the device, so the link completes the sign in even if the app was killed in the meantime. + +- **Link opens the app:** `EmailLinkSignInScreen` completes the sign in with the link that launched the app. Show it on startup when `isLaunchedFromSignInLink` is true: + + ```dart + final launchedFromSignInLink = + await emailLinkProvider.isLaunchedFromSignInLink(); + + MaterialApp( + initialRoute: launchedFromSignInLink ? '/email-link-sign-in' : '/login', + // ... + ); + ``` + + Disable Flutter's built-in deep linking so it does not replace `initialRoute` with the link: add `` to the `` in `AndroidManifest.xml` (not the ``), and set `FlutterDeepLinkingEnabled` to `false` in `Info.plist`. + +- **Link opened on another device:** the flow emits `EmailRequired` and `EmailLinkSignInView` asks the user to confirm their email before signing in. +- **Anonymous users:** if the current user is anonymous, the link upgrades that user and the flow emits `CredentialLinked` instead of `SignedIn`. The link must then be opened on the same device, otherwise sign in fails with an `email-link-wrong-device` error. + ## Using view If the pre-built screen don't suit the app's needs, you could use a `EmailLinkSignInView` to build your custom screen: @@ -88,8 +112,10 @@ class MyEmailLinkSignInScreen extends StatelessWidget { padding: const EdgeInsets.all(16), child: FirebaseUIActions( actions: [ - AuthStateChangeAction((context, state) { - Navigator.pushReplacementNamed(context, '/profile'); + AuthStateChangeAction((context, state) { + if (state is SignedIn || state is UserCreated) { + Navigator.pushReplacementNamed(context, '/profile'); + } } ], child: EmailLinkSignInView(provider: emailLinkAuthProvider), @@ -114,7 +140,7 @@ class MyCustomWidget extends StatelessWidget { return AuthFlowBuilder( provider: emailLinkProvider, listener: (oldState, newState, ctrl) { - if (newState is SignedIn) { + if (newState is SignedIn || newState is UserCreated) { Navigator.of(context).pushReplacementNamed('/profile'); } } @@ -128,6 +154,14 @@ class MyCustomWidget extends StatelessWidget { ); } else if (state is AwaitingDynamicLink) { return CircularProgressIndicator(); + } else if (state is EmailRequired) { + // The link was opened on another device. + return TextField( + decoration: InputDecoration(label: Text('Confirm your email')), + onSubmitted: (email) { + ctrl.confirmEmail(email); + }, + ); } else if (state is AuthFailed) { return ErrorText(exception: state.exception); } else { @@ -165,6 +199,13 @@ class _CustomEmailLinkSignInState extends State onSubmitted: provider.sendLink, ); + @override + void initState() { + super.initState(); + // Completes the sign in if a sign in link launched the app. + provider.handleIncomingLinks(); + } + @override void onBeforeLinkSent(String email) { setState(() { @@ -174,11 +215,24 @@ class _CustomEmailLinkSignInState extends State @override void onLinkSent(String email) { + provider.awaitLink(email); setState(() { child = Text('Check your email and click the link'); }); } + @override + void onEmailRequired(String link) { + setState(() { + child = TextField( + decoration: const InputDecoration( + labelText: 'Confirm your email', + ), + onSubmitted: (email) => provider.signInWithLink(email, link), + ); + }); + } + @override Widget build(BuildContext context) { return Center(child: child); diff --git a/packages/firebase_ui_auth/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_ui_auth/example/android/app/src/main/AndroidManifest.xml index 475e8d3d..2b189643 100644 --- a/packages/firebase_ui_auth/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_ui_auth/example/android/app/src/main/AndroidManifest.xml @@ -9,7 +9,6 @@ android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:usesCleartextTraffic="true"> - @@ -25,6 +24,9 @@ the Android process has started. This theme is visible to the user while the Flutter UI initializes. After that, this theme continues to determine the Window background behind the Flutter UI. --> + + main() async { ), ]); - runApp(const FirebaseAuthUIExample()); + final launchedFromSignInLink = await emailLinkProviderConfig + .isLaunchedFromSignInLink(); + + runApp(FirebaseAuthUIExample(launchedFromSignInLink: launchedFromSignInLink)); } // Overrides a label for en locale @@ -65,11 +68,19 @@ class LabelOverrides extends DefaultLocalizations { } class FirebaseAuthUIExample extends StatelessWidget { - const FirebaseAuthUIExample({super.key}); + const FirebaseAuthUIExample({super.key, this.launchedFromSignInLink = false}); + + /// Whether an email sign in link launched the app, so it can complete the + /// sign in on the email link screen. + final bool launchedFromSignInLink; String get initialRoute { final user = FirebaseAuth.instance.currentUser; + if (launchedFromSignInLink && (user == null || user.isAnonymous)) { + return '/email-link-sign-in'; + } + return switch (user) { null => '/', User(emailVerified: false, email: final String _) => '/verify-email', @@ -260,8 +271,12 @@ class FirebaseAuthUIExample extends StatelessWidget { '/email-link-sign-in': (context) { return EmailLinkSignInScreen( actions: [ - AuthStateChangeAction((context, state) { - Navigator.pushReplacementNamed(context, '/profile'); + AuthStateChangeAction((context, state) { + if (state is SignedIn || + state is UserCreated || + state is CredentialLinked) { + Navigator.pushReplacementNamed(context, '/profile'); + } }), ], provider: emailLinkProviderConfig, diff --git a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart index 7dbf3426..fd115b2b 100644 --- a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart +++ b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart @@ -19,10 +19,24 @@ class AwaitingDynamicLink extends AuthState { const AwaitingDynamicLink(); } +/// A state that indicates that a sign in link was opened on a device that did +/// not request it. The user should confirm their email to complete the sign +/// in, see [EmailLinkAuthController.confirmEmail]. +class EmailRequired extends AuthState { + /// The sign in link that was opened. + final String link; + + const EmailRequired(this.link); +} + /// A controller interface of the [EmailLinkFlow]. abstract class EmailLinkAuthController extends AuthController { /// Sends a sign in link to the [email]. void sendLink(String email); + + /// Completes the sign in after [EmailRequired] with the [email] the user + /// confirmed. + void confirmEmail(String email); } /// {@template ui.auth.flows.email_link_flow} @@ -38,7 +52,11 @@ class EmailLinkFlow extends AuthFlow /// {@macro ui.auth.auth_flow.ctor.provider} required super.provider, - }) : super(action: AuthAction.signIn, initialState: const Uninitialized()); + }) : super(action: AuthAction.signIn, initialState: const Uninitialized()) { + provider.handleIncomingLinks(); + } + + String? _pendingLink; @override void sendLink(String email) { @@ -55,6 +73,19 @@ class EmailLinkFlow extends AuthFlow value = const AwaitingDynamicLink(); provider.awaitLink(email); } + + @override + void onEmailRequired(String link) { + _pendingLink = link; + value = EmailRequired(link); + } + + @override + void confirmEmail(String email) { + final link = _pendingLink; + if (link == null) return; + provider.signInWithLink(email, link); + } } /// {@template ui.auth.flows.email_link_flow.email_link_sign_in_action} diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart index b0607adc..18c2173e 100644 --- a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart @@ -8,6 +8,8 @@ import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:app_links/app_links.dart'; import 'dart:async'; +import 'email_link_session.dart'; + /// A listener of the [EmailLinkFlow] lifecycle. abstract class EmailLinkAuthListener extends AuthListener { /// Called when the link being is sent to the user's [email]. @@ -15,11 +17,20 @@ abstract class EmailLinkAuthListener extends AuthListener { /// Called when the link was successfully sent to the [email]. void onLinkSent(String email); + + /// Called when a sign in [link] was opened on a device that did not request + /// it, so the email it was sent to is unknown. Complete the sign in with + /// [EmailLinkAuthProvider.signInWithLink] and the email the user confirms. + void onEmailRequired(String link); } /// {@template ui.auth.providers.email_link_auth_provider} /// An [AuthProvider] that allows to authenticate using a link that is being /// sent to the user's email. +/// +/// The email is stored on the device when the link is sent, so a link that +/// launches the app completes the sign in. If the link is opened on another +/// device, the user is asked to confirm their email instead. /// {@endtemplate} class EmailLinkAuthProvider extends AuthProvider { @@ -28,6 +39,8 @@ class EmailLinkAuthProvider final AppLinks _appLinks; StreamSubscription? _linkSubscription; + bool _initialLinkChecked = false; + String? _lastHandledLink; @override late EmailLinkAuthListener authListener; @@ -51,25 +64,94 @@ class EmailLinkAuthProvider }) : _appLinks = appLinks ?? AppLinks(); /// Sends a link to the [email]. + /// + /// If the current user is anonymous, the link upgrades that user instead of + /// signing in a new one. void sendLink(String email) { authListener.onBeforeLinkSent(email); - final future = auth.sendSignInLinkToEmail( + final session = EmailLinkSession( email: email, - actionCodeSettings: actionCodeSettings, + sessionId: EmailLinkSession.generateSessionId(), + anonymousUserId: shouldUpgradeAnonymous ? auth.currentUser!.uid : null, ); - future + auth + .sendSignInLinkToEmail( + email: email, + actionCodeSettings: _withSession(session), + ) + .then((_) => session.save()) .then((_) => authListener.onLinkSent(email)) .catchError(authListener.onError); } - void _onLinkReceived(String email, Uri uri) { + fba.ActionCodeSettings _withSession(EmailLinkSession session) { + final settings = actionCodeSettings; + + return fba.ActionCodeSettings( + url: session.appendTo(settings.url), + handleCodeInApp: settings.handleCodeInApp, + iOSBundleId: settings.iOSBundleId, + androidPackageName: settings.androidPackageName, + androidInstallApp: settings.androidInstallApp, + androidMinimumVersion: settings.androidMinimumVersion, + linkDomain: settings.linkDomain, + ); + } + + /// Listens for incoming app links and handles email authentication. + /// Should be called after [EmailLinkAuthListener.onLinkSent] was called. + /// + /// The [email] is read from the device storage written by [sendLink]. + void awaitLink(String email) { + _listen(); + } + + /// Whether the app was launched from an email sign in link. + /// + /// Use it on startup to show [EmailLinkSignInScreen], which completes the + /// sign in with that link. + Future isLaunchedFromSignInLink({fba.FirebaseAuth? auth}) async { + final uri = await _appLinks.getInitialLink(); + if (uri == null) return false; + + final firebaseAuth = auth ?? fba.FirebaseAuth.instance; + return firebaseAuth.isSignInWithEmailLink(uri.toString()); + } + + /// Handles the sign in link that launched the app, if any, and starts + /// listening for links opened while the app is running. + void handleIncomingLinks() { + _listen(); + + if (_initialLinkChecked) return; + _initialLinkChecked = true; + + _appLinks + .getInitialLink() + .then((uri) { + if (uri == null) return; + final link = uri.toString(); + if (auth.isSignInWithEmailLink(link)) _handleLink(link); + }) + .catchError(authListener.onError); + } + + void _listen() { + // Keep a single subscription: app_links closes its shared stream when the + // last listener cancels, so re-subscribing would drop the next link. + _linkSubscription ??= _appLinks.uriLinkStream.listen( + _onLinkReceived, + onError: (error) => authListener.onError(error), + ); + } + + void _onLinkReceived(Uri uri) { final link = uri.toString(); if (auth.isSignInWithEmailLink(link)) { - authListener.onBeforeSignIn(); - _signInWithEmailLink(email, link); + _handleLink(link); } else { authListener.onError( fba.FirebaseAuthException( @@ -80,26 +162,90 @@ class EmailLinkAuthProvider } } - /// Listens for incoming app links and handles email authentication. - /// Should be called after [EmailLinkAuthListener.onLinkSent] was called. - void awaitLink(String email) { - _linkSubscription?.cancel(); + Future _handleLink(String link) async { + // The link that launched the app can arrive both from getInitialLink and + // from uriLinkStream. + if (link == _lastHandledLink) return; + _lastHandledLink = link; + + try { + final params = parseEmailLinkParams(link); + final anonymousUserId = params[anonymousUserIdParam]; + final session = await EmailLinkSession.load(); + final isSameDevice = + session != null && session.sessionId == params[sessionIdParam]; + + if (!isSameDevice) { + if (anonymousUserId != null) { + throw fba.FirebaseAuthException( + code: 'email-link-wrong-device', + message: + 'The sign in link must be opened on the device that ' + 'requested it', + ); + } + + authListener.onEmailRequired(link); + return; + } + + _completeSignIn(session.email, link, anonymousUserId); + } catch (err) { + authListener.onError(err); + } + } - _linkSubscription = _appLinks.uriLinkStream.listen( - (Uri uri) => _onLinkReceived(email, uri), - onError: (error) => authListener.onError(error), + /// Completes the sign in with a [link] from + /// [EmailLinkAuthListener.onEmailRequired] and the [email] it was sent to. + void signInWithLink(String email, String link) { + _completeSignIn(email, link, null); + } + + void _completeSignIn(String email, String link, String? anonymousUserId) { + if (anonymousUserId == null) { + authListener.onBeforeSignIn(); + auth + .signInWithEmailLink(email: email, emailLink: link) + .then((credential) async { + await EmailLinkSession.clear(); + authListener.onSignedIn(credential); + }) + .catchError(authListener.onError); + return; + } + + final user = auth.currentUser; + if (user == null || !user.isAnonymous || user.uid != anonymousUserId) { + authListener.onError( + fba.FirebaseAuthException( + code: 'email-link-different-anonymous-user', + message: + 'The anonymous user that requested the link is no longer ' + 'signed in', + ), + ); + return; + } + + final credential = fba.EmailAuthProvider.credentialWithLink( + email: email, + emailLink: link, ); + + authListener.onBeforeSignIn(); + user + .linkWithCredential(credential) + .then((_) async { + await EmailLinkSession.clear(); + authListener.onCredentialLinked(credential); + }) + .catchError(authListener.onError); } void dispose() { _linkSubscription?.cancel(); _linkSubscription = null; - } - - void _signInWithEmailLink(String email, String link) { - auth - .signInWithEmailLink(email: email, emailLink: link) - .then(authListener.onSignedIn) - .catchError(authListener.onError); + _initialLinkChecked = false; + _lastHandledLink = null; } } diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart new file mode 100644 index 00000000..4d79fec1 --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart @@ -0,0 +1,103 @@ +// Copyright 2026, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math'; + +import 'package:shared_preferences/shared_preferences.dart'; + +const _emailKey = 'firebase_ui_auth.email_link.email'; +const _sessionIdKey = 'firebase_ui_auth.email_link.session_id'; +const _anonymousUserIdKey = 'firebase_ui_auth.email_link.anonymous_user_id'; + +/// Link parameter names shared with FirebaseUI-Android. +const sessionIdParam = 'ui_sid'; +const anonymousUserIdParam = 'ui_auid'; + +/// A pending email link sign in, persisted so that it survives the app being +/// killed while the user opens their email. +class EmailLinkSession { + final String email; + final String sessionId; + final String? anonymousUserId; + + const EmailLinkSession({ + required this.email, + required this.sessionId, + this.anonymousUserId, + }); + + static String generateSessionId() { + const chars = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final random = Random.secure(); + return List.generate(10, (_) => chars[random.nextInt(chars.length)]).join(); + } + + static Future load() async { + final prefs = await SharedPreferences.getInstance(); + final email = prefs.getString(_emailKey); + final sessionId = prefs.getString(_sessionIdKey); + if (email == null || sessionId == null) return null; + + return EmailLinkSession( + email: email, + sessionId: sessionId, + anonymousUserId: prefs.getString(_anonymousUserIdKey), + ); + } + + Future save() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_emailKey, email); + await prefs.setString(_sessionIdKey, sessionId); + if (anonymousUserId case final uid?) { + await prefs.setString(_anonymousUserIdKey, uid); + } else { + await prefs.remove(_anonymousUserIdKey); + } + } + + static Future clear() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_emailKey); + await prefs.remove(_sessionIdKey); + await prefs.remove(_anonymousUserIdKey); + } + + /// Returns [url] with the session parameters added to its query. + String appendTo(String url) { + final uri = Uri.parse(url); + return uri + .replace( + queryParameters: { + ...uri.queryParameters, + sessionIdParam: sessionId, + anonymousUserIdParam: ?anonymousUserId, + }, + ) + .toString(); + } +} + +/// Collects query parameters from an email sign in link, including those +/// nested in its `link` and `continueUrl` parameters. +Map parseEmailLinkParams(String link) { + final params = {}; + + void parse(String value) { + final uri = Uri.tryParse(value); + if (uri == null) return; + + uri.queryParameters.forEach((key, value) { + if (key == 'link' || key == 'continueUrl') { + parse(value); + } else { + params[key] = value; + } + }); + } + + parse(link); + return params; +} diff --git a/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart index 0d31542b..d1a68a7a 100644 --- a/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart @@ -32,8 +32,11 @@ class EmailLinkSignInScreen extends ProviderScreen { /// ```dart /// EmailLinkSignInScreen( /// actions: [ - /// AuthStateChangeAction((context, state) { - /// Navigator.pushReplacementNamed(context, '/'); + /// // First-time users emit UserCreated instead of SignedIn. + /// AuthStateChangeAction((context, state) { + /// if (state is SignedIn || state is UserCreated) { + /// Navigator.pushReplacementNamed(context, '/'); + /// } /// }), /// ], /// ); diff --git a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart index fe80ddd9..69a6c325 100644 --- a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart @@ -41,6 +41,9 @@ class _EmailLinkSignInViewState extends State { final emailCtrl = TextEditingController(); late final canPop = Navigator.canPop(context); + // Stays true after a failed attempt, so the user can correct the email. + bool isConfirmingEmail = false; + @override Widget build(BuildContext context) { final l = FirebaseUILocalizations.labelsOf(context); @@ -50,14 +53,32 @@ class _EmailLinkSignInViewState extends State { return AuthFlowBuilder( auth: widget.auth, provider: widget.provider, + listener: (oldState, newState, ctrl) { + if (newState is EmailRequired) { + setState(() => isConfirmingEmail = true); + } + }, builder: (context, state, ctrl, child) { final isFormHidden = statesToHideForm.contains(state.runtimeType); + + void submit() { + if (isConfirmingEmail) { + ctrl.confirmEmail(emailCtrl.text); + } else { + ctrl.sendLink(emailCtrl.text); + } + } + return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Title(text: l.signInWithEmailLinkViewTitleText), const SizedBox(height: 16), + if (!isFormHidden && isConfirmingEmail) ...[ + Text(l.emailLinkConfirmEmailText), + const SizedBox(height: 16), + ], if (!isFormHidden) Form( key: formKey, @@ -67,7 +88,7 @@ class _EmailLinkSignInViewState extends State { controller: emailCtrl, onSubmitted: (v) { if (formKey.currentState?.validate() ?? false) { - ctrl.sendLink(emailCtrl.text); + submit(); } }, ), @@ -80,10 +101,10 @@ class _EmailLinkSignInViewState extends State { const SizedBox(height: 8), LoadingButton( isLoading: state is SendingLink, - label: l.sendLinkButtonLabel, - onTap: () { - ctrl.sendLink(emailCtrl.text); - }, + label: isConfirmingEmail + ? l.continueText + : l.sendLinkButtonLabel, + onTap: submit, ), ], if (canPop) ...[ diff --git a/packages/firebase_ui_auth/lib/src/widgets/error_text.dart b/packages/firebase_ui_auth/lib/src/widgets/error_text.dart index 2ff8ea01..5cf0eeff 100644 --- a/packages/firebase_ui_auth/lib/src/widgets/error_text.dart +++ b/packages/firebase_ui_auth/lib/src/widgets/error_text.dart @@ -27,6 +27,10 @@ String? localizedErrorText( return labels.credentialAlreadyInUseErrorText; case 'invalid-verification-code': return labels.invalidVerificationCodeErrorText; + case 'email-link-wrong-device': + return labels.emailLinkWrongDeviceErrorText; + case 'email-link-different-anonymous-user': + return labels.emailLinkDifferentAnonymousUserErrorText; case 'weak-password': return labels.weakPasswordErrorText; diff --git a/packages/firebase_ui_auth/pubspec.yaml b/packages/firebase_ui_auth/pubspec.yaml index b39d1b95..288dc874 100644 --- a/packages/firebase_ui_auth/pubspec.yaml +++ b/packages/firebase_ui_auth/pubspec.yaml @@ -22,6 +22,7 @@ dependencies: flutter_localizations: sdk: flutter meta: ^1.10.0 + shared_preferences: ^2.5.0 dev_dependencies: flutter_test: diff --git a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart index a39c24e2..89b92b4f 100644 --- a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart +++ b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart @@ -2,11 +2,15 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:async'; + +import 'package:app_links/app_links.dart'; import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../test_utils.dart'; @@ -25,6 +29,7 @@ void main() { ); setUp(() { + SharedPreferences.setMockInitialValues({}); auth = MockAuth(); listener = MockListener(); appLinks = MockAppLinks(); @@ -39,6 +44,10 @@ void main() { ctrl = flow; }); + tearDown(() { + provider.dispose(); + }); + group('EmailLinkAuthProvider', () { test('has correct provider id', () { expect(provider.providerId, 'email_link'); @@ -55,33 +64,79 @@ void main() { expect(result.captured, ['test@test.com']); }); - test('calls fba.FirebaseAuth#sendSignInLinkToEmail', () { + test( + 'calls fba.FirebaseAuth#sendSignInLinkToEmail with a session id', + () { + provider.authListener = listener; + provider.sendLink('test@test.com'); + + final result = verify( + auth.sendSignInLinkToEmail( + actionCodeSettings: captureAnyNamed('actionCodeSettings'), + email: captureAnyNamed('email'), + ), + ); + + result.called(1); + final settings = result.captured[0] as fba.ActionCodeSettings; + final url = Uri.parse(settings.url); + expect(url.host, 'example.com'); + expect(url.queryParameters['ui_sid'], hasLength(10)); + expect(url.queryParameters, isNot(contains('ui_auid'))); + expect(settings.handleCodeInApp, true); + expect(settings.androidPackageName, 'com.test.app'); + expect(result.captured[1], 'test@test.com'); + }, + ); + + test('adds the anonymous user id to the link', () { + auth.user = AnonymousUser(); provider.authListener = listener; provider.sendLink('test@test.com'); final result = verify( auth.sendSignInLinkToEmail( actionCodeSettings: captureAnyNamed('actionCodeSettings'), - email: captureAnyNamed('email'), + email: anyNamed('email'), ), ); - result.called(1); - expect(result.captured[0], actionCodeSettings); - expect(result.captured[1], 'test@test.com'); + final settings = result.captured[0] as fba.ActionCodeSettings; + expect(Uri.parse(settings.url).queryParameters['ui_auid'], 'anon-uid'); }); - test('calls onLinkSent', () async { + test('stores the email and session id', () async { provider.authListener = listener; - provider.sendLink('test@test.com'); - await untilCalled( + await untilCalled(listener.onLinkSent(any)); + + final result = verify( auth.sendSignInLinkToEmail( + actionCodeSettings: captureAnyNamed('actionCodeSettings'), email: anyNamed('email'), - actionCodeSettings: anyNamed('actionCodeSettings'), ), ); + final settings = result.captured[0] as fba.ActionCodeSettings; + final sessionId = Uri.parse(settings.url).queryParameters['ui_sid']; + + final prefs = await SharedPreferences.getInstance(); + expect( + prefs.getString('firebase_ui_auth.email_link.email'), + 'test@test.com', + ); + expect( + prefs.getString('firebase_ui_auth.email_link.session_id'), + sessionId, + ); + }); + + test('calls onLinkSent', () async { + provider.authListener = listener; + + provider.sendLink('test@test.com'); + + await untilCalled(listener.onLinkSent(any)); final result = verify(listener.onLinkSent(captureAny)); result.called(1); @@ -110,6 +165,10 @@ void main() { }); group('#awaitLink', () { + setUp(() { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + }); + test( 'waits for a link from app links and calls onBeforeSignIn', () async { @@ -117,7 +176,7 @@ void main() { provider.awaitLink('test@test.com'); // Simulate receiving an app link - MockUriStream.addLink(Uri.parse('https://test.com')); + MockUriStream.addLink(signInLink(sessionId: 'session-1')); await untilCalled(listener.onBeforeSignIn()); @@ -143,13 +202,13 @@ void main() { }); test( - 'calls fba.FirebaseAuth#signInWithEmailLink when got a valid sign in link', + 'calls fba.FirebaseAuth#signInWithEmailLink with the stored email', () async { provider.authListener = listener; provider.awaitLink('test@test.com'); - // Simulate receiving a valid app link - MockUriStream.addLink(Uri.parse('https://test.com')); + final link = signInLink(sessionId: 'session-1'); + MockUriStream.addLink(link); await untilCalled(listener.onBeforeSignIn()); @@ -163,22 +222,24 @@ void main() { result.called(1); expect(result.captured[0], 'test@test.com'); - expect(result.captured[1], 'https://test.com'); + expect(result.captured[1], link.toString()); }, ); - test('calls onSignedIn when sign in succeded', () async { + test('calls onSignedIn and clears the session', () async { provider.authListener = listener; provider.awaitLink('test@test.com'); - // Simulate receiving a valid app link - MockUriStream.addLink(Uri.parse('https://test.com')); + MockUriStream.addLink(signInLink(sessionId: 'session-1')); await untilCalled(listener.onSignedIn(any)); final result = verify(listener.onSignedIn(captureAny)); result.called(1); expect(result.captured[0], isA()); + + final prefs = await SharedPreferences.getInstance(); + expect(prefs.getString('firebase_ui_auth.email_link.email'), isNull); }); test('calls onError if sing in failed', () async { @@ -194,8 +255,7 @@ void main() { provider.awaitLink('test@test.com'); - // Simulate receiving a valid app link - MockUriStream.addLink(Uri.parse('https://test.com')); + MockUriStream.addLink(signInLink(sessionId: 'session-1')); await untilCalled(listener.onError(any)); final result = verify(listener.onError(captureAny)); @@ -203,6 +263,215 @@ void main() { result.called(1); expect(result.captured, [exception]); }); + + test('receives a link after awaitLink was called again', () async { + final closingAppLinks = ClosingAppLinks(); + provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + appLinks: closingAppLinks, + ); + provider.auth = auth; + provider.authListener = listener; + + provider.awaitLink('test@test.com'); + provider.awaitLink('test@test.com'); + + // Let app_links' asynchronous onCancel run, if it was triggered. + await Future.delayed(Duration.zero); + + closingAppLinks.addLink(signInLink(sessionId: 'session-1')); + + await untilCalled(listener.onSignedIn(any)); + + verify( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).called(1); + }); + }); + + group('link opened on a different device', () { + test('calls onEmailRequired when no session is stored', () async { + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + final link = signInLink(sessionId: 'session-1'); + MockUriStream.addLink(link); + + await untilCalled(listener.onEmailRequired(any)); + + final result = verify(listener.onEmailRequired(captureAny)); + result.called(1); + expect(result.captured, [link.toString()]); + verifyNever( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ); + }); + + test('calls onEmailRequired when the session id differs', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + MockUriStream.addLink(signInLink(sessionId: 'session-2')); + + await untilCalled(listener.onEmailRequired(any)); + verify(listener.onEmailRequired(any)).called(1); + }); + + test('calls onError if the link upgrades an anonymous user', () async { + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + MockUriStream.addLink( + signInLink(sessionId: 'session-1', anonymousUserId: 'anon-uid'), + ); + + await untilCalled(listener.onError(any)); + + final result = verify(listener.onError(captureAny)); + expect(result.captured[0].code, 'email-link-wrong-device'); + verifyNever(listener.onEmailRequired(any)); + }); + + test('#signInWithLink signs in with the confirmed email', () async { + provider.authListener = listener; + + provider.signInWithLink('other@test.com', 'https://test.com/link'); + + await untilCalled(listener.onSignedIn(any)); + + final result = verify( + auth.signInWithEmailLink( + email: captureAnyNamed('email'), + emailLink: captureAnyNamed('emailLink'), + ), + ); + expect(result.captured, ['other@test.com', 'https://test.com/link']); + }); + }); + + group('anonymous upgrade', () { + setUp(() { + storeSession( + email: 'test@test.com', + sessionId: 'session-1', + anonymousUserId: 'anon-uid', + ); + }); + + test('links the credential to the anonymous user', () async { + final user = AnonymousUser(); + auth.user = user; + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + MockUriStream.addLink( + signInLink(sessionId: 'session-1', anonymousUserId: 'anon-uid'), + ); + + await untilCalled(listener.onCredentialLinked(any)); + + final result = verify(user.linkWithCredential(captureAny)); + result.called(1); + expect(result.captured[0], isA()); + verifyNever( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ); + }); + + test('calls onError if the anonymous user changed', () async { + auth.user = AnonymousUser(uid: 'other-uid'); + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + MockUriStream.addLink( + signInLink(sessionId: 'session-1', anonymousUserId: 'anon-uid'), + ); + + await untilCalled(listener.onError(any)); + + final result = verify(listener.onError(captureAny)); + expect(result.captured[0].code, 'email-link-different-anonymous-user'); + }); + }); + + group('#isLaunchedFromSignInLink', () { + test('returns true for a sign in launch link', () async { + when( + appLinks.getInitialLink(), + ).thenAnswer((_) async => signInLink(sessionId: 'session-1')); + + expect(await provider.isLaunchedFromSignInLink(auth: auth), true); + }); + + test('returns false without a launch link', () async { + expect(await provider.isLaunchedFromSignInLink(auth: auth), false); + }); + }); + + group('#handleIncomingLinks', () { + test( + 'completes the sign in with the link that launched the app', + () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + final link = signInLink(sessionId: 'session-1'); + when(appLinks.getInitialLink()).thenAnswer((_) async => link); + // The flow created in setUp already checked for a launch link. + provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + appLinks: appLinks, + ); + provider.auth = auth; + provider.authListener = listener; + + provider.handleIncomingLinks(); + provider.handleIncomingLinks(); + + await untilCalled(listener.onSignedIn(any)); + + // The same link delivered by uriLinkStream is ignored. + MockUriStream.addLink(link); + await Future.delayed(Duration.zero); + + // Once by the flow in setUp, once by this provider. + verify(appLinks.getInitialLink()).called(2); + verify( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).called(1); + }, + ); + + test('ignores a launch link that is not a sign in link', () async { + when( + appLinks.getInitialLink(), + ).thenAnswer((_) async => Uri.parse('https://test.com/other')); + when(auth.isSignInWithEmailLink(any)).thenReturn(false); + // The flow created in setUp already checked for a launch link. + provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + appLinks: appLinks, + ); + provider.auth = auth; + provider.authListener = listener; + + provider.handleIncomingLinks(); + await Future.delayed(Duration.zero); + + verifyNever(listener.onError(any)); + verifyNever(listener.onBeforeSignIn()); + }); }); }); @@ -221,6 +490,32 @@ void main() { }); group('EmailLinkFlow', () { + test('handles incoming links when created', () { + final provider = MockProvider(); + EmailLinkFlow(provider: provider, auth: auth); + + verify(provider.handleIncomingLinks()).called(1); + }); + + test('#onEmailRequired emits EmailRequired', () { + flow.onEmailRequired('https://test.com/link'); + + expect(flow.value, isA()); + expect((flow.value as EmailRequired).link, 'https://test.com/link'); + }); + + test('#confirmEmail calls EmailLinkAuthProvider#signInWithLink', () { + final provider = MockProvider(); + final flow = EmailLinkFlow(provider: provider, auth: auth); + + flow.onEmailRequired('https://test.com/link'); + flow.confirmEmail('test@test.com'); + + final result = verify(provider.signInWithLink(captureAny, captureAny)); + result.called(1); + expect(result.captured, ['test@test.com', 'https://test.com/link']); + }); + test('#onLinkSent calls EmailLinkAuthProvider#awaitLink', () { final provider = MockProvider(); flow = EmailLinkFlow(provider: provider, auth: auth); @@ -337,6 +632,11 @@ class MockProvider extends Mock implements EmailLinkAuthProvider { void awaitLink(String? email) { super.noSuchMethod(Invocation.method(#awaitLink, [email])); } + + @override + void signInWithLink(String? email, String? link) { + super.noSuchMethod(Invocation.method(#signInWithLink, [email, link])); + } } class MockListener extends Mock implements EmailLinkAuthListener { @@ -359,4 +659,72 @@ class MockListener extends Mock implements EmailLinkAuthListener { void onError(Object? error) { super.noSuchMethod(Invocation.method(#onError, [error])); } + + @override + void onEmailRequired(String? link) { + super.noSuchMethod(Invocation.method(#onEmailRequired, [link])); + } + + @override + void onCredentialLinked(fba.AuthCredential? credential) { + super.noSuchMethod(Invocation.method(#onCredentialLinked, [credential])); + } +} + +class AnonymousUser extends MockUser { + @override + final String uid; + + AnonymousUser({this.uid = 'anon-uid'}); + + @override + bool get isAnonymous => true; +} + +void storeSession({ + required String email, + required String sessionId, + String? anonymousUserId, +}) { + SharedPreferences.setMockInitialValues({ + 'firebase_ui_auth.email_link.email': email, + 'firebase_ui_auth.email_link.session_id': sessionId, + 'firebase_ui_auth.email_link.anonymous_user_id': ?anonymousUserId, + }); +} + +/// A sign in link in the Firebase Hosting format, with the session +/// parameters in the nested continue URL. +Uri signInLink({required String sessionId, String? anonymousUserId}) { + final continueUrl = Uri.https('example.com', '/', { + 'ui_sid': sessionId, + 'ui_auid': ?anonymousUserId, + }); + final action = Uri.https('test.com', '/__/auth/action', { + 'mode': 'signIn', + 'oobCode': 'code', + 'continueUrl': continueUrl.toString(), + }); + return Uri.https('test.com', '/__/auth/links', {'link': action.toString()}); +} + +/// Mimics app_links 6.x, which closes its shared broadcast controller +/// asynchronously once the last listener cancels. +class ClosingAppLinks extends Mock implements AppLinks { + StreamController? _controller; + + @override + Stream get uriLinkStream { + return (_controller ??= StreamController.broadcast( + onCancel: () async { + await Future.delayed(Duration.zero); + await _controller?.close(); + _controller = null; + }, + )).stream; + } + + void addLink(Uri uri) { + _controller?.add(uri); + } } diff --git a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart index 4bafc77d..ddfe336f 100644 --- a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart +++ b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart @@ -7,6 +7,8 @@ import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../test_utils.dart'; @@ -17,6 +19,7 @@ void main() { late EmailLinkAuthProvider emailLinkProvider; setUp(() { + SharedPreferences.setMockInitialValues({}); auth = MockAuth(); appLinks = MockAppLinks(); final actionCodeSettings = fba.ActionCodeSettings( @@ -70,4 +73,37 @@ void main() { final button = find.text(labels.goBackButtonLabel); expect(button, findsOneWidget); }); + + testWidgets('asks to confirm the email for a link from another device', ( + tester, + ) async { + await tester.pumpWidget( + TestMaterialApp( + child: EmailLinkSignInView(provider: emailLinkProvider, auth: auth), + ), + ); + + emailLinkProvider.authListener.onEmailRequired('https://test.com/link'); + await tester.pump(); + + expect(find.text(labels.emailLinkConfirmEmailText), findsOneWidget); + expect(find.text(labels.sendLinkButtonLabel), findsNothing); + + await tester.enterText(find.byType(TextFormField), 'test@test.com'); + await tester.tap(find.text(labels.continueText)); + await tester.pump(); + + verify( + auth.signInWithEmailLink( + email: 'test@test.com', + emailLink: 'https://test.com/link', + ), + ).called(1); + verifyNever( + auth.sendSignInLinkToEmail( + email: anyNamed('email'), + actionCodeSettings: anyNamed('actionCodeSettings'), + ), + ); + }); } From 8abe47c9859c113fba07383d72ba145e8928e1a5 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:38:28 +0100 Subject: [PATCH 03/10] fix(ui_auth): make email link anonymous upgrade opt-in and ignore unrelated links Adds EmailLinkAuthProvider.upgradeAnonymousUsers (default false, as in FirebaseUI-Android) so anonymous users can still sign in to an existing account. Other deep links are only reported once a link was requested, a link that failed can be opened again, dispose no longer re-reads the launch link, and confirming a link from another device keeps this device's pending session. --- docs/firebase-ui-auth/providers/email-link.md | 2 +- .../providers/email_link_auth_provider.dart | 77 ++++++++--- .../test/flows/email_link_flow_test.dart | 124 +++++++++++++++++- 3 files changed, 181 insertions(+), 22 deletions(-) diff --git a/docs/firebase-ui-auth/providers/email-link.md b/docs/firebase-ui-auth/providers/email-link.md index 7c43452c..89f9da88 100644 --- a/docs/firebase-ui-auth/providers/email-link.md +++ b/docs/firebase-ui-auth/providers/email-link.md @@ -93,7 +93,7 @@ When the link is sent, the email is stored on the device, so the link completes Disable Flutter's built-in deep linking so it does not replace `initialRoute` with the link: add `` to the `` in `AndroidManifest.xml` (not the ``), and set `FlutterDeepLinkingEnabled` to `false` in `Info.plist`. - **Link opened on another device:** the flow emits `EmailRequired` and `EmailLinkSignInView` asks the user to confirm their email before signing in. -- **Anonymous users:** if the current user is anonymous, the link upgrades that user and the flow emits `CredentialLinked` instead of `SignedIn`. The link must then be opened on the same device, otherwise sign in fails with an `email-link-wrong-device` error. +- **Anonymous users:** by default an anonymous user is replaced by the signed in user. Pass `upgradeAnonymousUsers: true` to `EmailLinkAuthProvider` to link the email to the anonymous user instead: the flow then emits `CredentialLinked` instead of `SignedIn`, the link must be opened on the same device (otherwise sign in fails with an `email-link-wrong-device` error), and an email that already has an account cannot be used. ## Using view diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart index 18c2173e..fccc4d9a 100644 --- a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart @@ -37,10 +37,22 @@ class EmailLinkAuthProvider /// A configuration of the dynamic link. final fba.ActionCodeSettings actionCodeSettings; + /// Whether a link requested by an anonymous user should be linked to that + /// user instead of signing in. Defaults to `false`. + /// + /// When enabled, the link must be opened on the device that requested it, + /// and signing in to an email that already has an account fails. + final bool upgradeAnonymousUsers; + final AppLinks _appLinks; StreamSubscription? _linkSubscription; bool _initialLinkChecked = false; - String? _lastHandledLink; + bool _isAwaitingLink = false; + + // Links being handled or already used, so the link that launched the app is + // not handled twice when it arrives from both getInitialLink and + // uriLinkStream. A link is removed if it fails, so it can be opened again. + final Set _handledLinks = {}; @override late EmailLinkAuthListener authListener; @@ -57,6 +69,7 @@ class EmailLinkAuthProvider /// {@macro ui.auth.providers.email_link_auth_provider} EmailLinkAuthProvider({ required this.actionCodeSettings, + this.upgradeAnonymousUsers = false, /// An instance of the [AppLinks] that should be used to handle /// the link. By default [AppLinks()] is used. @@ -65,15 +78,17 @@ class EmailLinkAuthProvider /// Sends a link to the [email]. /// - /// If the current user is anonymous, the link upgrades that user instead of - /// signing in a new one. + /// If [upgradeAnonymousUsers] is enabled and the current user is anonymous, + /// the link upgrades that user instead of signing in a new one. void sendLink(String email) { authListener.onBeforeLinkSent(email); final session = EmailLinkSession( email: email, sessionId: EmailLinkSession.generateSessionId(), - anonymousUserId: shouldUpgradeAnonymous ? auth.currentUser!.uid : null, + anonymousUserId: upgradeAnonymousUsers && shouldUpgradeAnonymous + ? auth.currentUser!.uid + : null, ); auth @@ -105,6 +120,7 @@ class EmailLinkAuthProvider /// /// The [email] is read from the device storage written by [sendLink]. void awaitLink(String email) { + _isAwaitingLink = true; _listen(); } @@ -152,7 +168,8 @@ class EmailLinkAuthProvider if (auth.isSignInWithEmailLink(link)) { _handleLink(link); - } else { + } else if (_isAwaitingLink) { + // Other deep links are only reported once a sign in link was requested. authListener.onError( fba.FirebaseAuthException( code: 'invalid-email-signin-link', @@ -163,10 +180,7 @@ class EmailLinkAuthProvider } Future _handleLink(String link) async { - // The link that launched the app can arrive both from getInitialLink and - // from uriLinkStream. - if (link == _lastHandledLink) return; - _lastHandledLink = link; + if (!_handledLinks.add(link)) return; try { final params = parseEmailLinkParams(link); @@ -189,34 +203,58 @@ class EmailLinkAuthProvider return; } - _completeSignIn(session.email, link, anonymousUserId); + _completeSignIn( + session.email, + link, + anonymousUserId: anonymousUserId, + clearSession: true, + ); } catch (err) { - authListener.onError(err); + _onSignInFailed(link, err); } } + void _onSignInFailed(String link, Object error) { + _handledLinks.remove(link); + authListener.onError(error); + } + /// Completes the sign in with a [link] from /// [EmailLinkAuthListener.onEmailRequired] and the [email] it was sent to. + /// + /// The session stored for this device's own pending link is kept. void signInWithLink(String email, String link) { - _completeSignIn(email, link, null); + _handledLinks.add(link); + _completeSignIn(email, link, clearSession: false); } - void _completeSignIn(String email, String link, String? anonymousUserId) { + void _completeSignIn( + String email, + String link, { + String? anonymousUserId, + required bool clearSession, + }) { + Future onCompleted() async { + _isAwaitingLink = false; + if (clearSession) await EmailLinkSession.clear(); + } + if (anonymousUserId == null) { authListener.onBeforeSignIn(); auth .signInWithEmailLink(email: email, emailLink: link) .then((credential) async { - await EmailLinkSession.clear(); + await onCompleted(); authListener.onSignedIn(credential); }) - .catchError(authListener.onError); + .catchError((Object err) => _onSignInFailed(link, err)); return; } final user = auth.currentUser; if (user == null || !user.isAnonymous || user.uid != anonymousUserId) { - authListener.onError( + _onSignInFailed( + link, fba.FirebaseAuthException( code: 'email-link-different-anonymous-user', message: @@ -236,16 +274,15 @@ class EmailLinkAuthProvider user .linkWithCredential(credential) .then((_) async { - await EmailLinkSession.clear(); + await onCompleted(); authListener.onCredentialLinked(credential); }) - .catchError(authListener.onError); + .catchError((Object err) => _onSignInFailed(link, err)); } void dispose() { _linkSubscription?.cancel(); _linkSubscription = null; - _initialLinkChecked = false; - _lastHandledLink = null; + _isAwaitingLink = false; } } diff --git a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart index 89b92b4f..149fc1c7 100644 --- a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart +++ b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart @@ -89,7 +89,32 @@ void main() { }, ); - test('adds the anonymous user id to the link', () { + test('does not upgrade anonymous users by default', () { + auth.user = AnonymousUser(); + provider.authListener = listener; + provider.sendLink('test@test.com'); + + final result = verify( + auth.sendSignInLinkToEmail( + actionCodeSettings: captureAnyNamed('actionCodeSettings'), + email: anyNamed('email'), + ), + ); + + final settings = result.captured[0] as fba.ActionCodeSettings; + expect( + Uri.parse(settings.url).queryParameters, + isNot(contains('ui_auid')), + ); + }); + + test('adds the anonymous user id to the link when enabled', () { + provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + appLinks: appLinks, + upgradeAnonymousUsers: true, + ); + provider.auth = auth; auth.user = AnonymousUser(); provider.authListener = listener; provider.sendLink('test@test.com'); @@ -264,6 +289,33 @@ void main() { expect(result.captured, [exception]); }); + test('handles a link again after it failed', () async { + provider.authListener = listener; + when( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).thenThrow(TestException()); + + provider.awaitLink('test@test.com'); + final link = signInLink(sessionId: 'session-1'); + + MockUriStream.addLink(link); + await untilCalled(listener.onError(any)); + + MockUriStream.addLink(link); + await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); + + verify( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).called(2); + }); + test('receives a link after awaitLink was called again', () async { final closingAppLinks = ClosingAppLinks(); provider = EmailLinkAuthProvider( @@ -339,6 +391,31 @@ void main() { verifyNever(listener.onEmailRequired(any)); }); + test('ignores other deep links until a link is requested', () async { + when(auth.isSignInWithEmailLink(any)).thenReturn(false); + provider.authListener = listener; + provider.handleIncomingLinks(); + + MockUriStream.addLink(Uri.parse('https://test.com/product/1')); + await Future.delayed(Duration.zero); + + verifyNever(listener.onError(any)); + }); + + test('#signInWithLink keeps the session of this device', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + provider.authListener = listener; + + provider.signInWithLink('other@test.com', 'https://test.com/link'); + await untilCalled(listener.onSignedIn(any)); + + final prefs = await SharedPreferences.getInstance(); + expect( + prefs.getString('firebase_ui_auth.email_link.session_id'), + 'session-1', + ); + }); + test('#signInWithLink signs in with the confirmed email', () async { provider.authListener = listener; @@ -453,6 +530,28 @@ void main() { }, ); + test('does not handle the launch link again after dispose', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + final link = signInLink(sessionId: 'session-1'); + when(appLinks.getInitialLink()).thenAnswer((_) async => link); + provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + appLinks: appLinks, + ); + provider.auth = auth; + provider.authListener = listener; + + provider.handleIncomingLinks(); + await untilCalled(listener.onSignedIn(any)); + + provider.dispose(); + provider.handleIncomingLinks(); + await Future.delayed(Duration.zero); + + // Once by the flow in setUp, once by this provider. + verify(appLinks.getInitialLink()).called(2); + }); + test('ignores a launch link that is not a sign in link', () async { when( appLinks.getInitialLink(), @@ -504,6 +603,29 @@ void main() { expect((flow.value as EmailRequired).link, 'https://test.com/link'); }); + test('confirms the email for a link from another device', () async { + provider.awaitLink('test@test.com'); + final link = signInLink(sessionId: 'session-1'); + + MockUriStream.addLink(link); + await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); + + expect(flow.value, isA()); + + flow.confirmEmail('test@test.com'); + await Future.delayed(Duration.zero); + + final result = verify( + auth.signInWithEmailLink( + email: captureAnyNamed('email'), + emailLink: captureAnyNamed('emailLink'), + ), + ); + result.called(1); + expect(result.captured, ['test@test.com', link.toString()]); + }); + test('#confirmEmail calls EmailLinkAuthProvider#signInWithLink', () { final provider = MockProvider(); final flow = EmailLinkFlow(provider: provider, auth: auth); From cb8850218d22542b788ffc00f95a6c3610d09483 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:06:41 +0100 Subject: [PATCH 04/10] docs(ui_auth): document the auth parameter of isLaunchedFromSignInLink --- .../lib/src/providers/email_link_auth_provider.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart index fccc4d9a..c65be986 100644 --- a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart @@ -128,6 +128,9 @@ class EmailLinkAuthProvider /// /// Use it on startup to show [EmailLinkSignInScreen], which completes the /// sign in with that link. + /// + /// Pass [auth] when using a non-default Firebase app. The provider's own + /// auth instance is only set once a flow is created. Future isLaunchedFromSignInLink({fba.FirebaseAuth? auth}) async { final uri = await _appLinks.getInitialLink(); if (uri == null) return false; From 4f7e808a1b4ab8cc2804dd10abae2452d1ec0739 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Wed, 23 Sep 2026 16:21:34 +0100 Subject: [PATCH 05/10] fix(ui_auth): show a loading indicator while an email link signs in EmailLinkSignInView showed the "link sent" message while signing in and the email form again after success, which was confusing when a link launched the app. It now shows a loading indicator while signing in and keeps the form hidden once sign in succeeded. --- .../src/views/email_link_sign_in_view.dart | 15 +++++++-- .../views/email_link_sign_in_view_test.dart | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart index 69a6c325..cc9d31c2 100644 --- a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart @@ -48,7 +48,15 @@ class _EmailLinkSignInViewState extends State { Widget build(BuildContext context) { final l = FirebaseUILocalizations.labelsOf(context); final formKey = GlobalKey(); - const statesToHideForm = [AwaitingDynamicLink, SigningIn]; + // SignedIn, UserCreated and CredentialLinked stay hidden until the app + // navigates away, so the form does not flash after a successful sign in. + const statesToHideForm = [ + AwaitingDynamicLink, + SigningIn, + SignedIn, + UserCreated, + CredentialLinked, + ]; return AuthFlowBuilder( auth: widget.auth, @@ -93,9 +101,12 @@ class _EmailLinkSignInViewState extends State { }, ), ) - else ...[ + else if (state is AwaitingDynamicLink) ...[ Text(l.signInWithEmailLinkSentText), const SizedBox(height: 16), + ] else ...[ + const Center(child: LoadingIndicator(size: 32, borderWidth: 2)), + const SizedBox(height: 16), ], if (!isFormHidden) ...[ const SizedBox(height: 8), diff --git a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart index ddfe336f..2e0bfe96 100644 --- a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart +++ b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart @@ -5,6 +5,7 @@ import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; +import 'package:firebase_ui_shared/firebase_ui_shared.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; @@ -106,4 +107,34 @@ void main() { ), ); }); + + testWidgets('shows a loading indicator while signing in', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: EmailLinkSignInView(provider: emailLinkProvider, auth: auth), + ), + ); + + emailLinkProvider.authListener.onBeforeSignIn(); + await tester.pump(); + + expect(find.byType(LoadingIndicator), findsOneWidget); + expect(find.text(labels.signInWithEmailLinkSentText), findsNothing); + expect(find.byType(EmailInput), findsNothing); + }); + + testWidgets('keeps the form hidden after signing in', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: EmailLinkSignInView(provider: emailLinkProvider, auth: auth), + ), + ); + + emailLinkProvider.authListener.onBeforeSignIn(); + emailLinkProvider.authListener.onSignedIn(MockCredential()); + await tester.pump(); + + expect(find.byType(EmailInput), findsNothing); + expect(find.byType(LoadingIndicator), findsOneWidget); + }); } From a6a8cf3d6846ecdeff6d3f4918ffc85d9b2127b3 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Thu, 24 Sep 2026 10:31:20 +0100 Subject: [PATCH 06/10] fix(ui_auth): keep recent email link sessions and reset link state on failure Stores the last 5 requested links so an older email still signs in on the requesting device, lets a link be reopened after the confirm email step, stops reporting unrelated deep links after a failure or once the flow is disposed (new EmailLinkAuthProvider.stopAwaitingLink), treats a session cleanup error after sign in as non-fatal, and no longer stores the anonymous user id. --- .../lib/src/flows/email_link_flow.dart | 1 + .../providers/email_link_auth_provider.dart | 36 +-- .../lib/src/providers/email_link_session.dart | 75 ++++-- .../test/flows/email_link_flow_test.dart | 217 +++++++++++++++--- 4 files changed, 263 insertions(+), 66 deletions(-) diff --git a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart index fd115b2b..98303b91 100644 --- a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart +++ b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart @@ -54,6 +54,7 @@ class EmailLinkFlow extends AuthFlow required super.provider, }) : super(action: AuthAction.signIn, initialState: const Uninitialized()) { provider.handleIncomingLinks(); + onDispose = provider.stopAwaitingLink; } String? _pendingLink; diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart index c65be986..884f34d2 100644 --- a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart @@ -188,11 +188,9 @@ class EmailLinkAuthProvider try { final params = parseEmailLinkParams(link); final anonymousUserId = params[anonymousUserIdParam]; - final session = await EmailLinkSession.load(); - final isSameDevice = - session != null && session.sessionId == params[sessionIdParam]; + final session = await EmailLinkSession.find(params[sessionIdParam]); - if (!isSameDevice) { + if (session == null) { if (anonymousUserId != null) { throw fba.FirebaseAuthException( code: 'email-link-wrong-device', @@ -202,6 +200,9 @@ class EmailLinkAuthProvider ); } + // Not kept as handled, so the link can be opened again if the user + // leaves the confirm email step. + _handledLinks.remove(link); authListener.onEmailRequired(link); return; } @@ -210,7 +211,7 @@ class EmailLinkAuthProvider session.email, link, anonymousUserId: anonymousUserId, - clearSession: true, + session: session, ); } catch (err) { _onSignInFailed(link, err); @@ -219,6 +220,7 @@ class EmailLinkAuthProvider void _onSignInFailed(String link, Object error) { _handledLinks.remove(link); + _isAwaitingLink = false; authListener.onError(error); } @@ -228,26 +230,28 @@ class EmailLinkAuthProvider /// The session stored for this device's own pending link is kept. void signInWithLink(String email, String link) { _handledLinks.add(link); - _completeSignIn(email, link, clearSession: false); + _completeSignIn(email, link); } void _completeSignIn( String email, String link, { String? anonymousUserId, - required bool clearSession, + EmailLinkSession? session, }) { - Future onCompleted() async { + void onCompleted() { _isAwaitingLink = false; - if (clearSession) await EmailLinkSession.clear(); + // A session left behind only means its link would ask for the email + // again, so a storage error must not turn a sign in into a failure. + session?.remove().catchError((_) {}); } if (anonymousUserId == null) { authListener.onBeforeSignIn(); auth .signInWithEmailLink(email: email, emailLink: link) - .then((credential) async { - await onCompleted(); + .then((credential) { + onCompleted(); authListener.onSignedIn(credential); }) .catchError((Object err) => _onSignInFailed(link, err)); @@ -276,13 +280,19 @@ class EmailLinkAuthProvider authListener.onBeforeSignIn(); user .linkWithCredential(credential) - .then((_) async { - await onCompleted(); + .then((_) { + onCompleted(); authListener.onCredentialLinked(credential); }) .catchError((Object err) => _onSignInFailed(link, err)); } + /// Stops reporting deep links that are not sign in links as errors, for + /// example when the screen that requested a link is closed. + void stopAwaitingLink() { + _isAwaitingLink = false; + } + void dispose() { _linkSubscription?.cancel(); _linkSubscription = null; diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart index 4d79fec1..0d3ddd3d 100644 --- a/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart @@ -2,13 +2,16 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +import 'dart:convert'; import 'dart:math'; import 'package:shared_preferences/shared_preferences.dart'; -const _emailKey = 'firebase_ui_auth.email_link.email'; -const _sessionIdKey = 'firebase_ui_auth.email_link.session_id'; -const _anonymousUserIdKey = 'firebase_ui_auth.email_link.anonymous_user_id'; +const _sessionsKey = 'firebase_ui_auth.email_link.sessions'; + +/// How many requested links are remembered, so an older email still signs in +/// on this device after the user asked for another link. +const _maxSessions = 5; /// Link parameter names shared with FirebaseUI-Android. const sessionIdParam = 'ui_sid'; @@ -16,6 +19,9 @@ const anonymousUserIdParam = 'ui_auid'; /// A pending email link sign in, persisted so that it survives the app being /// killed while the user opens their email. +/// +/// [anonymousUserId] is only added to the link; the check when the link is +/// opened compares it with the current user. class EmailLinkSession { final String email; final String sessionId; @@ -34,35 +40,56 @@ class EmailLinkSession { return List.generate(10, (_) => chars[random.nextInt(chars.length)]).join(); } - static Future load() async { + static List> _readAll(SharedPreferences prefs) { + final stored = prefs.getStringList(_sessionsKey) ?? const []; + return [ + for (final entry in stored) jsonDecode(entry) as Map, + ]; + } + + static Future _writeAll( + SharedPreferences prefs, + List> sessions, + ) async { + if (sessions.isEmpty) { + await prefs.remove(_sessionsKey); + } else { + await prefs.setStringList(_sessionsKey, [ + for (final session in sessions) jsonEncode(session), + ]); + } + } + + /// Returns the stored session that requested the link with [sessionId]. + static Future find(String? sessionId) async { + if (sessionId == null) return null; + final prefs = await SharedPreferences.getInstance(); - final email = prefs.getString(_emailKey); - final sessionId = prefs.getString(_sessionIdKey); - if (email == null || sessionId == null) return null; - - return EmailLinkSession( - email: email, - sessionId: sessionId, - anonymousUserId: prefs.getString(_anonymousUserIdKey), - ); + for (final session in _readAll(prefs)) { + if (session['sessionId'] == sessionId) { + return EmailLinkSession( + email: session['email'] as String, + sessionId: sessionId, + ); + } + } + return null; } Future save() async { final prefs = await SharedPreferences.getInstance(); - await prefs.setString(_emailKey, email); - await prefs.setString(_sessionIdKey, sessionId); - if (anonymousUserId case final uid?) { - await prefs.setString(_anonymousUserIdKey, uid); - } else { - await prefs.remove(_anonymousUserIdKey); - } + final sessions = _readAll(prefs); + sessions.add({'sessionId': sessionId, 'email': email}); + final start = max(0, sessions.length - _maxSessions); + await _writeAll(prefs, sessions.sublist(start)); } - static Future clear() async { + /// Removes this session once its link was used. + Future remove() async { final prefs = await SharedPreferences.getInstance(); - await prefs.remove(_emailKey); - await prefs.remove(_sessionIdKey); - await prefs.remove(_anonymousUserIdKey); + final sessions = _readAll(prefs); + sessions.removeWhere((session) => session['sessionId'] == sessionId); + await _writeAll(prefs, sessions); } /// Returns [url] with the session parameters added to its query. diff --git a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart index 149fc1c7..c77d5b4e 100644 --- a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart +++ b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:convert'; import 'package:app_links/app_links.dart'; import 'package:firebase_auth/firebase_auth.dart' as fba; @@ -12,6 +13,8 @@ import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:firebase_ui_auth/src/providers/email_link_session.dart'; + import '../test_utils.dart'; void main() { @@ -145,15 +148,9 @@ void main() { final settings = result.captured[0] as fba.ActionCodeSettings; final sessionId = Uri.parse(settings.url).queryParameters['ui_sid']; - final prefs = await SharedPreferences.getInstance(); - expect( - prefs.getString('firebase_ui_auth.email_link.email'), - 'test@test.com', - ); - expect( - prefs.getString('firebase_ui_auth.email_link.session_id'), - sessionId, - ); + expect(await storedSessions(), [ + {'sessionId': sessionId, 'email': 'test@test.com'}, + ]); }); test('calls onLinkSent', () async { @@ -263,8 +260,8 @@ void main() { result.called(1); expect(result.captured[0], isA()); - final prefs = await SharedPreferences.getInstance(); - expect(prefs.getString('firebase_ui_auth.email_link.email'), isNull); + await Future.delayed(Duration.zero); + expect(await storedSessions(), isEmpty); }); test('calls onError if sing in failed', () async { @@ -402,6 +399,53 @@ void main() { verifyNever(listener.onError(any)); }); + test('asks for the email again when the link is reopened', () async { + provider.authListener = listener; + provider.awaitLink('test@test.com'); + final link = signInLink(sessionId: 'session-1'); + + MockUriStream.addLink(link); + await untilCalled(listener.onEmailRequired(any)); + MockUriStream.addLink(link); + await Future.delayed(Duration.zero); + await Future.delayed(Duration.zero); + + verify(listener.onEmailRequired(any)).called(2); + }); + + test('ignores other deep links after a failed sign in', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + when( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).thenThrow(TestException()); + provider.authListener = listener; + provider.awaitLink('test@test.com'); + + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await untilCalled(listener.onError(any)); + + when(auth.isSignInWithEmailLink(any)).thenReturn(false); + MockUriStream.addLink(Uri.parse('https://test.com/product/1')); + await Future.delayed(Duration.zero); + + verify(listener.onError(any)).called(1); + }); + + test('#stopAwaitingLink ignores other deep links', () async { + when(auth.isSignInWithEmailLink(any)).thenReturn(false); + provider.authListener = listener; + provider.awaitLink('test@test.com'); + provider.stopAwaitingLink(); + + MockUriStream.addLink(Uri.parse('https://test.com/product/1')); + await Future.delayed(Duration.zero); + + verifyNever(listener.onError(any)); + }); + test('#signInWithLink keeps the session of this device', () async { storeSession(email: 'test@test.com', sessionId: 'session-1'); provider.authListener = listener; @@ -409,11 +453,10 @@ void main() { provider.signInWithLink('other@test.com', 'https://test.com/link'); await untilCalled(listener.onSignedIn(any)); - final prefs = await SharedPreferences.getInstance(); - expect( - prefs.getString('firebase_ui_auth.email_link.session_id'), - 'session-1', - ); + await Future.delayed(Duration.zero); + expect(await storedSessions(), [ + {'sessionId': 'session-1', 'email': 'test@test.com'}, + ]); }); test('#signInWithLink signs in with the confirmed email', () async { @@ -433,13 +476,53 @@ void main() { }); }); + group('several links', () { + Future> sendLinks(int count) async { + provider.authListener = listener; + for (var i = 0; i < count; i++) { + provider.sendLink('test@test.com'); + await pumpEventQueue(); + } + + final result = verify( + auth.sendSignInLinkToEmail( + actionCodeSettings: captureAnyNamed('actionCodeSettings'), + email: anyNamed('email'), + ), + ); + return [ + for (final settings in result.captured.cast()) + Uri.parse(settings.url).queryParameters['ui_sid']!, + ]; + } + + test('an older link still signs in on this device', () async { + final sessionIds = await sendLinks(2); + + provider.awaitLink('test@test.com'); + MockUriStream.addLink(signInLink(sessionId: sessionIds.first)); + await untilCalled(listener.onSignedIn(any)); + + verifyNever(listener.onEmailRequired(any)); + verify( + auth.signInWithEmailLink( + email: 'test@test.com', + emailLink: anyNamed('emailLink'), + ), + ).called(1); + }); + + test('keeps the 5 most recent links', () async { + final sessionIds = await sendLinks(6); + + final stored = await storedSessions(); + expect(stored.map((s) => s['sessionId']), sessionIds.sublist(1)); + }); + }); + group('anonymous upgrade', () { setUp(() { - storeSession( - email: 'test@test.com', - sessionId: 'session-1', - anonymousUserId: 'anon-uid', - ); + storeSession(email: 'test@test.com', sessionId: 'session-1'); }); test('links the credential to the anonymous user', () async { @@ -465,6 +548,47 @@ void main() { ); }); + test('an older link still upgrades the anonymous user', () async { + // The provider from setUp also listens to the shared mock stream. + provider.dispose(); + provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + appLinks: appLinks, + upgradeAnonymousUsers: true, + ); + final user = AnonymousUser(); + auth.user = user; + provider.auth = auth; + provider.authListener = listener; + + provider.sendLink('test@test.com'); + provider.sendLink('test@test.com'); + await untilCalled(listener.onLinkSent(any)); + await Future.delayed(Duration.zero); + + final result = verify( + auth.sendSignInLinkToEmail( + actionCodeSettings: captureAnyNamed('actionCodeSettings'), + email: anyNamed('email'), + ), + ); + final firstUrl = Uri.parse( + (result.captured.first as fba.ActionCodeSettings).url, + ); + + provider.awaitLink('test@test.com'); + MockUriStream.addLink( + signInLink( + sessionId: firstUrl.queryParameters['ui_sid']!, + anonymousUserId: firstUrl.queryParameters['ui_auid'], + ), + ); + await untilCalled(listener.onCredentialLinked(any)); + + verifyNever(listener.onError(any)); + verify(user.linkWithCredential(any)).called(1); + }); + test('calls onError if the anonymous user changed', () async { auth.user = AnonymousUser(uid: 'other-uid'); provider.authListener = listener; @@ -574,6 +698,27 @@ void main() { }); }); + group('parseEmailLinkParams', () { + test('reads the session from an action link', () { + final continueUrl = Uri.https('example.com', '/', {'ui_sid': 'sid'}); + final action = Uri.https('test.com', '/__/auth/action', { + 'mode': 'signIn', + 'continueUrl': continueUrl.toString(), + }); + + expect(parseEmailLinkParams(action.toString())['ui_sid'], 'sid'); + }); + + test('reads the session from a Hosting link', () { + final link = signInLink(sessionId: 'sid', anonymousUserId: 'uid'); + final params = parseEmailLinkParams(link.toString()); + + expect(params['ui_sid'], 'sid'); + expect(params['ui_auid'], 'uid'); + expect(params['mode'], 'signIn'); + }); + }); + group('EmailLinkFlowController', () { test('#sendLink calls EmailLinkAuthProvider#sendLink', () { final provider = MockProvider(); @@ -596,6 +741,15 @@ void main() { verify(provider.handleIncomingLinks()).called(1); }); + test('stops awaiting a link when disposed', () { + final provider = MockProvider(); + final flow = EmailLinkFlow(provider: provider, auth: auth); + + flow.onDispose(); + + verify(provider.stopAwaitingLink()).called(1); + }); + test('#onEmailRequired emits EmailRequired', () { flow.onEmailRequired('https://test.com/link'); @@ -803,18 +957,23 @@ class AnonymousUser extends MockUser { bool get isAnonymous => true; } -void storeSession({ - required String email, - required String sessionId, - String? anonymousUserId, -}) { +void storeSession({required String email, required String sessionId}) { SharedPreferences.setMockInitialValues({ - 'firebase_ui_auth.email_link.email': email, - 'firebase_ui_auth.email_link.session_id': sessionId, - 'firebase_ui_auth.email_link.anonymous_user_id': ?anonymousUserId, + 'firebase_ui_auth.email_link.sessions': [ + jsonEncode({'sessionId': sessionId, 'email': email}), + ], }); } +Future>> storedSessions() async { + final prefs = await SharedPreferences.getInstance(); + final stored = prefs.getStringList('firebase_ui_auth.email_link.sessions'); + return [ + for (final entry in stored ?? const []) + jsonDecode(entry) as Map, + ]; +} + /// A sign in link in the Firebase Hosting format, with the session /// parameters in the nested continue URL. Uri signInLink({required String sessionId, String? anonymousUserId}) { From 543663066388d61ca0a723a69c6a2ecd4e8d11b7 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:14:10 +0100 Subject: [PATCH 07/10] fix(ui_auth): keep an email link until an email link screen is showing A sign in link that arrived while no email link screen was showing was handled by the last, already closed flow: the user was signed in without the UI updating, or the confirm email prompt went nowhere, even when the app opened EmailLinkSignInScreen for the link. The provider now keeps such a link and handles it when the next email link screen opens. The docs explain how to open that screen when a link arrives. --- docs/firebase-ui-auth/providers/email-link.md | 1 + .../lib/src/flows/email_link_flow.dart | 12 +++- .../providers/email_link_auth_provider.dart | 32 +++++++++- .../src/views/email_link_sign_in_view.dart | 3 + .../test/flows/email_link_flow_test.dart | 58 +++++++++++++++++-- .../views/email_link_sign_in_view_test.dart | 22 +++++++ 6 files changed, 119 insertions(+), 9 deletions(-) diff --git a/docs/firebase-ui-auth/providers/email-link.md b/docs/firebase-ui-auth/providers/email-link.md index 89f9da88..8dbe8103 100644 --- a/docs/firebase-ui-auth/providers/email-link.md +++ b/docs/firebase-ui-auth/providers/email-link.md @@ -92,6 +92,7 @@ When the link is sent, the email is stored on the device, so the link completes Disable Flutter's built-in deep linking so it does not replace `initialRoute` with the link: add `` to the `` in `AndroidManifest.xml` (not the ``), and set `FlutterDeepLinkingEnabled` to `false` in `Info.plist`. +- **Link opened while the app is running:** the link is handled by the email link screen. If that screen is not showing, the link is kept until it opens, so open `EmailLinkSignInScreen` yourself when a link arrives, for example from an `app_links` listener when `FirebaseAuth.instance.isSignInWithEmailLink(link)` is true and the screen is not already open. - **Link opened on another device:** the flow emits `EmailRequired` and `EmailLinkSignInView` asks the user to confirm their email before signing in. - **Anonymous users:** by default an anonymous user is replaced by the signed in user. Pass `upgradeAnonymousUsers: true` to `EmailLinkAuthProvider` to link the email to the anonymous user instead: the flow then emits `CredentialLinked` instead of `SignedIn`, the link must be opened on the same device (otherwise sign in fails with an `email-link-wrong-device` error), and an email that already has an account cannot be used. diff --git a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart index 98303b91..4ef737d5 100644 --- a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart +++ b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart @@ -53,8 +53,16 @@ class EmailLinkFlow extends AuthFlow /// {@macro ui.auth.auth_flow.ctor.provider} required super.provider, }) : super(action: AuthAction.signIn, initialState: const Uninitialized()) { - provider.handleIncomingLinks(); - onDispose = provider.stopAwaitingLink; + // The provider getter makes this flow the provider's listener, so keep a + // reference that can be read without doing that. + final linkProvider = provider..handleIncomingLinks(); + onDispose = () { + // A newer flow may already have replaced this one, for example after + // Navigator.pushReplacement. + if (identical(linkProvider.authListener, this)) { + linkProvider.stopAwaitingLink(); + } + }; } String? _pendingLink; diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart index 884f34d2..91e5e52a 100644 --- a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart @@ -49,6 +49,12 @@ class EmailLinkAuthProvider bool _initialLinkChecked = false; bool _isAwaitingLink = false; + // Whether a flow (an email link screen) is showing. A sign in link that + // arrives while none is kept in _pendingLink until the next one opens, so + // its result is not reported to a screen that is gone. + bool _hasActiveFlow = false; + String? _pendingLink; + // Links being handled or already used, so the link that launched the app is // not handled twice when it arrives from both getInitialLink and // uriLinkStream. A link is removed if it fails, so it can be opened again. @@ -121,6 +127,7 @@ class EmailLinkAuthProvider /// The [email] is read from the device storage written by [sendLink]. void awaitLink(String email) { _isAwaitingLink = true; + _hasActiveFlow = true; _listen(); } @@ -141,9 +148,18 @@ class EmailLinkAuthProvider /// Handles the sign in link that launched the app, if any, and starts /// listening for links opened while the app is running. + /// + /// A link that arrived while no flow was showing is handled now. void handleIncomingLinks() { + _hasActiveFlow = true; _listen(); + final pendingLink = _pendingLink; + if (pendingLink != null) { + _pendingLink = null; + _handleLink(pendingLink); + } + if (_initialLinkChecked) return; _initialLinkChecked = true; @@ -170,7 +186,11 @@ class EmailLinkAuthProvider final link = uri.toString(); if (auth.isSignInWithEmailLink(link)) { - _handleLink(link); + if (_hasActiveFlow) { + _handleLink(link); + } else { + _pendingLink = link; + } } else if (_isAwaitingLink) { // Other deep links are only reported once a sign in link was requested. authListener.onError( @@ -287,15 +307,21 @@ class EmailLinkAuthProvider .catchError((Object err) => _onSignInFailed(link, err)); } - /// Stops reporting deep links that are not sign in links as errors, for - /// example when the screen that requested a link is closed. + /// Called when the flow that handles links is disposed, for example when + /// its screen is closed. + /// + /// Deep links that are not sign in links are no longer reported as errors, + /// and a sign in link is kept until [handleIncomingLinks] is called again. void stopAwaitingLink() { _isAwaitingLink = false; + _hasActiveFlow = false; } void dispose() { _linkSubscription?.cancel(); _linkSubscription = null; _isAwaitingLink = false; + _hasActiveFlow = false; + _pendingLink = null; } } diff --git a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart index cc9d31c2..8e7fe5df 100644 --- a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart @@ -68,6 +68,9 @@ class _EmailLinkSignInViewState extends State { }, builder: (context, state, ctrl, child) { final isFormHidden = statesToHideForm.contains(state.runtimeType); + // Also covers a flow that was already in EmailRequired when this view + // was built, which the listener above does not see. + if (state is EmailRequired) isConfirmingEmail = true; void submit() { if (isConfirmingEmail) { diff --git a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart index c77d5b4e..cf076982 100644 --- a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart +++ b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart @@ -741,13 +741,63 @@ void main() { verify(provider.handleIncomingLinks()).called(1); }); - test('stops awaiting a link when disposed', () { - final provider = MockProvider(); - final flow = EmailLinkFlow(provider: provider, auth: auth); + test('stops awaiting a link when disposed', () async { + when(auth.isSignInWithEmailLink(any)).thenReturn(false); + provider.awaitLink('test@test.com'); + final errors = []; + flow.addListener(() { + if (flow.value case AuthFailed(:final exception)) errors.add(exception); + }); + + flow.onDispose(); + MockUriStream.addLink(Uri.parse('https://test.com/product/1')); + await Future.delayed(Duration.zero); + + expect(errors, isEmpty); + }); + + test('a replaced flow does not stop the newer one', () async { + final newer = EmailLinkFlow(provider: provider, auth: auth); + storeSession(email: 'test@test.com', sessionId: 'session-1'); + + flow.onDispose(); + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + expect(newer.value, isA()); + }); + test('keeps a link that arrives while no flow is showing', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); flow.onDispose(); - verify(provider.stopAwaitingLink()).called(1); + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + verifyNever( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ); + + final next = EmailLinkFlow(provider: provider, auth: auth); + await pumpEventQueue(); + + expect(next.value, isA()); + }); + + test('asks the next flow to confirm a kept link', () async { + flow.onDispose(); + + MockUriStream.addLink(signInLink(sessionId: 'other-device')); + await pumpEventQueue(); + expect(flow.value, isA()); + + final next = EmailLinkFlow(provider: provider, auth: auth); + await pumpEventQueue(); + + expect(next.value, isA()); }); test('#onEmailRequired emits EmailRequired', () { diff --git a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart index 2e0bfe96..db099761 100644 --- a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart +++ b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart @@ -137,4 +137,26 @@ void main() { expect(find.byType(EmailInput), findsNothing); expect(find.byType(LoadingIndicator), findsOneWidget); }); + + testWidgets('confirms the email for a link kept while it was closed', ( + tester, + ) async { + Widget view() => TestMaterialApp( + child: EmailLinkSignInView(provider: emailLinkProvider, auth: auth), + ); + + // The screen is shown once, then closed. + await tester.pumpWidget(view()); + await tester.pumpWidget(const SizedBox()); + + // A link from another device arrives while no email link screen shows. + MockUriStream.addLink(Uri.parse('https://test.com/link')); + await tester.pump(); + + await tester.pumpWidget(view()); + await tester.pump(); + + expect(find.text(labels.emailLinkConfirmEmailText), findsOneWidget); + expect(find.text(labels.continueText), findsOneWidget); + }); } From 5b31007bbe758bf9abac663a2bec1f82560ee4fc Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:33:47 +0100 Subject: [PATCH 08/10] fix(ui_localizations): translate the email link labels Adds translations for emailLinkConfirmEmailText, emailLinkWrongDeviceErrorText and emailLinkDifferentAnonymousUserErrorText in every locale, which previously fell back to English. --- .../firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_de.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_es.arb | 6 +++--- .../lib/l10n/firebase_ui_es_419.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_he.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_id.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_it.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_th.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb | 6 +++--- .../firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb | 6 +++--- .../lib/l10n/firebase_ui_zh_TW.arb | 6 +++--- packages/firebase_ui_localizations/lib/src/lang/ar.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/de.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/es.dart | 7 ++++--- .../firebase_ui_localizations/lib/src/lang/es_419.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/fi.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/fr.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/he.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/hi.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/hu.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/id.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/it.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/ja.dart | 6 +++--- packages/firebase_ui_localizations/lib/src/lang/ko.dart | 6 +++--- packages/firebase_ui_localizations/lib/src/lang/nb.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/nl.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/pl.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/pt.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/ro.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/ru.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/th.dart | 6 +++--- packages/firebase_ui_localizations/lib/src/lang/tr.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/uk.dart | 7 ++++--- packages/firebase_ui_localizations/lib/src/lang/zh.dart | 8 +++----- .../firebase_ui_localizations/lib/src/lang/zh_tw.dart | 7 +++---- 48 files changed, 163 insertions(+), 147 deletions(-) diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb index 4c0e0aa3..88c9da53 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ar.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "أكِّد عنوان بريدك الإلكتروني لمتابعة تسجيل الدخول", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "يُرجى فتح الرابط على الجهاز أو المتصفح نفسه الذي بدأت منه تسجيل الدخول.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "انتهت صلاحية الجلسة المرتبطة بطلب تسجيل الدخول هذا أو تم محوها.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb index 1f127407..f8843580 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_de.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Bestätigen Sie Ihre E-Mail-Adresse, um die Anmeldung fortzusetzen", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Öffnen Sie den Link auf demselben Gerät oder in demselben Browser, mit dem Sie die Anmeldung gestartet haben.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Die Sitzung zu dieser Anmeldeanfrage ist abgelaufen oder wurde gelöscht.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb index a8190658..d6b9526e 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Confirma tu correo electrónico para continuar con el inicio de sesión", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Intenta abrir el enlace en el mismo dispositivo o navegador donde empezaste a iniciar sesión.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "La sesión asociada a esta solicitud de inicio de sesión ha caducado o se ha borrado.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb index c3c81c7d..720dae86 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_es_419.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Confirma tu correo electrónico para continuar con el acceso", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Intenta abrir el vínculo en el mismo dispositivo o navegador en el que comenzaste a acceder.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "La sesión asociada a esta solicitud de acceso venció o se borró.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb index bad55133..0c9aca2e 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fi.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Vahvista sähköpostiosoitteesi jatkaaksesi kirjautumista", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Avaa linkki samalla laitteella tai selaimella, jolla aloitit kirjautumisen.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Tähän kirjautumispyyntöön liittyvä istunto on vanhentunut tai poistettu.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb index 518a10ed..34b7dd2c 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_fr.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Confirmez votre adresse e-mail pour poursuivre la connexion", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Essayez d'ouvrir le lien sur l'appareil ou dans le navigateur à partir duquel vous avez commencé la connexion.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "La session associée à cette demande de connexion a expiré ou a été effacée.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb index f135c176..d00f3b3f 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_he.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "אשר את כתובת הדוא\"ל כדי להמשיך בכניסה", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "נסה לפתוח את הקישור באותו מכשיר או דפדפן שבו התחלת את תהליך הכניסה.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "תוקף ההפעלה של בקשת הכניסה הזו פג, או שהיא נמחקה.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb index 3ea76e3f..75651dcf 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hi.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "साइन इन जारी रखने के लिए अपने ईमेल की पुष्टि करें", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "लिंक को उसी डिवाइस या ब्राउज़र में खोलकर देखें, जिसमें आपने साइन इन शुरू किया था.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "साइन इन के इस अनुरोध से जुड़ा सेशन खत्म हो गया है या मिटा दिया गया है.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb index 45744950..e3c65cec 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_hu.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Erősítse meg az e-mail-címét a belépés folytatásához", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Próbálja meg ugyanazon az eszközön vagy böngészőben megnyitni a linket, amelyen a belépést elkezdte.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Az ehhez a belépési kérelemhez tartozó munkamenet lejárt vagy törlődött.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb index 55f84604..148d4907 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_id.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Konfirmasi email Anda untuk melanjutkan login", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Coba buka link di perangkat atau browser yang sama dengan yang Anda gunakan untuk memulai login.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Sesi yang terkait dengan permintaan login ini telah berakhir atau dihapus.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb index 447f4884..c1c1ec57 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_it.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Conferma la tua email per continuare l'accesso", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Prova ad aprire il link sullo stesso dispositivo o browser da cui hai iniziato l'accesso.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "La sessione associata a questa richiesta di accesso è scaduta o è stata cancellata.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb index 532ca0be..0a97cb17 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ja.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "サインインを続行するには、メールアドレスを確認してください", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "サインインを開始したのと同じデバイスまたはブラウザでリンクを開いてください。", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "このサインイン リクエストに関連付けられたセッションは、期限切れになったか削除されました。", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb index bc93d656..8c8ba298 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ko.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "로그인을 계속하려면 이메일을 확인하세요", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "로그인을 시작한 기기 또는 브라우저에서 링크를 열어 보세요.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "이 로그인 요청과 연결된 세션이 만료되었거나 삭제되었습니다.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb index 9e066f48..1d8c60f0 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nb.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Bekreft e-postadressen din for å fortsette innloggingen", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Prøv å åpne lenken på samme enhet eller i samme nettleser som du startet innloggingen på.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Økten som hører til denne innloggingsforespørselen, har utløpt eller blitt slettet.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb index 98a4667c..ce1b7cdd 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_nl.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Bevestig uw e-mailadres om verder te gaan met inloggen", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Open de link op hetzelfde apparaat of in dezelfde browser waarmee u bent begonnen met inloggen.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "De sessie voor dit inlogverzoek is verlopen of gewist.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb index 7c87714e..ac135674 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pl.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Potwierdź adres e-mail, aby kontynuować logowanie", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Spróbuj otworzyć link na tym samym urządzeniu lub w tej samej przeglądarce, na których rozpoczęto logowanie.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Sesja powiązana z tym żądaniem logowania wygasła lub została wyczyszczona.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb index 7c8bbd0d..470d9ace 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_pt.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Confirme seu email para continuar o login", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Tente abrir o link no mesmo dispositivo ou navegador em que você iniciou o login.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "A sessão associada a este pedido de login expirou ou foi apagada.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb index 04c95984..1f4ca99a 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ro.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Confirmă emailul pentru a continua conectarea", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Încearcă să deschizi linkul pe același dispozitiv sau în același browser în care ai început conectarea.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Sesiunea asociată acestei solicitări de conectare a expirat sau a fost ștearsă.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb index 81ca40be..3d49596e 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_ru.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Подтвердите адрес электронной почты, чтобы продолжить вход", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Откройте ссылку на том же устройстве или в том же браузере, где вы начали вход.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Сеанс, связанный с этим запросом на вход, истек или был удален.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb index 83c6f775..1d8b9918 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_th.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "ยืนยันอีเมลเพื่อลงชื่อเข้าใช้ต่อ", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "โปรดลองเปิดลิงก์ในอุปกรณ์หรือเบราว์เซอร์เดียวกันกับที่คุณเริ่มลงชื่อเข้าใช้", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "เซสชันที่เชื่อมโยงกับคำขอลงชื่อเข้าใช้นี้หมดอายุหรือถูกล้างแล้ว", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb index 93da272d..7ca0ebb5 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_tr.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Oturum açmaya devam etmek için e-posta adresinizi onaylayın", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Bağlantıyı, oturum açma işlemini başlattığınız cihazda veya tarayıcıda açmayı deneyin.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Bu oturum açma isteğiyle ilişkili oturumun süresi doldu veya oturum temizlendi.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb index 7ec45b2b..667b71ef 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_uk.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "Підтвердьте електронну адресу, щоб продовжити вхід", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "Відкрийте посилання на тому самому пристрої або в тому самому браузері, де ви почали вхід.", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "Сеанс, пов’язаний із цим запитом на вхід, закінчився або його було видалено.", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb index 69b4e028..4773f469 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "请确认您的电子邮件地址以继续登录", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "请尝试在您开始登录时所用的同一设备或浏览器中打开该链接。", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "与此登录请求关联的会话已过期或已被清除。", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb index ca6a83b6..45f02e90 100644 --- a/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb +++ b/packages/firebase_ui_localizations/lib/l10n/firebase_ui_zh_TW.arb @@ -534,17 +534,17 @@ "description": "Used as a button label to send a verification email", "placeholders": {} }, - "emailLinkConfirmEmailText": "Confirm email to continue sign in", + "emailLinkConfirmEmailText": "請確認你的電子郵件地址以繼續登入", "@emailLinkConfirmEmailText": { "description": "Shown on EmailLinkSignInView when a sign-in link was opened on a different device and the user must confirm their email.", "placeholders": {} }, - "emailLinkWrongDeviceErrorText": "Try opening the link using the same device or browser where you started the sign-in process.", + "emailLinkWrongDeviceErrorText": "請試著在你開始登入時使用的同一部裝置或瀏覽器中開啟該連結。", "@emailLinkWrongDeviceErrorText": { "description": "Used as an error message when an email sign-in link that must be completed on the device that requested it is opened elsewhere.", "placeholders": {} }, - "emailLinkDifferentAnonymousUserErrorText": "The session associated with this sign-in request has either expired or was cleared.", + "emailLinkDifferentAnonymousUserErrorText": "與這項登入要求相關聯的工作階段已過期或已遭清除。", "@emailLinkDifferentAnonymousUserErrorText": { "description": "Used as an error message when an email sign-in link was requested by an anonymous user who is no longer signed in.", "placeholders": {} diff --git a/packages/firebase_ui_localizations/lib/src/lang/ar.dart b/packages/firebase_ui_localizations/lib/src/lang/ar.dart index a18bb790..3a1b31bb 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ar.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ar.dart @@ -344,13 +344,14 @@ class ArLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "أكِّد عنوان بريدك الإلكتروني لمتابعة تسجيل الدخول"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "يُرجى فتح الرابط على الجهاز أو المتصفح نفسه الذي بدأت منه تسجيل الدخول."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "انتهت صلاحية الجلسة المرتبطة بطلب تسجيل الدخول هذا أو تم محوها."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/de.dart b/packages/firebase_ui_localizations/lib/src/lang/de.dart index 471a69f2..5623bf20 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/de.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/de.dart @@ -341,13 +341,14 @@ class DeLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Verifizierungs-E-Mail senden"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Bestätigen Sie Ihre E-Mail-Adresse, um die Anmeldung fortzusetzen"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Öffnen Sie den Link auf demselben Gerät oder in demselben Browser, mit dem Sie die Anmeldung gestartet haben."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Die Sitzung zu dieser Anmeldeanfrage ist abgelaufen oder wurde gelöscht."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/es.dart b/packages/firebase_ui_localizations/lib/src/lang/es.dart index 87a867ee..b1b56a23 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/es.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/es.dart @@ -347,13 +347,14 @@ class EsLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Confirma tu correo electrónico para continuar con el inicio de sesión"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Intenta abrir el enlace en el mismo dispositivo o navegador donde empezaste a iniciar sesión."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "La sesión asociada a esta solicitud de inicio de sesión ha caducado o se ha borrado."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/es_419.dart b/packages/firebase_ui_localizations/lib/src/lang/es_419.dart index fbe7dee0..505164ce 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/es_419.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/es_419.dart @@ -344,13 +344,14 @@ class Es419Localizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Confirma tu correo electrónico para continuar con el acceso"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Intenta abrir el vínculo en el mismo dispositivo o navegador en el que comenzaste a acceder."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "La sesión asociada a esta solicitud de acceso venció o se borró."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/fi.dart b/packages/firebase_ui_localizations/lib/src/lang/fi.dart index e117c0e4..473e9d62 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/fi.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/fi.dart @@ -341,13 +341,14 @@ class FiLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Lähetä vahvistussähköposti"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Vahvista sähköpostiosoitteesi jatkaaksesi kirjautumista"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Avaa linkki samalla laitteella tai selaimella, jolla aloitit kirjautumisen."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Tähän kirjautumispyyntöön liittyvä istunto on vanhentunut tai poistettu."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/fr.dart b/packages/firebase_ui_localizations/lib/src/lang/fr.dart index 99fc5491..14390249 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/fr.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/fr.dart @@ -350,13 +350,14 @@ class FrLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Envoyer un e-mail de vérification"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Confirmez votre adresse e-mail pour poursuivre la connexion"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Essayez d'ouvrir le lien sur l'appareil ou dans le navigateur à partir duquel vous avez commencé la connexion."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "La session associée à cette demande de connexion a expiré ou a été effacée."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/he.dart b/packages/firebase_ui_localizations/lib/src/lang/he.dart index c0f38950..1d81927f 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/he.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/he.dart @@ -338,13 +338,14 @@ class HeLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + """אשר את כתובת הדוא"ל כדי להמשיך בכניסה"""; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "נסה לפתוח את הקישור באותו מכשיר או דפדפן שבו התחלת את תהליך הכניסה."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "תוקף ההפעלה של בקשת הכניסה הזו פג, או שהיא נמחקה."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/hi.dart b/packages/firebase_ui_localizations/lib/src/lang/hi.dart index aca223de..27be018a 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/hi.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/hi.dart @@ -341,13 +341,14 @@ class HiLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "साइन इन जारी रखने के लिए अपने ईमेल की पुष्टि करें"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "लिंक को उसी डिवाइस या ब्राउज़र में खोलकर देखें, जिसमें आपने साइन इन शुरू किया था."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "साइन इन के इस अनुरोध से जुड़ा सेशन खत्म हो गया है या मिटा दिया गया है."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/hu.dart b/packages/firebase_ui_localizations/lib/src/lang/hu.dart index 8ab1b361..b2f6705a 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/hu.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/hu.dart @@ -339,13 +339,14 @@ class HuLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Erősítse meg az e-mail-címét a belépés folytatásához"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Próbálja meg ugyanazon az eszközön vagy böngészőben megnyitni a linket, amelyen a belépést elkezdte."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Az ehhez a belépési kérelemhez tartozó munkamenet lejárt vagy törlődött."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/id.dart b/packages/firebase_ui_localizations/lib/src/lang/id.dart index daba890c..a479c08c 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/id.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/id.dart @@ -339,13 +339,14 @@ class IdLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Konfirmasi email Anda untuk melanjutkan login"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Coba buka link di perangkat atau browser yang sama dengan yang Anda gunakan untuk memulai login."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Sesi yang terkait dengan permintaan login ini telah berakhir atau dihapus."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/it.dart b/packages/firebase_ui_localizations/lib/src/lang/it.dart index 4e555e44..220252fe 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/it.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/it.dart @@ -342,13 +342,14 @@ class ItLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Conferma la tua email per continuare l'accesso"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Prova ad aprire il link sullo stesso dispositivo o browser da cui hai iniziato l'accesso."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "La sessione associata a questa richiesta di accesso è scaduta o è stata cancellata."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ja.dart b/packages/firebase_ui_localizations/lib/src/lang/ja.dart index 494521c0..acd4c611 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ja.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ja.dart @@ -328,13 +328,13 @@ class JaLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => "サインインを続行するには、メールアドレスを確認してください"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "サインインを開始したのと同じデバイスまたはブラウザでリンクを開いてください。"; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "このサインイン リクエストに関連付けられたセッションは、期限切れになったか削除されました。"; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ko.dart b/packages/firebase_ui_localizations/lib/src/lang/ko.dart index 603cd2c2..e1a919b8 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ko.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ko.dart @@ -333,13 +333,13 @@ class KoLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => "로그인을 계속하려면 이메일을 확인하세요"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "로그인을 시작한 기기 또는 브라우저에서 링크를 열어 보세요."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "이 로그인 요청과 연결된 세션이 만료되었거나 삭제되었습니다."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/nb.dart b/packages/firebase_ui_localizations/lib/src/lang/nb.dart index df0eca9a..14e4015a 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/nb.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/nb.dart @@ -338,13 +338,14 @@ class NbLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send e-postbekreftelse"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Bekreft e-postadressen din for å fortsette innloggingen"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Prøv å åpne lenken på samme enhet eller i samme nettleser som du startet innloggingen på."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Økten som hører til denne innloggingsforespørselen, har utløpt eller blitt slettet."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/nl.dart b/packages/firebase_ui_localizations/lib/src/lang/nl.dart index cb7142bb..5303694a 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/nl.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/nl.dart @@ -341,13 +341,14 @@ class NlLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Bevestig uw e-mailadres om verder te gaan met inloggen"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Open de link op hetzelfde apparaat of in dezelfde browser waarmee u bent begonnen met inloggen."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "De sessie voor dit inlogverzoek is verlopen of gewist."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/pl.dart b/packages/firebase_ui_localizations/lib/src/lang/pl.dart index fd10800a..0185b84d 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/pl.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/pl.dart @@ -341,13 +341,14 @@ class PlLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Potwierdź adres e-mail, aby kontynuować logowanie"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Spróbuj otworzyć link na tym samym urządzeniu lub w tej samej przeglądarce, na których rozpoczęto logowanie."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Sesja powiązana z tym żądaniem logowania wygasła lub została wyczyszczona."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/pt.dart b/packages/firebase_ui_localizations/lib/src/lang/pt.dart index b872f24e..3cec83ff 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/pt.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/pt.dart @@ -342,13 +342,14 @@ class PtLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Enviar email de verificação"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Confirme seu email para continuar o login"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Tente abrir o link no mesmo dispositivo ou navegador em que você iniciou o login."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "A sessão associada a este pedido de login expirou ou foi apagada."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ro.dart b/packages/firebase_ui_localizations/lib/src/lang/ro.dart index 9fd54c61..20230e5f 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ro.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ro.dart @@ -343,13 +343,14 @@ class RoLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Confirmă emailul pentru a continua conectarea"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Încearcă să deschizi linkul pe același dispozitiv sau în același browser în care ai început conectarea."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Sesiunea asociată acestei solicitări de conectare a expirat sau a fost ștearsă."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ru.dart b/packages/firebase_ui_localizations/lib/src/lang/ru.dart index 2ee96853..ef7d2ebc 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/ru.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/ru.dart @@ -341,13 +341,14 @@ class RuLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Подтвердите адрес электронной почты, чтобы продолжить вход"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Откройте ссылку на том же устройстве или в том же браузере, где вы начали вход."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Сеанс, связанный с этим запросом на вход, истек или был удален."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/th.dart b/packages/firebase_ui_localizations/lib/src/lang/th.dart index 5e10a6a0..3e3b957b 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/th.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/th.dart @@ -338,13 +338,13 @@ class ThLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => "ยืนยันอีเมลเพื่อลงชื่อเข้าใช้ต่อ"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "โปรดลองเปิดลิงก์ในอุปกรณ์หรือเบราว์เซอร์เดียวกันกับที่คุณเริ่มลงชื่อเข้าใช้"; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "เซสชันที่เชื่อมโยงกับคำขอลงชื่อเข้าใช้นี้หมดอายุหรือถูกล้างแล้ว"; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/tr.dart b/packages/firebase_ui_localizations/lib/src/lang/tr.dart index f490ee78..b21c7593 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/tr.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/tr.dart @@ -341,13 +341,14 @@ class TrLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Oturum açmaya devam etmek için e-posta adresinizi onaylayın"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Bağlantıyı, oturum açma işlemini başlattığınız cihazda veya tarayıcıda açmayı deneyin."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Bu oturum açma isteğiyle ilişkili oturumun süresi doldu veya oturum temizlendi."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/uk.dart b/packages/firebase_ui_localizations/lib/src/lang/uk.dart index c40332d6..11e26ef0 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/uk.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/uk.dart @@ -343,13 +343,14 @@ class UkLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => + "Підтвердьте електронну адресу, щоб продовжити вхід"; @override String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + "Відкрийте посилання на тому самому пристрої або в тому самому браузері, де ви почали вхід."; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "Сеанс, пов’язаний із цим запитом на вхід, закінчився або його було видалено."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/zh.dart b/packages/firebase_ui_localizations/lib/src/lang/zh.dart index 25c5fcb0..37a4d76d 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/zh.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/zh.dart @@ -331,13 +331,11 @@ class ZhLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => "请确认您的电子邮件地址以继续登录"; @override - String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + String get emailLinkWrongDeviceErrorText => "请尝试在您开始登录时所用的同一设备或浏览器中打开该链接。"; @override - String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + String get emailLinkDifferentAnonymousUserErrorText => "与此登录请求关联的会话已过期或已被清除。"; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart b/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart index ada285bd..604e1131 100644 --- a/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart +++ b/packages/firebase_ui_localizations/lib/src/lang/zh_tw.dart @@ -331,13 +331,12 @@ class ZhTWLocalizations extends FirebaseUILocalizationLabels { String get sendVerificationEmailLabel => "Send verification email"; @override - String get emailLinkConfirmEmailText => "Confirm email to continue sign in"; + String get emailLinkConfirmEmailText => "請確認你的電子郵件地址以繼續登入"; @override - String get emailLinkWrongDeviceErrorText => - "Try opening the link using the same device or browser where you started the sign-in process."; + String get emailLinkWrongDeviceErrorText => "請試著在你開始登入時使用的同一部裝置或瀏覽器中開啟該連結。"; @override String get emailLinkDifferentAnonymousUserErrorText => - "The session associated with this sign-in request has either expired or was cleared."; + "與這項登入要求相關聯的工作階段已過期或已遭清除。"; } From 6d07241ad1025714b25036bf5c20b7e6f65a0964 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:33:47 +0100 Subject: [PATCH 09/10] fix(ui_auth): validate the email link form on tap and handle CredentialLinked in docs The send and confirm button now validates the email like pressing enter does, so an empty email is not sent or confirmed. The email link docs and EmailLinkSignInScreen example also navigate on CredentialLinked, which an upgraded anonymous user emits. --- docs/firebase-ui-auth/providers/email-link.md | 14 +++++-- .../screens/email_link_sign_in_screen.dart | 7 +++- .../src/views/email_link_sign_in_view.dart | 4 +- .../views/email_link_sign_in_view_test.dart | 41 +++++++++++++++++++ 4 files changed, 59 insertions(+), 7 deletions(-) diff --git a/docs/firebase-ui-auth/providers/email-link.md b/docs/firebase-ui-auth/providers/email-link.md index 8dbe8103..72121209 100644 --- a/docs/firebase-ui-auth/providers/email-link.md +++ b/docs/firebase-ui-auth/providers/email-link.md @@ -57,7 +57,9 @@ MaterialApp( '/email-link-sign-in': (context) => EmailLinkSignInScreen( actions: [ AuthStateChangeAction((context, state) { - if (state is SignedIn || state is UserCreated) { + if (state is SignedIn || + state is UserCreated || + state is CredentialLinked) { Navigator.pushReplacementNamed(context, '/profile'); } }), @@ -70,7 +72,7 @@ MaterialApp( > Notes: > -> - a user signing in with an email link for the first time emits `UserCreated` rather than `SignedIn`, so handle both. +> - a user signing in with an email link for the first time emits `UserCreated` rather than `SignedIn`, and an upgraded anonymous user (`upgradeAnonymousUsers`) emits `CredentialLinked`, so handle all three. > - see [navigation guide](../navigation.md) to learn how navigation works with Firebase UI. > - explore [FirebaseUIActions API docs](https://pub.dev/documentation/firebase_ui_auth/latest/firebase_ui_auth/FirebaseUIAction-class.html). @@ -114,7 +116,9 @@ class MyEmailLinkSignInScreen extends StatelessWidget { child: FirebaseUIActions( actions: [ AuthStateChangeAction((context, state) { - if (state is SignedIn || state is UserCreated) { + if (state is SignedIn || + state is UserCreated || + state is CredentialLinked) { Navigator.pushReplacementNamed(context, '/profile'); } } @@ -141,7 +145,9 @@ class MyCustomWidget extends StatelessWidget { return AuthFlowBuilder( provider: emailLinkProvider, listener: (oldState, newState, ctrl) { - if (newState is SignedIn || newState is UserCreated) { + if (newState is SignedIn || + newState is UserCreated || + newState is CredentialLinked) { Navigator.of(context).pushReplacementNamed('/profile'); } } diff --git a/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart index d1a68a7a..30524d48 100644 --- a/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart +++ b/packages/firebase_ui_auth/lib/src/screens/email_link_sign_in_screen.dart @@ -32,9 +32,12 @@ class EmailLinkSignInScreen extends ProviderScreen { /// ```dart /// EmailLinkSignInScreen( /// actions: [ - /// // First-time users emit UserCreated instead of SignedIn. + /// // First-time users emit UserCreated instead of SignedIn, and an + /// // upgraded anonymous user emits CredentialLinked. /// AuthStateChangeAction((context, state) { - /// if (state is SignedIn || state is UserCreated) { + /// if (state is SignedIn || + /// state is UserCreated || + /// state is CredentialLinked) { /// Navigator.pushReplacementNamed(context, '/'); /// } /// }), diff --git a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart index 8e7fe5df..fe979cb5 100644 --- a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart @@ -118,7 +118,9 @@ class _EmailLinkSignInViewState extends State { label: isConfirmingEmail ? l.continueText : l.sendLinkButtonLabel, - onTap: submit, + onTap: () { + if (formKey.currentState?.validate() ?? false) submit(); + }, ), ], if (canPop) ...[ diff --git a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart index db099761..42e5097a 100644 --- a/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart +++ b/packages/firebase_ui_auth/test/views/email_link_sign_in_view_test.dart @@ -159,4 +159,45 @@ void main() { expect(find.text(labels.emailLinkConfirmEmailText), findsOneWidget); expect(find.text(labels.continueText), findsOneWidget); }); + + testWidgets('does not send a link for an empty email', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: EmailLinkSignInView(provider: emailLinkProvider, auth: auth), + ), + ); + + await tester.tap(find.text(labels.sendLinkButtonLabel)); + await tester.pump(); + + expect(find.text(labels.emailIsRequiredErrorText), findsOneWidget); + verifyNever( + auth.sendSignInLinkToEmail( + email: anyNamed('email'), + actionCodeSettings: anyNamed('actionCodeSettings'), + ), + ); + }); + + testWidgets('does not confirm an empty email', (tester) async { + await tester.pumpWidget( + TestMaterialApp( + child: EmailLinkSignInView(provider: emailLinkProvider, auth: auth), + ), + ); + + emailLinkProvider.authListener.onEmailRequired('https://test.com/link'); + await tester.pump(); + + await tester.tap(find.text(labels.continueText)); + await tester.pump(); + + expect(find.text(labels.emailIsRequiredErrorText), findsOneWidget); + verifyNever( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ); + }); } From 2732b0799f08c19022513db7d6625575c9256040 Mon Sep 17 00:00:00 2001 From: Ademola Fadumo <48495111+demolaf@users.noreply.github.com> Date: Thu, 24 Sep 2026 16:01:02 +0100 Subject: [PATCH 10/10] fix(ui_auth): hold email links based on whether a flow is shown Decides whether an email link screen is showing by whether a widget listens to its EmailLinkFlow, instead of tracking flow creation and disposal. A reused flow (AuthFlowBuilder flow: or flowKey:) now receives held links when shown again, reset() no longer marks a visible screen as closed, and a replaced screen can no longer claim the listener back. A held link expires after 10 minutes or when a new link is sent. Removes the unreleased EmailLinkAuthProvider.stopAwaitingLink. --- .../lib/src/flows/email_link_flow.dart | 27 +-- .../providers/email_link_auth_provider.dart | 42 ++--- .../src/views/email_link_sign_in_view.dart | 3 - packages/firebase_ui_auth/pubspec.yaml | 1 + .../test/flows/email_link_flow_test.dart | 155 +++++++++++++----- 5 files changed, 154 insertions(+), 74 deletions(-) diff --git a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart index 4ef737d5..c054fff5 100644 --- a/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart +++ b/packages/firebase_ui_auth/lib/src/flows/email_link_flow.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; import 'package:firebase_ui_auth/firebase_ui_auth.dart'; /// A state that indicates that the sign in link is being sent. @@ -52,17 +53,21 @@ class EmailLinkFlow extends AuthFlow /// {@macro ui.auth.auth_flow.ctor.provider} required super.provider, - }) : super(action: AuthAction.signIn, initialState: const Uninitialized()) { - // The provider getter makes this flow the provider's listener, so keep a - // reference that can be read without doing that. - final linkProvider = provider..handleIncomingLinks(); - onDispose = () { - // A newer flow may already have replaced this one, for example after - // Navigator.pushReplacement. - if (identical(linkProvider.authListener, this)) { - linkProvider.stopAwaitingLink(); - } - }; + }) : super(action: AuthAction.signIn, initialState: const Uninitialized()); + + /// Whether a widget, such as [AuthFlowBuilder], is listening to this flow. + @internal + bool get hasWidgetListeners => hasListeners; + + @override + void addListener(VoidCallback listener) { + final wasShowing = hasListeners; + super.addListener(listener); + + // The first widget to show this flow, including one that reuses it with a + // flowKey, makes it the provider's listener and receives any sign in link + // that arrived while no email link screen was showing. + if (!wasShowing) provider.handleIncomingLinks(); } String? _pendingLink; diff --git a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart index 91e5e52a..2a427825 100644 --- a/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_auth_provider.dart @@ -6,6 +6,7 @@ import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/foundation.dart'; import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:app_links/app_links.dart'; +import 'package:clock/clock.dart'; import 'dart:async'; import 'email_link_session.dart'; @@ -49,11 +50,12 @@ class EmailLinkAuthProvider bool _initialLinkChecked = false; bool _isAwaitingLink = false; - // Whether a flow (an email link screen) is showing. A sign in link that - // arrives while none is kept in _pendingLink until the next one opens, so - // its result is not reported to a screen that is gone. - bool _hasActiveFlow = false; + // A sign in link that arrived while no email link screen was showing. It is + // handled when one opens, so its result is not reported to a screen that is + // gone, unless it is older than _pendingLinkLifetime. String? _pendingLink; + DateTime? _pendingLinkReceivedAt; + static const _pendingLinkLifetime = Duration(minutes: 10); // Links being handled or already used, so the link that launched the app is // not handled twice when it arrives from both getInitialLink and @@ -88,6 +90,7 @@ class EmailLinkAuthProvider /// the link upgrades that user instead of signing in a new one. void sendLink(String email) { authListener.onBeforeLinkSent(email); + _pendingLink = null; final session = EmailLinkSession( email: email, @@ -127,7 +130,6 @@ class EmailLinkAuthProvider /// The [email] is read from the device storage written by [sendLink]. void awaitLink(String email) { _isAwaitingLink = true; - _hasActiveFlow = true; _listen(); } @@ -149,14 +151,17 @@ class EmailLinkAuthProvider /// Handles the sign in link that launched the app, if any, and starts /// listening for links opened while the app is running. /// - /// A link that arrived while no flow was showing is handled now. + /// A link that arrived while no email link screen was showing is handled + /// now, if it arrived in the last 10 minutes. void handleIncomingLinks() { - _hasActiveFlow = true; _listen(); final pendingLink = _pendingLink; - if (pendingLink != null) { - _pendingLink = null; + final receivedAt = _pendingLinkReceivedAt; + _pendingLink = null; + if (pendingLink != null && + receivedAt != null && + clock.now().difference(receivedAt) < _pendingLinkLifetime) { _handleLink(pendingLink); } @@ -186,12 +191,13 @@ class EmailLinkAuthProvider final link = uri.toString(); if (auth.isSignInWithEmailLink(link)) { - if (_hasActiveFlow) { + if (_isShowing) { _handleLink(link); } else { _pendingLink = link; + _pendingLinkReceivedAt = clock.now(); } - } else if (_isAwaitingLink) { + } else if (_isAwaitingLink && _isShowing) { // Other deep links are only reported once a sign in link was requested. authListener.onError( fba.FirebaseAuthException( @@ -307,21 +313,17 @@ class EmailLinkAuthProvider .catchError((Object err) => _onSignInFailed(link, err)); } - /// Called when the flow that handles links is disposed, for example when - /// its screen is closed. - /// - /// Deep links that are not sign in links are no longer reported as errors, - /// and a sign in link is kept until [handleIncomingLinks] is called again. - void stopAwaitingLink() { - _isAwaitingLink = false; - _hasActiveFlow = false; + // Whether the listener is shown by a widget. A listener that is not an + // EmailLinkFlow, such as a custom stateful widget, always counts as shown. + bool get _isShowing { + final listener = authListener; + return listener is! EmailLinkFlow || listener.hasWidgetListeners; } void dispose() { _linkSubscription?.cancel(); _linkSubscription = null; _isAwaitingLink = false; - _hasActiveFlow = false; _pendingLink = null; } } diff --git a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart index fe979cb5..6f518843 100644 --- a/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart +++ b/packages/firebase_ui_auth/lib/src/views/email_link_sign_in_view.dart @@ -68,9 +68,6 @@ class _EmailLinkSignInViewState extends State { }, builder: (context, state, ctrl, child) { final isFormHidden = statesToHideForm.contains(state.runtimeType); - // Also covers a flow that was already in EmailRequired when this view - // was built, which the listener above does not see. - if (state is EmailRequired) isConfirmingEmail = true; void submit() { if (isConfirmingEmail) { diff --git a/packages/firebase_ui_auth/pubspec.yaml b/packages/firebase_ui_auth/pubspec.yaml index 288dc874..50fd08f3 100644 --- a/packages/firebase_ui_auth/pubspec.yaml +++ b/packages/firebase_ui_auth/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: app_links: ">=6.4.0 <8.0.0" + clock: ^1.1.1 email_validator: ^2.1.17 firebase_auth: ^6.7.0 firebase_core: ^4.15.0 diff --git a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart index cf076982..8d476b41 100644 --- a/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart +++ b/packages/firebase_ui_auth/test/flows/email_link_flow_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:app_links/app_links.dart'; +import 'package:clock/clock.dart'; import 'package:firebase_auth/firebase_auth.dart' as fba; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -434,18 +435,6 @@ void main() { verify(listener.onError(any)).called(1); }); - test('#stopAwaitingLink ignores other deep links', () async { - when(auth.isSignInWithEmailLink(any)).thenReturn(false); - provider.authListener = listener; - provider.awaitLink('test@test.com'); - provider.stopAwaitingLink(); - - MockUriStream.addLink(Uri.parse('https://test.com/product/1')); - await Future.delayed(Duration.zero); - - verifyNever(listener.onError(any)); - }); - test('#signInWithLink keeps the session of this device', () async { storeSession(email: 'test@test.com', sessionId: 'session-1'); provider.authListener = listener; @@ -626,7 +615,6 @@ void main() { storeSession(email: 'test@test.com', sessionId: 'session-1'); final link = signInLink(sessionId: 'session-1'); when(appLinks.getInitialLink()).thenAnswer((_) async => link); - // The flow created in setUp already checked for a launch link. provider = EmailLinkAuthProvider( actionCodeSettings: actionCodeSettings, appLinks: appLinks, @@ -643,8 +631,7 @@ void main() { MockUriStream.addLink(link); await Future.delayed(Duration.zero); - // Once by the flow in setUp, once by this provider. - verify(appLinks.getInitialLink()).called(2); + verify(appLinks.getInitialLink()).called(1); verify( auth.signInWithEmailLink( email: anyNamed('email'), @@ -672,8 +659,7 @@ void main() { provider.handleIncomingLinks(); await Future.delayed(Duration.zero); - // Once by the flow in setUp, once by this provider. - verify(appLinks.getInitialLink()).called(2); + verify(appLinks.getInitialLink()).called(1); }); test('ignores a launch link that is not a sign in link', () async { @@ -681,7 +667,6 @@ void main() { appLinks.getInitialLink(), ).thenAnswer((_) async => Uri.parse('https://test.com/other')); when(auth.isSignInWithEmailLink(any)).thenReturn(false); - // The flow created in setUp already checked for a launch link. provider = EmailLinkAuthProvider( actionCodeSettings: actionCodeSettings, appLinks: appLinks, @@ -734,42 +719,37 @@ void main() { }); group('EmailLinkFlow', () { - test('handles incoming links when created', () { + // What AuthFlowBuilder does when it shows and removes a flow. + void show(EmailLinkFlow flow) => flow.addListener(_noop); + void hide(EmailLinkFlow flow) => flow.removeListener(_noop); + + test('handles incoming links when first shown', () { final provider = MockProvider(); - EmailLinkFlow(provider: provider, auth: auth); + final flow = EmailLinkFlow(provider: provider, auth: auth); + verifyNever(provider.handleIncomingLinks()); + + show(flow); + flow.addListener(() {}); verify(provider.handleIncomingLinks()).called(1); }); - test('stops awaiting a link when disposed', () async { + test('ignores other deep links once its screen is gone', () async { when(auth.isSignInWithEmailLink(any)).thenReturn(false); + show(flow); provider.awaitLink('test@test.com'); - final errors = []; - flow.addListener(() { - if (flow.value case AuthFailed(:final exception)) errors.add(exception); - }); + hide(flow); - flow.onDispose(); MockUriStream.addLink(Uri.parse('https://test.com/product/1')); - await Future.delayed(Duration.zero); - - expect(errors, isEmpty); - }); - - test('a replaced flow does not stop the newer one', () async { - final newer = EmailLinkFlow(provider: provider, auth: auth); - storeSession(email: 'test@test.com', sessionId: 'session-1'); - - flow.onDispose(); - MockUriStream.addLink(signInLink(sessionId: 'session-1')); await pumpEventQueue(); - expect(newer.value, isA()); + expect(flow.value, isNot(isA())); }); test('keeps a link that arrives while no flow is showing', () async { storeSession(email: 'test@test.com', sessionId: 'session-1'); - flow.onDispose(); + show(flow); + hide(flow); MockUriStream.addLink(signInLink(sessionId: 'session-1')); await pumpEventQueue(); @@ -782,24 +762,116 @@ void main() { ); final next = EmailLinkFlow(provider: provider, auth: auth); + show(next); await pumpEventQueue(); expect(next.value, isA()); }); test('asks the next flow to confirm a kept link', () async { - flow.onDispose(); + show(flow); + hide(flow); MockUriStream.addLink(signInLink(sessionId: 'other-device')); await pumpEventQueue(); expect(flow.value, isA()); final next = EmailLinkFlow(provider: provider, auth: auth); + show(next); await pumpEventQueue(); expect(next.value, isA()); }); + test('gives a kept link to a reused flow when it is shown again', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + show(flow); + hide(flow); + + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + // The same flow is shown again, as AuthFlowBuilder does with a flowKey. + show(flow); + await pumpEventQueue(); + + expect(flow.value, isA()); + }); + + test('a replaced flow does not hide the newer one', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + show(flow); + final newer = EmailLinkFlow(provider: provider, auth: auth); + show(newer); + + // The old screen goes away after the new one is shown, and reading its + // provider on the way out does not make it the listener again. + hide(flow); + flow.reset(); + + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + expect(newer.value, isA()); + }); + + test('reset does not stop a showing flow from receiving links', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + show(flow); + flow.reset(); + + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + expect(flow.value, isA()); + }); + + test('drops a kept link after 10 minutes', () async { + var now = DateTime(2026, 9, 24, 12); + await withClock(Clock(() => now), () async { + final provider = EmailLinkAuthProvider( + actionCodeSettings: actionCodeSettings, + appLinks: appLinks, + ); + storeSession(email: 'test@test.com', sessionId: 'session-1'); + final first = EmailLinkFlow(provider: provider, auth: auth); + show(first); + hide(first); + + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + now = now.add(const Duration(minutes: 11)); + final next = EmailLinkFlow(provider: provider, auth: auth); + show(next); + await pumpEventQueue(); + + expect(next.value, isA()); + provider.dispose(); + }); + }); + + test('drops a kept link when a new link is sent', () async { + storeSession(email: 'test@test.com', sessionId: 'session-1'); + show(flow); + hide(flow); + + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + flow.sendLink('test@test.com'); + final next = EmailLinkFlow(provider: provider, auth: auth); + show(next); + await pumpEventQueue(); + + verifyNever( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ); + }); + test('#onEmailRequired emits EmailRequired', () { flow.onEmailRequired('https://test.com/link'); @@ -808,6 +880,7 @@ void main() { }); test('confirms the email for a link from another device', () async { + show(flow); provider.awaitLink('test@test.com'); final link = signInLink(sessionId: 'session-1'); @@ -1059,3 +1132,5 @@ class ClosingAppLinks extends Mock implements AppLinks { _controller?.add(uri); } } + +void _noop() {}