add update phone number and recovery email

This commit is contained in:
tzw
2025-01-14 17:10:10 +06:30
parent 21cf7a2517
commit ace3af1785
22 changed files with 1087 additions and 221 deletions

View File

@@ -0,0 +1,114 @@
// ignore_for_file: use_build_context_synchronously
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../domain/entities/user.dart';
import '../../helpers/theme.dart';
import '../../localization/app_translations.dart';
import '../main/util.dart';
import '../widgets/input_text.dart';
import '../widgets/local_app_bar.dart';
import '../widgets/local_text.dart';
import '../widgets/progress.dart';
import 'confirm_recovery_email.dart';
class AddRecoveryEmail extends StatefulWidget {
final User user;
const AddRecoveryEmail({super.key, required this.user});
@override
State<AddRecoveryEmail> createState() => _AddRecoveryEmailState();
}
class _AddRecoveryEmailState extends State<AddRecoveryEmail> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
final TextEditingController _emailCtl = TextEditingController();
@override
void initState() {
_emailCtl.text = widget.user.recoveryEmail ?? "";
super.initState();
}
@override
Widget build(BuildContext context) {
final emailBox = InputText(
labelTextKey: "profile.email",
iconData: Icons.email_outlined,
controller: _emailCtl,
validator: (value) {
if (value == null || value.isEmpty) {
return AppTranslations.of(context)!.text('profile.email.empty');
}
if (!isValidEmail(value)) {
return AppTranslations.of(context)!
.text('profile.email.mismatch_message');
}
return null;
},
);
final continueBtn = Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: fcsButton(
context,
getLocalString(context, 'btn.continue'),
callack: _continue,
),
);
return LocalProgress(
inAsyncCall: _isLoading,
child: Scaffold(
appBar: const LocalAppBar(
labelKey: 'profile.recovery_email.title',
backgroundColor: Colors.white,
labelColor: primaryColor,
arrowColor: primaryColor),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.only(left: 15, right: 15, top: 10),
children: [
LocalText(context, 'profile.email_instruction',
fontSize: 16, color: Colors.black),
const SizedBox(height: 15),
emailBox,
const SizedBox(height: 30),
continueBtn
],
)),
),
);
}
Future<void> _continue() async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
bool? updated = await Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) =>
ConfirmRecoveryEmail(email: _emailCtl.text)));
if (updated ?? false) {
Navigator.pop(context, true);
}
} catch (e) {
showMsgDialog(context, "Error", e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
}

View File

@@ -0,0 +1,142 @@
// ignore_for_file: use_build_context_synchronously
import 'package:country_picker/country_picker.dart';
import 'package:fcs/helpers/theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/auth_result.dart';
import '../../domain/entities/auth_status.dart';
import '../../domain/entities/user.dart';
import '../main/model/main_model.dart';
import '../main/util.dart';
import '../widgets/display_text.dart';
import '../widgets/input_phone.dart';
import '../widgets/local_app_bar.dart';
import '../widgets/progress.dart';
import 'confirm_phone_number.dart';
class ChangePhoneNumber extends StatefulWidget {
final User user;
const ChangePhoneNumber({super.key, required this.user});
@override
State<ChangePhoneNumber> createState() => _ChangePhoneNumberState();
}
class _ChangePhoneNumberState extends State<ChangePhoneNumber> {
bool _isLoading = false;
TextEditingController _newPhoneCtl = TextEditingController();
late Country _selectedCountry;
final _formKey = GlobalKey<FormState>();
@override
void initState() {
_selectedCountry = Country(
name: 'Myanmar',
countryCode: 'MM',
displayName: 'Myanmar(MM)',
displayNameNoCountryCode: 'Myanmar',
e164Key: '',
e164Sc: -1,
example: '',
geographic: false,
level: -1,
phoneCode: '95');
super.initState();
}
@override
Widget build(BuildContext context) {
final currentPhoneBox = DisplayText(
text: widget.user.phone,
labelTextKey: "profile.current_phone",
iconData: Icons.phone);
final newPhoneBox = InputPhone(
autofocus: true,
lableKey: "profile.new_phone",
validationLabel: "profile.new_phone_empty",
phoneCtl: _newPhoneCtl,
selectedCountry: _selectedCountry,
onValueChange: (country) {
setState(() {
_selectedCountry = country;
});
},
);
final sendBtn = Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: fcsButton(
context,
getLocalString(context, 'profile.send_sms'),
callack: _sendSMS,
),
);
return Form(
key: _formKey,
child: LocalProgress(
inAsyncCall: _isLoading,
child: Scaffold(
appBar: const LocalAppBar(
labelKey: 'profile.change_phone.title',
backgroundColor: Colors.white,
labelColor: primaryColor,
arrowColor: primaryColor,
),
body: ListView(
padding: const EdgeInsets.only(left: 15, right: 15, top: 10),
children: [
currentPhoneBox,
const SizedBox(height: 15),
newPhoneBox,
const SizedBox(height: 30),
sendBtn
],
),
),
),
);
}
Future<void> _sendSMS() async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
if (_newPhoneCtl.text.length < 5) {
showMsgDialog(context, "Error", "Invalid phone number");
return;
}
setState(() {
_isLoading = true;
});
try {
bool isMyanmar = _selectedCountry.phoneCode == "95";
String dialCode = isMyanmar ? "+959" : "+${_selectedCountry.phoneCode}";
String phoneNumber = dialCode + _newPhoneCtl.text;
AuthResult auth = await context.read<MainModel>().sendSms(phoneNumber);
if (auth.authStatus == AuthStatus.SMS_SENT) {
bool? updated = await Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => ConfirmPhoneNumber(
phoneNumber: phoneNumber,
forceResendingToken: auth.forceResendingToken)));
if (updated ?? false) {
Navigator.pop(context, true);
}
}
} catch (e) {
showMsgDialog(context, "Error", e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
}

View File

@@ -0,0 +1,232 @@
// ignore_for_file: use_build_context_synchronously
import 'dart:async';
import 'package:fcs/constants.dart';
import 'package:fcs/pages/widgets/local_app_bar.dart';
import 'package:fcs/pages/widgets/progress.dart';
import 'package:flutter/material.dart';
import 'package:pin_input_text_field/pin_input_text_field.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/auth_result.dart';
import '../../domain/entities/auth_status.dart';
import '../../helpers/theme.dart';
import '../main/model/main_model.dart';
import '../main/util.dart';
import '../widgets/local_text.dart';
class ConfirmPhoneNumber extends StatefulWidget {
final String phoneNumber;
final String? forceResendingToken;
const ConfirmPhoneNumber(
{super.key, required this.phoneNumber, this.forceResendingToken});
@override
State<ConfirmPhoneNumber> createState() => _ConfirmPhoneNumberState();
}
class _ConfirmPhoneNumberState extends State<ConfirmPhoneNumber> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
late String pin;
late bool allNumberEntered;
late Timer _timer;
int _start = resendCountSec;
bool canResend = false;
String? _forceResendingToken;
@override
void initState() {
pin = "";
allNumberEntered = false;
_forceResendingToken = widget.forceResendingToken;
super.initState();
startTimer();
}
void startTimer() {
_timer = Timer.periodic(
const Duration(seconds: 1),
(t) => setState(
() {
if (_start < 1) {
t.cancel();
canResend = true;
} else {
_start = _start - 1;
}
},
),
);
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
bool isMyanmar = widget.phoneNumber.startsWith("+959");
return LocalProgress(
inAsyncCall: _isLoading,
child: Scaffold(
appBar: const LocalAppBar(
labelKey: 'profile.confirm_new_phone.title',
backgroundColor: Colors.white,
labelColor: primaryColor,
arrowColor: primaryColor),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.only(left: 15, right: 15, top: 10),
children: [
Container(
padding: const EdgeInsets.only(top: 40),
child: Row(
children: [
Flexible(
child: LocalText(context, "sms.six.digit",
fontSize: 16, color: labelColor),
),
],
),
),
Text(
widget.phoneNumber.startsWith("+959")
? "0${widget.phoneNumber.substring(3)}"
: widget.phoneNumber,
style: const TextStyle(color: primaryColor, fontSize: 16),
),
const SizedBox(
height: 30,
),
LocalText(context, "sms.code", color: labelColor, fontSize: 16),
Container(
padding: const EdgeInsets.only(top: 10),
child: PinInputTextField(
pinLength: 6,
cursor: Cursor(
color: primaryColor,
enabled: true,
width: 2,
height: 23),
decoration: BoxLooseDecoration(
textStyle: TextStyle(
color:
Theme.of(context).textTheme.labelMedium!.color,
fontSize: 20),
strokeColorBuilder:
const FixedColorBuilder(labelColor)),
textInputAction: TextInputAction.done,
autoFocus: true,
onChanged: _pinChange,
),
),
const SizedBox(height: 40),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: fcsButton(
context,
getLocalString(context, 'profile.change_phone.btn'),
callack: allNumberEntered
? () {
_change();
}
: null,
),
),
const SizedBox(height: 30),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
canResend ? Colors.white : Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
side: BorderSide(
color: canResend
? primaryColor
: Colors.grey.shade400))),
onPressed: canResend
? () {
_resend(isMyanmar);
}
: null,
child: LocalText(context, "sms.resend",
color:
canResend ? primaryColor : Colors.grey.shade400,
fontSize: 16),
),
],
),
Row(
children: <Widget>[
LocalText(context, 'sms.resend.seconds',
fontSize: 15,
translationVariables: [_start.toString()],
color: primaryColor),
],
),
const SizedBox(height: 20),
],
)),
),
);
}
_pinChange(pin) {
setState(() {
this.pin = pin;
allNumberEntered = this.pin.length == 6;
});
}
_resend(bool isMyanmar) async {
try {
setState(() {
_start = resendCountSec;
canResend = false;
});
_timer.cancel();
startTimer();
var mainModel = context.read<MainModel>();
AuthResult auth = await mainModel.sendSms(widget.phoneNumber,
forceResendingToken: _forceResendingToken);
if (auth.authStatus == AuthStatus.SMS_SENT) {
_forceResendingToken = auth.forceResendingToken;
}
} catch (e) {
await showMsgDialog(context, "Error", e.toString());
}
}
_change() async {
setState(() {
_isLoading = true;
});
try {
// var mainModel = context.read<MainModel>();
// await mainModel.updatePhoneNumber(pin);
Navigator.pop(context, true);
} catch (e) {
await showMsgDialog(context, "Error", e.toString());
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}

View File

@@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'package:pin_input_text_field/pin_input_text_field.dart';
import 'package:provider/provider.dart';
import '../../helpers/theme.dart';
import '../../localization/app_translations.dart';
import '../main/model/language_model.dart';
import '../main/util.dart';
import '../widgets/local_app_bar.dart';
import '../widgets/local_text.dart';
import '../widgets/progress.dart';
class ConfirmRecoveryEmail extends StatefulWidget {
final String email;
const ConfirmRecoveryEmail({super.key, required this.email});
@override
State<ConfirmRecoveryEmail> createState() => _ConfirmRecoveryEmailState();
}
class _ConfirmRecoveryEmailState extends State<ConfirmRecoveryEmail> {
bool _isLoading = false;
late String pin;
late bool allNumberEntered;
@override
void initState() {
pin = "";
allNumberEntered = false;
super.initState();
}
@override
Widget build(BuildContext context) {
var isEng = context.watch<LanguageModel>().isEng;
return LocalProgress(
inAsyncCall: _isLoading,
child: Scaffold(
appBar: const LocalAppBar(
labelKey: 'profile.comfirm_email.title',
backgroundColor: Colors.white,
labelColor: primaryColor,
arrowColor: primaryColor),
body: ListView(
padding: const EdgeInsets.only(left: 15, right: 15, top: 20),
children: [
RichText(
text: TextSpan(children: [
TextSpan(
text: AppTranslations.of(context)!
.text("profile.confirm_email.send_to"),
style: isEng
? newLabelStyle(
fontWeight: FontWeight.normal,
color:
labelColor,
fontSize: 16)
: newLabelStyleMM(
fontWeight: FontWeight.normal,
color:
labelColor,
fontSize: 16)),
TextSpan(
text: " ${widget.email}",
style: const TextStyle(fontSize: 16, color: primaryColor))
]),
),
const SizedBox(height: 40),
LocalText(
context,
'profile.confirm_email.insruction',
fontSize: 16,
color: labelColor,
fontWeight: FontWeight.normal,
),
const SizedBox(height: 40),
LocalText(context, "sms.code",
color: labelColor, fontSize: 16),
Container(
padding: const EdgeInsets.only(top: 10),
child: PinInputTextField(
pinLength: 6,
cursor: Cursor(
color: primaryColor, enabled: true, width: 2, height: 23),
decoration: BoxLooseDecoration(
textStyle: TextStyle(
color: Theme.of(context).textTheme.labelMedium!.color,
fontSize: 20),
strokeColorBuilder: const FixedColorBuilder(labelColor)),
textInputAction: TextInputAction.done,
autoFocus: true,
onChanged: _pinChange,
),
),
const SizedBox(height: 40),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: fcsButton(
context,
getLocalString(context, 'btn.confirm'),
callack: allNumberEntered
? () {
_confirm();
}
: null,
),
)
],
),
),
);
}
_pinChange(pin) {
setState(() {
this.pin = pin;
allNumberEntered = this.pin.length == 6;
});
}
_confirm() async {
setState(() {
_isLoading = true;
});
try {
Navigator.pop(context, true);
} catch (e) {
// ignore: use_build_context_synchronously
await showMsgDialog(context, "Error", e.toString());
} finally {
setState(() {
_isLoading = false;
});
}
}
}

View File

@@ -25,6 +25,8 @@ import '../../helpers/theme.dart';
import 'package:collection/collection.dart';
import 'account_delection_page.dart';
import 'add_recovery_email.dart';
import 'change_phone_number.dart';
typedef void ProfileCallback();
@@ -69,44 +71,70 @@ class _ProfileState extends State<Profile> {
if (mainModel.user == null) {
return Container();
}
User user = mainModel.user!;
buildLanguage(languageModel);
DeliveryAddressModel deliveryAddressModel =
Provider.of<DeliveryAddressModel>(context);
var deliveryAddressModel = Provider.of<DeliveryAddressModel>(context);
final namebox = DisplayText(
text: "${mainModel.user!.name ?? ''}" +
" (${mainModel.user!.status ?? ''})",
text: "${user.name ?? ''}" + " (${user.status ?? ''})",
labelTextKey: "profile.name",
iconData: Icons.person,
);
final currencyBox = DisplayText(
text: mainModel.user!.preferCurrency ?? "",
labelTextKey: "profile.currency",
iconData: FontAwesome5Regular.money_bill_alt,
final currencyBox = Row(
children: <Widget>[
Expanded(
child: DisplayText(
text: user.preferCurrency ?? "",
labelTextKey: "profile.currency",
iconData: FontAwesome5Regular.money_bill_alt,
)),
Padding(
padding: const EdgeInsets.only(right: 0),
child: IconButton(
icon: Icon(Icons.edit, color: Colors.grey),
onPressed: _editCurrency),
)
],
);
final deleteAccountBox = DisplayText(
labelTextKey: "profile.delete.title",
iconData: MaterialCommunityIcons.account_remove,
);
final phonenumberbox = DisplayText(
text: mainModel.user!.phone,
labelTextKey: "profile.phone",
iconData: Icons.phone,
final phonenumberBox = Row(
children: [
Expanded(
child: DisplayText(
text: user.phone,
labelTextKey: "profile.phone",
iconData: Icons.phone),
),
IconButton(
icon: Icon(Icons.edit, color: Colors.grey),
onPressed: () {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => ChangePhoneNumber(user: user)));
})
],
);
final fcsIDBox = Row(
children: [
Expanded(
child: DisplayText(
text: mainModel.user!.fcsID ?? "",
text: user.fcsID ?? "",
labelTextKey: "customer.fcs.id",
icon: FcsIDIcon(),
),
),
IconButton(
icon: Icon(Icons.content_copy, color: Colors.grey),
onPressed: () => _copy(getLocalString(context, "customer.fcs.id"),
mainModel.user!.fcsID ?? ""),
onPressed: () => _copy(
getLocalString(context, "customer.fcs.id"), user.fcsID ?? ""),
)
],
);
@@ -141,6 +169,22 @@ class _ProfileState extends State<Profile> {
},
iconData: Icons.exit_to_app);
final emailBox = Row(
children: [
Expanded(
child: DisplayText(
text: user.recoveryEmail,
labelTextKey: "profile.recovery.email",
iconData: Icons.email_outlined,
),
),
IconButton(icon: Icon(Icons.edit, color: Colors.grey), onPressed: () {
Navigator.of(context, rootNavigator: true).push(
CupertinoPageRoute(
builder: (context) => AddRecoveryEmail(user: user)));
})
],
);
return LocalProgress(
inAsyncCall: _isLoading,
child: Scaffold(
@@ -165,20 +209,11 @@ class _ProfileState extends State<Profile> {
)
],
),
phonenumberbox,
phonenumberBox,
fcsIDBox,
usaShippingAddressBox,
Row(
children: <Widget>[
Expanded(child: currencyBox),
Padding(
padding: const EdgeInsets.only(right: 0),
child: IconButton(
icon: Icon(Icons.edit, color: Colors.grey),
onPressed: _editCurrency),
)
],
),
currencyBox,
emailBox,
DefaultDeliveryAddress(
labelKey: "profile.default.delivery.address",
deliveryAddress: deliveryAddressModel.defalutAddress,
@@ -298,20 +333,22 @@ class _ProfileState extends State<Profile> {
}
});
return Column(
children: <Widget>[
DisplayText(
labelTextKey: "profile.privileges",
iconData: MaterialCommunityIcons.clipboard_check_outline,
),
Padding(
padding: const EdgeInsets.only(left: 30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: getRowPrivilegeWidget(privileges)),
)
],
);
return privileges.isEmpty
? const SizedBox()
: Column(
children: <Widget>[
DisplayText(
labelTextKey: "profile.privileges",
iconData: MaterialCommunityIcons.clipboard_check_outline,
),
Padding(
padding: const EdgeInsets.only(left: 30.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: getRowPrivilegeWidget(privileges)),
)
],
);
}
List<Widget> getRowPrivilegeWidget(List<Privilege> privileges) {
@@ -365,7 +402,7 @@ class _ProfileState extends State<Profile> {
if (scaffold == null) {
scaffold = ScaffoldMessenger.of(context);
}
scaffold.showSnackBar(
SnackBar(
content: Text('copied "$title" data to clipboard'),