diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 10fc258..782b7c0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,7 @@ + + diff --git a/lib/api/api_client.dart b/lib/api/api_client.dart index 4908e53..aa7bbc2 100644 --- a/lib/api/api_client.dart +++ b/lib/api/api_client.dart @@ -67,9 +67,13 @@ class ApiClient { } var response = await postJsonForResponse(url, jsonObject, token: token, queryParameters: queryParameters, headers: _headers, retryTimes: retryTimes); try { + if (!kReleaseMode) { + print("res:"+response.body); + } var jsonData = jsonDecode(response.body); return factoryConstructor(jsonData); } catch (ex) { + print("exception:"+response.body); throw APIException(APIException.BAD_RESPONSE_FORMAT, arguments: ex); } } @@ -104,8 +108,13 @@ class ApiClient { url = url + '?' + queryString; } var response = await _post(Uri.parse(url), body: requestBody, headers: _headers).timeout(Duration(seconds: 15)); + if (!kReleaseMode) { - if (response.statusCode >= 200 && response.statusCode < 300) { + print("Url:$url"); + print("body:$requestBody"); + print("res: "+ response.body); + } + if (response.statusCode >= 200 && response.statusCode < 500) { return response; } else { throw _throwAPIException(response); diff --git a/lib/api/shared_prefrence.dart b/lib/api/shared_prefrence.dart new file mode 100644 index 0000000..cad63d1 --- /dev/null +++ b/lib/api/shared_prefrence.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:shared_preferences/shared_preferences.dart'; + +class SharedPrefManager { + static String USER_ID = "user.id"; + static String USER_TOKEN = "user.token"; + + + + static Future _prefs = SharedPreferences.getInstance(); + + static setUserId(String cookie) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString(USER_ID, cookie) ?? "NA"; + } + + static Future getUserId() async { + SharedPreferences prefs = await _prefs; + return prefs.getString(USER_ID) ?? ""; + } + + static setUserToken(String cookie) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString(USER_TOKEN, cookie) ?? "NA"; + } + + static Future getUserToken() async { + SharedPreferences prefs = await _prefs; + return prefs.getString(USER_TOKEN) ?? ""; + } + + +} diff --git a/lib/api/tangheem_user_api_client.dart b/lib/api/tangheem_user_api_client.dart index 802e26a..60851a6 100644 --- a/lib/api/tangheem_user_api_client.dart +++ b/lib/api/tangheem_user_api_client.dart @@ -15,19 +15,19 @@ class TangheemUserApiClient { factory TangheemUserApiClient() => _instance; Future getSurahs() async { - String url = "${ApiConsts.tangheemUsers}AlSuar_Get"; + String url = "${ApiConsts.baseUrl}AlSuar_Get"; var postParams = {}; return await ApiClient().postJsonForObject((json) => SurahModel.fromJson(json), url, postParams); } Future getMembers() async { - String url = "${ApiConsts.tangheemUsers}Committee_Get"; + String url = "${ApiConsts.baseUrl}Committee_Get"; var postParams = {}; return await ApiClient().postJsonForObject((json) => MemberModel.fromJson(json), url, postParams); } Future getContentInfo(int contentId) async { - String url = "${ApiConsts.tangheemUsers}ContentInfo_Get"; + String url = "${ApiConsts.baseUrl}ContentInfo_Get"; var postParams = {"contentTypeId": contentId}; return await ApiClient().postJsonForObject((json) => ContentInfoModel.fromJson(json), url, postParams); } diff --git a/lib/api/user_api_client.dart b/lib/api/user_api_client.dart new file mode 100644 index 0000000..a6d4d68 --- /dev/null +++ b/lib/api/user_api_client.dart @@ -0,0 +1,70 @@ +import 'dart:async'; +import 'package:http/http.dart'; +import 'package:car_customer_app/classes/consts.dart'; +import 'package:car_customer_app/models/content_info_model.dart'; +import 'package:car_customer_app/models/member_model.dart'; +import 'package:car_customer_app/models/surah_model.dart'; +import 'package:car_customer_app/models/user/basic_otp.dart'; +import 'package:car_customer_app/models/user/register_user.dart'; + +import 'api_client.dart'; + +class UserApiClent { + static final UserApiClent _instance = UserApiClent._internal(); + + UserApiClent._internal(); + + factory UserApiClent() => _instance; + + Future basicOtp(String phoneNo,{int otpType=1}) async { + var postParams = {"countryID": 1, "userMobileNo": phoneNo, "otpType": otpType, "userRole": 4}; + return await ApiClient().postJsonForObject((json) => BasicOtp.fromJson(json), ApiConsts.BasicOTP, postParams); + } + + Future basicVerify(String phoneNo, String otp, String userToken) async { + var postParams = { + "userMobileNo": phoneNo, + "userOTP": otp, + "userToken": userToken, + }; + return await ApiClient().postJsonForObject((json) => RegisterUser.fromJson(json), ApiConsts.BasicVerify, postParams); + } + + Future basicComplete(String userId, String firstName, String lastName,String email,String password) async { + var postParams = { + "userID": userId, + "firstName": firstName, + "lastName": lastName, + "email": email, + "companyName": "string", + "isEmailVerified": true, + "password": password + }; + return await ApiClient().postJsonForObject((json) => RegisterUser.fromJson(json), ApiConsts.BasicComplete, postParams); + } + + Future login_V1(String phoneNo, String password) async { + var postParams = { + "mobileorEmail": phoneNo, + "password": password, + }; + return await ApiClient().postJsonForResponse(ApiConsts.Login_V1, postParams); + //return await ApiClient().postJsonForObject((json) => BasicOtp.fromJson(json), ApiConsts.Login_V1, postParams); + } + + Future login_V2_OTP(String userToken, String loginType) async { + var postParams = { + "userToken": userToken, + "loginType": loginType, + }; + return await ApiClient().postJsonForResponse(ApiConsts.Login_V2_OTP, postParams); + } + + Future login_V2_OTPVerify(String userToken, String otp) async { + var postParams = { + "userToken": userToken, + "userOTP": otp + }; + return await ApiClient().postJsonForResponse(ApiConsts.Login_V2_OTPVerify, postParams); + } +} diff --git a/lib/classes/consts.dart b/lib/classes/consts.dart index 0c71f0c..046538a 100644 --- a/lib/classes/consts.dart +++ b/lib/classes/consts.dart @@ -1,14 +1,19 @@ class ApiConsts { //static String baseUrl = "http://10.200.204.20:2801/"; // Local server - static String baseUrl = "http://20.203.25.82"; // production server - static String baseUrlServices = baseUrl + "/services/"; // production server + static String baseUrl = "https://mdlaboratories.com"; // production server + static String baseUrlServices = baseUrl + "/mc/"; // production server // static String baseUrlServices = "https://api.cssynapses.com/tangheem/"; // Live server - static String authentication = baseUrlServices + "api/Authentication/"; - static String tangheemUsers = baseUrlServices + "api/TangheemUsers/"; - static String adminConfiguration = baseUrlServices + "api/AdminConfiguration/"; + static String BasicOTP = baseUrlServices + "api/Register/BasicOTP"; + static String BasicVerify = baseUrlServices + "api/Register/BasicVerify"; + static String BasicComplete = baseUrlServices + "api/Register/BasicComplete"; + + static String Login_V1 = baseUrlServices + "api/Account/Login_V1"; + static String Login_V2_OTP = baseUrlServices + "api/Account/Login_V2_OTP"; + static String Login_V2_OTPVerify = baseUrlServices + "api/Account/Login_V2_OTPVerify"; static String user = baseUrlServices + "api/User/"; } + class GlobalConsts { static String isRememberMe = "remember_me"; static String email = "email"; diff --git a/lib/classes/utils.dart b/lib/classes/utils.dart index 5ae025e..0a6c528 100644 --- a/lib/classes/utils.dart +++ b/lib/classes/utils.dart @@ -1,16 +1,17 @@ +import 'package:car_customer_app/widgets/loading_dialog.dart'; import 'package:flutter/material.dart'; // import 'package:fluttertoast/fluttertoast.dart'; import 'package:car_customer_app/exceptions/api_exception.dart'; - +import 'package:fluttertoast/fluttertoast.dart'; class Utils { static bool _isLoadingVisible = false; static bool get isLoading => _isLoadingVisible; static void showToast(String message) { - // Fluttertoast.showToast( - // msg: message, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, timeInSecForIosWeb: 1, backgroundColor: Colors.black54, textColor: Colors.white, fontSize: 16.0); + Fluttertoast.showToast( + msg: message, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, timeInSecForIosWeb: 1, backgroundColor: Colors.black54, textColor: Colors.white, fontSize: 16.0); } static dynamic getNotNullValue(List list, int index) { @@ -32,13 +33,13 @@ class Utils { static void showLoading(BuildContext context) { WidgetsBinding.instance?.addPostFrameCallback((_) { _isLoadingVisible = true; - // showDialog( - // context: context, - // barrierColor: ColorConsts.primaryBlack.withOpacity(0.5), - // builder: (BuildContext context) => LoadingDialog(), - // ).then((value) { - // _isLoadingVisible = false; - // }); + showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.5), + builder: (BuildContext context) => LoadingDialog(), + ).then((value) { + _isLoadingVisible = false; + }); }); } diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 075d6ac..f5bc288 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -1,8 +1,11 @@ +import 'package:car_customer_app/models/user/register_user.dart'; import 'package:car_customer_app/pages/dashboard/dashboard_page.dart'; import 'package:car_customer_app/pages/user/complete_profile_page.dart'; import 'package:car_customer_app/pages/user/forget_password_page.dart'; +import 'package:car_customer_app/pages/user/login_method_selection_page.dart'; import 'package:car_customer_app/pages/user/login_verification_page.dart'; import 'package:car_customer_app/pages/user/login_verify_account_page.dart'; +import 'package:car_customer_app/pages/user/login_with_password_page.dart'; import 'package:car_customer_app/pages/user/register_page.dart'; import 'package:car_customer_app/pages/user/register_selection_page.dart'; import 'package:car_customer_app/pages/user/splash_page.dart'; @@ -16,10 +19,11 @@ class AppRoutes { static final String register = "/register"; static final String forgetPassword = "/forgetPassword"; static final String loginVerification = "/loginVerification"; - static final String dashboard = "/dashboard"; + static final String loginWithPassword = "/loginWithPassword"; + static final String loginMethodSelection = "/loginMethodSelection"; static final String completeProfile = "/completeProfile"; - + static final String dashboard = "/dashboard"; static final String initialRoute = splash; @@ -31,7 +35,11 @@ class AppRoutes { register: (context) => RegisterPage(), forgetPassword: (context) => ForgetPasswordPage(), loginVerification: (context) => LoginVerificationPage(), + loginWithPassword: (context) => LoginWithPassword(), + loginMethodSelection: (context) => LoginMethodSelectionPage(ModalRoute.of(context)!.settings.arguments as String), + completeProfile: (context) => CompleteProfilePage(ModalRoute.of(context)!.settings.arguments as RegisterUser), + + //Home page dashboard: (context) => DashboardPage(), - completeProfile: (context) => CompleteProfilePage(), }; } diff --git a/lib/models/user.dart b/lib/models/user.dart deleted file mode 100644 index e09c282..0000000 --- a/lib/models/user.dart +++ /dev/null @@ -1,9 +0,0 @@ -class User { - int id; - - User(this.id, this.userName, this.userImage, this.createdDate); - - String userName; - String userImage; - String createdDate; -} diff --git a/lib/models/user/basic_otp.dart b/lib/models/user/basic_otp.dart new file mode 100644 index 0000000..647a488 --- /dev/null +++ b/lib/models/user/basic_otp.dart @@ -0,0 +1,61 @@ +// To parse this JSON data, do +// +// final basicOtp = basicOtpFromJson(jsonString); + +import 'dart:convert'; + +BasicOtp basicOtpFromJson(String str) => BasicOtp.fromJson(json.decode(str)); + +String basicOtpToJson(BasicOtp data) => json.encode(data.toJson()); + +class BasicOtp { + BasicOtp({ + this.totalItemsCount, + this.data, + this.messageStatus, + this.message, + }); + + dynamic totalItemsCount; + Data? data; + int? messageStatus; + String? message; + + factory BasicOtp.fromJson(Map json) => BasicOtp( + totalItemsCount: json["totalItemsCount"], + data: json["data"] == null ? null : Data.fromJson(json["data"]), + messageStatus: json["messageStatus"] == null ? null : json["messageStatus"], + message: json["message"] == null ? null : json["message"], + ); + + Map toJson() => { + "totalItemsCount": totalItemsCount, + "data": data == null ? null : data!.toJson(), + "messageStatus": messageStatus == null ? null : messageStatus, + "message": message == null ? null : message, + }; +} + +class Data { + Data({ + this.userToken, + }); + + String? userToken; + + factory Data.fromJson(Map json) => Data( + userToken: checkValue(json), + ); + + static String checkValue(Map json) { + try { + return json["userToken"] == null ? null : json["userToken"]; + } catch (e) { + return json["token"] == null ? null : json["token"]; + } + } + + Map toJson() => { + "userToken": userToken == null ? null : userToken, + }; +} diff --git a/lib/models/user/register_user.dart b/lib/models/user/register_user.dart new file mode 100644 index 0000000..fef636f --- /dev/null +++ b/lib/models/user/register_user.dart @@ -0,0 +1,117 @@ +// To parse this JSON data, do +// +// final user = userFromMap(jsonString); + +import 'dart:convert'; + +RegisterUser userFromMap(String str) => RegisterUser.fromJson(json.decode(str)); + +String userToMap(RegisterUser data) => json.encode(data.toMap()); + +class RegisterUser { + RegisterUser({ + this.totalItemsCount, + this.data, + this.messageStatus, + this.message, + }); + + dynamic totalItemsCount; + Data? data; + int? messageStatus; + String? message; + + factory RegisterUser.fromJson(Map json) => RegisterUser( + totalItemsCount: json["totalItemsCount"], + data: json["data"] == null ? null : Data.fromMap(json["data"]), + messageStatus: json["messageStatus"] == null ? null : json["messageStatus"], + message: json["message"] == null ? null : json["message"], + ); + + Map toMap() => { + "totalItemsCount": totalItemsCount, + "data": data == null ? null : data!.toMap(), + "messageStatus": messageStatus == null ? null : messageStatus, + "message": message == null ? null : message, + }; +} + +class Data { + Data({ + this.id, + this.userId, + this.firstName, + this.lastName, + this.mobileNo, + this.email, + this.userImageUrl, + this.roleId, + this.roleName, + this.isEmailVerified, + this.serviceProviderBranch, + this.isVerified, + this.userRoles, + this.isCustomer, + this.isProvider, + this.providerId, + this.customerId, + }); + + int? id; + String? userId; + dynamic? firstName; + dynamic? lastName; + String? mobileNo; + String? email; + dynamic? userImageUrl; + int? roleId; + dynamic? roleName; + bool? isEmailVerified; + List? serviceProviderBranch; + bool? isVerified; + List? userRoles; + bool? isCustomer; + bool? isProvider; + dynamic? providerId; + dynamic? customerId; + + factory Data.fromMap(Map json) => Data( + id: json["id"] == null ? null : json["id"], + userId: json["userID"] == null ? null : json["userID"], + firstName: json["firstName"], + lastName: json["lastName"], + mobileNo: json["mobileNo"] == null ? null : json["mobileNo"], + email: json["email"] == null ? null : json["email"], + userImageUrl: json["userImageUrl"], + roleId: json["roleID"] == null ? null : json["roleID"], + roleName: json["roleName"], + isEmailVerified: json["isEmailVerified"] == null ? null : json["isEmailVerified"], + serviceProviderBranch: json["serviceProviderBranch"] == null ? null : List.from(json["serviceProviderBranch"].map((x) => x)), + isVerified: json["isVerified"] == null ? null : json["isVerified"], + userRoles: json["userRoles"] == null ? null : List.from(json["userRoles"].map((x) => x)), + isCustomer: json["isCustomer"] == null ? null : json["isCustomer"], + isProvider: json["isProvider"] == null ? null : json["isProvider"], + providerId: json["providerID"], + customerId: json["customerID"], + ); + + Map toMap() => { + "id": id == null ? null : id, + "userID": userId == null ? null : userId, + "firstName": firstName, + "lastName": lastName, + "mobileNo": mobileNo == null ? null : mobileNo, + "email": email == null ? null : email, + "userImageUrl": userImageUrl, + "roleID": roleId == null ? null : roleId, + "roleName": roleName, + "isEmailVerified": isEmailVerified == null ? null : isEmailVerified, + "serviceProviderBranch": serviceProviderBranch == null ? null : List.from(serviceProviderBranch!.map((x) => x)), + "isVerified": isVerified == null ? null : isVerified, + "userRoles": userRoles == null ? null : List.from(userRoles!.map((x) => x)), + "isCustomer": isCustomer == null ? null : isCustomer, + "isProvider": isProvider == null ? null : isProvider, + "providerID": providerId, + "customerID": customerId, + }; +} diff --git a/lib/models/user/user.dart b/lib/models/user/user.dart new file mode 100644 index 0000000..aea036b --- /dev/null +++ b/lib/models/user/user.dart @@ -0,0 +1,117 @@ +// To parse this JSON data, do +// +// final user = userFromMap(jsonString); + +import 'dart:convert'; + +User userFromMap(String str) => User.fromMap(json.decode(str)); + +String userToMap(User data) => json.encode(data.toMap()); + +class User { + User({ + this.accessToken, + this.refreshToken, + this.expiryDate, + this.userInfo, + }); + + String? accessToken; + String? refreshToken; + DateTime? expiryDate; + UserInfo? userInfo; + + factory User.fromMap(Map json) => User( + accessToken: json["accessToken"] == null ? null : json["accessToken"], + refreshToken: json["refreshToken"] == null ? null : json["refreshToken"], + expiryDate: json["expiryDate"] == null ? null : DateTime.parse(json["expiryDate"]), + userInfo: json["userInfo"] == null ? null : UserInfo.fromMap(json["userInfo"]), + ); + + Map toMap() => { + "accessToken": accessToken == null ? null : accessToken, + "refreshToken": refreshToken == null ? null : refreshToken, + "expiryDate": expiryDate == null ? null : expiryDate!.toIso8601String(), + "userInfo": userInfo == null ? null : userInfo!.toMap(), + }; +} + +class UserInfo { + UserInfo({ + this.id, + this.userId, + this.firstName, + this.lastName, + this.mobileNo, + this.email, + this.userImageUrl, + this.roleId, + this.roleName, + this.isEmailVerified, + this.serviceProviderBranch, + this.isVerified, + this.userRoles, + this.isCustomer, + this.isProvider, + this.providerId, + this.customerId, + }); + + int? id; + String? userId; + String? firstName; + String? lastName; + String? mobileNo; + String? email; + dynamic? userImageUrl; + int? roleId; + String? roleName; + bool? isEmailVerified; + List? serviceProviderBranch; + bool? isVerified; + List? userRoles; + bool? isCustomer; + bool? isProvider; + dynamic? providerId; + int? customerId; + + factory UserInfo.fromMap(Map json) => UserInfo( + id: json["id"] == null ? null : json["id"], + userId: json["userID"] == null ? null : json["userID"], + firstName: json["firstName"] == null ? null : json["firstName"], + lastName: json["lastName"] == null ? null : json["lastName"], + mobileNo: json["mobileNo"] == null ? null : json["mobileNo"], + email: json["email"] == null ? null : json["email"], + userImageUrl: json["userImageUrl"], + roleId: json["roleID"] == null ? null : json["roleID"], + roleName: json["roleName"] == null ? null : json["roleName"], + isEmailVerified: json["isEmailVerified"] == null ? null : json["isEmailVerified"], + serviceProviderBranch: json["serviceProviderBranch"] == null ? null : List.from(json["serviceProviderBranch"].map((x) => x)), + isVerified: json["isVerified"] == null ? null : json["isVerified"], + userRoles: json["userRoles"] == null ? null : List.from(json["userRoles"].map((x) => x)), + isCustomer: json["isCustomer"] == null ? null : json["isCustomer"], + isProvider: json["isProvider"] == null ? null : json["isProvider"], + providerId: json["providerID"], + customerId: json["customerID"] == null ? null : json["customerID"], + ); + + Map toMap() => { + "id": id == null ? null : id, + "userID": userId == null ? null : userId, + "firstName": firstName == null ? null : firstName, + "lastName": lastName == null ? null : lastName, + "mobileNo": mobileNo == null ? null : mobileNo, + "email": email == null ? null : email, + "userImageUrl": userImageUrl, + "roleID": roleId == null ? null : roleId, + "roleName": roleName == null ? null : roleName, + "isEmailVerified": isEmailVerified == null ? null : isEmailVerified, + "serviceProviderBranch": serviceProviderBranch == null ? null : List.from(serviceProviderBranch!.map((x) => x)), + "isVerified": isVerified == null ? null : isVerified, + "userRoles": userRoles == null ? null : List.from(userRoles!.map((x) => x)), + "isCustomer": isCustomer == null ? null : isCustomer, + "isProvider": isProvider == null ? null : isProvider, + "providerID": providerId, + "customerID": customerId == null ? null : customerId, + }; +} diff --git a/lib/pages/user/complete_profile_page.dart b/lib/pages/user/complete_profile_page.dart index 1bd71cd..41dfc6f 100644 --- a/lib/pages/user/complete_profile_page.dart +++ b/lib/pages/user/complete_profile_page.dart @@ -1,5 +1,13 @@ +import 'package:car_customer_app/api/user_api_client.dart'; +import 'package:car_customer_app/classes/utils.dart'; +import 'package:car_customer_app/models/user/basic_otp.dart'; +import 'package:car_customer_app/models/user/register_user.dart'; +import 'package:car_customer_app/utils/navigator.dart'; import 'package:car_customer_app/utils/utils.dart'; import 'package:car_customer_app/widgets/app_bar.dart'; +import 'package:car_customer_app/widgets/dialog/dialogs.dart'; +import 'package:car_customer_app/widgets/dialog/message_dialog.dart'; +import 'package:car_customer_app/widgets/dialog/otp_dialog.dart'; import 'package:car_customer_app/widgets/show_fill_button.dart'; import 'package:car_customer_app/extensions/widget_extensions.dart'; import 'package:car_customer_app/extensions/int_extensions.dart'; @@ -7,7 +15,24 @@ import 'package:car_customer_app/extensions/string_extensions.dart'; import 'package:car_customer_app/widgets/txt_field.dart'; import 'package:flutter/material.dart'; -class CompleteProfilePage extends StatelessWidget { +class CompleteProfilePage extends StatefulWidget { + RegisterUser user; + + CompleteProfilePage(this.user); + + @override + State createState() => _CompleteProfilePageState(); +} + +class _CompleteProfilePageState extends State { + String? firstName = "", lastName = "", email = "", password = "", confirmPassword = ""; + + @override + void initState() { + // TODO: implement initState + super.initState(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -21,38 +46,55 @@ class CompleteProfilePage extends StatelessWidget { child: Column( children: [ "Complete Profile".toText24(), - 12.height, + 12.height, TxtField( hint: "First Name", + onChanged: (v) { + firstName = v; + }, ), 12.height, TxtField( hint: "Surname", + onChanged: (v) { + lastName = v; + }, ), 12.height, TxtField( hint: "Email", + onChanged: (v) { + email = v; + }, ), 12.height, TxtField( hint: "Create Password", + onChanged: (v) { + password = v; + }, ), 12.height, TxtField( hint: "Confirm Password", + onChanged: (v) { + confirmPassword = v; + }, ), - 12.height, - TxtField( - hint: "Phone Number", - ), + // 12.height, + // TxtField( + // hint: "Phone Number", + // ), 50.height, "By creating an account you agree to our Terms of Service and Privacy Policy".toText12(), 16.height, ShowFillButton( title: "Continue", width: double.infinity, - onPressed: () {}, + onPressed: () { + performCompleteProfile(); + }, ), ], ), @@ -61,4 +103,21 @@ class CompleteProfilePage extends StatelessWidget { ), ); } + + Future performCompleteProfile() async { + if (password == confirmPassword) { + print(widget.user.data!.userId??"userId"); + Utils.showLoading(context); + RegisterUser user = await UserApiClent().basicComplete(widget.user.data?.userId ?? "", firstName!, lastName!, email!, password!); + Utils.hideLoading(context); + if (user.messageStatus == 1) { + Utils.showToast(user.message ?? ""); + + } else { + Utils.showToast(user.message ?? ""); + } + } else { + Utils.showToast("Please enter same password"); + } + } } diff --git a/lib/pages/user/login_method_selection_page.dart b/lib/pages/user/login_method_selection_page.dart new file mode 100644 index 0000000..73c87ef --- /dev/null +++ b/lib/pages/user/login_method_selection_page.dart @@ -0,0 +1,122 @@ +import 'dart:convert'; + +import 'package:car_customer_app/api/shared_prefrence.dart'; +import 'package:car_customer_app/api/user_api_client.dart'; +import 'package:car_customer_app/classes/utils.dart'; +import 'package:car_customer_app/config/constants.dart'; +import 'package:car_customer_app/config/routes.dart'; +import 'package:car_customer_app/models/user/user.dart'; +import 'package:car_customer_app/utils/navigator.dart'; +import 'package:car_customer_app/utils/utils.dart'; +import 'package:car_customer_app/widgets/app_bar.dart'; +import 'package:car_customer_app/widgets/button/show_image_button.dart'; +import 'package:car_customer_app/widgets/dialog/dialogs.dart'; +import 'package:car_customer_app/widgets/dialog/message_dialog.dart'; +import 'package:car_customer_app/extensions/int_extensions.dart'; +import 'package:car_customer_app/extensions/string_extensions.dart'; +import 'package:car_customer_app/extensions/widget_extensions.dart'; +import 'package:car_customer_app/widgets/dialog/otp_dialog.dart'; +import 'package:car_customer_app/widgets/txt_field.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +class LoginMethodSelectionPage extends StatelessWidget { + String userToken; + + LoginMethodSelectionPage(this.userToken); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: appBar(title: "Log In"), + body: Container( + width: double.infinity, + height: double.infinity, + padding: EdgeInsets.all(40), + child: Column( + children: [ + "Login Selection".toText24(), + mFlex(2), + Row( + children: [ + Expanded( + child: ShowImageButton( + onClick: () { + performBasicOtp(context); + }, + title: 'Finger Print', + icon: icons + "ic_fingerprint.png", + ), + ), + 20.width, + Expanded( + child: ShowImageButton( + onClick: () { + performBasicOtp(context); + }, + title: 'Face Recognition', + icon: icons + "ic_face_id.png", + ), + ), + ], + ), + 40.height, + Row( + children: [ + Expanded( + child: ShowImageButton( + onClick: () { + performBasicOtp(context); + }, + title: 'With SMS', + icon: icons + "ic_sms.png", + ), + ), + 20.width, + Expanded( + child: ShowImageButton( + onClick: () { + // navigateWithName(context, AppRoutes.dashboard); + performBasicOtp(context); + }, + title: 'With Whatsapp', + icon: icons + "ic_whatsapp.png", + ), + ), + ], + ), + mFlex(10), + ], + ), + ), + ); + } + + Future performBasicOtp(BuildContext context) async { + Utils.showLoading(context); + Response response = await UserApiClent().login_V2_OTP(userToken, "1"); + Utils.hideLoading(context); + if (response.statusCode == 200) { + String userToken = jsonDecode(response.body)["token"]; + showMDialog(context, child: OtpDialog( + onClick: (String code) async { + pop(context); + Utils.showLoading(context); + Response response2 = await UserApiClent().login_V2_OTPVerify(userToken, code); + Utils.hideLoading(context); + if (response2.statusCode == 200) { + User user = User.fromMap(jsonDecode(response2.body)); + SharedPrefManager.setUserToken(user.accessToken ?? ""); + SharedPrefManager.setUserId(user.userInfo!.userId ?? ""); + navigateWithName(context, AppRoutes.dashboard); + } else { + Utils.showToast("Something went wrong"); + } + }, + )); + } else { + String res = jsonDecode(response.body)["errors"][0] ?? ""; + Utils.showToast(res); + } + } +} diff --git a/lib/pages/user/login_verification_page.dart b/lib/pages/user/login_verification_page.dart index 3a0166b..0f81c1b 100644 --- a/lib/pages/user/login_verification_page.dart +++ b/lib/pages/user/login_verification_page.dart @@ -1,5 +1,11 @@ +import 'dart:convert'; + +import 'package:car_customer_app/api/shared_prefrence.dart'; +import 'package:car_customer_app/api/user_api_client.dart'; +import 'package:car_customer_app/classes/utils.dart'; import 'package:car_customer_app/config/constants.dart'; import 'package:car_customer_app/config/routes.dart'; +import 'package:car_customer_app/models/user/user.dart'; import 'package:car_customer_app/utils/navigator.dart'; import 'package:car_customer_app/utils/utils.dart'; import 'package:car_customer_app/widgets/app_bar.dart'; @@ -9,8 +15,10 @@ import 'package:car_customer_app/widgets/dialog/message_dialog.dart'; import 'package:car_customer_app/extensions/int_extensions.dart'; import 'package:car_customer_app/extensions/string_extensions.dart'; import 'package:car_customer_app/extensions/widget_extensions.dart'; +import 'package:car_customer_app/widgets/dialog/otp_dialog.dart'; import 'package:car_customer_app/widgets/txt_field.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; class LoginVerificationPage extends StatelessWidget { @override @@ -31,16 +39,7 @@ class LoginVerificationPage extends StatelessWidget { Expanded( child: ShowImageButton( onClick: () { - showMDialog( - context, - child: MessageDialog( - title: "Account Verified", - onClick: () { - pop(context); - navigateWithName(context, AppRoutes.dashboard); - }, - ), - ); + performBasicOtp(context); }, title: 'Finger Print', icon: icons + "ic_fingerprint.png", @@ -50,7 +49,7 @@ class LoginVerificationPage extends StatelessWidget { Expanded( child: ShowImageButton( onClick: () { - navigateWithName(context, AppRoutes.dashboard); + performBasicOtp(context); }, title: 'Face Recognition', icon: icons + "ic_face_id.png", @@ -64,7 +63,7 @@ class LoginVerificationPage extends StatelessWidget { Expanded( child: ShowImageButton( onClick: () { - navigateWithName(context, AppRoutes.dashboard); + performBasicOtp(context); }, title: 'With SMS', icon: icons + "ic_sms.png", @@ -74,7 +73,7 @@ class LoginVerificationPage extends StatelessWidget { Expanded( child: ShowImageButton( onClick: () { - navigateWithName(context, AppRoutes.dashboard); + performBasicOtp(context); }, title: 'With Whatsapp', icon: icons + "ic_whatsapp.png", @@ -88,4 +87,33 @@ class LoginVerificationPage extends StatelessWidget { ), ); } + + Future performBasicOtp(BuildContext context) async { + Utils.showLoading(context); + String userToken=await SharedPrefManager.getUserToken(); + Response response = await UserApiClent().login_V2_OTP(userToken, "1"); + Utils.hideLoading(context); + if (response.statusCode == 200) { + String userToken = jsonDecode(response.body)["token"]; + showMDialog(context, child: OtpDialog( + onClick: (String code) async { + pop(context); + Utils.showLoading(context); + Response response2 = await UserApiClent().login_V2_OTPVerify(userToken, code); + Utils.hideLoading(context); + if (response2.statusCode == 200) { + User user = User.fromMap(jsonDecode(response2.body)); + SharedPrefManager.setUserToken(user.accessToken ?? ""); + SharedPrefManager.setUserId(user.userInfo!.userId ?? ""); + navigateWithName(context, AppRoutes.dashboard); + } else { + Utils.showToast("Something went wrong"); + } + }, + )); + } else { + String res = jsonDecode(response.body)["errors"][0] ?? ""; + Utils.showToast(res); + } + } } diff --git a/lib/pages/user/login_verify_account_page.dart b/lib/pages/user/login_verify_account_page.dart index f8cdfc8..2241044 100644 --- a/lib/pages/user/login_verify_account_page.dart +++ b/lib/pages/user/login_verify_account_page.dart @@ -1,5 +1,9 @@ +import 'package:car_customer_app/api/user_api_client.dart'; +import 'package:car_customer_app/classes/utils.dart'; import 'package:car_customer_app/config/constants.dart'; import 'package:car_customer_app/config/routes.dart'; +import 'package:car_customer_app/models/user/basic_otp.dart'; +import 'package:car_customer_app/models/user/register_user.dart'; import 'package:car_customer_app/utils/navigator.dart'; import 'package:car_customer_app/utils/utils.dart'; import 'package:car_customer_app/widgets/app_bar.dart'; @@ -14,6 +18,9 @@ import 'package:car_customer_app/widgets/txt_field.dart'; import 'package:flutter/material.dart'; class LoginVerifyAccountPage extends StatelessWidget { + int otpType = 1; + String phoneNum = ""; + @override Widget build(BuildContext context) { return Scaffold( @@ -28,6 +35,9 @@ class LoginVerifyAccountPage extends StatelessWidget { mFlex(1), TxtField( hint: "Enter Phone number to verify", + onChanged: (v) { + phoneNum = v; + }, ), mFlex(2), Row( @@ -35,22 +45,26 @@ class LoginVerifyAccountPage extends StatelessWidget { Expanded( child: ShowImageButton( onClick: () { - showMDialog(context, child: OtpDialog( - onClick: () { - pop(context); - delay(300).then( - (value) => showMDialog( - context, - child: MessageDialog( - title: "Phone Number Verified", - onClick: () { - navigateWithName(context, AppRoutes.completeProfile); - }, - ), - ), - ); - }, - )); + otpType = 1; + performBasicOtp(context); + // showMDialog(context, child: OtpDialog( + // onClick: (String code) { + // pop(context); + // delay(300).then( + // (value) => showMDialog( + // context, + // child: MessageDialog( + // title: "Phone Number Verified", + // onClick: () { + // otpType=1; + // performBasicOtp(context); + // // navigateWithName(context, AppRoutes.completeProfile); + // }, + // ), + // ), + // ); + // }, + // )); }, title: 'With SMS', icon: icons + "ic_sms.png", @@ -60,22 +74,26 @@ class LoginVerifyAccountPage extends StatelessWidget { Expanded( child: ShowImageButton( onClick: () { - showMDialog(context, child: OtpDialog( - onClick: () { - pop(context); - delay(300).then( - (value) => showMDialog( - context, - child: MessageDialog( - title: "Phone Number Verified", - onClick: () { - navigateWithName(context, AppRoutes.completeProfile); - }, - ), - ), - ); - }, - )); + otpType = 1; + performBasicOtp(context); + // showMDialog(context, child: OtpDialog( + // onClick: (String code) { + // pop(context); + // delay(300).then( + // (value) => showMDialog( + // context, + // child: MessageDialog( + // title: "Phone Number Verified", + // onClick: () { + // otpType=2; + // performBasicOtp(context); + // // navigateWithName(context, AppRoutes.completeProfile); + // }, + // ), + // ), + // ); + // }, + // )); }, title: 'With Whatsapp', icon: icons + "ic_whatsapp.png", @@ -89,4 +107,36 @@ class LoginVerifyAccountPage extends StatelessWidget { ), ); } + + Future performBasicOtp(BuildContext context) async { + Utils.showLoading(context); + BasicOtp basicOtp = await UserApiClent().basicOtp(phoneNum, otpType: otpType); + Utils.hideLoading(context); + if (basicOtp.messageStatus == 1) { + showMDialog(context, child: OtpDialog( + onClick: (String code) async { + pop(context); + Utils.showLoading(context); + RegisterUser user = await UserApiClent().basicVerify(phoneNum, code, basicOtp.data!.userToken ?? ""); + Utils.hideLoading(context); + if (user.messageStatus == 1) { + Utils.showToast(user.message ?? ""); + showMDialog( + context, + child: MessageDialog( + title: "Phone Number Verified", + onClick: () { + navigateWithName(context, AppRoutes.completeProfile,arguments: user); + }, + ), + ); + } else { + Utils.showToast(user.message ?? ""); + } + }, + )); + } else { + Utils.showToast(basicOtp.message ?? ""); + } + } } diff --git a/lib/pages/user/login_with_password_page.dart b/lib/pages/user/login_with_password_page.dart new file mode 100644 index 0000000..580598c --- /dev/null +++ b/lib/pages/user/login_with_password_page.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:car_customer_app/api/user_api_client.dart'; +import 'package:car_customer_app/classes/utils.dart'; +import 'package:car_customer_app/config/constants.dart'; +import 'package:car_customer_app/config/routes.dart'; +import 'package:car_customer_app/models/user/basic_otp.dart'; +import 'package:car_customer_app/models/user/register_user.dart'; +import 'package:car_customer_app/utils/navigator.dart'; +import 'package:car_customer_app/utils/utils.dart'; +import 'package:car_customer_app/widgets/app_bar.dart'; +import 'package:car_customer_app/widgets/button/show_image_button.dart'; +import 'package:car_customer_app/widgets/dialog/dialogs.dart'; +import 'package:car_customer_app/widgets/dialog/message_dialog.dart'; +import 'package:car_customer_app/widgets/dialog/otp_dialog.dart'; +import 'package:car_customer_app/extensions/int_extensions.dart'; +import 'package:car_customer_app/extensions/string_extensions.dart'; +import 'package:car_customer_app/extensions/widget_extensions.dart'; +import 'package:car_customer_app/widgets/show_fill_button.dart'; +import 'package:car_customer_app/widgets/txt_field.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart'; + +class LoginWithPassword extends StatelessWidget { + int otpType = 1; + String phoneNum = "", password = ""; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: appBar(title: "Log In"), + body: Container( + width: double.infinity, + height: double.infinity, + padding: EdgeInsets.all(40), + child: Column( + children: [ + "Login".toText24(), + mFlex(1), + TxtField( + hint: "Enter Phone number to verify", + onChanged: (v) { + phoneNum = v; + }, + ), + 12.height, + TxtField( + hint: "Password", + onChanged: (v) { + password = v; + }, + ), + 50.height, + ShowFillButton( + title: "Continue", + width: double.infinity, + onPressed: () { + performBasicOtp(context); + }, + ), + mFlex(10), + ], + ), + ), + ); + } + + Future performBasicOtp(BuildContext context) async { + Utils.showLoading(context); + Response response = await UserApiClent().login_V1(phoneNum, password); + Utils.hideLoading(context); + if (response.statusCode == 200) { + String userToken = jsonDecode(response.body)["userToken"]; + navigateWithName(context, AppRoutes.loginMethodSelection, arguments: userToken); + } else { + String res = jsonDecode(response.body)["errors"][0] ?? ""; + Utils.showToast(res); + } + } +} diff --git a/lib/pages/user/register_page.dart b/lib/pages/user/register_page.dart index cb93d7d..a7346c7 100644 --- a/lib/pages/user/register_page.dart +++ b/lib/pages/user/register_page.dart @@ -1,5 +1,13 @@ +import 'package:car_customer_app/api/api_client.dart'; +import 'package:car_customer_app/api/user_api_client.dart'; +import 'package:car_customer_app/classes/utils.dart'; +import 'package:car_customer_app/models/user/basic_otp.dart'; +import 'package:car_customer_app/models/user/register_user.dart'; +import 'package:car_customer_app/utils/navigator.dart'; import 'package:car_customer_app/utils/utils.dart'; import 'package:car_customer_app/widgets/app_bar.dart'; +import 'package:car_customer_app/widgets/dialog/dialogs.dart'; +import 'package:car_customer_app/widgets/dialog/otp_dialog.dart'; import 'package:car_customer_app/widgets/show_fill_button.dart'; import 'package:car_customer_app/extensions/int_extensions.dart'; import 'package:car_customer_app/extensions/string_extensions.dart'; @@ -8,6 +16,8 @@ import 'package:car_customer_app/widgets/txt_field.dart'; import 'package:flutter/material.dart'; class RegisterPage extends StatelessWidget { + String phoneNum = ""; + @override Widget build(BuildContext context) { return Scaffold( @@ -19,30 +29,58 @@ class RegisterPage extends StatelessWidget { child: Column( children: [ "Enter Phone Number".toText24(), - 12.height, TxtField( hint: "Enter Phone number to Register", - ), - TxtField( - hint: "Enter Phone number to Register", - ), - 40.height, - "Or Enter Email".toText24(), - 12.height, - - TxtField( - hint: "Enter Email to Register", + onChanged: (v) { + phoneNum = v; + }, ), 50.height, ShowFillButton( title: "Continue", width: double.infinity, - onPressed: () {}, + onPressed: () { + performBasicOtp(context); + }, ), ], ), ), ); } + + Future performBasicOtp(BuildContext context) async { + Utils.showLoading(context); + BasicOtp basicOtp = await UserApiClent().basicOtp(phoneNum); + Utils.hideLoading(context); + if (basicOtp.messageStatus == 1) { + showMDialog(context, child: OtpDialog( + onClick: (String code) async { + pop(context); + Utils.showLoading(context); + RegisterUser user = await UserApiClent().basicVerify(phoneNum, code, basicOtp.data!.userToken ?? ""); + Utils.hideLoading(context); + if (user.messageStatus == 1) { + Utils.showToast(user.message ?? ""); + } else { + Utils.showToast(user.message ?? ""); + } + // delay(300).then( + // (value) => showMDialog( + // context, + // child: MessageDialog( + // title: "Phone Number Verified", + // onClick: () { + // navigateWithName(context, AppRoutes.completeProfile); + // }, + // ), + // ), + // ); + }, + )); + } else { + Utils.showToast(basicOtp.message ?? ""); + } + } } diff --git a/lib/pages/user/register_selection_page.dart b/lib/pages/user/register_selection_page.dart index f49015a..0b9e71d 100644 --- a/lib/pages/user/register_selection_page.dart +++ b/lib/pages/user/register_selection_page.dart @@ -21,6 +21,14 @@ class RegisterSelectionPage extends StatelessWidget { children: [ "Welcome Message".toText24(), mFlex(1), + ShowFillButton( + title: "Log In With Password", + width: double.infinity, + onPressed: () { + navigateWithName(context, AppRoutes.loginWithPassword); + }, + ), + 20.height, ShowFillButton( title: "Log In", width: double.infinity, diff --git a/lib/widgets/dialog/otp_dialog.dart b/lib/widgets/dialog/otp_dialog.dart index deeb32e..6c393b6 100644 --- a/lib/widgets/dialog/otp_dialog.dart +++ b/lib/widgets/dialog/otp_dialog.dart @@ -1,3 +1,4 @@ +import 'package:car_customer_app/classes/colors.dart'; import 'package:car_customer_app/theme/colors.dart'; import 'package:car_customer_app/utils/navigator.dart'; import 'package:car_customer_app/utils/utils.dart'; @@ -6,67 +7,96 @@ import 'package:car_customer_app/extensions/string_extensions.dart'; import 'package:car_customer_app/extensions/int_extensions.dart'; import 'package:flutter/material.dart'; +import '../otp_widget.dart'; + class OtpDialog extends StatelessWidget { - VoidCallback onClick; + Function(String) onClick; + OtpDialog({required this.onClick}); + String code=""; + final TextEditingController _pinPutController = TextEditingController(); @override Widget build(BuildContext context) { return Container( color: Colors.white, - padding: EdgeInsets.all(30), + padding: EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ "Please insert OTP Code".toText24(), 20.height, - Row( - children: [ - Expanded( - child: Container( - width: double.infinity, - height: 60, - color: accentColor.withOpacity(0.3), - ), - ), - 12.width, - Expanded( - child: Container( - width: double.infinity, - height: 60, - color: accentColor.withOpacity(0.3), - ), - ), - 12.width, - Expanded( - child: Container( - width: double.infinity, - height: 60, - color: accentColor.withOpacity(0.3), - ), - ), - 12.width, - Expanded( - child: Container( - width: double.infinity, - height: 60, - color: accentColor.withOpacity(0.3), - ), - ), - ], + OTPWidget( + autoFocus: true, + controller: _pinPutController, + defaultBorderColor: const Color(0xffD8D8D8), + maxLength: 4, + onTextChanged: (text) {}, + pinBoxColor: Colors.white, + onDone: (code) => _onOtpCallBack(code, null), + textBorderColor: const Color(0xffD8D8D8), + pinBoxWidth: 60, + pinBoxHeight: 60, + pinTextStyle: const TextStyle(fontSize: 24.0, color: MyColors.darkTextColor), + pinTextAnimatedSwitcherTransition: ProvidedPinBoxTextAnimation.scalingTransition, + pinTextAnimatedSwitcherDuration: const Duration(milliseconds: 300), + pinBoxRadius: 10, + keyboardType: TextInputType.number, ), + // Row( + // children: [ + // Expanded( + // child: Container( + // width: double.infinity, + // height: 60, + // color: accentColor.withOpacity(0.3), + // ), + // ), + // 12.width, + // Expanded( + // child: Container( + // width: double.infinity, + // height: 60, + // color: accentColor.withOpacity(0.3), + // ), + // ), + // 12.width, + // Expanded( + // child: Container( + // width: double.infinity, + // height: 60, + // color: accentColor.withOpacity(0.3), + // ), + // ), + // 12.width, + // Expanded( + // child: Container( + // width: double.infinity, + // height: 60, + // color: accentColor.withOpacity(0.3), + // ), + // ), + // ], + // ), 40.height, ShowFillButton( title: "Check Code", width: double.infinity, onPressed: () { - onClick(); + onClick(code); }, ) ], ), ); } + + _onOtpCallBack(String otpCode, bool? isAutofill) { + if (otpCode.length == 4) { + // onSuccess(otpCode); + code=otpCode; + } + } } diff --git a/lib/widgets/loading_dialog.dart b/lib/widgets/loading_dialog.dart new file mode 100644 index 0000000..e4b1cf8 --- /dev/null +++ b/lib/widgets/loading_dialog.dart @@ -0,0 +1,43 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class LoadingDialog extends StatefulWidget { + LoadingDialog({Key? key}) : super(key: key); + + @override + _LoadingDialogState createState() { + return _LoadingDialogState(); + } +} + +class _LoadingDialogState extends State { + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + insetPadding: const EdgeInsets.symmetric(horizontal: 60.0, vertical: 24.0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + backgroundColor: Colors.transparent, + child: Directionality( + textDirection: TextDirection.rtl, + child: Center( + child: CircularProgressIndicator(), + ), + ), + ); + } +} diff --git a/lib/widgets/otp_widget.dart b/lib/widgets/otp_widget.dart new file mode 100644 index 0000000..a489e61 --- /dev/null +++ b/lib/widgets/otp_widget.dart @@ -0,0 +1,373 @@ +import 'dart:async'; + +import 'package:flutter/animation.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +typedef OnDone = void Function(String text); + +class ProvidedPinBoxTextAnimation { + static AnimatedSwitcherTransitionBuilder scalingTransition = (child, animation) { + return ScaleTransition( + child: child, + scale: animation, + ); + }; + + static AnimatedSwitcherTransitionBuilder defaultNoTransition = (Widget child, Animation animation) { + return child; + }; +} + +class OTPWidget extends StatefulWidget { + final int maxLength; + final TextEditingController? controller; + + final Color defaultBorderColor; + final Color pinBoxColor; + final double pinBoxBorderWidth; + final double pinBoxRadius; + final bool hideDefaultKeyboard; + + final TextStyle? pinTextStyle; + final double pinBoxHeight; + final double pinBoxWidth; + final OnDone? onDone; + final bool hasError; + final Color errorBorderColor; + final Color textBorderColor; + final Function(String)? onTextChanged; + final bool autoFocus; + final FocusNode? focusNode; + final AnimatedSwitcherTransitionBuilder? pinTextAnimatedSwitcherTransition; + final Duration pinTextAnimatedSwitcherDuration; + final TextDirection textDirection; + final TextInputType keyboardType; + final EdgeInsets pinBoxOuterPadding; + + const OTPWidget({ + Key? key, + this.maxLength: 4, + this.controller, + this.pinBoxWidth: 70.0, + this.pinBoxHeight: 70.0, + this.pinTextStyle, + this.onDone, + this.defaultBorderColor: Colors.black, + this.textBorderColor: Colors.black, + this.pinTextAnimatedSwitcherTransition, + this.pinTextAnimatedSwitcherDuration: const Duration(), + this.hasError: false, + this.errorBorderColor: Colors.red, + this.onTextChanged, + this.autoFocus: false, + this.focusNode, + this.textDirection: TextDirection.ltr, + this.keyboardType: TextInputType.number, + this.pinBoxOuterPadding = const EdgeInsets.symmetric(horizontal: 4.0), + this.pinBoxColor = Colors.white, + this.pinBoxBorderWidth = 2.0, + this.pinBoxRadius = 0, + this.hideDefaultKeyboard = false, + }) : super(key: key); + + @override + State createState() { + return OTPWidgetState(); + } +} + +class OTPWidgetState extends State with SingleTickerProviderStateMixin { + AnimationController? _highlightAnimationController; + FocusNode? focusNode; + String text = ""; + int currentIndex = 0; + List strList = []; + bool hasFocus = false; + + @override + void didUpdateWidget(OTPWidget oldWidget) { + super.didUpdateWidget(oldWidget); + focusNode = widget.focusNode ?? focusNode; + + if (oldWidget.maxLength < widget.maxLength) { + setState(() { + currentIndex = text.length; + }); + widget.controller?.text = text; + widget.controller?.selection = TextSelection.collapsed(offset: text.length); + } else if (oldWidget.maxLength > widget.maxLength && widget.maxLength > 0 && text.length > 0 && text.length > widget.maxLength) { + setState(() { + text = text.substring(0, widget.maxLength); + currentIndex = text.length; + }); + widget.controller?.text = text; + widget.controller?.selection = TextSelection.collapsed(offset: text.length); + } + } + + _calculateStrList() { + if (strList.length > widget.maxLength) { + strList.length = widget.maxLength; + } + while (strList.length < widget.maxLength) { + strList.add(""); + } + } + + @override + void initState() { + super.initState(); + focusNode = widget.focusNode ?? FocusNode(); + + _initTextController(); + _calculateStrList(); + widget.controller?.addListener(_controllerListener); + focusNode?.addListener(_focusListener); + } + + void _controllerListener() { + if (mounted == true) { + setState(() { + _initTextController(); + }); + var onTextChanged = widget.onTextChanged; + if (onTextChanged != null) { + onTextChanged(widget.controller?.text ?? ""); + } + } + } + + void _focusListener() { + if (mounted == true) { + setState(() { + hasFocus = focusNode?.hasFocus ?? false; + }); + } + } + + void _initTextController() { + if (widget.controller == null) { + return; + } + strList.clear(); + var text = widget.controller?.text ?? ""; + if (text.isNotEmpty) { + if (text.length > widget.maxLength) { + throw Exception("TextEditingController length exceeded maxLength!"); + } + } + for (var i = 0; i < text.length; i++) { + strList.add(text[i]); + } + } + + double get _width { + var width = 0.0; + for (var i = 0; i < widget.maxLength; i++) { + width += widget.pinBoxWidth; + if (i == 0) { + width += widget.pinBoxOuterPadding.left; + } else if (i + 1 == widget.maxLength) { + width += widget.pinBoxOuterPadding.right; + } else { + width += widget.pinBoxOuterPadding.left; + } + } + return width; + } + + @override + void dispose() { + if (widget.focusNode == null) { + focusNode?.dispose(); + } else { + focusNode?.removeListener(_focusListener); + } + _highlightAnimationController?.dispose(); + widget.controller?.removeListener(_controllerListener); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + _otpTextInput(), + _touchPinBoxRow(), + ], + ); + } + + Widget _touchPinBoxRow() { + return widget.hideDefaultKeyboard + ? _pinBoxRow(context) + : GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (hasFocus) { + FocusScope.of(context).requestFocus(FocusNode()); + Future.delayed(Duration(milliseconds: 100), () { + FocusScope.of(context).requestFocus(focusNode); + }); + } else { + FocusScope.of(context).requestFocus(focusNode); + } + }, + child: _pinBoxRow(context), + ); + } + + Widget _otpTextInput() { + var transparentBorder = OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + width: 0.0, + ), + ); + return Container( + width: _width, + height: widget.pinBoxHeight, + child: TextField( + autofocus: !kIsWeb ? widget.autoFocus : false, + enableInteractiveSelection: false, + focusNode: focusNode, + controller: widget.controller, + keyboardType: widget.keyboardType, + inputFormatters: widget.keyboardType == TextInputType.number ? [FilteringTextInputFormatter.digitsOnly] : null, + style: TextStyle( + height: 0.1, + color: Colors.transparent, + ), + decoration: InputDecoration( + contentPadding: EdgeInsets.all(0), + focusedErrorBorder: transparentBorder, + errorBorder: transparentBorder, + disabledBorder: transparentBorder, + enabledBorder: transparentBorder, + focusedBorder: transparentBorder, + counterText: null, + counterStyle: null, + helperStyle: TextStyle( + height: 0.0, + color: Colors.transparent, + ), + labelStyle: TextStyle(height: 0.1), + fillColor: Colors.transparent, + border: InputBorder.none, + ), + cursorColor: Colors.transparent, + showCursor: false, + maxLength: widget.maxLength, + onChanged: _onTextChanged, + ), + ); + } + + void _onTextChanged(text) { + var onTextChanged = widget.onTextChanged; + if (onTextChanged != null) { + onTextChanged(text); + } + setState(() { + this.text = text; + if (text.length >= currentIndex) { + for (int i = currentIndex; i < text.length; i++) { + strList[i] = text[i]; + } + } + currentIndex = text.length; + }); + if (text.length == widget.maxLength) { + FocusScope.of(context).requestFocus(FocusNode()); + var onDone = widget.onDone; + if (onDone != null) { + onDone(text); + } + } + } + + Widget _pinBoxRow(BuildContext context) { + _calculateStrList(); + List pinCodes = List.generate(widget.maxLength, (int i) { + return _buildPinCode(i, context); + }); + return Row(children: pinCodes, mainAxisSize: MainAxisSize.min); + } + + Widget _buildPinCode(int i, BuildContext context) { + Color borderColor; + Color pinBoxColor = widget.pinBoxColor; + + if (widget.hasError) { + borderColor = widget.errorBorderColor; + } else if (i < text.length) { + borderColor = widget.textBorderColor; + } else { + borderColor = widget.defaultBorderColor; + pinBoxColor = widget.pinBoxColor; + } + + EdgeInsets insets; + if (i == 0) { + insets = EdgeInsets.only( + left: 0, + top: widget.pinBoxOuterPadding.top, + right: widget.pinBoxOuterPadding.right, + bottom: widget.pinBoxOuterPadding.bottom, + ); + } else if (i == strList.length - 1) { + insets = EdgeInsets.only( + left: widget.pinBoxOuterPadding.left, + top: widget.pinBoxOuterPadding.top, + right: 0, + bottom: widget.pinBoxOuterPadding.bottom, + ); + } else { + insets = widget.pinBoxOuterPadding; + } + return Container( + key: ValueKey("container$i"), + alignment: Alignment.center, + padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 1.0), + margin: insets, + child: _animatedTextBox(strList[i], i), + decoration: BoxDecoration( + border: Border.all( + color: borderColor, + width: widget.pinBoxBorderWidth, + ), + color: pinBoxColor, + borderRadius: BorderRadius.circular(widget.pinBoxRadius), + ), + width: widget.pinBoxWidth, + height: widget.pinBoxHeight, + ); + } + + Widget _animatedTextBox(String text, int i) { + if (widget.pinTextAnimatedSwitcherTransition != null) { + return AnimatedSwitcher( + duration: widget.pinTextAnimatedSwitcherDuration, + transitionBuilder: widget.pinTextAnimatedSwitcherTransition ?? + (Widget child, Animation animation) { + return child; + }, + child: Text( + text, + key: ValueKey("$text$i"), + style: widget.pinTextStyle, + ), + ); + } else { + return Text( + text, + key: ValueKey("${strList[i]}$i"), + style: widget.pinTextStyle, + ); + } + } +} diff --git a/lib/widgets/txt_field.dart b/lib/widgets/txt_field.dart index 48a99d0..a5c8679 100644 --- a/lib/widgets/txt_field.dart +++ b/lib/widgets/txt_field.dart @@ -21,6 +21,7 @@ class TxtField extends StatelessWidget { int? maxLines; bool isSidePaddingZero; bool isNeedBorder; + Function(String)? onChanged; TxtField({ this.title, @@ -37,6 +38,7 @@ class TxtField extends StatelessWidget { this.maxLines, this.isSidePaddingZero=false, this.isNeedBorder=true, + this.onChanged, }); @override @@ -61,6 +63,7 @@ class TxtField extends StatelessWidget { enabled: isNeedClickAll == true ? false : true, maxLines: maxLines, onTap: () {}, + onChanged: onChanged, decoration: InputDecoration( labelText: lable, alignLabelWithHint: true, diff --git a/pubspec.lock b/pubspec.lock index 6250209..13c55cd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -133,6 +133,13 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct dev" + description: + name: fluttertoast + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.8" http: dependency: "direct dev" description: @@ -316,7 +323,7 @@ packages: source: hosted version: "6.0.1" shared_preferences: - dependency: transitive + dependency: "direct dev" description: name: shared_preferences url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 31b51c0..9390e9e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,8 @@ dev_dependencies: permission_handler: 7.1.0 flutter_svg: ^0.22.0 sizer: ^2.0.15 + fluttertoast: ^8.0.8 + shared_preferences: ^2.0.6 # For information on the generic Dart part of this file, see the