Two factor auth support
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:thingsboard_app/config/routes/router.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/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 'package:thingsboard_app/core/context/tb_context.dart';
|
||||||
|
|
||||||
import 'login/login_page.dart';
|
import 'login/login_page.dart';
|
||||||
@@ -18,6 +19,11 @@ class AuthRoutes extends TbRoutes {
|
|||||||
return ResetPasswordRequestPage(tbContext);
|
return ResetPasswordRequestPage(tbContext);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
late var twoFactorAuthenticationHandler = Handler(
|
||||||
|
handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
|
||||||
|
return TwoFactorAuthenticationPage(tbContext);
|
||||||
|
});
|
||||||
|
|
||||||
AuthRoutes(TbContext tbContext) : super(tbContext);
|
AuthRoutes(TbContext tbContext) : super(tbContext);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -25,5 +31,6 @@ class AuthRoutes extends TbRoutes {
|
|||||||
router.define("/login", handler: loginHandler);
|
router.define("/login", handler: loginHandler);
|
||||||
router.define("/login/resetPasswordRequest",
|
router.define("/login/resetPasswordRequest",
|
||||||
handler: resetPasswordRequestHandler);
|
handler: resetPasswordRequestHandler);
|
||||||
|
router.define("/login/mfa", handler: twoFactorAuthenticationHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
@@ -38,6 +39,11 @@ class _LoginPageState extends TbPageState<LoginPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
if (tbClient.isPreVerificationToken()) {
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
navigateTo('/login/mfa');
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -107,6 +113,8 @@ class _LoginPageState extends TbPageState<LoginPage> {
|
|||||||
children: [
|
children: [
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: 'username',
|
name: 'username',
|
||||||
|
keyboardType:
|
||||||
|
TextInputType.emailAddress,
|
||||||
validator:
|
validator:
|
||||||
FormBuilderValidators.compose([
|
FormBuilderValidators.compose([
|
||||||
FormBuilderValidators.required(
|
FormBuilderValidators.required(
|
||||||
|
|||||||
504
lib/core/auth/login/two_factor_authentication_page.dart
Normal file
504
lib/core/auth/login/two_factor_authentication_page.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,6 +104,7 @@ class TbContext {
|
|||||||
final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false);
|
final ValueNotifier<bool> _isAuthenticated = ValueNotifier(false);
|
||||||
PlatformType? _oauth2PlatformType;
|
PlatformType? _oauth2PlatformType;
|
||||||
List<OAuth2ClientInfo>? oauth2ClientInfos;
|
List<OAuth2ClientInfo>? oauth2ClientInfos;
|
||||||
|
List<TwoFaProviderInfo>? twoFactorAuthProviders;
|
||||||
User? userDetails;
|
User? userDetails;
|
||||||
HomeDashboardInfo? homeDashboard;
|
HomeDashboardInfo? homeDashboard;
|
||||||
final _isLoadingNotifier = ValueNotifier<bool>(false);
|
final _isLoadingNotifier = ValueNotifier<bool>(false);
|
||||||
@@ -256,7 +257,7 @@ class TbContext {
|
|||||||
try {
|
try {
|
||||||
log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}');
|
log.debug('onUserLoaded: isAuthenticated=${tbClient.isAuthenticated()}');
|
||||||
isUserLoaded = true;
|
isUserLoaded = true;
|
||||||
if (tbClient.isAuthenticated()) {
|
if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) {
|
||||||
log.debug('authUser: ${tbClient.getAuthUser()}');
|
log.debug('authUser: ${tbClient.getAuthUser()}');
|
||||||
if (tbClient.getAuthUser()!.userId != null) {
|
if (tbClient.getAuthUser()!.userId != null) {
|
||||||
try {
|
try {
|
||||||
@@ -272,6 +273,14 @@ class TbContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (tbClient.isPreVerificationToken()) {
|
||||||
|
log.debug('authUser: ${tbClient.getAuthUser()}');
|
||||||
|
twoFactorAuthProviders = await tbClient
|
||||||
|
.getTwoFactorAuthService()
|
||||||
|
.getAvailableLoginTwoFaProviders();
|
||||||
|
} else {
|
||||||
|
twoFactorAuthProviders = null;
|
||||||
|
}
|
||||||
userDetails = null;
|
userDetails = null;
|
||||||
homeDashboard = null;
|
homeDashboard = null;
|
||||||
oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients(
|
oauth2ClientInfos = await tbClient.getOAuth2Service().getOAuth2Clients(
|
||||||
@@ -308,14 +317,15 @@ class TbContext {
|
|||||||
|
|
||||||
Listenable get isAuthenticatedListenable => _isAuthenticated;
|
Listenable get isAuthenticatedListenable => _isAuthenticated;
|
||||||
|
|
||||||
bool get isAuthenticated => _isAuthenticated.value;
|
bool get isAuthenticated =>
|
||||||
|
_isAuthenticated.value && !tbClient.isPreVerificationToken();
|
||||||
|
|
||||||
bool get hasOAuthClients =>
|
bool get hasOAuthClients =>
|
||||||
oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty;
|
oauth2ClientInfos != null && oauth2ClientInfos!.isNotEmpty;
|
||||||
|
|
||||||
Future<void> updateRouteState() async {
|
Future<void> updateRouteState() async {
|
||||||
if (currentState != null) {
|
if (currentState != null) {
|
||||||
if (tbClient.isAuthenticated()) {
|
if (tbClient.isAuthenticated() && !tbClient.isPreVerificationToken()) {
|
||||||
var defaultDashboardId = _defaultDashboardId();
|
var defaultDashboardId = _defaultDashboardId();
|
||||||
if (defaultDashboardId != null) {
|
if (defaultDashboardId != null) {
|
||||||
bool fullscreen = _userForceFullscreen();
|
bool fullscreen = _userForceFullscreen();
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
|
|||||||
class MessageLookup extends MessageLookupByLibrary {
|
class MessageLookup extends MessageLookupByLibrary {
|
||||||
String get localeName => 'en';
|
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);
|
final messages = _notInlinedMessages(_notInlinedMessages);
|
||||||
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||||
"No": MessageLookupByLibrary.simpleMessage("No"),
|
"No": MessageLookupByLibrary.simpleMessage("No"),
|
||||||
@@ -46,9 +55,14 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"auditLogDetails":
|
"auditLogDetails":
|
||||||
MessageLookupByLibrary.simpleMessage("Audit log details"),
|
MessageLookupByLibrary.simpleMessage("Audit log details"),
|
||||||
"auditLogs": MessageLookupByLibrary.simpleMessage("Audit Logs"),
|
"auditLogs": MessageLookupByLibrary.simpleMessage("Audit Logs"),
|
||||||
|
"backupCodeAuthDescription": MessageLookupByLibrary.simpleMessage(
|
||||||
|
"Please enter one of your backup codes."),
|
||||||
|
"backupCodeAuthPlaceholder":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Backup code"),
|
||||||
"changePassword":
|
"changePassword":
|
||||||
MessageLookupByLibrary.simpleMessage("Change Password"),
|
MessageLookupByLibrary.simpleMessage("Change Password"),
|
||||||
"city": MessageLookupByLibrary.simpleMessage("City"),
|
"city": MessageLookupByLibrary.simpleMessage("City"),
|
||||||
|
"continueText": MessageLookupByLibrary.simpleMessage("Continue"),
|
||||||
"country": MessageLookupByLibrary.simpleMessage("Country"),
|
"country": MessageLookupByLibrary.simpleMessage("Country"),
|
||||||
"currentPassword":
|
"currentPassword":
|
||||||
MessageLookupByLibrary.simpleMessage("currentPassword"),
|
MessageLookupByLibrary.simpleMessage("currentPassword"),
|
||||||
@@ -60,6 +74,9 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"customers": MessageLookupByLibrary.simpleMessage("Customers"),
|
"customers": MessageLookupByLibrary.simpleMessage("Customers"),
|
||||||
"devices": MessageLookupByLibrary.simpleMessage("Devices"),
|
"devices": MessageLookupByLibrary.simpleMessage("Devices"),
|
||||||
"email": MessageLookupByLibrary.simpleMessage("Email"),
|
"email": MessageLookupByLibrary.simpleMessage("Email"),
|
||||||
|
"emailAuthDescription": m0,
|
||||||
|
"emailAuthPlaceholder":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Email code"),
|
||||||
"emailInvalidText":
|
"emailInvalidText":
|
||||||
MessageLookupByLibrary.simpleMessage("Invalid email format."),
|
MessageLookupByLibrary.simpleMessage("Invalid email format."),
|
||||||
"emailRequireText":
|
"emailRequireText":
|
||||||
@@ -83,6 +100,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"logoDefaultValue":
|
"logoDefaultValue":
|
||||||
MessageLookupByLibrary.simpleMessage("Thingsboard Logo"),
|
MessageLookupByLibrary.simpleMessage("Thingsboard Logo"),
|
||||||
"logout": MessageLookupByLibrary.simpleMessage("Log Out"),
|
"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"),
|
"more": MessageLookupByLibrary.simpleMessage("More"),
|
||||||
"newPassword": MessageLookupByLibrary.simpleMessage("newPassword"),
|
"newPassword": MessageLookupByLibrary.simpleMessage("newPassword"),
|
||||||
"newPassword2": MessageLookupByLibrary.simpleMessage("newPassword2"),
|
"newPassword2": MessageLookupByLibrary.simpleMessage("newPassword2"),
|
||||||
@@ -117,6 +140,12 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"Profile successfully updated"),
|
"Profile successfully updated"),
|
||||||
"requestPasswordReset":
|
"requestPasswordReset":
|
||||||
MessageLookupByLibrary.simpleMessage("Request password reset"),
|
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":
|
"stateOrProvince":
|
||||||
MessageLookupByLibrary.simpleMessage("State / Province"),
|
MessageLookupByLibrary.simpleMessage("State / Province"),
|
||||||
"systemAdministrator":
|
"systemAdministrator":
|
||||||
@@ -124,8 +153,21 @@ class MessageLookup extends MessageLookupByLibrary {
|
|||||||
"tenantAdministrator":
|
"tenantAdministrator":
|
||||||
MessageLookupByLibrary.simpleMessage("Tenant Administrator"),
|
MessageLookupByLibrary.simpleMessage("Tenant Administrator"),
|
||||||
"title": MessageLookupByLibrary.simpleMessage("Title"),
|
"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"),
|
"tryAgain": MessageLookupByLibrary.simpleMessage("Try Again"),
|
||||||
|
"tryAnotherWay":
|
||||||
|
MessageLookupByLibrary.simpleMessage("Try another way"),
|
||||||
"type": MessageLookupByLibrary.simpleMessage("Type"),
|
"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")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -779,6 +779,216 @@ class S {
|
|||||||
args: [],
|
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> {
|
class AppLocalizationDelegate extends LocalizationsDelegate<S> {
|
||||||
|
|||||||
@@ -86,6 +86,27 @@
|
|||||||
"notImplemented": "Not implemented!",
|
"notImplemented": "Not implemented!",
|
||||||
|
|
||||||
"listIsEmptyText": "The list is currently empty.",
|
"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"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
alt_sms_autofill:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: alt_sms_autofill
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
archive:
|
archive:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -586,7 +593,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "."
|
path: "."
|
||||||
ref: master
|
ref: master
|
||||||
resolved-ref: d8dcad26dade6fca574d600ab88a6cc0ccaf1a7d
|
resolved-ref: cb439261f3f54f76d1df14b8e2597b20f9c38a88
|
||||||
url: "git@github.com:thingsboard/dart_thingsboard_client.git"
|
url: "git@github.com:thingsboard/dart_thingsboard_client.git"
|
||||||
source: git
|
source: git
|
||||||
version: "1.0.3"
|
version: "1.0.3"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ dependencies:
|
|||||||
preload_page_view: ^0.1.6
|
preload_page_view: ^0.1.6
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
alt_sms_autofill: ^1.0.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user