Add Customers/Tenants pages. Improve login page. Implemented profile page, change and request password reset pages.

This commit is contained in:
Igor Kulikov
2021-06-15 16:38:37 +03:00
parent 21e42820fd
commit 17ce15c98d
33 changed files with 1312 additions and 326 deletions

View File

@@ -2,6 +2,7 @@ import 'package:fluro/fluro.dart';
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/context/tb_context.dart';
import 'login/login_page.dart';
@@ -12,11 +13,16 @@ class AuthRoutes extends TbRoutes {
return LoginPage(tbContext);
});
late var resetPasswordRequestHandler = Handler(handlerFunc: (BuildContext? context, Map<String, dynamic> params) {
return ResetPasswordRequestPage(tbContext);
});
AuthRoutes(TbContext tbContext) : super(tbContext);
@override
void doRegisterRoutes(router) {
router.define("/login", handler: loginHandler);
router.define("/login/resetPasswordRequest", handler: resetPasswordRequestHandler);
}
}

View File

@@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:thingsboard_app/constants/assets_path.dart';
@@ -11,6 +12,8 @@ import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/widgets/tb_progress_indicator.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
import 'login_page_background.dart';
class LoginPage extends TbPageWidget<LoginPage, _LoginPageState> {
LoginPage(TbContext tbContext) : super(tbContext);
@@ -33,8 +36,7 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
final _isLoginNotifier = ValueNotifier<bool>(false);
final _showPasswordNotifier = ValueNotifier<bool>(false);
final usernameController = TextEditingController();
final passwordController = TextEditingController();
final _loginFormKey = GlobalKey<FormBuilderState>();
@override
void initState() {
@@ -43,8 +45,6 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
@override
void dispose() {
usernameController.dispose();
passwordController.dispose();
super.dispose();
}
@@ -53,183 +53,164 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
return Scaffold(
backgroundColor: Colors.white,
resizeToAvoidBottomInset: false,
body: ValueListenableBuilder<bool>(
valueListenable: _isLoginNotifier,
builder: (BuildContext context, bool loading, child) {
List<Widget> children = [
LoginPageBackground(),
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(28, 71, 28, 28),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight - (71 + 28)),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle,
height: 25,
color: Theme.of(context).primaryColor,
semanticsLabel: 'ThingsBoard Logo')
]
),
Container(height: 32),
Row(
children: [
Text(
'Login to your account',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 28,
height: 36 / 28
)
)]
),
Container(height: 48),
if (tbContext.hasOAuthClients)
_buildOAuth2Buttons(tbContext.oauth2ClientInfos!),
if (tbContext.hasOAuthClients)
Padding(padding: EdgeInsets.only(top: 10, bottom: 16),
child: Row(
children: [
Flexible(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('OR'),
),
Flexible(child: Divider())
],
)
),
TextField(
enabled: !loading,
controller: usernameController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email',
hintText: 'Enter valid email id as abc@gmail.com'),
),
Container(height: 28),
ValueListenableBuilder(
valueListenable: _showPasswordNotifier,
builder: (BuildContext context, bool showPassword, child) {
return TextField(
enabled: !loading,
controller: passwordController,
obscureText: !showPassword,
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: loading ? null : () {
_showPasswordNotifier.value = !_showPasswordNotifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'Password',
hintText: 'Enter secure password'),
);
}
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: loading ? null : () {
//TODO FORGOT PASSWORD SCREEN GOES HERE
},
child: Text(
'Forgot Password?',
style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary,
letterSpacing: 1,
fontSize: 12,
height: 16 / 12),
),
)
],
),
Spacer(),
ElevatedButton(
child: Text('Log In'),
style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
onPressed: loading ? null : () async {
_isLoginNotifier.value = true;
try {
await tbClient.login(
LoginRequest(usernameController.text,
passwordController.text));
} catch (e) {
_isLoginNotifier.value = false;
}
},
),
Container(
height: 24,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('New User?',
style: TextStyle(
fontSize: 14,
height: 14 / 20
)),
TextButton(
onPressed: loading ? null : () {
//TODO CREATE ACCOUNT SCREEN GOES HERE
},
child: Text(
'Create Account',
style: TextStyle(color: loading ? Colors.black12 : Theme.of(context).colorScheme.primary,
letterSpacing: 1,
fontSize: 14,
height: 14 / 20),
),
)
],
)
]
),
)
)
);
},
)
)
];
if (loading) {
var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window);
var bottomPadding = data.padding.top;
bottomPadding += kToolbarHeight;
children.add(
SizedBox.expand(
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
child: Container(
decoration: new BoxDecoration(
color: Colors.grey.shade200.withOpacity(0.2)
body: Stack(
children: [
LoginPageBackground(),
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(24, 71, 24, 24),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight - (71 + 24)),
child: IntrinsicHeight(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
SvgPicture.asset(ThingsboardImage.thingsBoardWithTitle,
height: 25,
color: Theme.of(context).primaryColor,
semanticsLabel: 'ThingsBoard Logo')
]
),
child: Container(
padding: EdgeInsets.only(bottom: bottomPadding),
alignment: Alignment.center,
child: TbProgressIndicator(size: 50.0),
),
)
SizedBox(height: 32),
Row(
children: [
Text(
'Login to your account',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 28,
height: 36 / 28
)
)]
),
SizedBox(height: 48),
if (tbContext.hasOAuthClients)
_buildOAuth2Buttons(tbContext.oauth2ClientInfos!),
if (tbContext.hasOAuthClients)
Padding(padding: EdgeInsets.only(top: 10, bottom: 16),
child: Row(
children: [
Flexible(child: Divider()),
Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Text('OR'),
),
Flexible(child: Divider())
],
)
),
FormBuilder(
key: _loginFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FormBuilderTextField(
name: 'username',
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Email is required.'),
FormBuilderValidators.email(context, errorText: 'Invalid email format.')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email'
),
),
SizedBox(height: 28),
ValueListenableBuilder(
valueListenable: _showPasswordNotifier,
builder: (BuildContext context, bool showPassword, child) {
return FormBuilderTextField(
name: 'password',
obscureText: !showPassword,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Password is required.')
]),
decoration: InputDecoration(
suffixIcon: IconButton(
icon: Icon(showPassword ? Icons.visibility : Icons.visibility_off),
onPressed: () {
_showPasswordNotifier.value = !_showPasswordNotifier.value;
},
),
border: OutlineInputBorder(),
labelText: 'Password'
),
);
}
)
],
)
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
_forgotPassword();
},
child: Text(
'Forgot Password?',
style: TextStyle(color: Theme.of(context).colorScheme.primary,
letterSpacing: 1,
fontSize: 12,
height: 16 / 12),
),
)
],
),
Spacer(),
ElevatedButton(
child: Text('Log In'),
style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
onPressed: () {
_login();
},
),
SizedBox(height: 48)
]
),
)
)
)
);
);
},
)
),
ValueListenableBuilder<bool>(
valueListenable: _isLoginNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
var data = MediaQueryData.fromWindow(WidgetsBinding.instance!.window);
var bottomPadding = data.padding.top;
bottomPadding += kToolbarHeight;
return SizedBox.expand(
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5.0, sigmaY: 5.0),
child: Container(
decoration: new BoxDecoration(
color: Colors.grey.shade200.withOpacity(0.2)
),
child: Container(
padding: EdgeInsets.only(bottom: bottomPadding),
alignment: Alignment.center,
child: TbProgressIndicator(size: 50.0),
),
)
)
)
);
} else {
return SizedBox.shrink();
}
}
return Stack(
children: children,
);
})
)
]
)
);
}
@@ -321,48 +302,23 @@ class _LoginPageState extends TbPageState<LoginPage, _LoginPageState> {
_isLoginNotifier.value = false;
}
}
}
class LoginPageBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: CustomPaint(
painter: _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor),
)
);
void _login() async {
FocusScope.of(context).unfocus();
if (_loginFormKey.currentState?.saveAndValidate() ?? false) {
var formValue = _loginFormKey.currentState!.value;
String username = formValue['username'];
String password = formValue['password'];
_isLoginNotifier.value = true;
try {
await tbClient.login(LoginRequest(username, password));
} catch (e) {
_isLoginNotifier.value = false;
}
}
}
}
class _LoginPageBackgroundPainter extends CustomPainter {
final Color color;
const _LoginPageBackgroundPainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color.withAlpha(14);
paint.style = PaintingStyle.fill;
var topPath = Path();
topPath.moveTo(0, 0);
topPath.lineTo(size.width / 2, 0);
topPath.lineTo(0, size.height / 10);
topPath.close();
canvas.drawPath(topPath, paint);
var bottomPath = Path();
bottomPath.moveTo(0, size.height * 0.98);
bottomPath.lineTo(size.width, size.height * 0.78);
bottomPath.lineTo(size.width, size.height);
bottomPath.lineTo(0, size.height);
bottomPath.close();
canvas.drawPath(bottomPath, paint);
}
@override
bool shouldRepaint(covariant _LoginPageBackgroundPainter oldDelegate) {
return color != oldDelegate.color;
void _forgotPassword() async {
navigateTo('/login/resetPasswordRequest');
}
}

View File

@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class LoginPageBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: CustomPaint(
painter: _LoginPageBackgroundPainter(color: Theme.of(context).primaryColor),
)
);
}
}
class _LoginPageBackgroundPainter extends CustomPainter {
final Color color;
const _LoginPageBackgroundPainter({required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = color.withAlpha(14);
paint.style = PaintingStyle.fill;
var topPath = Path();
topPath.moveTo(0, 0);
topPath.lineTo(size.width / 2, 0);
topPath.lineTo(0, size.height / 10);
topPath.close();
canvas.drawPath(topPath, paint);
var bottomPath = Path();
bottomPath.moveTo(0, size.height * 0.98);
bottomPath.lineTo(size.width, size.height * 0.78);
bottomPath.lineTo(size.width, size.height);
bottomPath.lineTo(0, size.height);
bottomPath.close();
canvas.drawPath(bottomPath, paint);
}
@override
bool shouldRepaint(covariant _LoginPageBackgroundPainter oldDelegate) {
return color != oldDelegate.color;
}
}

View File

@@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_form_builder/flutter_form_builder.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/widgets/tb_progress_indicator.dart';
class ResetPasswordRequestPage extends TbPageWidget<ResetPasswordRequestPage, _ResetPasswordRequestPageState> {
ResetPasswordRequestPage(TbContext tbContext) : super(tbContext);
@override
_ResetPasswordRequestPageState createState() => _ResetPasswordRequestPageState();
}
class _ResetPasswordRequestPageState extends TbPageState<ResetPasswordRequestPage, _ResetPasswordRequestPageState> {
final _isLoadingNotifier = ValueNotifier<bool>(false);
final _resetPasswordFormKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack( children: [
LoginPageBackground(),
SizedBox.expand(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: TbAppBar(
tbContext,
title: Text('Reset password'),
),
body: Stack(
children: [
SizedBox.expand(
child: Padding(
padding: EdgeInsets.all(24),
child: FormBuilder(
key: _resetPasswordFormKey,
autovalidateMode: AutovalidateMode.disabled,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: 16),
Text('Enter the email associated with your account and we\'ll send an email with password reset link',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 14,
height: 24 / 14
),
),
SizedBox(height: 61),
FormBuilderTextField(
name: 'email',
autofocus: true,
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context, errorText: 'Email is required.'),
FormBuilderValidators.email(context, errorText: 'Invalid email format.')
]),
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Email *'
),
),
Spacer(),
ElevatedButton(
child: Text('Request password reset'),
style: ElevatedButton.styleFrom(padding: EdgeInsets.symmetric(vertical: 16)),
onPressed: () {
_requestPasswordReset();
},
)
]
)
)
)
),
ValueListenableBuilder<bool>(
valueListenable: _isLoadingNotifier,
builder: (BuildContext context, bool loading, child) {
if (loading) {
return SizedBox.expand(
child: Container(
color: Color(0x99FFFFFF),
child: Center(child: TbProgressIndicator(size: 50.0)),
)
);
} else {
return SizedBox.shrink();
}
}
)
]
)
)
)
])
);
}
void _requestPasswordReset() async {
FocusScope.of(context).unfocus();
if (_resetPasswordFormKey.currentState?.saveAndValidate() ?? false) {
var formValue = _resetPasswordFormKey.currentState!.value;
String email = formValue['email'];
_isLoadingNotifier.value = true;
try {
await Future.delayed(Duration(milliseconds: 300));
await tbClient.sendResetPasswordLink(email);
_isLoadingNotifier.value = false;
showSuccessNotification('Password reset link was successfully sent!');
} catch(e) {
_isLoadingNotifier.value = false;
}
}
}
}

View File

@@ -371,7 +371,7 @@ class TbContext {
replace = true;
clearStack = true;
}
if (isOpenedDashboard) {
if (transition != TransitionType.nativeModal && isOpenedDashboard) {
transition = TransitionType.none;
} else if (transition == null) {
if (replace) {
@@ -392,6 +392,15 @@ class TbContext {
await _mainDashboardHolder?.navigateToDashboard(dashboardId, dashboardTitle: dashboardTitle, state: state, hideToolbar: hideToolbar, animate: animate);
}
Future<T?> showFullScreenDialog<T>(Widget dialog) {
return Navigator.of(currentState!.context).push<T>(new MaterialPageRoute<T>(
builder: (BuildContext context) {
return dialog;
},
fullscreenDialog: true
));
}
void pop<T>([T? result, BuildContext? context]) {
var targetContext = context ?? currentState?.context;
if (targetContext != null) {

View File

@@ -6,6 +6,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:intl/intl.dart';
import 'package:thingsboard_app/core/context/tb_context.dart';
import 'package:thingsboard_app/core/context/tb_context_widget.dart';
import 'package:thingsboard_app/utils/utils.dart';
import 'package:thingsboard_client/thingsboard_client.dart';
const Map<EntityType, String> entityTypeTranslations = {
@@ -75,6 +76,77 @@ mixin EntitiesBase<T, P> on HasTbContext {
}
mixin ContactBasedBase<T extends ContactBased, P> on EntitiesBase<T,P> {
@override
Widget buildEntityListCard(BuildContext context, T contact) {
var address = Utils.contactToShortAddress(contact);
return Container(
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
fit: FlexFit.tight,
child:
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text('${contact.getName()}',
style: TextStyle(
color: Color(0xFF282828),
fontSize: 14,
fontWeight: FontWeight.w500,
height: 20 / 14
))
),
Text(entityDateFormat.format(DateTime.fromMillisecondsSinceEpoch(contact.createdTime!)),
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
),
SizedBox(height: 4),
if (contact.email != null) Text(contact.email!,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
)),
if (contact.email == null)
SizedBox(height: 16),
if (address != null) SizedBox(height: 4),
if (address != null) Text(address,
style: TextStyle(
color: Color(0xFFAFAFAF),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
)),
],
)
),
SizedBox(width: 16),
Icon(Icons.chevron_right, color: Color(0xFFACACAC)),
SizedBox(width: 8)
],
),
);
}
}
abstract class PageKeyController<P> extends ValueNotifier<PageKeyValue<P>> {
PageKeyController(P initialPageKey) : super(PageKeyValue(initialPageKey));

View File

@@ -9,8 +9,21 @@ import 'package:thingsboard_client/thingsboard_client.dart';
abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget<EntityDetailsPage<T>, _EntityDetailsPageState<T>> {
final labelTextStyle = TextStyle(
color: Color(0xFF757575),
fontSize: 14,
height: 20 / 14
);
final valueTextStyle = TextStyle(
color: Color(0xFF282828),
fontSize: 14,
height: 20 / 14
);
final String _defaultTitle;
final String _entityId;
final String? _subTitle;
final bool _showLoadingIndicator;
final bool _hideAppBar;
final double? _appBarElevation;
@@ -18,11 +31,13 @@ abstract class EntityDetailsPage<T extends BaseData> extends TbPageWidget<Entity
EntityDetailsPage(TbContext tbContext,
{required String defaultTitle,
required String entityId,
String? subTitle,
bool showLoadingIndicator = true,
bool hideAppBar = false,
double? appBarElevation}):
this._defaultTitle = defaultTitle,
this._entityId = entityId,
this._subTitle = subTitle,
this._showLoadingIndicator = showLoadingIndicator,
this._hideAppBar = hideAppBar,
this._appBarElevation = appBarElevation,
@@ -66,17 +81,33 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: widget._hideAppBar ? null : TbAppBar(
tbContext,
showLoadingIndicator: widget._showLoadingIndicator,
elevation: widget._appBarElevation,
title: ValueListenableBuilder<String>(
valueListenable: titleValue,
builder: (context, title, widget) {
return FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title)
builder: (context, title, _widget) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FittedBox(
fit: BoxFit.fitWidth,
alignment: Alignment.centerLeft,
child: Text(title,
style: widget._subTitle != null ? Theme.of(context).primaryTextTheme.headline6!.copyWith(
fontSize: 16
) : null
)
),
if (widget._subTitle != null) Text(widget._subTitle!, style: TextStyle(
color: Theme.of(context).primaryTextTheme.headline6!.color!.withAlpha((0.38 * 255).ceil()),
fontSize: 12,
fontWeight: FontWeight.normal,
height: 16 / 12
))
]
);
},
),
@@ -98,3 +129,77 @@ class _EntityDetailsPageState<T extends BaseData> extends TbPageState<EntityDeta
}
}
abstract class ContactBasedDetailsPage<T extends ContactBased> extends EntityDetailsPage<T> {
ContactBasedDetailsPage(TbContext tbContext,
{ required String defaultTitle,
required String entityId,
String? subTitle,
bool showLoadingIndicator = true,
bool hideAppBar = false,
double? appBarElevation}):
super(tbContext, defaultTitle: defaultTitle, entityId: entityId,
subTitle: subTitle, showLoadingIndicator: showLoadingIndicator,
hideAppBar: hideAppBar, appBarElevation: appBarElevation);
@override
Widget buildEntityDetails(BuildContext context, T contact) {
return Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text('Title', style: labelTextStyle),
Text(contact.getName(), style: valueTextStyle),
SizedBox(height: 16),
Text('Country', style: labelTextStyle),
Text(contact.country ?? '', style: valueTextStyle),
SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.max,
children: [
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text('City', style: labelTextStyle),
Text(contact.city ?? '', style: valueTextStyle),
],
)),
Flexible(
fit: FlexFit.tight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Text('State / Province', style: labelTextStyle),
Text(contact.state ?? '', style: valueTextStyle),
],
)),
],
),
SizedBox(height: 16),
Text('Zip / Postal Code', style: labelTextStyle),
Text(contact.zip ?? '', style: valueTextStyle),
SizedBox(height: 16),
Text('Address', style: labelTextStyle),
Text(contact.address ?? '', style: valueTextStyle),
SizedBox(height: 16),
Text('Address 2', style: labelTextStyle),
Text(contact.address2 ?? '', style: valueTextStyle),
SizedBox(height: 16),
Text('Phone', style: labelTextStyle),
Text(contact.phone ?? '', style: valueTextStyle),
SizedBox(height: 16),
Text('Email', style: labelTextStyle),
Text(contact.email ?? '', style: valueTextStyle),
]
)
);
}
}