ios permissions

This commit is contained in:
Hillel Coren 2022-01-16 21:07:43 +02:00
parent 159e82811a
commit 755e73d63a
3 changed files with 442 additions and 438 deletions

View File

@ -295,449 +295,451 @@ class _LoginState extends State<LoginView> {
final double horizontalPadding = final double horizontalPadding =
calculateLayout(context) == AppLayout.desktop ? 40 : 16; calculateLayout(context) == AppLayout.desktop ? 40 : 16;
return ScrollableListView( return SafeArea(
children: <Widget>[ child: ScrollableListView(
if (isDesktopOS()) children: <Widget>[
AppTitleBar() if (isDesktopOS())
else AppTitleBar()
Container( else
width: double.infinity, Container(
height: 24, width: double.infinity,
color: state.accentColor, height: 24,
), color: state.accentColor,
Padding( ),
padding: EdgeInsets.symmetric(vertical: 25), Padding(
child: Center( padding: EdgeInsets.symmetric(vertical: 25),
child: InkWell( child: Center(
// TODO correct this child: InkWell(
child: Image.asset( // TODO correct this
state.prefState.enableDarkMode child: Image.asset(
? 'assets/images/logo_dark.png' state.prefState.enableDarkMode
: 'assets/images/logo_light.png', ? 'assets/images/logo_dark.png'
height: 50), : 'assets/images/logo_light.png',
onTap: isApple() height: 50),
? null onTap: isApple()
: () { ? null
launch(kSiteUrl, : () {
forceSafariVC: false, forceWebView: false); launch(kSiteUrl,
}, forceSafariVC: false, forceWebView: false);
onLongPress: () { },
if (kReleaseMode) { onLongPress: () {
return; if (kReleaseMode) {
} return;
}
setState(() => _tokenLogin = !_tokenLogin); setState(() => _tokenLogin = !_tokenLogin);
}, },
),
), ),
), ),
), if (_tokenLogin)
if (_tokenLogin) FormCard(
FormCard( forceNarrow: calculateLayout(context) != AppLayout.mobile,
forceNarrow: calculateLayout(context) != AppLayout.mobile, children: [
children: [ DecoratedFormField(
DecoratedFormField( autofocus: true,
autofocus: true, label: localization.token,
label: localization.token, controller: _tokenController,
controller: _tokenController, keyboardType: TextInputType.text,
keyboardType: TextInputType.text, ),
), AppButton(
AppButton( label: localization.submit.toUpperCase(),
label: localization.submit.toUpperCase(), onPressed: () {
onPressed: () { final Completer<Null> completer = Completer<Null>();
final Completer<Null> completer = Completer<Null>(); viewModel.onTokenLoginPressed(context, completer,
viewModel.onTokenLoginPressed(context, completer, token: _tokenController.text);
token: _tokenController.text); },
}, )
) ],
], ),
), AnimatedOpacity(
AnimatedOpacity( duration: Duration(milliseconds: 500),
duration: Duration(milliseconds: 500), opacity: viewModel.authState.isAuthenticated ? 0 : 1,
opacity: viewModel.authState.isAuthenticated ? 0 : 1, child: Form(
child: Form( key: _formKey,
key: _formKey, child: AutofillGroup(
child: AutofillGroup( child: FormCard(
child: FormCard( elevation: 20,
elevation: 20, forceNarrow: calculateLayout(context) != AppLayout.mobile,
forceNarrow: calculateLayout(context) != AppLayout.mobile, internalPadding: const EdgeInsets.all(0),
internalPadding: const EdgeInsets.all(0), children: <Widget>[
children: <Widget>[ Column(
Column( children: <Widget>[
children: <Widget>[ if (!isApple() &&
if (!isApple() && (!kIsWeb || !state.authState.isSelfHost))
(!kIsWeb || !state.authState.isSelfHost)) Row(
Row( children: [
children: [ Expanded(
Expanded( child: Material(
child: Material( color: _createAccount
color: _createAccount ? state.accentColor
? state.accentColor : Colors.transparent,
: Colors.transparent, child: InkWell(
child: InkWell( child: Center(
child: Center( child: Padding(
child: Padding( padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16), child: Text(
child: Text( localization.signUp,
localization.signUp, style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .headline6
.headline6 .copyWith(
.copyWith( fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontSize: 18,
fontSize: 18, color: _createAccount
color: _createAccount ? Colors.white
? Colors.white : null),
: null), ),
), )),
)), onTap: () {
onTap: () { setState(() {
setState(() { _createAccount = true;
_createAccount = true; _isSelfHosted = false;
_isSelfHosted = false; _loginError = '';
_loginError = ''; });
}); },
}, ),
), ),
), ),
), Expanded(
Expanded( child: Material(
child: Material( color: _createAccount
color: _createAccount ? Colors.transparent
? Colors.transparent : state.accentColor,
: state.accentColor, child: InkWell(
child: InkWell( child: Center(
child: Center( child: Padding(
child: Padding( padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16), child: Text(
child: Text( localization.login,
localization.login, style: Theme.of(context)
style: Theme.of(context) .textTheme
.textTheme .headline6
.headline6 .copyWith(
.copyWith( fontWeight: FontWeight.w600,
fontWeight: FontWeight.w600, fontSize: 18,
fontSize: 18, color: _createAccount
color: _createAccount ? null
? null : Colors.white),
: Colors.white), ),
), )),
)), onTap: () {
onTap: () { setState(() {
setState(() { _createAccount = false;
_createAccount = false; _loginError = '';
_loginError = ''; });
}); },
}, ),
), ),
), ),
],
),
SizedBox(height: 20),
if (!_recoverPassword) ...[
if (!_createAccount && (!kIsWeb || !kReleaseMode)) ...[
RuledText(localization.selectPlatform),
AppToggleButtons(
tabLabels: [
localization.hosted,
localization.selfhosted,
],
selectedIndex: _isSelfHosted ? 1 : 0,
onTabChanged: (index) {
setState(() {
_isSelfHosted = index == 1;
_loginError = '';
if (index == 1) {
_emailLogin = true;
}
});
},
), ),
], ],
), if (!_isSelfHosted && !_hideGoogle) ...[
SizedBox(height: 20), RuledText(localization.selectMethod),
if (!_recoverPassword) ...[ AppToggleButtons(
if (!_createAccount && (!kIsWeb || !kReleaseMode)) ...[ tabLabels:
RuledText(localization.selectPlatform), calculateLayout(context) == AppLayout.mobile
AppToggleButtons( ? [
tabLabels: [ 'Google',
localization.hosted, localization.email,
localization.selfhosted, ]
: [
_createAccount
? localization.googleSignUp
: localization.googleSignIn,
_createAccount
? localization.emailSignUp
: localization.emailSignIn,
],
selectedIndex: _emailLogin ? 1 : 0,
onTabChanged: (index) {
setState(() {
_emailLogin = index == 1;
_loginError = '';
});
},
),
],
],
Padding(
padding:
EdgeInsets.symmetric(horizontal: horizontalPadding),
child: Column(
children: [
if (_emailLogin)
DecoratedFormField(
controller: _emailController,
label: localization.email,
keyboardType: TextInputType.emailAddress,
autovalidate: _autoValidate,
validator: (val) =>
val.isEmpty || val.trim().isEmpty
? localization.pleaseEnterYourEmail
: null,
autofillHints: [AutofillHints.email],
autofocus: true,
onSavePressed: (_) => _submitForm(),
),
if (_emailLogin && !_recoverPassword)
PasswordFormField(
controller: _passwordController,
autoValidate: false,
newPassword: _createAccount,
onSavePressed: (_) => _submitForm(),
),
if (!_createAccount && !_recoverPassword)
DecoratedFormField(
controller: _oneTimePasswordController,
label:
'${localization.oneTimePassword} (${localization.optional})',
onSavePressed: (_) => _submitForm(),
keyboardType: TextInputType.number,
autofillHints: [AutofillHints.oneTimeCode],
),
if (_isSelfHosted && !kIsWeb)
DecoratedFormField(
controller: _urlController,
label: localization.url,
validator: (val) =>
val.isEmpty || val.trim().isEmpty
? localization.pleaseEnterYourUrl
: null,
keyboardType: TextInputType.url,
onSavePressed: (_) => _submitForm(),
),
if (_isSelfHosted && !_recoverPassword)
PasswordFormField(
labelText:
'${localization.secret} (${localization.optional})',
controller: _secretController,
autoValidate: _autoValidate,
validate: false,
onSavePressed: (_) => _submitForm(),
),
if (_createAccount)
Padding(
padding: EdgeInsets.only(top: 10),
child: Column(
children: <Widget>[
CheckboxListTile(
onChanged: (value) =>
setState(() => _termsChecked = value),
controlAffinity:
ListTileControlAffinity.leading,
activeColor: convertHexStringToColor(
kDefaultAccentColor),
value: _termsChecked,
title: RichText(
text: TextSpan(
children: <TextSpan>[
TextSpan(
style: aboutTextStyle,
text: localization.iAgreeToThe +
' ',
),
LinkTextSpan(
style: linkStyle,
url: kTermsOfServiceURL,
text: localization.termsOfService,
),
],
),
),
),
CheckboxListTile(
onChanged: (value) => setState(
() => _privacyChecked = value),
controlAffinity:
ListTileControlAffinity.leading,
activeColor: convertHexStringToColor(
kDefaultAccentColor),
value: _privacyChecked,
title: RichText(
text: TextSpan(
children: <TextSpan>[
TextSpan(
style: aboutTextStyle,
text: localization.iAgreeToThe +
' ',
),
LinkTextSpan(
style: linkStyle,
url: kPrivacyPolicyURL,
text: localization.privacyPolicy,
),
],
),
),
),
],
),
),
], ],
selectedIndex: _isSelfHosted ? 1 : 0,
onTabChanged: (index) {
setState(() {
_isSelfHosted = index == 1;
_loginError = '';
if (index == 1) {
_emailLogin = true;
}
});
},
), ),
], ),
if (!_isSelfHosted && !_hideGoogle) ...[
RuledText(localization.selectMethod),
AppToggleButtons(
tabLabels:
calculateLayout(context) == AppLayout.mobile
? [
'Google',
localization.email,
]
: [
_createAccount
? localization.googleSignUp
: localization.googleSignIn,
_createAccount
? localization.emailSignUp
: localization.emailSignIn,
],
selectedIndex: _emailLogin ? 1 : 0,
onTabChanged: (index) {
setState(() {
_emailLogin = index == 1;
_loginError = '';
});
},
),
],
], ],
Padding( ),
padding: if (_loginError.isNotEmpty &&
EdgeInsets.symmetric(horizontal: horizontalPadding), !_loginError.contains(OTP_ERROR))
child: Column( Container(
padding: EdgeInsets.only(
top: 20,
left: horizontalPadding,
right: horizontalPadding),
child: Row(
children: [
Expanded(
child: SelectableText(
_loginError,
style: TextStyle(
color: Colors.red,
),
),
),
IconButton(
icon: Icon(Icons.content_copy),
tooltip: localization.copyError,
onPressed: () {
Clipboard.setData(
ClipboardData(text: _loginError));
}),
],
),
),
Padding(
padding: EdgeInsets.only(
top: 20, bottom: 10, left: 16, right: 16),
child: RoundedLoadingButton(
height: 50,
borderRadius: 4,
width: 430,
controller: _buttonController,
color: state.accentColor,
onPressed: () => _submitForm(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [ children: [
if (_emailLogin) if (_emailLogin)
DecoratedFormField( Icon(Icons.mail, color: Colors.white)
controller: _emailController, else
label: localization.email, ClipOval(
keyboardType: TextInputType.emailAddress, child: Image.asset(
autovalidate: _autoValidate, 'assets/images/google_logo.png',
validator: (val) => width: 30,
val.isEmpty || val.trim().isEmpty height: 30),
? localization.pleaseEnterYourEmail
: null,
autofillHints: [AutofillHints.email],
autofocus: true,
onSavePressed: (_) => _submitForm(),
),
if (_emailLogin && !_recoverPassword)
PasswordFormField(
controller: _passwordController,
autoValidate: false,
newPassword: _createAccount,
onSavePressed: (_) => _submitForm(),
),
if (!_createAccount && !_recoverPassword)
DecoratedFormField(
controller: _oneTimePasswordController,
label:
'${localization.oneTimePassword} (${localization.optional})',
onSavePressed: (_) => _submitForm(),
keyboardType: TextInputType.number,
autofillHints: [AutofillHints.oneTimeCode],
),
if (_isSelfHosted && !kIsWeb)
DecoratedFormField(
controller: _urlController,
label: localization.url,
validator: (val) =>
val.isEmpty || val.trim().isEmpty
? localization.pleaseEnterYourUrl
: null,
keyboardType: TextInputType.url,
onSavePressed: (_) => _submitForm(),
),
if (_isSelfHosted && !_recoverPassword)
PasswordFormField(
labelText:
'${localization.secret} (${localization.optional})',
controller: _secretController,
autoValidate: _autoValidate,
validate: false,
onSavePressed: (_) => _submitForm(),
),
if (_createAccount)
Padding(
padding: EdgeInsets.only(top: 10),
child: Column(
children: <Widget>[
CheckboxListTile(
onChanged: (value) =>
setState(() => _termsChecked = value),
controlAffinity:
ListTileControlAffinity.leading,
activeColor: convertHexStringToColor(
kDefaultAccentColor),
value: _termsChecked,
title: RichText(
text: TextSpan(
children: <TextSpan>[
TextSpan(
style: aboutTextStyle,
text: localization.iAgreeToThe +
' ',
),
LinkTextSpan(
style: linkStyle,
url: kTermsOfServiceURL,
text: localization.termsOfService,
),
],
),
),
),
CheckboxListTile(
onChanged: (value) => setState(
() => _privacyChecked = value),
controlAffinity:
ListTileControlAffinity.leading,
activeColor: convertHexStringToColor(
kDefaultAccentColor),
value: _privacyChecked,
title: RichText(
text: TextSpan(
children: <TextSpan>[
TextSpan(
style: aboutTextStyle,
text: localization.iAgreeToThe +
' ',
),
LinkTextSpan(
style: linkStyle,
url: kPrivacyPolicyURL,
text: localization.privacyPolicy,
),
],
),
),
),
],
),
), ),
SizedBox(width: 10),
Text(
_recoverPassword
? localization.recoverPassword
: _createAccount
? (_emailLogin
? localization.emailSignUp
: localization.googleSignUp)
: (_emailLogin
? localization.emailSignIn
: localization.googleSignIn),
style: TextStyle(fontSize: 18, color: Colors.white),
)
], ],
), ),
), ),
], ),
), SizedBox(height: 4),
if (_loginError.isNotEmpty && Flex(
!_loginError.contains(OTP_ERROR)) direction: calculateLayout(context) == AppLayout.desktop
Container( ? Axis.horizontal
padding: EdgeInsets.only( : Axis.vertical,
top: 20, mainAxisAlignment: MainAxisAlignment.center,
left: horizontalPadding, crossAxisAlignment: CrossAxisAlignment.center,
right: horizontalPadding), children: <Widget>[
child: Row( if (!_createAccount && _emailLogin)
children: [ InkWell(
Expanded( onTap: () {
child: SelectableText( setState(() {
_loginError, _recoverPassword = !_recoverPassword;
style: TextStyle( });
color: Colors.red, },
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (!_recoverPassword)
Icon(MdiIcons.lock, size: 16),
SizedBox(width: 8),
Text(_recoverPassword
? localization.cancel
: localization.recoverPassword),
]),
),
),
if (!_recoverPassword && !_isSelfHosted)
InkWell(
onTap: () {
launch(kStatusCheckUrl);
},
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.security, size: 16),
SizedBox(width: 8),
Text(localization.checkStatus)
],
), ),
), ),
), ),
IconButton( if (!_recoverPassword && kIsWeb)
icon: Icon(Icons.content_copy), InkWell(
tooltip: localization.copyError, onTap: () => launch(getNativeAppUrl(platform)),
onPressed: () { child: Padding(
Clipboard.setData( padding: const EdgeInsets.all(14),
ClipboardData(text: _loginError)); child: Row(
}),
],
),
),
Padding(
padding: EdgeInsets.only(
top: 20, bottom: 10, left: 16, right: 16),
child: RoundedLoadingButton(
height: 50,
borderRadius: 4,
width: 430,
controller: _buttonController,
color: state.accentColor,
onPressed: () => _submitForm(),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_emailLogin)
Icon(Icons.mail, color: Colors.white)
else
ClipOval(
child: Image.asset(
'assets/images/google_logo.png',
width: 30,
height: 30),
),
SizedBox(width: 10),
Text(
_recoverPassword
? localization.recoverPassword
: _createAccount
? (_emailLogin
? localization.emailSignUp
: localization.googleSignUp)
: (_emailLogin
? localization.emailSignIn
: localization.googleSignIn),
style: TextStyle(fontSize: 18, color: Colors.white),
)
],
),
),
),
SizedBox(height: 4),
Flex(
direction: calculateLayout(context) == AppLayout.desktop
? Axis.horizontal
: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
if (!_createAccount && _emailLogin)
InkWell(
onTap: () {
setState(() {
_recoverPassword = !_recoverPassword;
});
},
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: [
if (!_recoverPassword) Icon(getNativeAppIcon(platform), size: 16),
Icon(MdiIcons.lock, size: 16),
SizedBox(width: 8), SizedBox(width: 8),
Text(_recoverPassword Text('$platform ${localization.app}')
? localization.cancel ],
: localization.recoverPassword), ),
]),
),
),
if (!_recoverPassword && !_isSelfHosted)
InkWell(
onTap: () {
launch(kStatusCheckUrl);
},
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.security, size: 16),
SizedBox(width: 8),
Text(localization.checkStatus)
],
), ),
), ),
), ],
if (!_recoverPassword && kIsWeb) ),
InkWell( SizedBox(height: 16),
onTap: () => launch(getNativeAppUrl(platform)), ],
child: Padding( ),
padding: const EdgeInsets.all(14),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(getNativeAppIcon(platform), size: 16),
SizedBox(width: 8),
Text('$platform ${localization.app}')
],
),
),
),
],
),
SizedBox(height: 16),
],
), ),
), ),
), ),
), ],
], ),
); );
} }
} }

View File

@ -11,6 +11,7 @@ import 'package:contacts_service/contacts_service.dart';
// Project imports: // Project imports:
import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/redux/static/static_selectors.dart'; import 'package:invoiceninja_flutter/redux/static/static_selectors.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart';
@ -131,24 +132,24 @@ class ClientEditDetailsState extends State<ClientEditDetails> {
String countryId; String countryId;
countryMap.keys.forEach((countryId) { countryMap.keys.forEach((countryId) {
final country = countryMap[countryId]; final country = countryMap[countryId] ?? CountryEntity();
if (country.name.toLowerCase() == contactAddress.country.toLowerCase()) { if (country.name.toLowerCase() == contactAddress?.country?.toLowerCase()) {
countryId = country.id; countryId = country.id;
} }
}); });
widget.viewModel.onChanged(client.rebuild((b) => b widget.viewModel.onChanged(client.rebuild((b) => b
..name = contact.company ?? '' ..name = (contact?.company ?? '').trim()
..address1 = contactAddress.street ?? '' ..address1 = (contactAddress?.street ?? '').trim()
..city = contactAddress.city ?? '' ..city = (contactAddress?.city ?? '').trim()
..state = contactAddress.region ?? '' ..state = (contactAddress?.region ?? '').trim()
..postalCode = contactAddress.postcode ?? '' ..postalCode = (contactAddress?.postcode ?? '').trim()
..countryId = countryId ..countryId = countryId ?? ''
..contacts[0] = client.contacts[0].rebuild((b) => b ..contacts[0] = client.contacts[0].rebuild((b) => b
..firstName = contact.givenName ?? '' ..firstName = (contact?.givenName ?? '').trim()
..lastName = contact.familyName ?? '' ..lastName = (contact?.familyName ?? '').trim()
..email = contactEmail.value ?? '' ..email = (contactEmail?.value ?? '').trim()
..phone = contactPhone.value ?? '') ..phone = (contactPhone?.value ?? '').trim())
..updatedAt = DateTime.now().millisecondsSinceEpoch)); ..updatedAt = DateTime.now().millisecondsSinceEpoch));
} }

View File

@ -11,6 +11,7 @@ import 'package:contacts_service/contacts_service.dart';
// Project imports: // Project imports:
import 'package:invoiceninja_flutter/constants.dart'; import 'package:invoiceninja_flutter/constants.dart';
import 'package:invoiceninja_flutter/data/models/entities.dart'; import 'package:invoiceninja_flutter/data/models/entities.dart';
import 'package:invoiceninja_flutter/data/models/models.dart';
import 'package:invoiceninja_flutter/ui/app/form_card.dart'; import 'package:invoiceninja_flutter/ui/app/form_card.dart';
import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/custom_field.dart';
import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart'; import 'package:invoiceninja_flutter/ui/app/forms/decorated_form_field.dart';
@ -129,24 +130,24 @@ class VendorEditDetailsState extends State<VendorEditDetails> {
String countryId; String countryId;
countryMap.keys.forEach((countryId) { countryMap.keys.forEach((countryId) {
final country = countryMap[countryId]; final country = countryMap[countryId] ?? CountryEntity();
if (country.name.toLowerCase() == contactAddress.country.toLowerCase()) { if (country.name.toLowerCase() == contactAddress?.country?.toLowerCase()) {
countryId = country.id; countryId = country.id;
} }
}); });
widget.viewModel.onChanged(vendor.rebuild((b) => b widget.viewModel.onChanged(vendor.rebuild((b) => b
..name = contact.company ?? '' ..name = (contact?.company ?? '').trim()
..address1 = contactAddress.street ?? '' ..address1 = (contactAddress?.street ?? '').trim()
..city = contactAddress.city ?? '' ..city = (contactAddress?.city ?? '').trim()
..state = contactAddress.region ?? '' ..state = (contactAddress?.region ?? '').trim()
..postalCode = contactAddress.postcode ?? '' ..postalCode = (contactAddress?.postcode ?? '').trim()
..countryId = countryId ..countryId = countryId ?? ''
..contacts[0] = vendor.contacts[0].rebuild((b) => b ..contacts[0] = vendor.contacts[0].rebuild((b) => b
..firstName = contact.givenName ?? '' ..firstName = (contact?.givenName ?? '').trim()
..lastName = contact.familyName ?? '' ..lastName = (contact?.familyName ?? '').trim()
..email = contactEmail.value ?? '' ..email = (contactEmail?.value ?? '').trim()
..phone = contactPhone.value ?? '') ..phone = (contactPhone?.value ?? '').trim())
..updatedAt = DateTime.now().millisecondsSinceEpoch)); ..updatedAt = DateTime.now().millisecondsSinceEpoch));
} }