Cw-263-TOTP-2FA-In-Security-Settings (#892)

* CW-263-TOTP-2FA-in-security-settings WIP

* Implement TOTP 2FA WIP

* Implement TOTP 2FA Authentication

* chore: Remove unneeded formatting

* revert formatting

* fixes

* CW-263-TOTP-2FA-in-security-settings WIP

* Setup TOTP Complete, left with Modify TOTF

* CW-263-TOTP-2FA-in-security-settings

* CW-263-TOTP-2FA-in-security-settings

* CW-263-TOTP-2FA-in-security-settings

* fix: Add copy-to-clipboard for qr secret key

* fix: Translation

* chore: Move strings into translation files

* feat: End to end flow for TOTP

* hotfix: Switch totp to use sha512

* Update strings; 8 digits and error explanation

* fix: Totp 2fa implementation feedback

* hotfix: same action for button and alert close

* feat: App should show both normal and totp auths when totp is enabled

* hotfix: prevent barrier from dismissing

* fix: Changes requested during PR review

* - Minor Enhancements
- Minor UI fixes

---------

Co-authored-by: Justin Ehrenhofer <justin.ehrenhofer@gmail.com>
Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Adegoke David
2023-05-17 17:43:23 +03:00
committed by GitHub
co-authored by Justin Ehrenhofer OmarHatem
parent 40f3ccbe42
commit 43e062d1ac
43 changed files with 1951 additions and 494 deletions
+44 -11
View File
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:flutter/material.dart';
@@ -10,6 +11,8 @@ import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:uni_links/uni_links.dart';
import '../setup_2fa/setup_2fa_enter_code_page.dart';
class Root extends StatefulWidget {
Root({
required Key key,
@@ -114,19 +117,49 @@ class RootState extends State<Root> with WidgetsBindingObserver {
if (_isInactive && !_postFrameCallback && _requestAuth) {
_postFrameCallback = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.navigatorKey.currentState?.pushNamed(Routes.unlock,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
if (!isAuthenticatedSuccessfully) {
return;
}
widget.navigatorKey.currentState?.pushNamed(
Routes.unlock,
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
if (!isAuthenticatedSuccessfully) {
return;
} else {
final useTotp = widget.appStore.settingsStore.useTOTP2FA;
if (useTotp) {
_reset();
auth.close(
route: Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
onTotpAuthenticationFinished:
(bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) {
if (!isAuthenticatedSuccessfully) {
return;
}
_reset();
totpAuth.close(
route: launchUri != null ? Routes.send : null,
arguments: PaymentRequest.fromUri(launchUri),
);
launchUri = null;
},
isForSetup: false,
isClosable: false,
),
);
} else {
_reset();
auth.close(
route: launchUri != null ? Routes.send : null,
arguments: PaymentRequest.fromUri(launchUri),
);
launchUri = null;
}
}
_reset();
auth.close(
route: launchUri != null ? Routes.send : null,
arguments: PaymentRequest.fromUri(launchUri),
},
);
launchUri = null;
});
});
} else if (launchUri != null) {
widget.navigatorKey.currentState?.pushNamed(
@@ -26,64 +26,81 @@ class SecurityBackupPage extends BasePage {
@override
Widget body(BuildContext context) {
return Container(
padding: EdgeInsets.only(top: 10),
child: Column(mainAxisSize: MainAxisSize.min, children: [
SettingsCellWithArrow(
title: S.current.show_keys,
handler: (_) => _authService.authenticateAction(context, route: Routes.showKeys),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
title: S.current.create_backup,
handler: (_) => _authService.authenticateAction(context, route: Routes.backup),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
title: S.current.settings_change_pin,
handler: (_) => _authService.authenticateAction(
context,
route: Routes.setupPin,
arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) {
setupPinContext.close();
},
padding: EdgeInsets.only(top: 10),
child: Column(mainAxisSize: MainAxisSize.min, children: [
SettingsCellWithArrow(
title: S.current.show_keys,
handler: (_) => _authService.authenticateAction(context, route: Routes.showKeys),
),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
if (DeviceInfo.instance.isMobile)
Observer(builder: (_) {
return SettingsSwitcherCell(
title: S.current.settings_allow_biometrical_authentication,
value: _securitySettingsViewModel.allowBiometricalAuthentication,
onValueChange: (BuildContext context, bool value) {
if (value) {
_authService.authenticateAction(context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (isAuthenticatedSuccessfully) {
if (await _securitySettingsViewModel.biometricAuthenticated()) {
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
title: S.current.create_backup,
handler: (_) => _authService.authenticateAction(context, route: Routes.backup),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
SettingsCellWithArrow(
title: S.current.settings_change_pin,
handler: (_) => _authService.authenticateAction(
context,
route: Routes.setupPin,
arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) {
setupPinContext.close();
},
),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
if (DeviceInfo.instance.isMobile)
Observer(builder: (_) {
return SettingsSwitcherCell(
title: S.current.settings_allow_biometrical_authentication,
value: _securitySettingsViewModel.allowBiometricalAuthentication,
onValueChange: (BuildContext context, bool value) {
if (value) {
_authService.authenticateAction(context,
onAuthSuccess: (isAuthenticatedSuccessfully) async {
if (isAuthenticatedSuccessfully) {
if (await _securitySettingsViewModel.biometricAuthenticated()) {
_securitySettingsViewModel
.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
}
} else {
_securitySettingsViewModel
.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
}
} else {
_securitySettingsViewModel
.setAllowBiometricalAuthentication(isAuthenticatedSuccessfully);
}
});
} else {
_securitySettingsViewModel.setAllowBiometricalAuthentication(value);
}
});
});
} else {
_securitySettingsViewModel.setAllowBiometricalAuthentication(value);
}
});
}),
Observer(builder: (_) {
return SettingsPickerCell<PinCodeRequiredDuration>(
title: S.current.require_pin_after,
items: PinCodeRequiredDuration.values,
selectedItem: _securitySettingsViewModel.pinCodeRequiredDuration,
onItemSelected: (PinCodeRequiredDuration code) {
_securitySettingsViewModel.setPinCodeRequiredDuration(code);
},
);
}),
Observer(builder: (_) {
return SettingsPickerCell<PinCodeRequiredDuration>(
title: S.current.require_pin_after,
items: PinCodeRequiredDuration.values,
selectedItem: _securitySettingsViewModel.pinCodeRequiredDuration,
onItemSelected: (PinCodeRequiredDuration code) {
_securitySettingsViewModel.setPinCodeRequiredDuration(code);
Observer(
builder: (context) {
return SettingsCellWithArrow(
title: _securitySettingsViewModel.useTotp2FA
? S.current.modify_2fa
: S.current.setup_2fa,
handler: (_) => _authService.authenticateAction(
context,
route: _securitySettingsViewModel.useTotp2FA
? Routes.modify2FAPage
: Routes.setup_2faPage,
),
);
},
);
}),
]),
),
],
),
);
}
}
@@ -0,0 +1,55 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import '../../../routes.dart';
class Modify2FAPage extends BasePage {
Modify2FAPage({required this.setup2FAViewModel});
final Setup2FAViewModel setup2FAViewModel;
@override
String get title => S.current.modify_2fa;
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SettingsCellWithArrow(
title: S.current.disable_cake_2fa,
handler: (_) async {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.current.disable_cake_2fa,
alertContent: S.current.question_to_disable_2fa,
leftButtonText: S.current.cancel,
rightButtonText: S.current.disable,
actionLeftButton: () {
Navigator.of(context).pop();
},
actionRightButton: () {
setup2FAViewModel.setUseTOTP2FA(false);
Navigator.pushNamedAndRemoveUntil(
context, Routes.dashboard, (route) => false);
},
);
},
);
}),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
],
),
);
}
}
+62
View File
@@ -0,0 +1,62 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import '../../widgets/standard_list.dart';
class Setup2FAPage extends BasePage {
Setup2FAPage({required this.setup2FAViewModel});
final Setup2FAViewModel setup2FAViewModel;
@override
String get title => S.current.setup_2fa;
@override
Widget body(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.current.important_note,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
height: 1.571,
color: Theme.of(context).primaryTextTheme.headline6!.color!,
),
),
SizedBox(height: 16),
Text(
S.current.setup_2fa_text,
style: TextStyle(
fontWeight: FontWeight.w400,
fontSize: 14,
height: 1.571,
color: Theme.of(context).primaryTextTheme.headline6!.color!,
),
),
],
),
),
SizedBox(height: 86),
SettingsCellWithArrow(
title: S.current.setup_totp_recommended,
handler: (_) => Navigator.of(context).pushNamed(Routes.setup_2faQRPage),
),
StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)),
],
),
);
}
}
@@ -0,0 +1,221 @@
import 'package:another_flushbar/flushbar.dart';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/auth_state.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart';
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
import '../../../palette.dart';
import '../../../routes.dart';
typedef OnTotpAuthenticationFinished = void Function(bool, TotpAuthCodePageState);
class TotpAuthCodePage extends StatefulWidget {
TotpAuthCodePage(
this.setup2FAViewModel, {
required this.totpArguments,
});
final Setup2FAViewModel setup2FAViewModel;
final TotpAuthArgumentsModel totpArguments;
@override
TotpAuthCodePageState createState() => TotpAuthCodePageState();
}
class TotpAuthCodePageState extends State<TotpAuthCodePage> {
final _key = GlobalKey<ScaffoldState>();
ReactionDisposer? _reaction;
Flushbar<void>? _authBar;
Flushbar<void>? _progressBar;
@override
void initState() {
_reaction ??= reaction((_) => widget.setup2FAViewModel.state, (ExecutionState state) {
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.totpArguments.onTotpAuthenticationFinished!(true, this);
});
}
if (state is FailureState) {
print(state.error);
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.totpArguments.onTotpAuthenticationFinished!(false, this);
});
}
if (state is AuthenticationBanned) {
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.totpArguments.onTotpAuthenticationFinished!(false, this);
});
}
});
super.initState();
}
@override
void dispose() {
_reaction?.reaction.dispose();
super.dispose();
}
void changeProcessText(String text) {
dismissFlushBar(_authBar);
_progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!);
}
Future<void> close({String? route, dynamic arguments}) async {
if (_key.currentContext == null) {
throw Exception('Key context is null. Should be not happened');
}
await Future<void>.delayed(Duration(milliseconds: 50));
if (route != null) {
Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments);
} else {
Navigator.of(_key.currentContext!).pop();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
key: _key,
resizeToAvoidBottomInset: false,
body: TOTPEnterCode(
setup2FAViewModel: widget.setup2FAViewModel,
isForSetup: widget.totpArguments.isForSetup ?? false,
isClosable: widget.totpArguments.isClosable ?? true,
),
);
}
void dismissFlushBar(Flushbar<dynamic>? bar) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await bar?.dismiss();
});
}
}
class TOTPEnterCode extends BasePage {
TOTPEnterCode({
required this.setup2FAViewModel,
required this.isForSetup,
required this.isClosable,
}) : totpController = TextEditingController() {
totpController.addListener(() {
setup2FAViewModel.enteredOTPCode = totpController.text;
});
}
@override
String get title => isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa;
Widget? leading(BuildContext context) {
return isClosable ? super.leading(context) : null;
}
final TextEditingController totpController;
final Setup2FAViewModel setup2FAViewModel;
final bool isForSetup;
final bool isClosable;
@override
Widget body(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
),
child: Column(
children: [
BaseTextFormField(
textAlign: TextAlign.left,
hintText: S.current.totp_code,
controller: totpController,
keyboardType: TextInputType.number,
placeholderTextStyle: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 16),
Text(
S.current.please_fill_totp,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.2,
color: Palette.darkGray,
),
textAlign: TextAlign.center,
),
Spacer(),
Observer(
builder: (context) {
return PrimaryButton(
isDisabled: setup2FAViewModel.enteredOTPCode.length != 8,
onPressed: () async {
final result =
await setup2FAViewModel.totp2FAAuth(totpController.text, isForSetup);
final bannedState = setup2FAViewModel.state is AuthenticationBanned;
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return PopUpCancellableAlertDialog(
contentText: _textDisplayedInPopupOnResult(result, bannedState, context),
actionButtonText: S.of(context).ok,
buttonAction: () {
result ? setup2FAViewModel.success() : null;
if (isForSetup && result) {
Navigator.pushNamedAndRemoveUntil(
context, Routes.dashboard, (route) => false);
} else {
Navigator.of(context).pop(result);
}
},
);
},
);
},
text: S.of(context).continue_text,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white,
);
},
),
SizedBox(height: 24),
],
),
);
}
String _textDisplayedInPopupOnResult(bool result, bool bannedState, BuildContext context) {
switch (result) {
case true:
return isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success;
case false:
if (bannedState) {
final state = setup2FAViewModel.state as AuthenticationBanned;
return S.of(context).failed_authentication(state.error);
} else {
return S.current.totp_2fa_failure;
}
default:
return S.current.enter_totp_code;
}
}
}
@@ -0,0 +1,145 @@
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart';
import 'package:qr_flutter/qr_flutter.dart' as qr;
import '../../../palette.dart';
import '../../widgets/primary_button.dart';
import '../../widgets/standard_list.dart';
class Setup2FAQRPage extends BasePage {
Setup2FAQRPage({required this.setup2FAViewModel});
final Setup2FAViewModel setup2FAViewModel;
@override
String get title => S.current.setup_2fa;
@override
Widget body(BuildContext context) {
final copyImage = Image.asset(
'assets/images/copy_content.png',
height: 12,
width: 12,
color: Color(0xFF355688),
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Column(
children: [
SizedBox(height: 58),
Text(
S.current.add_secret_code,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
height: 1.5714,
color: Palette.darkBlueCraiola,
),
),
SizedBox(height: 10),
AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context).accentTextTheme.headline2!.backgroundColor!,
),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Colors.white,
),
),
child: QrImage(
data: setup2FAViewModel.totpVersionOneLink,
version: qr.QrVersions.auto,
)),
),
),
SizedBox(height: 13),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
S.current.totp_secret_code,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Palette.darkGray,
height: 1.8333,
),
),
SizedBox(height: 8),
Text(
'${setup2FAViewModel.secretKey}',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
height: 1.375,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
SizedBox(width: 8),
Container(
width: 32,
height: 32,
child: InkWell(
onTap: () {
Clipboard.setData(ClipboardData(text: '${setup2FAViewModel.secretKey}'));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
color: Color(0xFFF2F0FA),
),
child: copyImage,
),
),
)
],
),
SizedBox(height: 8),
StandardListSeparator(),
Spacer(),
PrimaryButton(
onPressed: () {
Navigator.of(context).pushReplacementNamed(
Routes.totpAuthCodePage,
arguments: TotpAuthArgumentsModel(
isForSetup: true,
)
);
},
text: S.current.continue_text,
color: Theme.of(context).accentTextTheme.bodyLarge!.color!,
textColor: Colors.white,
),
SizedBox(height: 24),
],
),
);
}
}
@@ -0,0 +1,92 @@
import 'package:cake_wallet/src/widgets/alert_close_button.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/widgets/alert_background.dart';
class PopUpCancellableAlertDialog extends StatelessWidget {
final String contentText;
final String actionButtonText;
final VoidCallback? buttonAction;
final bool sameActionForButtonAndClose;
const PopUpCancellableAlertDialog({
super.key,
this.contentText = '',
this.actionButtonText = '',
this.buttonAction,
this.sameActionForButtonAndClose = true,
});
bool get barrierDismissible => false;
Color? get actionButtonTextColor => null;
Color? get actionButtonColor => null;
Widget content(BuildContext context) {
return Text(
contentText,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: 'Lato',
color: Theme.of(context).primaryTextTheme.titleLarge!.color!,
decoration: TextDecoration.none,
),
);
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => barrierDismissible ? Navigator.of(context).pop() : null,
child: AlertBackground(
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned(
top: 280,
child: Column(
children: [
Padding(
padding: EdgeInsets.only(left: 24, right: 24, top: 24),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
width: 340,
padding: EdgeInsets.all(10),
color: Theme.of(context).accentTextTheme.titleLarge!.decorationColor!,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Column(
children: [
Padding(
padding: EdgeInsets.fromLTRB(24, 8, 24, 32),
child: content(context),
),
PrimaryButton(
onPressed: buttonAction,
text: actionButtonText,
color: Color(0xffE9F2FC),
textColor: Palette.darkBlueCraiola,
),
],
),
],
),
),
),
),
],
),
),
AlertCloseButton(
onTap: sameActionForButtonAndClose ? buttonAction : null,
),
],
),
),
);
}
}
+3 -2
View File
@@ -2,7 +2,8 @@ import 'package:cake_wallet/palette.dart';
import 'package:flutter/material.dart';
class AlertCloseButton extends StatelessWidget {
AlertCloseButton({this.image, this.bottom});
AlertCloseButton({this.image, this.bottom, this.onTap});
final VoidCallback? onTap;
final Image? image;
final double? bottom;
@@ -17,7 +18,7 @@ class AlertCloseButton extends StatelessWidget {
return Positioned(
bottom: bottom ?? 60,
child: GestureDetector(
onTap: () => Navigator.of(context).pop(),
onTap: onTap ?? () => Navigator.of(context).pop(),
child: Container(
height: 42,
width: 42,