Merge branch 'master' of tzw/fcs into master

This commit is contained in:
2025-01-14 23:51:06 +06:30
committed by Gogs
22 changed files with 1087 additions and 221 deletions

View File

@@ -19,6 +19,7 @@
"btn.clear":"Clear Filter",
"btn.filter":"Filter",
"btn.exit_confirm":"Are you sure you want to exit?",
"btn.confirm":"Confirm",
"Buttons End ================================================================":"",
"Offline Start ================================================================":"",
@@ -76,6 +77,8 @@
"Login Start ================================================================":"",
"login.title":"Sign in to FCS",
"login.phone":"Enter phone number",
"login.phone_number":"Phone Number",
"login.phone_empty":"Enter phone number",
"Login End ================================================================":"",
"SMS Start ================================================================":"",
@@ -83,6 +86,7 @@
"sms.six.digit":"Enter 6 digit sms code sent to",
"sms.resend":"Resend",
"sms.resend.seconds":"Resend again in {0} seconds",
"sms.code":"SMS Code",
"SMS End ================================================================":"",
"FAQ Start ================================================================":"",
@@ -132,6 +136,7 @@
"customer.list.title":"Customers",
"customer.name":"Name",
"customer.phone":"Phone number",
"customer.phone_empty":"Please insert phone number",
"customer.status":"Status",
"customer.fcs.id":"FCS ID",
"customer.invitation.request.confirm":"Accept customer",
@@ -183,6 +188,21 @@
"profile.email":"Email",
"profile.privileges":"Privileges",
"profile.default.delivery.address":"Default delivery address",
"profile.recovery.email":"Recovery Email",
"profile.change_phone.title":"Change Phone Number",
"profile.current_phone":"Current Phone Number",
"profile.new_phone":"New Phone Number",
"profile.new_phone_empty":"Enter new phone number",
"profile.send_sms":"Send SMS Code",
"profile.confirm_new_phone.title":"Confirm New Phone Number",
"profile.change_phone.btn":"Change",
"profile.recovery_email.title":"Recovery Email",
"profile.email.empty":"Enter recovery email",
"profile.email.mismatch_message":"Email format is incorrect",
"profile.email_instruction":"Your recovery email is used to help you get back into your account when you cant sign in.",
"profile.comfirm_email.title":"Confirm Your Email",
"profile.confirm_email.send_to":"Sent email to",
"profile.confirm_email.insruction":"Please open the email to confirm recovery email",
"Profile End ================================================================":"",
"Package Start ================================================================":"",

View File

@@ -18,6 +18,7 @@
"btn.clear":"Clear Filter",
"btn.filter":"Filter",
"btn.exit_confirm":"Are you sure you want to exit?",
"btn.confirm":"အတည်ပြုမည်",
"Buttons End ================================================================":"",
"Offline Start ================================================================":"",
@@ -75,6 +76,8 @@
"Login Start ================================================================":"",
"login.title":"FCS သို့အကောင့်ဒ်၀င်ပါ",
"login.phone":"ဖုန်းနံပါတ်ထည့်ပါ",
"login.phone_number":"ဖုန်းနံပါတ်",
"login.phone_empty":"ဖုန်းနံပါတ်ထည့်ပါ",
"Login End ================================================================":"",
"SMS Start ================================================================":"",
@@ -82,6 +85,7 @@
"sms.six.digit":"SMS ဂဏန်း ခြောက်လုံး ကိုရိုက်ထဲ့ပါ",
"sms.resend":"ပြန်ပို့ရန်",
"sms.resend.seconds":"SMS ပြန်ပို့ရန် {0} စက္ကန့် စောင့်ပါ",
"sms.code":"SMS Code",
"SMS End ================================================================":"",
"FAQ Start ================================================================":"",
@@ -132,6 +136,7 @@
"customer.list.title":"ဝယ်ယူသူများ",
"customer.name":"နာမည်",
"customer.phone":"ဖုန်းနံပါတ်",
"customer.phone_empty":"Please insert phone number",
"customer.status":"အခြေအနေ",
"customer.fcs.id":"FCS ID",
"customer.invitation.request.confirm":"လက်ခံ လိုက်ပါ",
@@ -184,6 +189,21 @@
"profile.email":"အီးမေးလ်",
"profile.privileges":"လုပ်ပိုင်ခွင့်များ",
"profile.default.delivery.address":"အမြဲတမ်း ပို့ဆောင်ရမည့်လိပ်စာ",
"profile.recovery.email":"အီးမေးလ်",
"profile.change_phone.title":"ဖုန်းနံပါတ်ပြောင်းခြင်း",
"profile.current_phone":"လက်ရှိဖုန်းနံပါတ်",
"profile.new_phone":"ဖုန်းနံပါတ်အသစ်",
"profile.new_phone_empty":"ဖုန်းနံပါတ်အသစ်ထည့်ပါ",
"profile.send_sms":"SMS ကုဒ်ပို့မည်",
"profile.confirm_new_phone.title":"ဖုန်းနံပါတ်အသစ်ကို အတည်ပြုခြင်း",
"profile.change_phone.btn":"ပြောင်းမည်",
"profile.recovery_email.title":"အီးမေးလ်ထည့်ခြင်း",
"profile.email.empty":"အီးမေးလ်ထည့်ပါ",
"profile.email.mismatch_message":"အီးမေးလ်မမှန်ပါ",
"profile.email_instruction":"အကောင့်ဝင်လို့မရသောအခါတွင် သင့်အကောင့်ထဲသို့ ပြန်လည်ရောက်ရှိစေရန်အတွက် သင်၏အီးမေးလ်ကို အသုံးပြုပါသည်။",
"profile.comfirm_email.title":"သင့်အီးမေးလ်ကိုအတည်ပြုခြင်း",
"profile.confirm_email.send_to":"သင့်အီးမေးလ်သို့ ပေးပို့ခဲ့သည်",
"profile.confirm_email.insruction":"အီးမေးလ်ကို အတည်ပြုရန် သင့်အီးမေးလ်ကိုဖွင့်ပါ",
"Profile End ================================================================":"",
"Package Start ================================================================":"",

View File

@@ -29,6 +29,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'helpers/theme.dart';
import 'pages/carton/model/carton_selection_model.dart';
import 'pages/carton/model/consignee_selection_model.dart';
import 'pages/carton/model/package_selection_model.dart';
@@ -165,6 +166,8 @@ class _AppState extends State<App> {
debugShowCheckedModeBanner: false,
theme: ThemeData(
// useMaterial3: false,
textSelectionTheme:
const TextSelectionThemeData(cursorColor: primaryColor),
dividerTheme: DividerThemeData(color: Colors.grey.shade200),
colorScheme: ColorScheme.light(primary: Colors.white),
dialogTheme: DialogTheme(

View File

@@ -1,5 +1,6 @@
const uploadPhotoLimit = 10;
const shipmentCountForCartonFilter = 10;
const resendCountSec = 30;
const config_collection = "configs";
const user_collection = "users";

View File

@@ -29,7 +29,8 @@ class AuthFb {
StreamSubscription<DocumentSnapshot>? userListener;
StreamSubscription<DocumentSnapshot>? userAuthListener;
Future<fcs.AuthResult> sendSmsCodeToPhoneNumber(String phoneNumber) {
Future<fcs.AuthResult> sendSmsCodeToPhoneNumber(String phoneNumber,
{String? forceResendingToken}) {
Completer<fcs.AuthResult> completer = Completer();
bool codeSentCompleted = false;
@@ -66,7 +67,9 @@ class AuthFb {
print("codeSent " + phoneNumber);
codeSentCompleted = true;
if (!completer.isCompleted)
completer.complete(fcs.AuthResult(authStatus: AuthStatus.SMS_SENT));
completer.complete(fcs.AuthResult(
authStatus: AuthStatus.SMS_SENT,
forceResendingToken: forceResendingToken?.toString()));
};
final fb.PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout =
@@ -83,6 +86,9 @@ class AuthFb {
};
_fb.verifyPhoneNumber(
forceResendingToken: forceResendingToken != null
? int.tryParse(forceResendingToken)
: null,
phoneNumber: phoneNumber,
timeout: const Duration(seconds: 0),
verificationCompleted: verificationCompleted,

View File

@@ -16,8 +16,10 @@ class AuthServiceImp implements AuthService {
final AuthFb authFb;
@override
Future<AuthResult> sendSmsCodeToPhoneNumber(String phoneNumber) {
return authFb.sendSmsCodeToPhoneNumber(phoneNumber);
Future<AuthResult> sendSmsCodeToPhoneNumber(String phoneNumber,
{String? forceResendingToken}) {
return authFb.sendSmsCodeToPhoneNumber(phoneNumber,
forceResendingToken: forceResendingToken);
}
@override

View File

@@ -3,7 +3,8 @@ import 'package:fcs/domain/entities/setting.dart';
import 'package:fcs/domain/entities/user.dart';
abstract class AuthService {
Future<AuthResult> sendSmsCodeToPhoneNumber(String phoneNumber);
Future<AuthResult> sendSmsCodeToPhoneNumber(String phoneNumber,
{String? forceResendingToken});
Future<AuthResult> signInWithSmsCode(String smsCode);
Future<void> signoutStart();
Future<void> signoutEnd();

View File

@@ -4,6 +4,11 @@ class AuthResult {
AuthStatus? authStatus;
String? authErrorCode;
String? authErrorMsg;
String? forceResendingToken;
AuthResult({this.authStatus, this.authErrorCode, this.authErrorMsg});
AuthResult(
{this.authStatus,
this.authErrorCode,
this.authErrorMsg,
this.forceResendingToken});
}

View File

@@ -22,6 +22,7 @@ class User {
bool enablePinLogin;
String? pinDigit;
List<String> privileges = [];
String? recoveryEmail;
String get initial =>
name != null && name != "" ? name!.substring(0, 1) : "?";
@@ -51,7 +52,7 @@ class User {
// for pin login
String? pinToken;
bool get isPinLogin => pinToken!=null;
bool get isPinLogin => pinToken != null;
String get phone => phoneNumber != null && phoneNumber!.startsWith("959")
? "0${phoneNumber!.substring(2)}"
@@ -62,8 +63,8 @@ class User {
bool get disabled => status != null && status == user_disabled_status;
String get share => "Your phone number:$phoneNumber";
User({
this.id,
User(
{this.id,
this.name,
this.phoneNumber,
this.fcsID,
@@ -76,7 +77,7 @@ class User {
this.preferCurrency,
this.enablePinLogin = false,
this.pinDigit,
});
this.recoveryEmail});
factory User.fromJson(Map<String, dynamic> json) {
return User(

View File

@@ -9,6 +9,7 @@ const buttonBkColor = const Color(0xFF268944);
const labelColor = const Color(0xFF757575);
var dividerColor = Colors.grey.shade400;
const dangerColor = const Color(0xffff0606);
const hintTextColor = Color.fromARGB(255, 187, 187, 187);
const TextStyle labelStyle =
TextStyle(fontSize: 20, color: labelColor, fontWeight: FontWeight.w500);

View File

@@ -1,4 +1,4 @@
import 'package:country_code_picker/country_code_picker.dart';
import 'package:country_picker/country_picker.dart';
import 'package:fcs/helpers/theme.dart';
import 'package:fcs/pages/customer/model/customer_model.dart';
import 'package:fcs/pages/main/util.dart';
@@ -8,6 +8,8 @@ import 'package:fcs/pages/widgets/progress.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../widgets/input_phone.dart';
class InvitationCreate extends StatefulWidget {
@override
_InvitationCreateState createState() => _InvitationCreateState();
@@ -18,13 +20,23 @@ class _InvitationCreateState extends State<InvitationCreate> {
TextEditingController _phoneController = new TextEditingController();
bool _isLoading = false;
late String dialCode;
final _inviteFormKey = GlobalKey<FormState>();
late Country _selectedCountry;
@override
void initState() {
super.initState();
dialCode = "+95";
_selectedCountry = Country(
name: 'Myanmar',
countryCode: 'MM',
displayName: 'Myanmar(MM)',
displayNameNoCountryCode: 'Myanmar',
e164Key: '',
e164Sc: -1,
example: '',
geographic: false,
level: -1,
phoneCode: '95');
}
@override
@@ -47,6 +59,18 @@ class _InvitationCreateState extends State<InvitationCreate> {
},
);
final phoneBox = InputPhone(
lableKey: "customer.phone",
validationLabel: "customer.phone_empty",
phoneCtl: _phoneController,
selectedCountry: _selectedCountry,
onValueChange: (country) {
setState(() {
_selectedCountry = country;
});
},
);
return LocalProgress(
inAsyncCall: _isLoading,
child: Scaffold(
@@ -73,76 +97,8 @@ class _InvitationCreateState extends State<InvitationCreate> {
children: <Widget>[
nameBox,
SizedBox(height: 10),
Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.phone,
color: primaryColor,
),
),
Container(
decoration: BoxDecoration(
border:
Border.all(color: Colors.grey.shade400, width: 1),
borderRadius:
BorderRadius.all(Radius.circular(12.0))),
child: CountryCodePicker(
onChanged: _countryChange,
initialSelection: dialCode,
countryFilter: ['mm', 'us'],
showCountryOnly: false,
showOnlyCountryWhenClosed: false,
alignLeft: false,
textStyle:
TextStyle(fontSize: 16, color: Colors.black87),
searchDecoration: InputDecoration(
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.black, width: 1.0))),
),
),
SizedBox(
width: 10,
),
Flexible(
child: Container(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField(
controller: _phoneController,
cursorColor: primaryColor,
textAlign: TextAlign.left,
keyboardType: TextInputType.phone,
style: TextStyle(
fontSize: 18,
),
decoration: InputDecoration(
fillColor: Colors.white,
labelText:
getLocalString(context, "customer.phone"),
labelStyle:
TextStyle(fontSize: 16, color: Colors.grey),
filled: true,
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.grey, width: 1.0)),
errorStyle: TextStyle(
color: dangerColor,
)),
validator: (value) {
if (value == null || value.isEmpty) {
return "Please insert phone no";
}
return null;
},
autovalidateMode: AutovalidateMode.onUserInteraction,
),
),
),
],
),
SizedBox(height: 20),
phoneBox,
SizedBox(height: 30),
fcsButton(context, getLocalString(context, "invite.btn"),
callack: _invite),
],
@@ -153,23 +109,21 @@ class _InvitationCreateState extends State<InvitationCreate> {
);
}
_countryChange(CountryCode countryCode) {
setState(() {
dialCode = countryCode.dialCode!;
});
}
_invite() async {
String userName = _nameController.text;
String phoneNumber = dialCode + _phoneController.text;
if (!_inviteFormKey.currentState!.validate()) {
return null;
}
setState(() {
_isLoading = true;
});
try {
String userName = _nameController.text;
bool isMyanmar = _selectedCountry.phoneCode == "95";
String dialCode = isMyanmar ? "+959" : "+${_selectedCountry.phoneCode}";
String phoneNumber = dialCode + _phoneController.text;
CustomerModel customerModel =
Provider.of<CustomerModel>(context, listen: false);
await customerModel.inviteUser(userName, phoneNumber);

View File

@@ -153,8 +153,10 @@ class MainModel extends ChangeNotifier {
return int.parse(packageInfo!.buildNumber) >= setting!.supportBuildNum;
}
Future<AuthResult> sendSms(String phoneNumber) {
return Services.instance.authService.sendSmsCodeToPhoneNumber(phoneNumber);
Future<AuthResult> sendSms(String phoneNumber,
{String? forceResendingToken}) {
return Services.instance.authService.sendSmsCodeToPhoneNumber(phoneNumber,
forceResendingToken: forceResendingToken);
}
Future<AuthResult> signin(String smsCode) async {

View File

@@ -403,3 +403,12 @@ String removeTrailingZeros(double number) {
: result;
return result;
}
bool isValidEmail(String email) {
// Define a regular expression for validating an email
final emailRegex = RegExp(r'^[^@\s]+@[^@\s]+\.[^@\s]+$');
// Check if the email matches the pattern
return emailRegex.hasMatch(email);
}

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 ?? "",
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,
final phonenumberBox = Row(
children: [
Expanded(
child: DisplayText(
text: user.phone,
labelTextKey: "profile.phone",
iconData: Icons.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,7 +333,9 @@ class _ProfileState extends State<Profile> {
}
});
return Column(
return privileges.isEmpty
? const SizedBox()
: Column(
children: <Widget>[
DisplayText(
labelTextKey: "profile.privileges",

View File

@@ -1,4 +1,4 @@
import 'package:country_code_picker/country_code_picker.dart';
import 'package:country_picker/country_picker.dart';
import 'package:fcs/domain/entities/auth_result.dart';
import 'package:fcs/domain/entities/auth_status.dart';
import 'package:fcs/pages/main/model/main_model.dart';
@@ -13,6 +13,7 @@ import 'package:provider/provider.dart';
import '../../helpers/theme.dart';
import '../main/util.dart';
import '../widgets/input_phone.dart';
import 'sms_code_page.dart';
class SigninPage extends StatefulWidget {
@@ -25,15 +26,22 @@ class _SigninPageState extends State<SigninPage> {
late String dialCode;
TextEditingController phonenumberCtl = new TextEditingController();
late Country _selectedCountry;
@override
void initState() {
super.initState();
phonenumberCtl = new TextEditingController(text: "09");
phonenumberCtl.selection = TextSelection.fromPosition(
TextPosition(offset: phonenumberCtl.text.length));
dialCode = "+95";
_selectedCountry = Country(
name: 'Myanmar',
countryCode: 'MM',
displayName: 'Myanmar(MM)',
displayNameNoCountryCode: 'Myanmar',
e164Key: '',
e164Sc: -1,
example: '',
geographic: false,
level: -1,
phoneCode: '95');
}
@override
@@ -67,53 +75,16 @@ class _SigninPageState extends State<SigninPage> {
fontSize: 16,
),
),
Row(
children: <Widget>[
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400, width: 1),
borderRadius: BorderRadius.all(Radius.circular(12.0))),
child: CountryCodePicker(
onChanged: _countryChange,
initialSelection: dialCode,
countryFilter: ['mm', 'us'],
showCountryOnly: false,
showOnlyCountryWhenClosed: false,
alignLeft: false,
textStyle: TextStyle(fontSize: 16, color: Colors.black87),
searchDecoration: InputDecoration(
focusedBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: Colors.black, width: 1.0)))),
),
SizedBox(
width: 10,
),
Flexible(
child: Container(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField(
controller: phonenumberCtl,
cursorColor: primaryColor,
textAlign: TextAlign.left,
InputPhone(
autofocus: true,
keyboardType: TextInputType.phone,
style: TextStyle(
fontSize: 18,
),
decoration: new InputDecoration(
contentPadding: EdgeInsets.only(top: 8),
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: primaryColor, width: 1.0)),
focusedBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: primaryColor, width: 1.0)),
),
),
),
),
],
validationLabel: "login.phone_empty",
phoneCtl: phonenumberCtl,
selectedCountry: _selectedCountry,
onValueChange: (country) {
setState(() {
_selectedCountry = country;
});
},
),
SizedBox(
height: 20,
@@ -140,15 +111,8 @@ class _SigninPageState extends State<SigninPage> {
);
}
_countryChange(CountryCode countryCode) {
setState(() {
dialCode = countryCode.dialCode ?? '+95';
});
}
_next() async {
String phoneNumber = phonenumberCtl.text;
if (phoneNumber.length < 5) {
if (phonenumberCtl.text.length < 5) {
showMsgDialog(context, "Error", "Invalid phone number");
return;
}
@@ -157,16 +121,17 @@ class _SigninPageState extends State<SigninPage> {
_isLoading = true;
});
try {
phoneNumber = phoneNumber[0] == "0"
? phoneNumber.replaceFirst("0", "")
: phoneNumber;
phoneNumber = dialCode + phoneNumber;
bool isMyanmar = _selectedCountry.phoneCode == "95";
String dialCode = isMyanmar ? "+959" : "+${_selectedCountry.phoneCode}";
try {
String phoneNumber = dialCode + phonenumberCtl.text;
AuthResult auth = await context.read<MainModel>().sendSms(phoneNumber);
if (auth.authStatus == AuthStatus.SMS_SENT) {
await Navigator.of(context).push(CupertinoPageRoute(
builder: (context) => SmsCodePage(phoneNumber: phoneNumber)));
builder: (context) => SmsCodePage(
phoneNumber: phoneNumber,
forceResendingToken: auth.forceResendingToken)));
Navigator.pop(context);
} else if (auth.authStatus == AuthStatus.AUTH_VERIFIED) {
await navigateAfterAuthVerified(context);

View File

@@ -12,13 +12,15 @@ import 'package:flutter/material.dart';
import 'package:pin_input_text_field/pin_input_text_field.dart';
import 'package:provider/provider.dart';
import '../../constants.dart';
import '../../helpers/theme.dart';
const resend_count_sec = 30;
class SmsCodePage extends StatefulWidget {
final String phoneNumber;
const SmsCodePage({Key? key, required this.phoneNumber}) : super(key: key);
final String? forceResendingToken;
const SmsCodePage(
{Key? key, required this.phoneNumber, this.forceResendingToken})
: super(key: key);
@override
_SmsCodePageState createState() => _SmsCodePageState();
}
@@ -26,16 +28,18 @@ class SmsCodePage extends StatefulWidget {
class _SmsCodePageState extends State<SmsCodePage> {
bool _isLoading = false;
bool canResend = false;
int _start = resend_count_sec;
int _start = resendCountSec;
late String pin;
late bool allNumberEntered;
late Timer _timer;
String? _forceResendingToken;
@override
void initState() {
pin = "";
allNumberEntered = false;
_forceResendingToken = widget.forceResendingToken;
super.initState();
startTimer();
}
@@ -186,7 +190,28 @@ class _SmsCodePageState extends State<SmsCodePage> {
});
}
_resend() async {}
_resend() 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());
}
}
_verify() async {
setState(() {

View File

@@ -0,0 +1,188 @@
import 'package:fcs/helpers/theme.dart';
import 'package:flutter/material.dart';
import 'package:country_picker/country_picker.dart';
import 'package:provider/provider.dart';
import '../../localization/app_translations.dart';
import '../main/model/language_model.dart';
typedef OnValueChange = Function(Country country);
class InputPhone extends StatefulWidget {
final String? lableKey;
final String? validationLabel;
final OnValueChange onValueChange;
final Country selectedCountry;
final TextEditingController phoneCtl;
final Function(String)? onInputChanged;
final bool autofocus;
const InputPhone(
{super.key,
this.lableKey,
required this.onValueChange,
required this.selectedCountry,
this.onInputChanged,
required this.phoneCtl,
this.validationLabel,
this.autofocus = false});
@override
State<InputPhone> createState() => _InputPhoneState();
}
class _InputPhoneState extends State<InputPhone> {
List<String>? signinCountries = [];
@override
void initState() {
init();
super.initState();
}
init() {
List<String>? countries = ['mm', 'us'];
for (var c in countries) {
setState(() {
signinCountries?.add(c.toUpperCase());
});
}
}
@override
Widget build(BuildContext context) {
bool isMyanmar = widget.selectedCountry.phoneCode == "95";
var languageModel = Provider.of<LanguageModel>(context);
return Padding(
padding: const EdgeInsets.only(top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
GestureDetector(
onTap: () {
showCountryPicker(
context: context,
countryFilter: signinCountries,
showPhoneCode: true,
showSearch: true,
countryListTheme: const CountryListThemeData(
flagSize: 25,
backgroundColor: Colors.white,
textStyle: TextStyle(fontSize: 15, color: Colors.black),
bottomSheetHeight: 500,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
inputDecoration: InputDecoration(
labelText: 'Search',
labelStyle: TextStyle(color: Colors.black),
floatingLabelStyle: TextStyle(color: Colors.black),
hintText: 'Search',
hintStyle: TextStyle(color: Colors.black),
hintMaxLines: 1,
prefixIcon: Icon(Icons.search, color: Colors.black),
border: OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide:
BorderSide(width: 1, color: Colors.black)),
focusedBorder: OutlineInputBorder(
borderRadius:
BorderRadius.all(Radius.circular(5.0)),
borderSide: BorderSide(color: Colors.black)),
),
searchTextStyle: TextStyle(color: Colors.black)),
onSelect: (Country country) {
widget.onValueChange(country);
},
);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
width: 95,
padding: const EdgeInsets.only(
left: 5, bottom: 12, right: 5, top: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(5),
),
child: Row(
children: [
Container(
padding: const EdgeInsets.fromLTRB(0, 0, 5, 0),
child: Text(widget.selectedCountry.flagEmoji)),
Text(
isMyanmar
? "+95"
: "+${widget.selectedCountry.phoneCode}",
style: const TextStyle(
fontSize: 16, color: Colors.black),
),
const Icon(Icons.arrow_drop_down),
],
),
)
],
),
),
const SizedBox(width: 5),
Flexible(
child: Container(
padding: const EdgeInsets.only(left: 10),
child: TextFormField(
autofocus: widget.autofocus,
keyboardType: TextInputType.phone,
onChanged: widget.onInputChanged,
controller: widget.phoneCtl,
maxLines: 1,
cursorColor: primaryColor,
style: const TextStyle(fontSize: 16, color: Colors.black),
decoration: InputDecoration(
labelStyle: languageModel.isEng
? newLabelStyle(color: Colors.black54, fontSize: 20)
: newLabelStyleMM(
color: Colors.black54, fontSize: 20),
labelText: widget.lableKey != null
? AppTranslations.of(context)!
.text(widget.lableKey!)
: null,
hintStyle: const TextStyle(color: Color(0xFFBBBBBB)),
errorStyle: const TextStyle(color: dangerColor),
prefix: Padding(
padding: EdgeInsets.only(left: isMyanmar ? 0 : 13),
child: Text(isMyanmar ? "9 " : "",
style: const TextStyle(color: Colors.black))),
enabledBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: primaryColor, width: 1.0)),
focusedBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: primaryColor, width: 1.0)),
errorBorder: null,
focusedErrorBorder: null),
validator: (value) {
if (value!.isEmpty) {
if (widget.validationLabel == null) return null;
return AppTranslations.of(context)!
.text(widget.validationLabel!);
}
return null;
},
),
),
),
],
),
],
),
);
}
}

View File

@@ -42,7 +42,6 @@ dependencies:
connectivity_plus: ^6.1.0
logging: ^1.0.1
permission_handler: ^11.1.0
country_code_picker: ^3.0.0
pin_input_text_field: ^4.1.0
flutter_icons_null_safety: ^1.1.0
country_icons: ^2.0.2
@@ -59,6 +58,7 @@ dependencies:
qr_flutter: ^4.1.0
flutter_markdown: ^0.6.20+1
flutter_slidable: ^3.1.1
country_picker: ^2.0.26
dev_dependencies:
flutter_test: