Two factor auth support

This commit is contained in:
Igor Kulikov
2022-08-17 17:12:19 +03:00
parent 519e67cce8
commit 4cc90ca20b
9 changed files with 816 additions and 6 deletions

View File

@@ -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<String, dynamic> 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);
}
}

View File

@@ -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<LoginPage> {
@override
void initState() {
super.initState();
if (tbClient.isPreVerificationToken()) {
SchedulerBinding.instance.addPostFrameCallback((_) {
navigateTo('/login/mfa');
});
}
}
@override
@@ -107,6 +113,8 @@ class _LoginPageState extends TbPageState<LoginPage> {
children: [
FormBuilderTextField(
name: 'username',
keyboardType:
TextInputType.emailAddress,
validator:
FormBuilderValidators.compose([
FormBuilderValidators.required(

View File

@@ -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<TwoFaProviderType, TwoFactorAuthProviderLoginData>
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<TwoFactorAuthenticationPage> {
static RegExp smsCodeRegExp = new RegExp(r"(\d{6})");
final _twoFactorAuthFormKey = GlobalKey<FormBuilderState>();
ValueNotifier<TwoFaProviderType?> _selectedProvider =
ValueNotifier<TwoFaProviderType?>(null);
TwoFaProviderType? _prevProvider;
int? _minVerificationPeriod;
List<TwoFaProviderType> _allowProviders = [];
ValueNotifier<bool> _disableSendButton = ValueNotifier<bool>(false);
ValueNotifier<bool> _showResendAction = ValueNotifier<bool>(false);
ValueNotifier<bool> _hideResendButton = ValueNotifier<bool>(true);
Timer? _timer;
Timer? _tooManyRequestsTimer;
ValueNotifier<int> _countDownTime = ValueNotifier<int>(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<TwoFaProviderType?>(
valueListenable: _selectedProvider,
builder: (context, providerType, _widget) {
if (providerType == null) {
var children = <Widget>[
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<bool>(
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<FormFieldValidator<String>> 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<void> _startListenForSmsCode() async {
_listenForSms = true;
_listenForSmsCode();
}
Future<void> _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<void> _cancelSmsCodeListen() async {
_listenForSms = false;
AltSmsAutofill().unregisterListener();
}
Future<void> _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<void> _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<void> _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<bool> _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;
}
}
}
}

View File

@@ -104,6 +104,7 @@ class TbContext {
final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false);
PlatformType? _oauth2PlatformType;
List<OAuth2ClientInfo>? oauth2ClientInfos;
List<TwoFaProviderInfo>? twoFactorAuthProviders;
User? userDetails;
HomeDashboardInfo? homeDashboard;
final _isLoadingNotifier = ValueNotifier<bool>(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<void> updateRouteState() async {
if (currentState != null) {
if (tbClient.isAuthenticated()) {
if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) {
var defaultDashboardId = _defaultDashboardId();
if (defaultDashboardId != null) {
bool fullscreen = _userForceFullscreen();

View File

@@ -20,6 +20,15 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> 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<String, Function> _notInlinedMessages(_) => <String, Function>{
"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")
};
}

View File

@@ -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<S> {

View File

@@ -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"
}

View File

@@ -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"

View File

@@ -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: