diff --git a/docs/firebase-ui-auth/providers/email-link.md b/docs/firebase-ui-auth/providers/email-link.md index bbd63339..72121209 100644 --- a/docs/firebase-ui-auth/providers/email-link.md +++ b/docs/firebase-ui-auth/providers/email-link.md @@ -56,8 +56,12 @@ 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 || + state is CredentialLinked) { + Navigator.pushReplacementNamed(context, '/profile'); + } }), ], ), @@ -68,9 +72,32 @@ MaterialApp( > Notes: > +> - 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). +## 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 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. + ## 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 +115,12 @@ 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 || + state is CredentialLinked) { + Navigator.pushReplacementNamed(context, '/profile'); + } } ], child: EmailLinkSignInView(provider: emailLinkAuthProvider), @@ -114,7 +145,9 @@ class MyCustomWidget extends StatelessWidget { return AuthFlowBuilder( provider: emailLinkProvider, listener: (oldState, newState, ctrl) { - if (newState is SignedIn) { + if (newState is SignedIn || + newState is UserCreated || + newState is CredentialLinked) { Navigator.of(context).pushReplacementNamed('/profile'); } } @@ -128,6 +161,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 +206,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 +222,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..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. @@ -19,10 +20,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} @@ -40,6 +55,23 @@ class EmailLinkFlow extends AuthFlow required super.provider, }) : 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; + @override void sendLink(String email) { provider.sendLink(email); @@ -55,6 +87,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..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,8 +6,11 @@ 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'; + /// A listener of the [EmailLinkFlow] lifecycle. abstract class EmailLinkAuthListener extends AuthListener { /// Called when the link being is sent to the user's [email]. @@ -15,19 +18,49 @@ 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 { /// 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; + bool _isAwaitingLink = 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 + // uriLinkStream. A link is removed if it fails, so it can be opened again. + final Set _handledLinks = {}; @override late EmailLinkAuthListener authListener; @@ -44,6 +77,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. @@ -51,26 +85,120 @@ class EmailLinkAuthProvider }) : _appLinks = appLinks ?? AppLinks(); /// Sends a link to the [email]. + /// + /// 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); + _pendingLink = null; - final future = auth.sendSignInLinkToEmail( + final session = EmailLinkSession( email: email, - actionCodeSettings: actionCodeSettings, + sessionId: EmailLinkSession.generateSessionId(), + anonymousUserId: upgradeAnonymousUsers && 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) { + _isAwaitingLink = true; + _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. + /// + /// 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; + + 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. + /// + /// A link that arrived while no email link screen was showing is handled + /// now, if it arrived in the last 10 minutes. + void handleIncomingLinks() { + _listen(); + + final pendingLink = _pendingLink; + final receivedAt = _pendingLinkReceivedAt; + _pendingLink = null; + if (pendingLink != null && + receivedAt != null && + clock.now().difference(receivedAt) < _pendingLinkLifetime) { + _handleLink(pendingLink); + } + + 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); - } else { + if (_isShowing) { + _handleLink(link); + } else { + _pendingLink = link; + _pendingLinkReceivedAt = clock.now(); + } + } else if (_isAwaitingLink && _isShowing) { + // Other deep links are only reported once a sign in link was requested. authListener.onError( fba.FirebaseAuthException( code: 'invalid-email-signin-link', @@ -80,26 +208,122 @@ 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 { + if (!_handledLinks.add(link)) return; - _linkSubscription = _appLinks.uriLinkStream.listen( - (Uri uri) => _onLinkReceived(email, uri), - onError: (error) => authListener.onError(error), + try { + final params = parseEmailLinkParams(link); + final anonymousUserId = params[anonymousUserIdParam]; + final session = await EmailLinkSession.find(params[sessionIdParam]); + + if (session == null) { + 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', + ); + } + + // 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; + } + + _completeSignIn( + session.email, + link, + anonymousUserId: anonymousUserId, + session: session, + ); + } catch (err) { + _onSignInFailed(link, err); + } + } + + void _onSignInFailed(String link, Object error) { + _handledLinks.remove(link); + _isAwaitingLink = false; + 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) { + _handledLinks.add(link); + _completeSignIn(email, link); + } + + void _completeSignIn( + String email, + String link, { + String? anonymousUserId, + EmailLinkSession? session, + }) { + void onCompleted() { + _isAwaitingLink = false; + // 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) { + onCompleted(); + authListener.onSignedIn(credential); + }) + .catchError((Object err) => _onSignInFailed(link, err)); + return; + } + + final user = auth.currentUser; + if (user == null || !user.isAnonymous || user.uid != anonymousUserId) { + _onSignInFailed( + link, + 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((_) { + onCompleted(); + authListener.onCredentialLinked(credential); + }) + .catchError((Object err) => _onSignInFailed(link, err)); + } + + // 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; - } - - void _signInWithEmailLink(String email, String link) { - auth - .signInWithEmailLink(email: email, emailLink: link) - .then(authListener.onSignedIn) - .catchError(authListener.onError); + _isAwaitingLink = false; + _pendingLink = 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..0d3ddd3d --- /dev/null +++ b/packages/firebase_ui_auth/lib/src/providers/email_link_session.dart @@ -0,0 +1,130 @@ +// 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:convert'; +import 'dart:math'; + +import 'package:shared_preferences/shared_preferences.dart'; + +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'; +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; + 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 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(); + 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(); + final sessions = _readAll(prefs); + sessions.add({'sessionId': sessionId, 'email': email}); + final start = max(0, sessions.length - _maxSessions); + await _writeAll(prefs, sessions.sublist(start)); + } + + /// Removes this session once its link was used. + Future remove() async { + final prefs = await SharedPreferences.getInstance(); + final sessions = _readAll(prefs); + sessions.removeWhere((session) => session['sessionId'] == sessionId); + await _writeAll(prefs, sessions); + } + + /// 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..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,8 +32,14 @@ class EmailLinkSignInScreen extends ProviderScreen { /// ```dart /// EmailLinkSignInScreen( /// actions: [ - /// AuthStateChangeAction((context, state) { - /// Navigator.pushReplacementNamed(context, '/'); + /// // 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 || + /// 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 fe80ddd9..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 @@ -41,23 +41,52 @@ 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); 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, 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,22 +96,27 @@ class _EmailLinkSignInViewState extends State { controller: emailCtrl, onSubmitted: (v) { if (formKey.currentState?.validate() ?? false) { - ctrl.sendLink(emailCtrl.text); + submit(); } }, ), ) - 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), LoadingButton( isLoading: state is SendingLink, - label: l.sendLinkButtonLabel, + label: isConfirmingEmail + ? l.continueText + : l.sendLinkButtonLabel, onTap: () { - ctrl.sendLink(emailCtrl.text); + if (formKey.currentState?.validate() ?? false) submit(); }, ), ], 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..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 @@ -22,6 +23,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..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 @@ -2,11 +2,19 @@ // 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 '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'; 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'; @@ -25,6 +33,7 @@ void main() { ); setUp(() { + SharedPreferences.setMockInitialValues({}); auth = MockAuth(); listener = MockListener(); appLinks = MockAppLinks(); @@ -39,6 +48,10 @@ void main() { ctrl = flow; }); + tearDown(() { + provider.dispose(); + }); + group('EmailLinkAuthProvider', () { test('has correct provider id', () { expect(provider.providerId, 'email_link'); @@ -55,33 +68,98 @@ 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('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: 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, + isNot(contains('ui_auid')), + ); }); - test('calls onLinkSent', () async { + 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'); + + 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['ui_auid'], 'anon-uid'); + }); + + 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']; + + expect(await storedSessions(), [ + {'sessionId': sessionId, 'email': 'test@test.com'}, + ]); + }); + + 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 +188,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 +199,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 +225,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 +245,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()); + + await Future.delayed(Duration.zero); + expect(await storedSessions(), isEmpty); }); test('calls onError if sing in failed', () async { @@ -194,8 +278,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 +286,421 @@ void main() { result.called(1); 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( + 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('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('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('#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)); + + await Future.delayed(Duration.zero); + expect(await storedSessions(), [ + {'sessionId': 'session-1', 'email': 'test@test.com'}, + ]); + }); + + 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('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'); + }); + + 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('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; + 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); + 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); + + verify(appLinks.getInitialLink()).called(1); + verify( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ).called(1); + }, + ); + + 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); + + verify(appLinks.getInitialLink()).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); + 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()); + }); + }); + }); + + 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'); }); }); @@ -221,6 +719,202 @@ void main() { }); group('EmailLinkFlow', () { + // 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(); + final flow = EmailLinkFlow(provider: provider, auth: auth); + verifyNever(provider.handleIncomingLinks()); + + show(flow); + flow.addListener(() {}); + + verify(provider.handleIncomingLinks()).called(1); + }); + + test('ignores other deep links once its screen is gone', () async { + when(auth.isSignInWithEmailLink(any)).thenReturn(false); + show(flow); + provider.awaitLink('test@test.com'); + hide(flow); + + MockUriStream.addLink(Uri.parse('https://test.com/product/1')); + await pumpEventQueue(); + + 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'); + show(flow); + hide(flow); + + MockUriStream.addLink(signInLink(sessionId: 'session-1')); + await pumpEventQueue(); + + verifyNever( + auth.signInWithEmailLink( + email: anyNamed('email'), + emailLink: anyNamed('emailLink'), + ), + ); + + 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 { + 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'); + + expect(flow.value, isA()); + expect((flow.value as EmailRequired).link, 'https://test.com/link'); + }); + + test('confirms the email for a link from another device', () async { + show(flow); + 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); + + 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 +1031,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 +1058,79 @@ 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}) { + SharedPreferences.setMockInitialValues({ + '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}) { + 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); + } +} + +void _noop() {} 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..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 @@ -5,8 +5,11 @@ 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'; +import 'package:shared_preferences/shared_preferences.dart'; import '../test_utils.dart'; @@ -17,6 +20,7 @@ void main() { late EmailLinkAuthProvider emailLinkProvider; setUp(() { + SharedPreferences.setMockInitialValues({}); auth = MockAuth(); appLinks = MockAppLinks(); final actionCodeSettings = fba.ActionCodeSettings( @@ -70,4 +74,130 @@ 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'), + ), + ); + }); + + 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); + }); + + 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); + }); + + 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'), + ), + ); + }); } 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..88c9da53 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": "أكِّد عنوان بريدك الإلكتروني لمتابعة تسجيل الدخول", + "@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": "يُرجى فتح الرابط على الجهاز أو المتصفح نفسه الذي بدأت منه تسجيل الدخول.", + "@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": "انتهت صلاحية الجلسة المرتبطة بطلب تسجيل الدخول هذا أو تم محوها.", + "@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..f8843580 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": "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": "Ö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": "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_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..d6b9526e 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": "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": "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": "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 2ece068b..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 @@ -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": "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": "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": "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 67a7e8f0..0c9aca2e 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": "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": "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": "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 1a14e4d1..34b7dd2c 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": "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": "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": "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 37be21d0..d00f3b3f 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": "אשר את כתובת הדוא\"ל כדי להמשיך בכניסה", + "@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": "נסה לפתוח את הקישור באותו מכשיר או דפדפן שבו התחלת את תהליך הכניסה.", + "@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": "תוקף ההפעלה של בקשת הכניסה הזו פג, או שהיא נמחקה.", + "@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..75651dcf 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": "साइन इन जारी रखने के लिए अपने ईमेल की पुष्टि करें", + "@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": "लिंक को उसी डिवाइस या ब्राउज़र में खोलकर देखें, जिसमें आपने साइन इन शुरू किया था.", + "@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": "साइन इन के इस अनुरोध से जुड़ा सेशन खत्म हो गया है या मिटा दिया गया है.", + "@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..e3c65cec 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": "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": "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": "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 8b0a538b..148d4907 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": "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": "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": "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 954e04b4..c1c1ec57 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": "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": "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": "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 75edac0c..0a97cb17 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": "サインインを続行するには、メールアドレスを確認してください", + "@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": "サインインを開始したのと同じデバイスまたはブラウザでリンクを開いてください。", + "@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": "このサインイン リクエストに関連付けられたセッションは、期限切れになったか削除されました。", + "@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..8c8ba298 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": "로그인을 계속하려면 이메일을 확인하세요", + "@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": "로그인을 시작한 기기 또는 브라우저에서 링크를 열어 보세요.", + "@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": "이 로그인 요청과 연결된 세션이 만료되었거나 삭제되었습니다.", + "@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..1d8c60f0 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": "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": "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": "Ø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 26a94100..ce1b7cdd 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": "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": "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": "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 dff09d94..ac135674 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": "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": "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": "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 6e37c6c6..470d9ace 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": "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": "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": "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 e62dc767..1f4ca99a 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ă 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": "Î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": "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 ce34c350..3d49596e 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": "Подтвердите адрес электронной почты, чтобы продолжить вход", + "@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": "Откройте ссылку на том же устройстве или в том же браузере, где вы начали вход.", + "@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": "Сеанс, связанный с этим запросом на вход, истек или был удален.", + "@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..1d8b9918 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": "ยืนยันอีเมลเพื่อลงชื่อเข้าใช้ต่อ", + "@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": "โปรดลองเปิดลิงก์ในอุปกรณ์หรือเบราว์เซอร์เดียวกันกับที่คุณเริ่มลงชื่อเข้าใช้", + "@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": "เซสชันที่เชื่อมโยงกับคำขอลงชื่อเข้าใช้นี้หมดอายุหรือถูกล้างแล้ว", + "@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..7ca0ebb5 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": "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": "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": "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 056bdbc1..667b71ef 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": "Підтвердьте електронну адресу, щоб продовжити вхід", + "@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": "Відкрийте посилання на тому самому пристрої або в тому самому браузері, де ви почали вхід.", + "@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": "Сеанс, пов’язаний із цим запитом на вхід, закінчився або його було видалено.", + "@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..4773f469 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": "请确认您的电子邮件地址以继续登录", + "@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": "请尝试在您开始登录时所用的同一设备或浏览器中打开该链接。", + "@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": "与此登录请求关联的会话已过期或已被清除。", + "@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..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 @@ -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": "請確認你的電子郵件地址以繼續登入", + "@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": "請試著在你開始登入時使用的同一部裝置或瀏覽器中開啟該連結。", + "@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": "與這項登入要求相關聯的工作階段已過期或已遭清除。", + "@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..3a1b31bb 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,16 @@ class ArLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "أكِّد عنوان بريدك الإلكتروني لمتابعة تسجيل الدخول"; + + @override + String get emailLinkWrongDeviceErrorText => + "يُرجى فتح الرابط على الجهاز أو المتصفح نفسه الذي بدأت منه تسجيل الدخول."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "انتهت صلاحية الجلسة المرتبطة بطلب تسجيل الدخول هذا أو تم محوها."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/de.dart b/packages/firebase_ui_localizations/lib/src/lang/de.dart index 42176bb0..5623bf20 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,16 @@ class DeLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Verifizierungs-E-Mail senden"; + + @override + String get emailLinkConfirmEmailText => + "Bestätigen Sie Ihre E-Mail-Adresse, um die Anmeldung fortzusetzen"; + + @override + String get emailLinkWrongDeviceErrorText => + "Öffnen Sie den Link auf demselben Gerät oder in demselben Browser, mit dem Sie die Anmeldung gestartet haben."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "Die Sitzung zu dieser Anmeldeanfrage ist abgelaufen oder wurde gelöscht."; } 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..b1b56a23 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,16 @@ class EsLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Confirma tu correo electrónico para continuar con el inicio de sesión"; + + @override + String get emailLinkWrongDeviceErrorText => + "Intenta abrir el enlace en el mismo dispositivo o navegador donde empezaste a iniciar sesión."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 3a9502e5..505164ce 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,16 @@ class Es419Localizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Confirma tu correo electrónico para continuar con el acceso"; + + @override + String get emailLinkWrongDeviceErrorText => + "Intenta abrir el vínculo en el mismo dispositivo o navegador en el que comenzaste a acceder."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 6261abce..473e9d62 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,16 @@ class FiLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Lähetä vahvistussähköposti"; + + @override + String get emailLinkConfirmEmailText => + "Vahvista sähköpostiosoitteesi jatkaaksesi kirjautumista"; + + @override + String get emailLinkWrongDeviceErrorText => + "Avaa linkki samalla laitteella tai selaimella, jolla aloitit kirjautumisen."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 eeb975c2..14390249 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,16 @@ class FrLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Envoyer un e-mail de vérification"; + + @override + String get emailLinkConfirmEmailText => + "Confirmez votre adresse e-mail pour poursuivre la connexion"; + + @override + String get emailLinkWrongDeviceErrorText => + "Essayez d'ouvrir le lien sur l'appareil ou dans le navigateur à partir duquel vous avez commencé la connexion."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 d4d20e2b..1d81927f 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,16 @@ class HeLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + """אשר את כתובת הדוא"ל כדי להמשיך בכניסה"""; + + @override + String get emailLinkWrongDeviceErrorText => + "נסה לפתוח את הקישור באותו מכשיר או דפדפן שבו התחלת את תהליך הכניסה."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "תוקף ההפעלה של בקשת הכניסה הזו פג, או שהיא נמחקה."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/hi.dart b/packages/firebase_ui_localizations/lib/src/lang/hi.dart index 74ffdc49..27be018a 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,16 @@ class HiLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "साइन इन जारी रखने के लिए अपने ईमेल की पुष्टि करें"; + + @override + String get emailLinkWrongDeviceErrorText => + "लिंक को उसी डिवाइस या ब्राउज़र में खोलकर देखें, जिसमें आपने साइन इन शुरू किया था."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "साइन इन के इस अनुरोध से जुड़ा सेशन खत्म हो गया है या मिटा दिया गया है."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/hu.dart b/packages/firebase_ui_localizations/lib/src/lang/hu.dart index 3e641b52..b2f6705a 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,16 @@ class HuLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Erősítse meg az e-mail-címét a belépés folytatásához"; + + @override + String get emailLinkWrongDeviceErrorText => + "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 => + "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 ed67e92a..a479c08c 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,16 @@ class IdLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Konfirmasi email Anda untuk melanjutkan login"; + + @override + String get emailLinkWrongDeviceErrorText => + "Coba buka link di perangkat atau browser yang sama dengan yang Anda gunakan untuk memulai login."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 5adaa83c..220252fe 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,16 @@ class ItLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Conferma la tua email per continuare l'accesso"; + + @override + String get emailLinkWrongDeviceErrorText => + "Prova ad aprire il link sullo stesso dispositivo o browser da cui hai iniziato l'accesso."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 fd8f160c..acd4c611 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 => "サインインを続行するには、メールアドレスを確認してください"; + + @override + String get emailLinkWrongDeviceErrorText => + "サインインを開始したのと同じデバイスまたはブラウザでリンクを開いてください。"; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "このサインイン リクエストに関連付けられたセッションは、期限切れになったか削除されました。"; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/ko.dart b/packages/firebase_ui_localizations/lib/src/lang/ko.dart index 7df3034a..e1a919b8 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 => "로그인을 계속하려면 이메일을 확인하세요"; + + @override + String get emailLinkWrongDeviceErrorText => + "로그인을 시작한 기기 또는 브라우저에서 링크를 열어 보세요."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "이 로그인 요청과 연결된 세션이 만료되었거나 삭제되었습니다."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/nb.dart b/packages/firebase_ui_localizations/lib/src/lang/nb.dart index 61d886e8..14e4015a 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,16 @@ class NbLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send e-postbekreftelse"; + + @override + String get emailLinkConfirmEmailText => + "Bekreft e-postadressen din for å fortsette innloggingen"; + + @override + String get emailLinkWrongDeviceErrorText => + "Prøv å åpne lenken på samme enhet eller i samme nettleser som du startet innloggingen på."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "Ø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 f7d5f73a..5303694a 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,16 @@ class NlLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Bevestig uw e-mailadres om verder te gaan met inloggen"; + + @override + String get emailLinkWrongDeviceErrorText => + "Open de link op hetzelfde apparaat of in dezelfde browser waarmee u bent begonnen met inloggen."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 d3532f32..0185b84d 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,16 @@ class PlLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Potwierdź adres e-mail, aby kontynuować logowanie"; + + @override + String get emailLinkWrongDeviceErrorText => + "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 => + "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 8f39f3fe..3cec83ff 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,16 @@ class PtLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Enviar email de verificação"; + + @override + String get emailLinkConfirmEmailText => + "Confirme seu email para continuar o login"; + + @override + String get emailLinkWrongDeviceErrorText => + "Tente abrir o link no mesmo dispositivo ou navegador em que você iniciou o login."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 956792d1..20230e5f 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,16 @@ class RoLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Confirmă emailul pentru a continua conectarea"; + + @override + String get emailLinkWrongDeviceErrorText => + "Încearcă să deschizi linkul pe același dispozitiv sau în același browser în care ai început conectarea."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 ee16d1b3..ef7d2ebc 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,16 @@ class RuLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Подтвердите адрес электронной почты, чтобы продолжить вход"; + + @override + String get emailLinkWrongDeviceErrorText => + "Откройте ссылку на том же устройстве или в том же браузере, где вы начали вход."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "Сеанс, связанный с этим запросом на вход, истек или был удален."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/th.dart b/packages/firebase_ui_localizations/lib/src/lang/th.dart index 5c47cc5a..3e3b957b 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 => "ยืนยันอีเมลเพื่อลงชื่อเข้าใช้ต่อ"; + + @override + String get emailLinkWrongDeviceErrorText => + "โปรดลองเปิดลิงก์ในอุปกรณ์หรือเบราว์เซอร์เดียวกันกับที่คุณเริ่มลงชื่อเข้าใช้"; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "เซสชันที่เชื่อมโยงกับคำขอลงชื่อเข้าใช้นี้หมดอายุหรือถูกล้างแล้ว"; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/tr.dart b/packages/firebase_ui_localizations/lib/src/lang/tr.dart index 98e46787..b21c7593 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,16 @@ class TrLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Oturum açmaya devam etmek için e-posta adresinizi onaylayın"; + + @override + String get emailLinkWrongDeviceErrorText => + "Bağlantıyı, oturum açma işlemini başlattığınız cihazda veya tarayıcıda açmayı deneyin."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "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 c01910b6..11e26ef0 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,16 @@ class UkLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => + "Підтвердьте електронну адресу, щоб продовжити вхід"; + + @override + String get emailLinkWrongDeviceErrorText => + "Відкрийте посилання на тому самому пристрої або в тому самому браузері, де ви почали вхід."; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "Сеанс, пов’язаний із цим запитом на вхід, закінчився або його було видалено."; } diff --git a/packages/firebase_ui_localizations/lib/src/lang/zh.dart b/packages/firebase_ui_localizations/lib/src/lang/zh.dart index 58cd6005..37a4d76d 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,13 @@ class ZhLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "请确认您的电子邮件地址以继续登录"; + + @override + String get emailLinkWrongDeviceErrorText => "请尝试在您开始登录时所用的同一设备或浏览器中打开该链接。"; + + @override + 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 50449c36..604e1131 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,14 @@ class ZhTWLocalizations extends FirebaseUILocalizationLabels { @override String get sendVerificationEmailLabel => "Send verification email"; + + @override + String get emailLinkConfirmEmailText => "請確認你的電子郵件地址以繼續登入"; + + @override + String get emailLinkWrongDeviceErrorText => "請試著在你開始登入時使用的同一部裝置或瀏覽器中開啟該連結。"; + + @override + String get emailLinkDifferentAnonymousUserErrorText => + "與這項登入要求相關聯的工作階段已過期或已遭清除。"; }