diff --git a/lib/core/auth/auth_routes.dart b/lib/core/auth/auth_routes.dart index 59e25d1..ba6ddb8 100644 --- a/lib/core/auth/auth_routes.dart +++ b/lib/core/auth/auth_routes.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:thingsboard_app/config/routes/router.dart'; import 'package:thingsboard_app/core/auth/login/reset_password_request_page.dart'; +import 'package:thingsboard_app/core/auth/login/two_factor_authentication_page.dart'; import 'package:thingsboard_app/core/context/tb_context.dart'; import 'login/login_page.dart'; @@ -18,6 +19,11 @@ class AuthRoutes extends TbRoutes { return ResetPasswordRequestPage(tbContext); }); + late var twoFactorAuthenticationHandler = Handler( + handlerFunc: (BuildContext? context, Map params) { + return TwoFactorAuthenticationPage(tbContext); + }); + AuthRoutes(TbContext tbContext) : super(tbContext); @override @@ -25,5 +31,6 @@ class AuthRoutes extends TbRoutes { router.define("/login", handler: loginHandler); router.define("/login/resetPasswordRequest", handler: resetPasswordRequestHandler); + router.define("/login/mfa", handler: twoFactorAuthenticationHandler); } } diff --git a/lib/core/auth/login/login_page.dart b/lib/core/auth/login/login_page.dart index 2cc49c5..b133b14 100644 --- a/lib/core/auth/login/login_page.dart +++ b/lib/core/auth/login/login_page.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -38,6 +39,11 @@ class _LoginPageState extends TbPageState { @override void initState() { super.initState(); + if (tbClient.isPreVerificationToken()) { + SchedulerBinding.instance.addPostFrameCallback((_) { + navigateTo('/login/mfa'); + }); + } } @override @@ -107,6 +113,8 @@ class _LoginPageState extends TbPageState { children: [ FormBuilderTextField( name: 'username', + keyboardType: + TextInputType.emailAddress, validator: FormBuilderValidators.compose([ FormBuilderValidators.required( diff --git a/lib/core/auth/login/two_factor_authentication_page.dart b/lib/core/auth/login/two_factor_authentication_page.dart new file mode 100644 index 0000000..94bb126 --- /dev/null +++ b/lib/core/auth/login/two_factor_authentication_page.dart @@ -0,0 +1,504 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:form_builder_validators/form_builder_validators.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:alt_sms_autofill/alt_sms_autofill.dart'; +import 'package:thingsboard_app/core/auth/login/login_page_background.dart'; +import 'package:thingsboard_app/core/context/tb_context.dart'; +import 'package:thingsboard_app/core/context/tb_context_widget.dart'; +import 'package:thingsboard_app/widgets/tb_app_bar.dart'; +import 'package:thingsboard_app/generated/l10n.dart'; +import 'package:thingsboard_client/thingsboard_client.dart'; +import 'package:collection/collection.dart'; + +typedef ProviderDescFunction = String Function( + BuildContext context, String? contact); +typedef TextFunction = String Function(BuildContext context); + +class TwoFactorAuthProviderLoginData { + TextFunction nameFunction; + ProviderDescFunction descFunction; + TextFunction placeholderFunction; + String icon; + TwoFactorAuthProviderLoginData( + {required this.nameFunction, + required this.descFunction, + required this.placeholderFunction, + required this.icon}); +} + +final Map + twoFactorAuthProvidersLoginData = { + TwoFaProviderType.TOTP: TwoFactorAuthProviderLoginData( + nameFunction: (context) => S.of(context).mfaProviderTopt, + descFunction: (context, contact) => S.of(context).totpAuthDescription, + placeholderFunction: (context) => S.of(context).toptAuthPlaceholder, + icon: 'cellphone-key'), + TwoFaProviderType.SMS: TwoFactorAuthProviderLoginData( + nameFunction: (context) => S.of(context).mfaProviderSms, + descFunction: (context, contact) => + S.of(context).smsAuthDescription(contact ?? ''), + placeholderFunction: (context) => S.of(context).smsAuthPlaceholder, + icon: 'message-reply-text-outline'), + TwoFaProviderType.EMAIL: TwoFactorAuthProviderLoginData( + nameFunction: (context) => S.of(context).mfaProviderEmail, + descFunction: (context, contact) => + S.of(context).emailAuthDescription(contact ?? ''), + placeholderFunction: (context) => S.of(context).emailAuthPlaceholder, + icon: 'email-outline'), + TwoFaProviderType.BACKUP_CODE: TwoFactorAuthProviderLoginData( + nameFunction: (context) => S.of(context).mfaProviderBackupCode, + descFunction: (context, contact) => + S.of(context).backupCodeAuthDescription, + placeholderFunction: (context) => S.of(context).backupCodeAuthPlaceholder, + icon: 'lock-outline') +}; + +class TwoFactorAuthenticationPage extends TbPageWidget { + TwoFactorAuthenticationPage(TbContext tbContext) : super(tbContext); + + @override + _TwoFactorAuthenticationPageState createState() => + _TwoFactorAuthenticationPageState(); +} + +class _TwoFactorAuthenticationPageState + extends TbPageState { + static RegExp smsCodeRegExp = new RegExp(r"(\d{6})"); + + final _twoFactorAuthFormKey = GlobalKey(); + ValueNotifier _selectedProvider = + ValueNotifier(null); + TwoFaProviderType? _prevProvider; + int? _minVerificationPeriod; + List _allowProviders = []; + ValueNotifier _disableSendButton = ValueNotifier(false); + ValueNotifier _showResendAction = ValueNotifier(false); + ValueNotifier _hideResendButton = ValueNotifier(true); + Timer? _timer; + Timer? _tooManyRequestsTimer; + ValueNotifier _countDownTime = ValueNotifier(0); + bool _listenForSms = false; + + @override + void initState() { + super.initState(); + var providersInfo = tbContext.twoFactorAuthProviders; + TwoFaProviderType.values.forEach((provider) { + var providerConfig = + providersInfo!.firstWhereOrNull((config) => config.type == provider); + if (providerConfig != null) { + if (providerConfig.isDefault) { + _minVerificationPeriod = + providerConfig.minVerificationCodeSendPeriod ?? 30; + _selectedProvider.value = providerConfig.type; + } + _allowProviders.add(providerConfig.type); + } + }); + if (this._selectedProvider.value != TwoFaProviderType.TOTP) { + _sendCode(); + _showResendAction.value = true; + if (this._selectedProvider.value == TwoFaProviderType.SMS) { + _startListenForSmsCode(); + } + } + _timer = Timer.periodic(Duration(seconds: 1), (timer) { + _updatedTime(); + }); + } + + @override + void dispose() { + if (_timer != null) { + _timer!.cancel(); + } + if (_tooManyRequestsTimer != null) { + _tooManyRequestsTimer!.cancel(); + } + _cancelSmsCodeListen(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + return await _goBack(); + }, + child: Scaffold( + backgroundColor: Colors.white, + resizeToAvoidBottomInset: false, + body: Stack(children: [ + LoginPageBackground(), + SizedBox.expand( + child: Scaffold( + backgroundColor: Colors.transparent, + appBar: TbAppBar( + tbContext, + title: Text('${S.of(context).verifyYourIdentity}'), + ), + body: Stack(children: [ + SizedBox.expand( + child: Padding( + padding: EdgeInsets.all(24), + child: ValueListenableBuilder( + valueListenable: _selectedProvider, + builder: (context, providerType, _widget) { + if (providerType == null) { + var children = [ + Padding( + padding: EdgeInsets.only(bottom: 16), + child: Text( + '${S.of(context).selectWayToVerify}', + style: TextStyle( + color: Colors.black87, + fontSize: 16, + height: 24 / 16))) + ]; + _allowProviders.forEach((type) { + var providerData = + twoFactorAuthProvidersLoginData[ + type]!; + Widget? icon; + var iconData = MdiIcons.fromString( + providerData.icon); + if (iconData != null) { + icon = Icon(iconData, + size: 24, + color: + Theme.of(context).primaryColor); + } else { + icon = Icon(Icons.login, + size: 24, + color: + Theme.of(context).primaryColor); + } + children.add(Container( + padding: + EdgeInsets.symmetric(vertical: 8), + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + padding: EdgeInsets.all(16), + alignment: + Alignment.centerLeft), + onPressed: () async => + await _selectProvider(type), + icon: icon, + label: Text(providerData + .nameFunction(context))))); + }); + return ListView( + padding: + EdgeInsets.symmetric(vertical: 8), + children: children, + ); + } else { + var providerConfig = tbContext + .twoFactorAuthProviders + ?.firstWhereOrNull((config) => + config.type == providerType); + if (providerConfig == null) { + return SizedBox.shrink(); + } + var providerDescription = + twoFactorAuthProvidersLoginData[ + providerType]! + .descFunction; + return FormBuilder( + key: _twoFactorAuthFormKey, + autovalidateMode: + AutovalidateMode.disabled, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + SizedBox(height: 16), + Text( + providerDescription(context, + providerConfig.contact), + textAlign: TextAlign.start, + style: TextStyle( + color: Color(0xFF7F7F7F), + fontSize: 14, + height: 24 / 14), + ), + SizedBox(height: 16), + _buildVerificationCodeField( + context, providerType), + Spacer(), + ValueListenableBuilder( + valueListenable: + _disableSendButton, + builder: (context, + disableSendButton, + _widget) { + return ElevatedButton( + child: Text( + '${S.of(context).continueText}'), + style: ElevatedButton + .styleFrom( + padding: EdgeInsets + .symmetric( + vertical: + 16)), + onPressed: disableSendButton + ? null + : () => + _sendVerificationCode( + context)); + }), + SizedBox(height: 16), + SizedBox( + height: 48, + child: Row( + mainAxisSize: + MainAxisSize.max, + children: [ + ValueListenableBuilder< + bool>( + valueListenable: + _showResendAction, + builder: (context, + showResendActionValue, + _widget) { + if (showResendActionValue) { + return Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: [ + ValueListenableBuilder< + int>( + valueListenable: + _countDownTime, + builder: (context, + countDown, + _widget) { + if (countDown > + 0) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Text( + S.of(context).resendCodeWait(countDown), + textAlign: TextAlign.center, + style: TextStyle(color: Color(0xFF7F7F7F), fontSize: 12, height: 24 / 12), + ), + ); + } else { + return SizedBox.shrink(); + } + }), + ValueListenableBuilder< + bool>( + valueListenable: + _hideResendButton, + builder: (context, + hideResendButton, + _widget) { + if (!hideResendButton) { + return TextButton( + child: Text('${S.of(context).resendCode}'), + style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)), + onPressed: () { + _sendCode(); + }, + ); + } else { + return SizedBox.shrink(); + } + }) + ])); + } else { + return SizedBox + .shrink(); + } + }), + if (_allowProviders + .length > + 1) + Expanded( + child: TextButton( + child: Text( + '${S.of(context).tryAnotherWay}'), + style: ElevatedButton.styleFrom( + padding: EdgeInsets + .symmetric( + vertical: + 16)), + onPressed: + () async { + await _selectProvider( + null); + }, + )) + ])) + ])); + } + }))) + ]), + ), + ) + ]))); + } + + FormBuilderTextField _buildVerificationCodeField( + BuildContext context, TwoFaProviderType providerType) { + int maxLengthInput = 6; + TextInputType keyboardType = TextInputType.number; + String pattern = '[0-9]*'; + + if (providerType == TwoFaProviderType.BACKUP_CODE) { + maxLengthInput = 8; + pattern = '[0-9abcdef]*'; + keyboardType = TextInputType.text; + } + + List> validators = [ + FormBuilderValidators.required( + errorText: '${S.of(context).verificationCodeInvalid}'), + FormBuilderValidators.equalLength(maxLengthInput, + errorText: '${S.of(context).verificationCodeInvalid}'), + FormBuilderValidators.match(pattern, + errorText: '${S.of(context).verificationCodeInvalid}') + ]; + + var providerFormData = twoFactorAuthProvidersLoginData[providerType]!; + + return FormBuilderTextField( + name: 'verificationCode', + autofocus: true, + maxLength: maxLengthInput, + keyboardType: keyboardType, + validator: FormBuilderValidators.compose(validators), + decoration: InputDecoration( + border: OutlineInputBorder(), + labelText: providerFormData.placeholderFunction(context))); + } + + Future _startListenForSmsCode() async { + _listenForSms = true; + _listenForSmsCode(); + } + + Future _listenForSmsCode() async { + String? comingSms; + try { + comingSms = await AltSmsAutofill().listenForSms; + } catch (e) { + _listenForSms = false; + comingSms = null; + } + if (comingSms != null) { + RegExpMatch? match = smsCodeRegExp.firstMatch(comingSms); + if (match != null) { + String? codeStr = match.group(1); + if (codeStr != null) { + _twoFactorAuthFormKey.currentState + ?.patchValue({'verificationCode': codeStr}); + } + } + } + if (_listenForSms) { + _listenForSmsCode(); + } + } + + Future _cancelSmsCodeListen() async { + _listenForSms = false; + AltSmsAutofill().unregisterListener(); + } + + Future _sendVerificationCode(BuildContext context) async { + FocusScope.of(context).unfocus(); + if (_twoFactorAuthFormKey.currentState?.saveAndValidate() ?? false) { + var formValue = _twoFactorAuthFormKey.currentState!.value; + String verificationCode = formValue['verificationCode']; + try { + await tbClient.checkTwoFaVerificationCode( + _selectedProvider.value!, verificationCode, + requestConfig: RequestConfig(ignoreErrors: true)); + } catch (e) { + if (e is ThingsboardError) { + if (e.status == 400) { + _twoFactorAuthFormKey.currentState!.fields['verificationCode']! + .invalidate(S.of(context).verificationCodeIncorrect); + } else if (e.status == 429) { + _twoFactorAuthFormKey.currentState!.fields['verificationCode']! + .invalidate(S.of(context).verificationCodeManyRequest); + _disableSendButton.value = true; + if (_tooManyRequestsTimer != null) { + _tooManyRequestsTimer!.cancel(); + } + _tooManyRequestsTimer = Timer(Duration(seconds: 5), () { + _twoFactorAuthFormKey.currentState!.fields['verificationCode']! + .validate(); + _disableSendButton.value = false; + }); + } else { + showErrorNotification(e.message ?? 'Code verification failed!'); + } + } else { + showErrorNotification('Code verification failed!'); + } + } + } + } + + Future _selectProvider(TwoFaProviderType? type) async { + _prevProvider = type == null ? _selectedProvider.value : null; + _selectedProvider.value = type; + _showResendAction.value = false; + await _cancelSmsCodeListen(); + if (type != null) { + var providersInfo = tbContext.twoFactorAuthProviders; + var providerConfig = + providersInfo!.firstWhereOrNull((config) => config.type == type)!; + if (type != TwoFaProviderType.TOTP && + type != TwoFaProviderType.BACKUP_CODE) { + _sendCode(); + _showResendAction.value = true; + _minVerificationPeriod = + providerConfig.minVerificationCodeSendPeriod ?? 30; + if (type == TwoFaProviderType.SMS) { + _startListenForSmsCode(); + } + } + } + } + + Future _sendCode() async { + _hideResendButton.value = true; + _countDownTime.value = 0; + try { + await tbContext.tbClient + .getTwoFactorAuthService() + .requestTwoFaVerificationCode(_selectedProvider.value!, + requestConfig: RequestConfig(ignoreErrors: true)); + } catch (e) { + } finally { + _countDownTime.value = _minVerificationPeriod!; + } + } + + Future _goBack() async { + if (_prevProvider != null) { + await _selectProvider(_prevProvider); + _prevProvider = null; + } else { + tbClient.logout(requestConfig: RequestConfig(ignoreErrors: true)); + } + return false; + } + + void _updatedTime() { + if (_countDownTime.value > 0) { + _countDownTime.value--; + if (_countDownTime.value == 0) { + _hideResendButton.value = false; + } + } + } +} diff --git a/lib/core/context/tb_context.dart b/lib/core/context/tb_context.dart index a821cea..459580f 100644 --- a/lib/core/context/tb_context.dart +++ b/lib/core/context/tb_context.dart @@ -104,6 +104,7 @@ class TbContext { final ValueNotifier _isAuthenticated = ValueNotifier(false); PlatformType? _oauth2PlatformType; List? oauth2ClientInfos; + List? twoFactorAuthProviders; User? userDetails; HomeDashboardInfo? homeDashboard; final _isLoadingNotifier = ValueNotifier(false); @@ -256,7 +257,7 @@ class TbContext { try { log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}'); isUserLoaded = true; - if (tbClient.isAuthenticated()) { + if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { log.debug('authUser: ${tbClient.getAuthUser()}'); if (tbClient.getAuthUser()!.userId != null) { try { @@ -272,6 +273,14 @@ class TbContext { } } } else { + if (tbClient.isPreVerificationToken()) { + log.debug('authUser: ${tbClient.getAuthUser()}'); + twoFactorAuthProviders = await tbClient + .getTwoFactorAuthService() + .getAvailableLoginTwoFaProviders(); + } else { + twoFactorAuthProviders = null; + } userDetails = null; homeDashboard = null; oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients( @@ -308,14 +317,15 @@ class TbContext { Listenable get isAuthenticatedListenable => _isAuthenticated; - bool get isAuthenticated => _isAuthenticated.value; + bool get isAuthenticated => + _isAuthenticated.value && !tbClient.isPreVerificationToken(); bool get hasOAuthClients => oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty; Future updateRouteState() async { if (currentState != null) { - if (tbClient.isAuthenticated()) { + if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) { var defaultDashboardId = _defaultDashboardId(); if (defaultDashboardId != null) { bool fullscreen = _userForceFullscreen(); diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 365c3c6..346751a 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -20,6 +20,15 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; + static String m0(contact) => + "A security code has been sent to your email address at ${contact}."; + + static String m1(time) => + "Resend code in ${Intl.plural(time, one: '1 second', other: '${time} seconds')}"; + + static String m2(contact) => + "A security code has been sent to your phone at ${contact}."; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "No": MessageLookupByLibrary.simpleMessage("No"), @@ -46,9 +55,14 @@ class MessageLookup extends MessageLookupByLibrary { "auditLogDetails": MessageLookupByLibrary.simpleMessage("Audit log details"), "auditLogs": MessageLookupByLibrary.simpleMessage("Audit Logs"), + "backupCodeAuthDescription": MessageLookupByLibrary.simpleMessage( + "Please enter one of your backup codes."), + "backupCodeAuthPlaceholder": + MessageLookupByLibrary.simpleMessage("Backup code"), "changePassword": MessageLookupByLibrary.simpleMessage("Change Password"), "city": MessageLookupByLibrary.simpleMessage("City"), + "continueText": MessageLookupByLibrary.simpleMessage("Continue"), "country": MessageLookupByLibrary.simpleMessage("Country"), "currentPassword": MessageLookupByLibrary.simpleMessage("currentPassword"), @@ -60,6 +74,9 @@ class MessageLookup extends MessageLookupByLibrary { "customers": MessageLookupByLibrary.simpleMessage("Customers"), "devices": MessageLookupByLibrary.simpleMessage("Devices"), "email": MessageLookupByLibrary.simpleMessage("Email"), + "emailAuthDescription": m0, + "emailAuthPlaceholder": + MessageLookupByLibrary.simpleMessage("Email code"), "emailInvalidText": MessageLookupByLibrary.simpleMessage("Invalid email format."), "emailRequireText": @@ -83,6 +100,12 @@ class MessageLookup extends MessageLookupByLibrary { "logoDefaultValue": MessageLookupByLibrary.simpleMessage("Thingsboard Logo"), "logout": MessageLookupByLibrary.simpleMessage("Log Out"), + "mfaProviderBackupCode": + MessageLookupByLibrary.simpleMessage("Backup code"), + "mfaProviderEmail": MessageLookupByLibrary.simpleMessage("Email"), + "mfaProviderSms": MessageLookupByLibrary.simpleMessage("SMS"), + "mfaProviderTopt": + MessageLookupByLibrary.simpleMessage("Authenticator app"), "more": MessageLookupByLibrary.simpleMessage("More"), "newPassword": MessageLookupByLibrary.simpleMessage("newPassword"), "newPassword2": MessageLookupByLibrary.simpleMessage("newPassword2"), @@ -117,6 +140,12 @@ class MessageLookup extends MessageLookupByLibrary { "Profile successfully updated"), "requestPasswordReset": MessageLookupByLibrary.simpleMessage("Request password reset"), + "resendCode": MessageLookupByLibrary.simpleMessage("Resend code"), + "resendCodeWait": m1, + "selectWayToVerify": + MessageLookupByLibrary.simpleMessage("Select a way to verify"), + "smsAuthDescription": m2, + "smsAuthPlaceholder": MessageLookupByLibrary.simpleMessage("SMS code"), "stateOrProvince": MessageLookupByLibrary.simpleMessage("State / Province"), "systemAdministrator": @@ -124,8 +153,21 @@ class MessageLookup extends MessageLookupByLibrary { "tenantAdministrator": MessageLookupByLibrary.simpleMessage("Tenant Administrator"), "title": MessageLookupByLibrary.simpleMessage("Title"), + "toptAuthPlaceholder": MessageLookupByLibrary.simpleMessage("Code"), + "totpAuthDescription": MessageLookupByLibrary.simpleMessage( + "Please enter the security code from your authenticator app."), "tryAgain": MessageLookupByLibrary.simpleMessage("Try Again"), + "tryAnotherWay": + MessageLookupByLibrary.simpleMessage("Try another way"), "type": MessageLookupByLibrary.simpleMessage("Type"), - "username": MessageLookupByLibrary.simpleMessage("username") + "username": MessageLookupByLibrary.simpleMessage("username"), + "verificationCodeIncorrect": MessageLookupByLibrary.simpleMessage( + "Verification code is incorrect"), + "verificationCodeInvalid": MessageLookupByLibrary.simpleMessage( + "Invalid verification code format"), + "verificationCodeManyRequest": MessageLookupByLibrary.simpleMessage( + "Too many requests check verification code"), + "verifyYourIdentity": + MessageLookupByLibrary.simpleMessage("Verify your identity") }; } diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 649bc3d..5fb26b6 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -779,6 +779,216 @@ class S { args: [], ); } + + /// `Verify your identity` + String get verifyYourIdentity { + return Intl.message( + 'Verify your identity', + name: 'verifyYourIdentity', + desc: '', + args: [], + ); + } + + /// `Continue` + String get continueText { + return Intl.message( + 'Continue', + name: 'continueText', + desc: '', + args: [], + ); + } + + /// `Resend code` + String get resendCode { + return Intl.message( + 'Resend code', + name: 'resendCode', + desc: '', + args: [], + ); + } + + /// `Resend code in {time,plural, =1{1 second}other{{time} seconds}}` + String resendCodeWait(num time) { + return Intl.message( + 'Resend code in ${Intl.plural(time, one: '1 second', other: '$time seconds')}', + name: 'resendCodeWait', + desc: '', + args: [time], + ); + } + + /// `Please enter the security code from your authenticator app.` + String get totpAuthDescription { + return Intl.message( + 'Please enter the security code from your authenticator app.', + name: 'totpAuthDescription', + desc: '', + args: [], + ); + } + + /// `A security code has been sent to your phone at {contact}.` + String smsAuthDescription(Object contact) { + return Intl.message( + 'A security code has been sent to your phone at $contact.', + name: 'smsAuthDescription', + desc: '', + args: [contact], + ); + } + + /// `A security code has been sent to your email address at {contact}.` + String emailAuthDescription(Object contact) { + return Intl.message( + 'A security code has been sent to your email address at $contact.', + name: 'emailAuthDescription', + desc: '', + args: [contact], + ); + } + + /// `Please enter one of your backup codes.` + String get backupCodeAuthDescription { + return Intl.message( + 'Please enter one of your backup codes.', + name: 'backupCodeAuthDescription', + desc: '', + args: [], + ); + } + + /// `Invalid verification code format` + String get verificationCodeInvalid { + return Intl.message( + 'Invalid verification code format', + name: 'verificationCodeInvalid', + desc: '', + args: [], + ); + } + + /// `Code` + String get toptAuthPlaceholder { + return Intl.message( + 'Code', + name: 'toptAuthPlaceholder', + desc: '', + args: [], + ); + } + + /// `SMS code` + String get smsAuthPlaceholder { + return Intl.message( + 'SMS code', + name: 'smsAuthPlaceholder', + desc: '', + args: [], + ); + } + + /// `Email code` + String get emailAuthPlaceholder { + return Intl.message( + 'Email code', + name: 'emailAuthPlaceholder', + desc: '', + args: [], + ); + } + + /// `Backup code` + String get backupCodeAuthPlaceholder { + return Intl.message( + 'Backup code', + name: 'backupCodeAuthPlaceholder', + desc: '', + args: [], + ); + } + + /// `Verification code is incorrect` + String get verificationCodeIncorrect { + return Intl.message( + 'Verification code is incorrect', + name: 'verificationCodeIncorrect', + desc: '', + args: [], + ); + } + + /// `Too many requests check verification code` + String get verificationCodeManyRequest { + return Intl.message( + 'Too many requests check verification code', + name: 'verificationCodeManyRequest', + desc: '', + args: [], + ); + } + + /// `Try another way` + String get tryAnotherWay { + return Intl.message( + 'Try another way', + name: 'tryAnotherWay', + desc: '', + args: [], + ); + } + + /// `Select a way to verify` + String get selectWayToVerify { + return Intl.message( + 'Select a way to verify', + name: 'selectWayToVerify', + desc: '', + args: [], + ); + } + + /// `Authenticator app` + String get mfaProviderTopt { + return Intl.message( + 'Authenticator app', + name: 'mfaProviderTopt', + desc: '', + args: [], + ); + } + + /// `SMS` + String get mfaProviderSms { + return Intl.message( + 'SMS', + name: 'mfaProviderSms', + desc: '', + args: [], + ); + } + + /// `Email` + String get mfaProviderEmail { + return Intl.message( + 'Email', + name: 'mfaProviderEmail', + desc: '', + args: [], + ); + } + + /// `Backup code` + String get mfaProviderBackupCode { + return Intl.message( + 'Backup code', + name: 'mfaProviderBackupCode', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 81ce3ce..a77ba01 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -86,6 +86,27 @@ "notImplemented": "Not implemented!", "listIsEmptyText": "The list is currently empty.", - "tryAgain": "Try Again" + "tryAgain": "Try Again", + "verifyYourIdentity": "Verify your identity", + "continueText": "Continue", + "resendCode": "Resend code", + "resendCodeWait": "Resend code in {time,plural, =1{1 second}other{{time} seconds}}", + "totpAuthDescription": "Please enter the security code from your authenticator app.", + "smsAuthDescription": "A security code has been sent to your phone at {contact}.", + "emailAuthDescription": "A security code has been sent to your email address at {contact}.", + "backupCodeAuthDescription": "Please enter one of your backup codes.", + "verificationCodeInvalid": "Invalid verification code format", + "toptAuthPlaceholder": "Code", + "smsAuthPlaceholder": "SMS code", + "emailAuthPlaceholder": "Email code", + "backupCodeAuthPlaceholder": "Backup code", + "verificationCodeIncorrect": "Verification code is incorrect", + "verificationCodeManyRequest": "Too many requests check verification code", + "tryAnotherWay": "Try another way", + "selectWayToVerify": "Select a way to verify", + "mfaProviderTopt": "Authenticator app", + "mfaProviderSms": "SMS", + "mfaProviderEmail": "Email", + "mfaProviderBackupCode": "Backup code" } \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 7a5b615..6f84a97 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + alt_sms_autofill: + dependency: "direct main" + description: + name: alt_sms_autofill + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" archive: dependency: transitive description: @@ -586,7 +593,7 @@ packages: description: path: "." ref: master - resolved-ref: d8dcad26dade6fca574d600ab88a6cc0ccaf1a7d + resolved-ref: cb439261f3f54f76d1df14b8e2597b20f9c38a88 url: "git@github.com:thingsboard/dart_thingsboard_client.git" source: git version: "1.0.3" diff --git a/pubspec.yaml b/pubspec.yaml index f511a6c..776e1fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: preload_page_view: ^0.1.6 flutter_localizations: sdk: flutter + alt_sms_autofill: ^1.0.0 dev_dependencies: flutter_test: