diff --git a/.env b/.env
new file mode 100644
index 0000000..2731127
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
\ No newline at end of file
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 7c90784..cc58a0d 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -4,18 +4,18 @@
# https://docs.microsoft.com/azure/devops/pipelines/languages/javascript
trigger:
-- develop
+ - develop
pool:
vmImage: ubuntu-latest
steps:
-- task: NodeTool@0
- inputs:
- versionSpec: '20.x'
- displayName: 'Install Node.js'
+ - task: NodeTool@0
+ inputs:
+ versionSpec: '20.x'
+ displayName: 'Install Node.js'
-- script: |
- npm install
- npm run build
- displayName: 'npm install and build'
+ - script: |
+ npm install
+ npm run build
+ displayName: 'npm install and build'
diff --git a/index.html b/index.html
index 6e5d786..6255f17 100644
--- a/index.html
+++ b/index.html
@@ -22,6 +22,7 @@
})();
+
diff --git a/package-lock.json b/package-lock.json
index 9252efe..1ab4b3a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,13 +20,18 @@
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.0.0-0",
"dayjs": "^1.11.13",
+ "@rkheftan/harmony-ui": "^0.1.6",
+ "@types/stylis": "^4.2.7",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"iconsax-react": "^0.0.8",
+ "libphonenumber-js": "^1.12.10",
"react": "^19.1.0",
+ "react-country-flag": "^3.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0",
+ "react-router-dom": "^7.8.0",
"stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1"
},
@@ -35,6 +40,7 @@
"@types/node": "^24.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
+ "@types/stylis": "^4.2.7",
"@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1",
"@vitejs/plugin-react": "^4.5.2",
@@ -2402,6 +2408,13 @@
"@types/react": "*"
}
},
+ "node_modules/@types/stylis": {
+ "version": "4.2.7",
+ "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.7.tgz",
+ "integrity": "sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.39.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
@@ -2686,6 +2699,7 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -3222,18 +3236,10 @@
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18"
}
},
- "node_modules/core-util-is": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
- "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
- "license": "MIT",
- "peer": true
- },
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -3802,6 +3808,36 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -4491,27 +4527,6 @@
"react": "*"
}
},
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause",
- "peer": true
- },
"node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@@ -4705,7 +4720,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/json-schema-typed": {
"version": "8.0.1",
@@ -4849,6 +4865,11 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.12.10",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.10.tgz",
+ "integrity": "sha512-E91vHJD61jekHHR/RF/E83T/CMoaLXT7cwYA75T4gim4FZjnM6hbJjVIGg7chqlSqRsSvQ3izGmOjHy1SQzcGQ==",
+ "license": "MIT"
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
@@ -5497,6 +5518,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-country-flag": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz",
+ "integrity": "sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "react": ">=16"
+ }
+ },
"node_modules/react-dom": {
"version": "19.1.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
@@ -5556,7 +5589,6 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.0.tgz",
"integrity": "sha512-r15M3+LHKgM4SOapNmsH3smAizWds1vJ0Z9C4mWaKnT9/wD7+d/0jYcj6LmOvonkrO4Rgdyp4KQ/29gWN2i1eg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -5579,7 +5611,6 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.0.tgz",
"integrity": "sha512-ntInsnDVnVRdtSu6ODmTQ41cbluak/ENeTif7GBce0L6eztFg6/e1hXAysFQI8X25C8ipKmT9cClbJwxx3Kaqw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"react-router": "7.8.0"
},
@@ -5834,8 +5865,7 @@
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
diff --git a/package.json b/package.json
index f38a567..732bb9f 100644
--- a/package.json
+++ b/package.json
@@ -18,18 +18,22 @@
"@mui/stylis-plugin-rtl": "^7.2.0",
"@mui/x-date-pickers": "^8.9.0",
"@mui/x-date-pickers-pro": "^8.9.0",
- "@rkheftan/harmony-ui": "^0.1.5",
"axios": "^1.11.0",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.0.0-0",
"dayjs": "^1.11.13",
+ "@rkheftan/harmony-ui": "^0.1.6",
+ "@types/stylis": "^4.2.7",
"i18next": "^25.3.0",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"iconsax-react": "^0.0.8",
+ "libphonenumber-js": "^1.12.10",
"react": "^19.1.0",
+ "react-country-flag": "^3.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.6.0",
+ "react-router-dom": "^7.8.0",
"stylis": "^4.3.6",
"stylis-plugin-rtl": "^2.1.1"
},
@@ -38,6 +42,7 @@
"@types/node": "^24.0.10",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
+ "@types/stylis": "^4.2.7",
"@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1",
"@vitejs/plugin-react": "^4.5.2",
diff --git a/public/locales/en/authentication.json b/public/locales/en/authentication.json
new file mode 100644
index 0000000..6633ea7
--- /dev/null
+++ b/public/locales/en/authentication.json
@@ -0,0 +1,53 @@
+{
+ "loginForm": {
+ "title": "Login/Register",
+ "description": "Please enter your email/password to start",
+ "emailOrPhoneLabel": "Email/Password",
+ "submitButton": "Login/Register",
+ "loginWithGoogle": "Login with google",
+ "emailIsInvalid": "Email is invalid",
+ "phoneNumberIsInvalid": "Phone number is invalid",
+ "thisFieldIsRequired": "This field is requried"
+ },
+ "verify": {
+ "verify": "Verify",
+ "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "A 4-digit verification code has been sent to your mobile number. Please enter it.",
+ "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "There is no account with this number. A 4-digit verification code has been sent to this number to create a new account.",
+ "a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "A 4-digit verification code has been sent to your email address. Please enter it.",
+ "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "There is no account with this email address. A 4-digit verification code has been sent to this email address to create a new account.",
+ "theVerificationCodeIsIncorrect": "The verification code is incorrect.",
+ "youHaveSuccessfullyLoggedIn": "You have successfully logged in",
+ "youHaveSuccessfullySignedIn": "You have successfully signed in",
+ "resendCodeIn": "Resend code in",
+ "moreMinute": "minute",
+ "resendCode": "Resend code"
+ },
+ "completeSignUp": {
+ "completeSignUp": "Complete Sign Up",
+ "emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "Email {{ email }} has been successfully verified. Please enter your contact number to continue.",
+ "phoneNumber": "Phone number"
+ },
+ "enterPassword": {
+ "loginWithPassword": "Login with password",
+ "enterThePasswordYouSetForYourAccount": "Enter the password you set for your account.",
+ "loginPassword": "Login password",
+ "loginWithOneTimeCode": "Login with one-time code",
+ "iForgotMyPassword": "I forgot my password."
+ },
+ "forgetPassword": {
+ "forgetPassword": "Forget password",
+ "pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "Please enter your mobile number/email to recover your password.",
+ "anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "An email containing a recovery code has been sent to this email address.",
+ "anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "An recovery code has been sent to this phone number.",
+ "confirm": "Confirm",
+ "changePassword": "Change password",
+ "createANewPassword": "Create a new password",
+ "newPassword": "New password",
+ "includingANumber": "Including a number",
+ "atLeast8Characters": "At least 8 characters",
+ "containsAnUppercaseAndLowercaseLetter": "Contains an uppercase and lowercase letter",
+ "ContainsASymbol": "Contains the symbol (!@#$%&*^)",
+ "confirmPassword": "Confirm password",
+ "passwordChangedSuccessfully": "Password changed successfully"
+ }
+}
diff --git a/public/locales/en/common.json b/public/locales/en/common.json
index bc4b6ab..72b36d8 100644
--- a/public/locales/en/common.json
+++ b/public/locales/en/common.json
@@ -1,3 +1,250 @@
{
- "helloWorld": "hello world"
+ "helloWorld": "hello world",
+ "country": {
+ "afghanistan": "Afghanistan",
+ "aland_islands": "Aland islands",
+ "albania": "Albania",
+ "algeria": "Algeria",
+ "american_samoa": "American samoa",
+ "andorra": "Andorra",
+ "angola": "Angola",
+ "anguilla": "Anguilla",
+ "antarctica": "Antarctica",
+ "antigua_and_barbuda": "Antigua and barbuda",
+ "argentina": "Argentina",
+ "armenia": "Armenia",
+ "aruba": "Aruba",
+ "australia": "Australia",
+ "austria": "Austria",
+ "azerbaijan": "Azerbaijan",
+ "bahamas": "Bahamas",
+ "bahrain": "Bahrain",
+ "bangladesh": "Bangladesh",
+ "barbados": "Barbados",
+ "belarus": "Belarus",
+ "belgium": "Belgium",
+ "belize": "Belize",
+ "benin": "Benin",
+ "bermuda": "Bermuda",
+ "bhutan": "Bhutan",
+ "bolivia": "Bolivia",
+ "bosnia_and_herzegovina": "Bosnia and herzegovina",
+ "botswana": "Botswana",
+ "brazil": "Brazil",
+ "british_indian_ocean_territory": "British indian ocean territory",
+ "british_virgin_islands": "British virgin islands",
+ "brunei": "Brunei",
+ "bulgaria": "Bulgaria",
+ "burkina_faso": "Burkina faso",
+ "burundi": "Burundi",
+ "cambodia": "Cambodia",
+ "cameroon": "Cameroon",
+ "canada": "Canada",
+ "cape_verde": "Cape verde",
+ "cayman_islands": "Cayman islands",
+ "central_african_republic": "Central african republic",
+ "chad": "Chad",
+ "chile": "Chile",
+ "china": "China",
+ "christmas_island": "Christmas island",
+ "cocos_keeling_islands": "Cocos keeling islands",
+ "colombia": "Colombia",
+ "comoros": "Comoros",
+ "congo_brazzaville": "Congo brazzaville",
+ "congo_kinshasa": "Congo kinshasa",
+ "cook_islands": "Cook islands",
+ "costa_rica": "Costa rica",
+ "cote_divoire": "Cote divoire",
+ "croatia": "Croatia",
+ "cuba": "Cuba",
+ "curacao": "Curacao",
+ "cyprus": "Cyprus",
+ "czech_republic": "Czech republic",
+ "denmark": "Denmark",
+ "djibouti": "Djibouti",
+ "dominica": "Dominica",
+ "dominican_republic": "Dominican republic",
+ "ecuador": "Ecuador",
+ "egypt": "Egypt",
+ "el_salvador": "El salvador",
+ "equatorial_guinea": "Equatorial guinea",
+ "eritrea": "Eritrea",
+ "estonia": "Estonia",
+ "eswatini": "Eswatini",
+ "ethiopia": "Ethiopia",
+ "falkland_islands": "Falkland islands",
+ "faroe_islands": "Faroe islands",
+ "fiji": "Fiji",
+ "finland": "Finland",
+ "france": "France",
+ "french_guiana": "French guiana",
+ "french_polynesia": "French polynesia",
+ "gabon": "Gabon",
+ "gambia": "Gambia",
+ "georgia": "Georgia",
+ "germany": "Germany",
+ "ghana": "Ghana",
+ "gibraltar": "Gibraltar",
+ "greece": "Greece",
+ "greenland": "Greenland",
+ "grenada": "Grenada",
+ "guadeloupe": "Guadeloupe",
+ "guam": "Guam",
+ "guatemala": "Guatemala",
+ "guernsey": "Guernsey",
+ "guinea": "Guinea",
+ "guinea_bissau": "Guinea bissau",
+ "guyana": "Guyana",
+ "haiti": "Haiti",
+ "honduras": "Honduras",
+ "hong_kong": "Hong kong",
+ "hungary": "Hungary",
+ "iceland": "Iceland",
+ "india": "India",
+ "indonesia": "Indonesia",
+ "iran": "Iran",
+ "iraq": "Iraq",
+ "ireland": "Ireland",
+ "isle_of_man": "Isle of man",
+ "israel": "Israel",
+ "italy": "Italy",
+ "jamaica": "Jamaica",
+ "japan": "Japan",
+ "jersey": "Jersey",
+ "jordan": "Jordan",
+ "kazakhstan": "Kazakhstan",
+ "kenya": "Kenya",
+ "kiribati": "Kiribati",
+ "kosovo": "Kosovo",
+ "kuwait": "Kuwait",
+ "kyrgyzstan": "Kyrgyzstan",
+ "laos": "Laos",
+ "latvia": "Latvia",
+ "lebanon": "Lebanon",
+ "lesotho": "Lesotho",
+ "liberia": "Liberia",
+ "libya": "Libya",
+ "liechtenstein": "Liechtenstein",
+ "lithuania": "Lithuania",
+ "luxembourg": "Luxembourg",
+ "macau": "Macau",
+ "madagascar": "Madagascar",
+ "malawi": "Malawi",
+ "malaysia": "Malaysia",
+ "maldives": "Maldives",
+ "mali": "Mali",
+ "malta": "Malta",
+ "marshall_islands": "Marshall islands",
+ "martinique": "Martinique",
+ "mauritania": "Mauritania",
+ "mauritius": "Mauritius",
+ "mayotte": "Mayotte",
+ "mexico": "Mexico",
+ "micronesia": "Micronesia",
+ "moldova": "Moldova",
+ "monaco": "Monaco",
+ "mongolia": "Mongolia",
+ "montenegro": "Montenegro",
+ "montserrat": "Montserrat",
+ "morocco": "Morocco",
+ "mozambique": "Mozambique",
+ "myanmar": "Myanmar",
+ "namibia": "Namibia",
+ "nauru": "Nauru",
+ "nepal": "Nepal",
+ "netherlands": "Netherlands",
+ "new_caledonia": "New caledonia",
+ "new_zealand": "New zealand",
+ "nicaragua": "Nicaragua",
+ "niger": "Niger",
+ "nigeria": "Nigeria",
+ "niue": "Niue",
+ "norfolk_island": "Norfolk island",
+ "north_korea": "North korea",
+ "north_macedonia": "North macedonia",
+ "northern_mariana_islands": "Northern mariana islands",
+ "norway": "Norway",
+ "oman": "Oman",
+ "pakistan": "Pakistan",
+ "palau": "Palau",
+ "palestine": "Palestine",
+ "panama": "Panama",
+ "papua_new_guinea": "Papua new guinea",
+ "paraguay": "Paraguay",
+ "peru": "Peru",
+ "philippines": "Philippines",
+ "pitcairn_islands": "Pitcairn islands",
+ "poland": "Poland",
+ "portugal": "Portugal",
+ "puerto_rico": "Puerto rico",
+ "qatar": "Qatar",
+ "reunion": "Reunion",
+ "romania": "Romania",
+ "russia": "Russia",
+ "rwanda": "Rwanda",
+ "saint_barthelemy": "Saint barthelemy",
+ "saint_helena": "Saint helena",
+ "saint_kitts_and_nevis": "Saint kitts and nevis",
+ "saint_lucia": "Saint lucia",
+ "saint_martin": "Saint martin",
+ "saint_pierre_and_miquelon": "Saint pierre and miquelon",
+ "saint_vincent_and_the_grenadines": "Saint vincent and the grenadines",
+ "samoa": "Samoa",
+ "san_marino": "San marino",
+ "sao_tome_and_principe": "Sao tome and principe",
+ "saudi_arabia": "Saudi arabia",
+ "senegal": "Senegal",
+ "serbia": "Serbia",
+ "seychelles": "Seychelles",
+ "sierra_leone": "Sierra leone",
+ "singapore": "Singapore",
+ "sint_maarten": "Sint maarten",
+ "slovakia": "Slovakia",
+ "slovenia": "Slovenia",
+ "solomon_islands": "Solomon islands",
+ "somalia": "Somalia",
+ "south_africa": "South africa",
+ "south_georgia_and_south_sandwich_islands": "South georgia and south sandwich islands",
+ "south_korea": "South korea",
+ "south_sudan": "South sudan",
+ "spain": "Spain",
+ "sri_lanka": "Sri lanka",
+ "sudan": "Sudan",
+ "suriname": "Suriname",
+ "svalbard_and_jan_mayen": "Svalbard and jan mayen",
+ "sweden": "Sweden",
+ "switzerland": "Switzerland",
+ "syria": "Syria",
+ "taiwan": "Taiwan",
+ "tajikistan": "Tajikistan",
+ "tanzania": "Tanzania",
+ "thailand": "Thailand",
+ "timor_leste": "Timor leste",
+ "togo": "Togo",
+ "tokelau": "Tokelau",
+ "tonga": "Tonga",
+ "trinidad_and_tobago": "Trinidad and tobago",
+ "tunisia": "Tunisia",
+ "turkey": "Turkey",
+ "turkmenistan": "Turkmenistan",
+ "turks_and_caicos_islands": "Turks and caicos islands",
+ "tuvalu": "Tuvalu",
+ "us_virgin_islands": "Us virgin islands",
+ "uganda": "Uganda",
+ "ukraine": "Ukraine",
+ "united_arab_emirates": "United arab emirates",
+ "united_kingdom": "United kingdom",
+ "united_states": "United states",
+ "uruguay": "Uruguay",
+ "uzbekistan": "Uzbekistan",
+ "vanuatu": "Vanuatu",
+ "vatican_city": "Vatican city",
+ "venezuela": "Venezuela",
+ "vietnam": "Vietnam",
+ "wallis_and_futuna": "Wallis and futuna",
+ "western_sahara": "Western sahara",
+ "yemen": "Yemen",
+ "zambia": "Zambia",
+ "zimbabwe": "Zimbabwe"
+ }
}
diff --git a/public/locales/fa/authentication.json b/public/locales/fa/authentication.json
new file mode 100644
index 0000000..bfb8610
--- /dev/null
+++ b/public/locales/fa/authentication.json
@@ -0,0 +1,55 @@
+{
+ "loginForm": {
+ "title": "ورود/ثبتنام",
+ "description": "لطفا برای شروع شماره موبایل/ایمیل خود را وارد کنید.",
+ "emailOrPhoneLabel": "شماره موبایل/ایمیل",
+ "submitButton": "ورود/ثبتنام",
+ "loginWithGoogle": "ورود با گوگل",
+ "emailIsInvalid": "ایمیل وارد شده نامعتبر میباشد",
+ "phoneNumberIsInvalid": "شماره وارد شده نامعتبر میباشد",
+ "thisFieldIsRequired": "این فیلد الزامی است"
+ },
+ "verify": {
+ "verify": "اعتبارسنجی",
+ "a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt": "کد تایید ۴ رقمی به شماره موبایل شما ارسال شد. لطفا آن را وارد کنید.",
+ "confirmAndLogin": "تایید و ورود",
+ "confirmAndContinue": "تایید و ادامه",
+ "thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount": "حساب کاربری با این شماره وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این شماره ارسال گردید.",
+ "a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt": "کد تایید ۴ رقمی به شماره ایمیل شما ارسال شد. لطفا آن را وارد کنید.",
+ "thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount": "حساب کاربری با این ایمیل وجود ندارد. برای ساخت حساب جدید، کد تایید ۴ رقمی برای این ایمیل ارسال گردید.",
+ "theVerificationCodeIsIncorrect": "کد تایید اشتباه می باشد",
+ "youHaveSuccessfullyLoggedIn": "با موفقیت وارد شدید",
+ "youHaveSuccessfullySignedIn": "ثبت نام با موفقیت انجام شد",
+ "resendCodeIn": "ارسال مجدد کد تا",
+ "moreMinute": "دقیقه دیگر",
+ "resendCode": "ارسال مجدد"
+ },
+ "completeSignUp": {
+ "completeSignUp": "تکمیل ثبت نام",
+ "emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue": "ایمیل {{ email }} با موفقیت تایید شد. برای ادامه لطفا شماره تماس خود را وارد کنید",
+ "phoneNumber": "شماره تماس"
+ },
+ "enterPassword": {
+ "loginWithPassword": "ورود با رمز",
+ "enterThePasswordYouSetForYourAccount": "رمز ورودی که برای اکانت خود تعیین کردید را وارد کنید",
+ "loginPassword": "رمز ورود",
+ "loginWithOneTimeCode": "ورود با کد یکبار مصرف",
+ "iForgotMyPassword": "رمز ورودم را فراموش کردم"
+ },
+ "forgetPassword": {
+ "forgetPassword": "فراموشی رمز",
+ "pleaseEnterYourMobileNumberEmailToRecoverYourPassword": "لطفا برای بازیابی رمز عبور شماره موبایل/ایمیل خود را وارد کنید.",
+ "anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress": "یک ایمیل حاوی کد بازیابی به این ایمیل ارسال شد",
+ "anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber": "یک کد بازیابی به این شماره ارسال شد",
+ "confirm": "تایید",
+ "changePassword": "تغییر رمز عبور",
+ "createANewPassword": "یک رمز عبور جدید ایجاد کنید",
+ "newPassword": "رمز عبور جدید",
+ "includingANumber": "شامل عدد",
+ "atLeast8Characters": "حداقل ۸ حرف",
+ "containsAnUppercaseAndLowercaseLetter": "شامل یک حرف بزرگ و کوچک",
+ "ContainsASymbol": "شامل علامت (!@#$%&*^)",
+ "confirmPassword": "تکرار رمز عبور",
+ "passwordChangedSuccessfully": "رمز عبور با موفقیت تغییر یافت"
+ }
+}
diff --git a/public/locales/fa/common.json b/public/locales/fa/common.json
index 3f4cd0d..60424fb 100644
--- a/public/locales/fa/common.json
+++ b/public/locales/fa/common.json
@@ -1,3 +1,202 @@
{
- "helloWorld": "سلام دنیا"
+ "labels": {
+ "search": "جست و جو"
+ },
+ "country": {
+ "afghanistan": "افغانستان",
+ "aland_islands": "جزایر آلند",
+ "albania": "آلبانی",
+ "algeria": "الجزایر",
+ "american_samoa": "ساموای آمریکایی",
+ "andorra": "آندورا",
+ "angola": "آنگولا",
+ "anguilla": "آنگویلا",
+ "antarctica": "جنوبگان",
+ "antigua_and_barbuda": "آنتیگوا و باربودا",
+ "argentina": "آرژانتین",
+ "armenia": "ارمنستان",
+ "aruba": "آروبا",
+ "australia": "استرالیا",
+ "austria": "اتریش",
+ "azerbaijan": "آذربایجان",
+ "bahamas": "باهاما",
+ "bahrain": "بحرین",
+ "bangladesh": "بنگلادش",
+ "barbados": "باربادوس",
+ "belarus": "بلاروس",
+ "belgium": "بلژیک",
+ "belize": "بلیز",
+ "benin": "بنین",
+ "bermuda": "برمودا",
+ "bhutan": "بوتان",
+ "bolivia": "بولیوی",
+ "bosnia_and_herzegovina": "بوسنی و هرزگوین",
+ "botswana": "بوتسوانا",
+ "brazil": "برزیل",
+ "british_virgin_islands": "جزایر ویرجین بریتانیا",
+ "brunei": "برونئی",
+ "bulgaria": "بلغارستان",
+ "burkina_faso": "بورکینافاسو",
+ "burundi": "بوروندی",
+ "cambodia": "کامبوج",
+ "cameroon": "کامرون",
+ "canada": "کانادا",
+ "cape_verde": "کیپ ورد",
+ "cayman_islands": "جزایر کیمن",
+ "central_african_republic": "جمهوری آفریقای مرکزی",
+ "chad": "چاد",
+ "chile": "شیلی",
+ "china": "چین",
+ "colombia": "کلمبیا",
+ "comoros": "کومور",
+ "costa_rica": "کاستاریکا",
+ "cote_divoire": "ساحل عاج",
+ "croatia": "کرواسی",
+ "cuba": "کوبا",
+ "cyprus": "قبرس",
+ "czech_republic": "جمهوری چک",
+ "denmark": "دانمارک",
+ "djibouti": "جیبوتی",
+ "dominica": "دومینیکا",
+ "dominican_republic": "جمهوری دومینیکن",
+ "ecuador": "اکوادور",
+ "egypt": "مصر",
+ "el_salvador": "السالوادور",
+ "equatorial_guinea": "گینه استوایی",
+ "eritrea": "اریتره",
+ "estonia": "استونی",
+ "eswatini": "سوازیلند",
+ "ethiopia": "اتیوپی",
+ "fiji": "فیجی",
+ "finland": "فنلاند",
+ "france": "فرانسه",
+ "gabon": "گابن",
+ "gambia": "گامبیا",
+ "georgia": "گرجستان",
+ "germany": "آلمان",
+ "ghana": "غنا",
+ "greece": "یونان",
+ "guatemala": "گواتمالا",
+ "guinea": "گینه",
+ "guinea_bissau": "گینه بیسائو",
+ "guyana": "گویان",
+ "haiti": "هائیتی",
+ "honduras": "هندوراس",
+ "hungary": "مجارستان",
+ "iceland": "ایسلند",
+ "india": "هندوستان",
+ "indonesia": "اندونزی",
+ "iran": "ایران",
+ "iraq": "عراق",
+ "ireland": "ایرلند",
+ "israel": "اسرائیل",
+ "italy": "ایتالیا",
+ "jamaica": "جامائیکا",
+ "japan": "ژاپن",
+ "jordan": "اردن",
+ "kazakhstan": "قزاقستان",
+ "kenya": "کنیا",
+ "kuwait": "کویت",
+ "kyrgyzstan": "قرقیزستان",
+ "laos": "لائوس",
+ "latvia": "لتونی",
+ "lebanon": "لبنان",
+ "lesotho": "لسوتو",
+ "liberia": "لیبریا",
+ "libya": "لیبی",
+ "luxembourg": "لوکزامبورگ",
+ "malaysia": "مالزی",
+ "maldives": "مالدیو",
+ "mali": "مالی",
+ "malta": "مالت",
+ "mauritania": "موریتانی",
+ "mauritius": "موریس",
+ "mexico": "مکزیک",
+ "moldova": "مولداوی",
+ "monaco": "موناکو",
+ "mongolia": "مغولستان",
+ "morocco": "مراکش",
+ "mozambique": "موزامبیک",
+ "myanmar": "میانمار",
+ "namibia": "نامیبیا",
+ "nepal": "نپال",
+ "netherlands": "هلند",
+ "new_zealand": "نیوزیلند",
+ "nicaragua": "نیکاراگوئه",
+ "niger": "نیجر",
+ "nigeria": "نیجریه",
+ "north_korea": "کره شمالی",
+ "north_macedonia": "مقدونیه",
+ "norway": "نروژ",
+ "oman": "عمان",
+ "pakistan": "پاکستان",
+ "palau": "پالائو",
+ "panama": "پاناما",
+ "papua_new_guinea": "پاپوآ گینه نو",
+ "paraguay": "پاراگوئه",
+ "peru": "پرو",
+ "philippines": "فیلیپین",
+ "poland": "لهستان",
+ "portugal": "پرتغال",
+ "qatar": "قطر",
+ "romania": "رومانی",
+ "russia": "روسیه",
+ "rwanda": "رواندا",
+ "saudi_arabia": "عربستان سعودی",
+ "senegal": "سنگال",
+ "serbia": "صربستان",
+ "seychelles": "سیشل",
+ "sierra_leone": "سیرالئون",
+ "singapore": "سنگاپور",
+ "south_africa": "آفریقای جنوبی",
+ "south_korea": "کره جنوبی",
+ "south_sudan": "سودان جنوبی",
+ "spain": "اسپانیا",
+ "sri_lanka": "سریلانکا",
+ "sudan": "سودان",
+ "suriname": "سورینام",
+ "sweden": "سوئد",
+ "switzerland": "سوئیس",
+ "syria": "سوریه",
+ "taiwan": "تایوان",
+ "tajikistan": "تاجیکستان",
+ "tanzania": "تانزانیا",
+ "thailand": "تایلند",
+ "timor_leste": "تیمور شرقی",
+ "togo": "توگو",
+ "tonga": "تونگا",
+ "trinidad_and_tobago": "ترینیداد و توباگو",
+ "tunisia": "تونس",
+ "turkey": "ترکیه",
+ "turkmenistan": "ترکمنستان",
+ "tuvalu": "تووالو",
+ "uganda": "اوگاندا",
+ "ukraine": "اوکراین",
+ "united_arab_emirates": "امارات متحده عربی",
+ "united_kingdom": "انگلستان",
+ "united_states": "ایالات متحده آمریکا",
+ "uruguay": "اروگوئه",
+ "uzbekistan": "ازبکستان",
+ "vanuatu": "وانواتو",
+ "venezuela": "ونزوئلا",
+ "vietnam": "ویتنام",
+ "yemen": "یمن",
+ "zambia": "زامبیا",
+ "zimbabwe": "زیمبابوه"
+ },
+ "messages": {
+ "noResualtFound": "نتیجه ای یافت نشد."
+ },
+ "side": {
+ "account": "حساب کاربری",
+ "personalInfo": "اطلاعات شخصی",
+ "contactInfo": "شماره تماس",
+ "email": "ایمیل",
+ "security": "امنیت",
+ "password": "رمز عبور",
+ "confirmedIps": "آدرس های تایید شده",
+ "recentSessions": "ورود های اخیر",
+ "activeSessions": "نشست های فعال",
+ "setting": "تنظیمات"
+ }
}
diff --git a/src/App.tsx b/src/App.tsx
index 1e33759..ec7f7b6 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,32 +1,17 @@
-import { CssBaseline, useColorScheme } from '@mui/material';
+import { CssBaseline } from '@mui/material';
import './App.css';
-import { LanguageManager } from './components/LanguageManager';
-import { UserCompletionForm } from './features/authentication/components/UserCompletionForm';
+import { LanguageManager } from '@/components/LanguageManager';
+import { RouterProvider } from 'react-router-dom';
+import { router } from '@/routes';
function App() {
return (
<>
-
-
+
>
);
}
export default App;
-
-import { Button } from '@mui/material';
-
-export const ThemeToggleButton = () => {
- const { mode, setMode } = useColorScheme();
-
- return (
-
- );
-};
diff --git a/src/components/Layout/Header.tsx b/src/components/Layout/Header.tsx
new file mode 100644
index 0000000..39f4eb9
--- /dev/null
+++ b/src/components/Layout/Header.tsx
@@ -0,0 +1,35 @@
+import { Box, IconButton, Typography } from '@mui/material';
+import { Icon } from '@rkheftan/harmony-ui';
+import { More } from 'iconsax-react';
+import type { User } from './type';
+
+interface HeaderProps {
+ user: User;
+}
+
+export const Header: React.FC = ({ user }) => {
+ return (
+ t.spacing(10.5),
+ }}
+ >
+
+
+ {user.firstName + ' ' + user.lastName}
+
+
+ {user.phoneNumber}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx
new file mode 100644
index 0000000..2652dfe
--- /dev/null
+++ b/src/components/Layout/Layout.tsx
@@ -0,0 +1,69 @@
+import { SideNav } from '@rkheftan/harmony-ui';
+import { buildNavItems } from './buildNavItems';
+import { appRoutes } from '@/routes/config';
+import { Outlet, useLocation } from 'react-router-dom';
+import { Box, useMediaQuery, useTheme } from '@mui/material';
+import { Header } from './Header';
+import { useState } from 'react';
+import { Toolbar } from './Toolbar';
+import type { User } from './type';
+
+export const Layout = () => {
+ const navItemConfigs = buildNavItems(appRoutes);
+ const location = useLocation();
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
+ const [sideNavOpen, setSideNavOpen] = useState(false);
+ const [user] = useState({
+ firstName: 'محمد حسین',
+ lastName: 'برزه گر',
+ phoneNumber: '09123456789',
+ });
+
+ return (
+
+
+
+
+
+
+
+ setSideNavOpen(false)}
+ header={isMobile ? undefined : }
+ footer={isMobile ? : undefined}
+ navConfig={navItemConfigs}
+ activePath={location.pathname + location.hash}
+ selectedVariant="textOnly"
+ positioning="absolute"
+ sideNavVariant={isMobile ? 'temporary' : 'full'}
+ top={8.125}
+ />
+
+
+ );
+};
diff --git a/src/components/Layout/Toolbar.tsx b/src/components/Layout/Toolbar.tsx
new file mode 100644
index 0000000..46b482e
--- /dev/null
+++ b/src/components/Layout/Toolbar.tsx
@@ -0,0 +1,75 @@
+import {
+ Avatar,
+ Box,
+ IconButton,
+ Toolbar as MuiToolbar,
+ Typography,
+} from '@mui/material';
+import { Icon } from '@rkheftan/harmony-ui';
+import { HambergerMenu, Menu } from 'iconsax-react';
+import type { Dispatch, SetStateAction } from 'react';
+import type { User } from './type';
+
+interface ToolbarProps {
+ sideNavOpen: boolean;
+ setSideNavOpen: Dispatch>;
+ isMobile: boolean;
+ user: User;
+}
+
+export const Toolbar: React.FC = ({
+ sideNavOpen,
+ setSideNavOpen,
+ isMobile,
+ user,
+}) => {
+ return (
+ t.spacing(isMobile ? 8 : 10.5),
+ px: isMobile ? 3 : 2,
+ borderBottom: (t) => `1px solid ${t.palette.divider}`,
+ boxSizing: 'content-box',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ }}
+ >
+
+ {isMobile && (
+ setSideNavOpen(!sideNavOpen)}
+ >
+
+
+ )}
+ {/* */}
+ LOGO placeholder
+
+
+ {isMobile && (
+
+ {user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
+
+ )}
+
+
+
+
+
+ );
+};
diff --git a/src/components/Layout/buildNavItems.tsx b/src/components/Layout/buildNavItems.tsx
new file mode 100644
index 0000000..4cfaba0
--- /dev/null
+++ b/src/components/Layout/buildNavItems.tsx
@@ -0,0 +1,34 @@
+// src/components/SideNav.tsx (Conceptual Example)
+
+import { useTranslation } from 'react-i18next';
+import { type RouteConfig } from '@/routes/config';
+import { Icon, type NavItemConfig } from '@rkheftan/harmony-ui';
+import type { Icon as Iconsax } from 'iconsax-react';
+
+const getIcon = (icon?: Iconsax) => (isSelected: boolean) =>
+ icon ? (
+
+ ) : undefined;
+
+export function buildNavItems(routes: RouteConfig[]): NavItemConfig[] {
+ const { t } = useTranslation();
+
+ return routes.flatMap((route) => {
+ // Check if route itself does not have a navItem but its child has
+ if (!route.navConfig && route.children) {
+ return buildNavItems(route.children);
+ }
+
+ // Check if route.navConfig is defined before destructuring
+ if (!route.navConfig) {
+ return []; // Return an empty array to be flattened
+ }
+ const { title, icon } = route.navConfig;
+ return {
+ text: t(title),
+ getIcon: getIcon(icon),
+ path: route.path,
+ children: route.children ? buildNavItems(route.children) : undefined,
+ };
+ });
+}
diff --git a/src/components/Layout/type.ts b/src/components/Layout/type.ts
new file mode 100644
index 0000000..ab31407
--- /dev/null
+++ b/src/components/Layout/type.ts
@@ -0,0 +1,8 @@
+// TODO: this type file is temporary and should replace it with the actual use type and value when api is ready
+
+export interface User {
+ firstName: string;
+ lastName: string;
+ phoneNumber: string;
+ profileUrl?: string;
+}
diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx
index 713c36f..d9376be 100644
--- a/src/components/Toast.tsx
+++ b/src/components/Toast.tsx
@@ -9,7 +9,11 @@ export interface ToastProps extends PropsWithChildren {
export const Toast = ({ color, open, onClose, children }: ToastProps) => {
return (
-
+ `calc(100% - ${t.spacing(6)})`, maxWidth: '396px' }}
+ open={open}
+ onClose={onClose}
+ >
>;
+}
+
+const DigitInput: React.FC = ({
+ onChange,
+ error,
+ success,
+}) => {
+ const [code, setCode] = useState(['', '', '', '']);
+ const inputRefs = useRef>([]);
+
+ useEffect(() => {
+ inputRefs.current[0]?.focus();
+ }, []);
+
+ const handleDigitInputValueChange = (value: string[]) => {
+ const formatted = value.filter((char) => char !== '').join('');
+ onChange(formatted);
+ };
+
+ const handleChange = (value: string, index: number) => {
+ if (!/^\d$/.test(value) && value !== '') return;
+
+ const newCode = [...code];
+ newCode[index] = value;
+ setCode(newCode);
+ handleDigitInputValueChange(newCode);
+
+ if (value && index < 4 - 1) {
+ inputRefs.current[index + 1]?.focus();
+ }
+ };
+
+ const handleBackspace = (
+ event: KeyboardEvent,
+ index: number,
+ ) => {
+ event.preventDefault();
+ if (index >= 0) {
+ handleChange('', index);
+ inputRefs.current[index - 1]?.focus();
+ }
+ };
+
+ const handlePaste = (event: React.ClipboardEvent) => {
+ event.preventDefault();
+ const pastedData = event.clipboardData.getData('text').replace(/\D/g, ''); // Remove non-digit characters
+ const newCode = [...code];
+
+ pastedData.split('').forEach((digit, i) => {
+ if (i < code.length) {
+ newCode[i] = digit;
+ }
+ });
+
+ setCode(newCode);
+ handleDigitInputValueChange(newCode);
+
+ // Focus the next empty input after the last pasted character
+ const lastIndex = Math.min(pastedData.length, code.length) - 1;
+ if (lastIndex >= 0 && inputRefs.current[lastIndex]) {
+ inputRefs.current[lastIndex]?.focus();
+ }
+ };
+
+ return (
+
+ {code.map((digit, index) => (
+ (inputRefs.current[index] = el)}
+ value={digit}
+ onChange={(e) => handleChange(e.target.value, index)}
+ onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e, index)}
+ onPaste={(e) => handlePaste(e)}
+ slotProps={{
+ htmlInput: {
+ maxLength: 1,
+ sx: {
+ height: '72px',
+ color: error
+ ? 'error.main'
+ : success
+ ? 'success.main'
+ : 'text.primary',
+ },
+ style: {
+ textAlign: 'center',
+ fontSize: '48px',
+ },
+ },
+ }}
+ variant="standard"
+ size="medium"
+ sx={{
+ width: '83px',
+ }}
+ />
+ ))}
+
+ );
+};
+
+export default DigitInput;
diff --git a/src/components/components/common/Container.tsx b/src/components/components/common/Container.tsx
new file mode 100644
index 0000000..c9efc78
--- /dev/null
+++ b/src/components/components/common/Container.tsx
@@ -0,0 +1,8 @@
+import { Box, styled } from '@mui/material';
+
+export const Container = styled(Box)(() => ({
+ width: '100%',
+ maxWidth: '100vw',
+ height: '100vh',
+ margin: '0 auto',
+}));
diff --git a/src/components/components/common/FlexBox.tsx b/src/components/components/common/FlexBox.tsx
new file mode 100644
index 0000000..e15bb8c
--- /dev/null
+++ b/src/components/components/common/FlexBox.tsx
@@ -0,0 +1,21 @@
+import { Box, styled, type BoxProps } from '@mui/material';
+
+// Define the props our component will accept
+interface FlexBoxProps extends BoxProps {
+ direction?: 'row' | 'column';
+ justify?: string;
+ align?: string;
+}
+
+export const FlexBox = styled(Box, {
+ // Do not forward these custom props to the DOM element
+ shouldForwardProp: (prop) =>
+ prop !== 'direction' && prop !== 'justify' && prop !== 'align',
+})(
+ ({ direction = 'row', justify = 'flex-start', align = 'stretch' }) => ({
+ display: 'flex',
+ flexDirection: direction,
+ justifyContent: justify,
+ alignItems: align,
+ }),
+);
diff --git a/src/components/components/common/Stack.tsx b/src/components/components/common/Stack.tsx
new file mode 100644
index 0000000..0e00bfd
--- /dev/null
+++ b/src/components/components/common/Stack.tsx
@@ -0,0 +1,19 @@
+import { Box, styled, type BoxProps } from '@mui/material';
+
+interface StackProps extends BoxProps {
+ direction?: 'row' | 'column';
+ spacing?: number; // Spacing factor (multiplied by theme.spacing)
+ align?: string;
+}
+
+export const Stack = styled(Box, {
+ shouldForwardProp: (prop) =>
+ prop !== 'direction' && prop !== 'spacing' && prop !== 'align',
+})(
+ ({ theme, direction = 'column', spacing = 2, align = 'stretch' }) => ({
+ display: 'flex',
+ flexDirection: direction,
+ alignItems: align,
+ gap: theme.spacing(spacing),
+ }),
+);
diff --git a/src/countries.ts b/src/countries.ts
new file mode 100644
index 0000000..a2a0166
--- /dev/null
+++ b/src/countries.ts
@@ -0,0 +1,267 @@
+import type { CountryCode } from '@/types/commonTypes';
+
+export interface Country {
+ code: string;
+ label: string;
+ phone: CountryCode;
+}
+
+export const countries: readonly Country[] = [
+ { code: 'AF', label: 'country.afghanistan', phone: '+93' },
+ { code: 'AX', label: 'country.aland_islands', phone: '+358' },
+ { code: 'AL', label: 'country.albania', phone: '+355' },
+ { code: 'DZ', label: 'country.algeria', phone: '+213' },
+ { code: 'AS', label: 'country.american_samoa', phone: '+1684' },
+ { code: 'AD', label: 'country.andorra', phone: '+376' },
+ { code: 'AO', label: 'country.angola', phone: '+244' },
+ { code: 'AI', label: 'country.anguilla', phone: '+1264' },
+ { code: 'AQ', label: 'country.antarctica', phone: '+672' },
+ { code: 'AG', label: 'country.antigua_and_barbuda', phone: '+1268' },
+ { code: 'AR', label: 'country.argentina', phone: '+54' },
+ { code: 'AM', label: 'country.armenia', phone: '+374' },
+ { code: 'AW', label: 'country.aruba', phone: '+297' },
+ { code: 'AU', label: 'country.australia', phone: '+61' },
+ { code: 'AT', label: 'country.austria', phone: '+43' },
+ { code: 'AZ', label: 'country.azerbaijan', phone: '+994' },
+ { code: 'BS', label: 'country.bahamas', phone: '+1242' },
+ { code: 'BH', label: 'country.bahrain', phone: '+973' },
+ { code: 'BD', label: 'country.bangladesh', phone: '+880' },
+ { code: 'BB', label: 'country.barbados', phone: '+1246' },
+ { code: 'BY', label: 'country.belarus', phone: '+375' },
+ { code: 'BE', label: 'country.belgium', phone: '+32' },
+ { code: 'BZ', label: 'country.belize', phone: '+501' },
+ { code: 'BJ', label: 'country.benin', phone: '+229' },
+ { code: 'BM', label: 'country.bermuda', phone: '+1441' },
+ { code: 'BT', label: 'country.bhutan', phone: '+975' },
+ { code: 'BO', label: 'country.bolivia', phone: '+591' },
+ { code: 'BA', label: 'country.bosnia_and_herzegovina', phone: '+387' },
+ { code: 'BW', label: 'country.botswana', phone: '+267' },
+ { code: 'BR', label: 'country.brazil', phone: '+55' },
+ {
+ code: 'IO',
+ label: 'country.british_indian_ocean_territory',
+ phone: '+246',
+ },
+ { code: 'VG', label: 'country.british_virgin_islands', phone: '+1284' },
+ { code: 'BN', label: 'country.brunei', phone: '+673' },
+ { code: 'BG', label: 'country.bulgaria', phone: '+359' },
+ { code: 'BF', label: 'country.burkina_faso', phone: '+226' },
+ { code: 'BI', label: 'country.burundi', phone: '+257' },
+ { code: 'KH', label: 'country.cambodia', phone: '+855' },
+ { code: 'CM', label: 'country.cameroon', phone: '+237' },
+ { code: 'CA', label: 'country.canada', phone: '+1' },
+ { code: 'CV', label: 'country.cape_verde', phone: '+238' },
+ { code: 'KY', label: 'country.cayman_islands', phone: '+1345' },
+ { code: 'CF', label: 'country.central_african_republic', phone: '+236' },
+ { code: 'TD', label: 'country.chad', phone: '+235' },
+ { code: 'CL', label: 'country.chile', phone: '+56' },
+ { code: 'CN', label: 'country.china', phone: '+86' },
+ { code: 'CX', label: 'country.christmas_island', phone: '+61' },
+ { code: 'CC', label: 'country.cocos_keeling_islands', phone: '+61' },
+ { code: 'CO', label: 'country.colombia', phone: '+57' },
+ { code: 'KM', label: 'country.comoros', phone: '+269' },
+ { code: 'CG', label: 'country.congo_brazzaville', phone: '+242' },
+ { code: 'CD', label: 'country.congo_kinshasa', phone: '+243' },
+ { code: 'CK', label: 'country.cook_islands', phone: '+682' },
+ { code: 'CR', label: 'country.costa_rica', phone: '+506' },
+ { code: 'CI', label: 'country.cote_divoire', phone: '+225' },
+ { code: 'HR', label: 'country.croatia', phone: '+385' },
+ { code: 'CU', label: 'country.cuba', phone: '+53' },
+ { code: 'CW', label: 'country.curacao', phone: '+599' },
+ { code: 'CY', label: 'country.cyprus', phone: '+357' },
+ { code: 'CZ', label: 'country.czech_republic', phone: '+420' },
+ { code: 'DK', label: 'country.denmark', phone: '+45' },
+ { code: 'DJ', label: 'country.djibouti', phone: '+253' },
+ { code: 'DM', label: 'country.dominica', phone: '+1767' },
+ { code: 'DO', label: 'country.dominican_republic', phone: '+1' },
+ { code: 'EC', label: 'country.ecuador', phone: '+593' },
+ { code: 'EG', label: 'country.egypt', phone: '+20' },
+ { code: 'SV', label: 'country.el_salvador', phone: '+503' },
+ { code: 'GQ', label: 'country.equatorial_guinea', phone: '+240' },
+ { code: 'ER', label: 'country.eritrea', phone: '+291' },
+ { code: 'EE', label: 'country.estonia', phone: '+372' },
+ { code: 'SZ', label: 'country.eswatini', phone: '+268' },
+ { code: 'ET', label: 'country.ethiopia', phone: '+251' },
+ { code: 'FK', label: 'country.falkland_islands', phone: '+500' },
+ { code: 'FO', label: 'country.faroe_islands', phone: '+298' },
+ { code: 'FJ', label: 'country.fiji', phone: '+679' },
+ { code: 'FI', label: 'country.finland', phone: '+358' },
+ { code: 'FR', label: 'country.france', phone: '+33' },
+ { code: 'GF', label: 'country.french_guiana', phone: '+594' },
+ { code: 'PF', label: 'country.french_polynesia', phone: '+689' },
+ { code: 'GA', label: 'country.gabon', phone: '+241' },
+ { code: 'GM', label: 'country.gambia', phone: '+220' },
+ { code: 'GE', label: 'country.georgia', phone: '+995' },
+ { code: 'DE', label: 'country.germany', phone: '+49' },
+ { code: 'GH', label: 'country.ghana', phone: '+233' },
+ { code: 'GI', label: 'country.gibraltar', phone: '+350' },
+ { code: 'GR', label: 'country.greece', phone: '+30' },
+ { code: 'GL', label: 'country.greenland', phone: '+299' },
+ { code: 'GD', label: 'country.grenada', phone: '+1473' },
+ { code: 'GP', label: 'country.guadeloupe', phone: '+590' },
+ { code: 'GU', label: 'country.guam', phone: '+1671' },
+ { code: 'GT', label: 'country.guatemala', phone: '+502' },
+ { code: 'GG', label: 'country.guernsey', phone: '+44' },
+ { code: 'GN', label: 'country.guinea', phone: '+224' },
+ { code: 'GW', label: 'country.guinea_bissau', phone: '+245' },
+ { code: 'GY', label: 'country.guyana', phone: '+592' },
+ { code: 'HT', label: 'country.haiti', phone: '+509' },
+ { code: 'HN', label: 'country.honduras', phone: '+504' },
+ { code: 'HK', label: 'country.hong_kong', phone: '+852' },
+ { code: 'HU', label: 'country.hungary', phone: '+36' },
+ { code: 'IS', label: 'country.iceland', phone: '+354' },
+ { code: 'IN', label: 'country.india', phone: '+91' },
+ { code: 'ID', label: 'country.indonesia', phone: '+62' },
+ { code: 'IR', label: 'country.iran', phone: '+98' },
+ { code: 'IQ', label: 'country.iraq', phone: '+964' },
+ { code: 'IE', label: 'country.ireland', phone: '+353' },
+ { code: 'IM', label: 'country.isle_of_man', phone: '+44' },
+ { code: 'IL', label: 'country.israel', phone: '+972' },
+ { code: 'IT', label: 'country.italy', phone: '+39' },
+ { code: 'JM', label: 'country.jamaica', phone: '+1876' },
+ { code: 'JP', label: 'country.japan', phone: '+81' },
+ { code: 'JE', label: 'country.jersey', phone: '+44' },
+ { code: 'JO', label: 'country.jordan', phone: '+962' },
+ { code: 'KZ', label: 'country.kazakhstan', phone: '+7' },
+ { code: 'KE', label: 'country.kenya', phone: '+254' },
+ { code: 'KI', label: 'country.kiribati', phone: '+686' },
+ { code: 'XK', label: 'country.kosovo', phone: '+383' },
+ { code: 'KW', label: 'country.kuwait', phone: '+965' },
+ { code: 'KG', label: 'country.kyrgyzstan', phone: '+996' },
+ { code: 'LA', label: 'country.laos', phone: '+856' },
+ { code: 'LV', label: 'country.latvia', phone: '+371' },
+ { code: 'LB', label: 'country.lebanon', phone: '+961' },
+ { code: 'LS', label: 'country.lesotho', phone: '+266' },
+ { code: 'LR', label: 'country.liberia', phone: '+231' },
+ { code: 'LY', label: 'country.libya', phone: '+218' },
+ { code: 'LI', label: 'country.liechtenstein', phone: '+423' },
+ { code: 'LT', label: 'country.lithuania', phone: '+370' },
+ { code: 'LU', label: 'country.luxembourg', phone: '+352' },
+ { code: 'MO', label: 'country.macau', phone: '+853' },
+ { code: 'MG', label: 'country.madagascar', phone: '+261' },
+ { code: 'MW', label: 'country.malawi', phone: '+265' },
+ { code: 'MY', label: 'country.malaysia', phone: '+60' },
+ { code: 'MV', label: 'country.maldives', phone: '+960' },
+ { code: 'ML', label: 'country.mali', phone: '+223' },
+ { code: 'MT', label: 'country.malta', phone: '+356' },
+ { code: 'MH', label: 'country.marshall_islands', phone: '+692' },
+ { code: 'MQ', label: 'country.martinique', phone: '+596' },
+ { code: 'MR', label: 'country.mauritania', phone: '+222' },
+ { code: 'MU', label: 'country.mauritius', phone: '+230' },
+ { code: 'YT', label: 'country.mayotte', phone: '+262' },
+ { code: 'MX', label: 'country.mexico', phone: '+52' },
+ { code: 'FM', label: 'country.micronesia', phone: '+691' },
+ { code: 'MD', label: 'country.moldova', phone: '+373' },
+ { code: 'MC', label: 'country.monaco', phone: '+377' },
+ { code: 'MN', label: 'country.mongolia', phone: '+976' },
+ { code: 'ME', label: 'country.montenegro', phone: '+382' },
+ { code: 'MS', label: 'country.montserrat', phone: '+1664' },
+ { code: 'MA', label: 'country.morocco', phone: '+212' },
+ { code: 'MZ', label: 'country.mozambique', phone: '+258' },
+ { code: 'MM', label: 'country.myanmar', phone: '+95' },
+ { code: 'NA', label: 'country.namibia', phone: '+264' },
+ { code: 'NR', label: 'country.nauru', phone: '+674' },
+ { code: 'NP', label: 'country.nepal', phone: '+977' },
+ { code: 'NL', label: 'country.netherlands', phone: '+31' },
+ { code: 'NC', label: 'country.new_caledonia', phone: '+687' },
+ { code: 'NZ', label: 'country.new_zealand', phone: '+64' },
+ { code: 'NI', label: 'country.nicaragua', phone: '+505' },
+ { code: 'NE', label: 'country.niger', phone: '+227' },
+ { code: 'NG', label: 'country.nigeria', phone: '+234' },
+ { code: 'NU', label: 'country.niue', phone: '+683' },
+ { code: 'NF', label: 'country.norfolk_island', phone: '+672' },
+ { code: 'KP', label: 'country.north_korea', phone: '+850' },
+ { code: 'MK', label: 'country.north_macedonia', phone: '+389' },
+ { code: 'MP', label: 'country.northern_mariana_islands', phone: '+1670' },
+ { code: 'NO', label: 'country.norway', phone: '+47' },
+ { code: 'OM', label: 'country.oman', phone: '+968' },
+ { code: 'PK', label: 'country.pakistan', phone: '+92' },
+ { code: 'PW', label: 'country.palau', phone: '+680' },
+ { code: 'PS', label: 'country.palestine', phone: '+970' },
+ { code: 'PA', label: 'country.panama', phone: '+507' },
+ { code: 'PG', label: 'country.papua_new_guinea', phone: '+675' },
+ { code: 'PY', label: 'country.paraguay', phone: '+595' },
+ { code: 'PE', label: 'country.peru', phone: '+51' },
+ { code: 'PH', label: 'country.philippines', phone: '+63' },
+ { code: 'PN', label: 'country.pitcairn_islands', phone: '+64' },
+ { code: 'PL', label: 'country.poland', phone: '+48' },
+ { code: 'PT', label: 'country.portugal', phone: '+351' },
+ { code: 'PR', label: 'country.puerto_rico', phone: '+1' },
+ { code: 'QA', label: 'country.qatar', phone: '+974' },
+ { code: 'RE', label: 'country.reunion', phone: '+262' },
+ { code: 'RO', label: 'country.romania', phone: '+40' },
+ { code: 'RU', label: 'country.russia', phone: '+7' },
+ { code: 'RW', label: 'country.rwanda', phone: '+250' },
+ { code: 'BL', label: 'country.saint_barthelemy', phone: '+590' },
+ { code: 'SH', label: 'country.saint_helena', phone: '+290' },
+ { code: 'KN', label: 'country.saint_kitts_and_nevis', phone: '+1869' },
+ { code: 'LC', label: 'country.saint_lucia', phone: '+1758' },
+ { code: 'MF', label: 'country.saint_martin', phone: '+590' },
+ { code: 'PM', label: 'country.saint_pierre_and_miquelon', phone: '+508' },
+ {
+ code: 'VC',
+ label: 'country.saint_vincent_and_the_grenadines',
+ phone: '+1784',
+ },
+ { code: 'WS', label: 'country.samoa', phone: '+685' },
+ { code: 'SM', label: 'country.san_marino', phone: '+378' },
+ { code: 'ST', label: 'country.sao_tome_and_principe', phone: '+239' },
+ { code: 'SA', label: 'country.saudi_arabia', phone: '+966' },
+ { code: 'SN', label: 'country.senegal', phone: '+221' },
+ { code: 'RS', label: 'country.serbia', phone: '+381' },
+ { code: 'SC', label: 'country.seychelles', phone: '+248' },
+ { code: 'SL', label: 'country.sierra_leone', phone: '+232' },
+ { code: 'SG', label: 'country.singapore', phone: '+65' },
+ { code: 'SX', label: 'country.sint_maarten', phone: '+1721' },
+ { code: 'SK', label: 'country.slovakia', phone: '+421' },
+ { code: 'SI', label: 'country.slovenia', phone: '+386' },
+ { code: 'SB', label: 'country.solomon_islands', phone: '+677' },
+ { code: 'SO', label: 'country.somalia', phone: '+252' },
+ { code: 'ZA', label: 'country.south_africa', phone: '+27' },
+ {
+ code: 'GS',
+ label: 'country.south_georgia_and_south_sandwich_islands',
+ phone: '+500',
+ },
+ { code: 'KR', label: 'country.south_korea', phone: '+82' },
+ { code: 'SS', label: 'country.south_sudan', phone: '+211' },
+ { code: 'ES', label: 'country.spain', phone: '+34' },
+ { code: 'LK', label: 'country.sri_lanka', phone: '+94' },
+ { code: 'SD', label: 'country.sudan', phone: '+249' },
+ { code: 'SR', label: 'country.suriname', phone: '+597' },
+ { code: 'SJ', label: 'country.svalbard_and_jan_mayen', phone: '+47' },
+ { code: 'SE', label: 'country.sweden', phone: '+46' },
+ { code: 'CH', label: 'country.switzerland', phone: '+41' },
+ { code: 'SY', label: 'country.syria', phone: '+963' },
+ { code: 'TW', label: 'country.taiwan', phone: '+886' },
+ { code: 'TJ', label: 'country.tajikistan', phone: '+992' },
+ { code: 'TZ', label: 'country.tanzania', phone: '+255' },
+ { code: 'TH', label: 'country.thailand', phone: '+66' },
+ { code: 'TL', label: 'country.timor_leste', phone: '+670' },
+ { code: 'TG', label: 'country.togo', phone: '+228' },
+ { code: 'TK', label: 'country.tokelau', phone: '+690' },
+ { code: 'TO', label: 'country.tonga', phone: '+676' },
+ { code: 'TT', label: 'country.trinidad_and_tobago', phone: '+1868' },
+ { code: 'TN', label: 'country.tunisia', phone: '+216' },
+ { code: 'TR', label: 'country.turkey', phone: '+90' },
+ { code: 'TM', label: 'country.turkmenistan', phone: '+993' },
+ { code: 'TC', label: 'country.turks_and_caicos_islands', phone: '+1649' },
+ { code: 'TV', label: 'country.tuvalu', phone: '+688' },
+ { code: 'VI', label: 'country.us_virgin_islands', phone: '+1340' },
+ { code: 'UG', label: 'country.uganda', phone: '+256' },
+ { code: 'UA', label: 'country.ukraine', phone: '+380' },
+ { code: 'AE', label: 'country.united_arab_emirates', phone: '+971' },
+ { code: 'GB', label: 'country.united_kingdom', phone: '+44' },
+ { code: 'US', label: 'country.united_states', phone: '+1' },
+ { code: 'UY', label: 'country.uruguay', phone: '+598' },
+ { code: 'UZ', label: 'country.uzbekistan', phone: '+998' },
+ { code: 'VU', label: 'country.vanuatu', phone: '+678' },
+ { code: 'VA', label: 'country.vatican_city', phone: '+39' },
+ { code: 'VE', label: 'country.venezuela', phone: '+58' },
+ { code: 'VN', label: 'country.vietnam', phone: '+84' },
+ { code: 'WF', label: 'country.wallis_and_futuna', phone: '+681' },
+ { code: 'EH', label: 'country.western_sahara', phone: '+212' },
+ { code: 'YE', label: 'country.yemen', phone: '+967' },
+ { code: 'ZM', label: 'country.zambia', phone: '+260' },
+ { code: 'ZW', label: 'country.zimbabwe', phone: '+263' },
+];
diff --git a/src/features/authorization/api/authorizationAPI.ts b/src/features/authorization/api/authorizationAPI.ts
new file mode 100644
index 0000000..51b1dfe
--- /dev/null
+++ b/src/features/authorization/api/authorizationAPI.ts
@@ -0,0 +1,104 @@
+import type { ApiResponse } from '@/types/apiResponse';
+import type { FetchPromise } from '@/types/fetchPromise';
+import type {
+ CompleteUserInformationRequest,
+ ConfirmEmailOtpRequest,
+ ConfirmForgetPassCodeRequest,
+ ConfirmOtpResponse,
+ ConfirmSmsOtpRequest,
+ GetUserStatusByPhoneNumberOrEmailRequest,
+ GetUserStatusByPhoneNumberOrEmailResponse,
+ LoginOrSignUpWithGoogleRequest,
+ LoginOrSignUpWithGoogleResponse,
+ LoginRequest,
+ LoginResponse,
+ PasswordLoginRequest,
+ ResetPasswordRequest,
+ ResetPasswordResponse,
+ SendEmailOtpRequest,
+ SendForgetPassCodeRequest,
+ SendSmsOtpRequest,
+} from '../types/userTypes';
+
+const API_URL = 'https://accounts.business-harmony.com/api';
+
+export const fetchRequest = (
+ url: string,
+ body: Object | null,
+): FetchPromise => {
+ return fetch(`${API_URL}/${url}`, {
+ body: JSON.stringify(body),
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+};
+
+// GetUserStatusByPhoneNumberOrEmail
+
+export const getUserStatusByPhoneNumberOrEmail = async (
+ body: GetUserStatusByPhoneNumberOrEmailRequest,
+) => {
+ return fetchRequest(
+ 'User/GetUserStatusByPhoneNumberOrEmail',
+ body,
+ );
+};
+
+export const loginOrSignUpWithOtp = async (body: LoginRequest) => {
+ return fetchRequest('User/LoginOrSignUpWithOtp', body);
+};
+
+export const loginWithPassword = async (body: PasswordLoginRequest) => {
+ return fetchRequest('User/LoginWithPassword', body);
+};
+
+export const sendSmsOtp = async (body: SendSmsOtpRequest) => {
+ return fetchRequest('User/SendSmsOtp', body);
+};
+
+export const sendEmailOtp = async (body: SendEmailOtpRequest) => {
+ return fetchRequest('User/SendEmailOtp', body);
+};
+
+export const confirmSmsOtp = async (body: ConfirmSmsOtpRequest) => {
+ return fetchRequest('User/ConfirmSmsOtp', body);
+};
+
+export const confirmEmailOtp = async (body: ConfirmEmailOtpRequest) => {
+ return fetchRequest('User/ConfirmEmailOtp', body);
+};
+
+export const resetPassword = async (body: ResetPasswordRequest) => {
+ return fetchRequest('User/ResetPassword', body);
+};
+
+export const sendForgetPassCode = async (body: SendForgetPassCodeRequest) => {
+ return fetchRequest('User/SendForgetPassCode', body);
+};
+
+export const confirmForgetPassCode = async (
+ body: ConfirmForgetPassCodeRequest,
+) => {
+ return fetchRequest('User/ConfirmForgetPassCode', body);
+};
+
+export const loginOrSignUpWithGoogle = async (
+ body: LoginOrSignUpWithGoogleRequest,
+) => {
+ return fetchRequest(
+ 'User/LoginOrSignUpWithGoogle',
+ body,
+ );
+};
+
+export const completeUserInformation = async (
+ body: CompleteUserInformationRequest,
+) => {
+ return fetchRequest('User/CompleteUserInformation', body);
+};
+
+export const logOut = async () => {
+ return fetchRequest('User/LogOut', {});
+};
diff --git a/src/features/authorization/components/AuthenticationCard.tsx b/src/features/authorization/components/AuthenticationCard.tsx
new file mode 100644
index 0000000..6c49575
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationCard.tsx
@@ -0,0 +1,23 @@
+import { Paper } from '@mui/material';
+import { type PropsWithChildren } from 'react';
+
+// Beacuse in the otp verify there is a element outside of the authentication card
+export const AuthenticationCard = ({ children }: PropsWithChildren) => {
+ return (
+ `calc(100% - ${t.spacing(2)})`,
+ maxWidth: '552px',
+ }}
+ >
+ {children}
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx
new file mode 100644
index 0000000..e02bd3a
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/AuthenticationSteps.tsx
@@ -0,0 +1,139 @@
+import { useState, type JSX } from 'react';
+import { LoginRegisterForm } from './LoginRegiserForm';
+import type { AuthMode, AuthStep, AuthType } from '../../types/authTypes';
+import { OtpVerifyForm } from './OtpVerifyForm';
+import { isNumeric } from '@/utils/regexes/isNumeric';
+import { CompleteSignUp } from './CompleteSignUp';
+import { EnterPasswordForm } from './EnterPasswordForm';
+import { UserStatus } from '../../types/userTypes';
+import type { CountryCode, GUID } from '@/types/commonTypes';
+import { VerifyPhoneNumber } from './VerifyPhoneNumber';
+import { useNavigate, useSearchParams } from 'react-router-dom';
+
+export const AuthenticationSteps = (): JSX.Element => {
+ const navigate = useNavigate();
+ const DEFAULT_RETURN_URL = '/profile';
+ const [searchParams] = useSearchParams();
+ const authReturnUrl: string =
+ searchParams.get('returnUrl') ?? DEFAULT_RETURN_URL;
+ const [authMode, setAuthMode] = useState('register');
+ const [authType, setAuthType] = useState('phone');
+ const [currentStep, setCurrentStep] = useState('emailOrPhone');
+ const [loginRegisterValue, setLoginRegisterValue] = useState('');
+ const [countryCode, setCountryCode] = useState('+98');
+ const [addPhoneCountryCode, setAddPhoneCountryCode] =
+ useState('+98');
+ const [addedPhoneNumberValue, setAddedPhoneNumberValue] =
+ useState('');
+
+ const handleLoginRegister = (value: string, userStatus: UserStatus) => {
+ setAuthType(isNumeric(value) ? 'phone' : 'email');
+
+ switch (userStatus) {
+ case UserStatus.NotRegistered:
+ setAuthMode('register');
+ setCurrentStep('verify');
+ break;
+
+ case UserStatus.RegisteredWithoutPassword:
+ setAuthMode('login');
+ setCurrentStep('verify');
+
+ break;
+
+ case UserStatus.RegisteredWithPassword:
+ setAuthMode('login');
+ setCurrentStep('enterPassword');
+
+ break;
+ }
+ };
+
+ const handleUserLoggedIn = (userId: GUID) => {
+ localStorage.setItem('userID', userId);
+
+ redirectToReturnUrl();
+ };
+
+ const handleConfrimPhoneNumber = (userId: GUID) => {
+ localStorage.setItem('userID', userId);
+
+ setCurrentStep('addPhoneNumber');
+ };
+
+ const handlePhoneNumberVerified = () => {
+ redirectToReturnUrl();
+ };
+
+ const redirectToReturnUrl = () => {
+ if (authReturnUrl === DEFAULT_RETURN_URL) {
+ navigate(DEFAULT_RETURN_URL);
+ } else {
+ location.href = authReturnUrl;
+ }
+ };
+
+ return (
+ <>
+ {currentStep === 'emailOrPhone' && (
+
+ )}
+
+ {currentStep === 'verify' && (
+ setCurrentStep('emailOrPhone')}
+ authMode={authMode}
+ authType={authType}
+ value={loginRegisterValue}
+ />
+ )}
+
+ {currentStep === 'enterPassword' && (
+ setCurrentStep('emailOrPhone')}
+ onLoginWithOTP={() => setCurrentStep('verify')}
+ emailOrPhone={loginRegisterValue}
+ />
+ )}
+
+ {currentStep === 'addPhoneNumber' && (
+ setCurrentStep('addedPhoneNumberVerify')}
+ />
+ )}
+
+ {currentStep === 'addedPhoneNumberVerify' && (
+ setCurrentStep('addPhoneNumber')}
+ value={addedPhoneNumberValue}
+ onPhoneNumberVerified={handlePhoneNumberVerified}
+ />
+ )}
+ >
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx
new file mode 100644
index 0000000..132f136
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/CompleteSignUp.tsx
@@ -0,0 +1,118 @@
+import { Button, TextField, Typography } from '@mui/material';
+import parsePhoneNumberFromString from 'libphonenumber-js';
+import { useRef, useState, type Dispatch } from 'react';
+import { useTranslation } from 'react-i18next';
+import { AuthenticationCard } from '../AuthenticationCard';
+import { CountryCodeSelector } from '../CountryCodeSelector';
+import { sendSmsOtp } from '../../api/authorizationAPI';
+import type { CountryCode } from '@/types/commonTypes';
+
+export interface CompleteSignUpProps {
+ email: string;
+ value: string;
+ setValue: Dispatch;
+ countryCode: CountryCode;
+ setCountryCode: Dispatch;
+ onCompleteSignUp: (countryCode: string, value: string) => void;
+}
+
+export const CompleteSignUp = ({
+ email,
+ value,
+ setValue,
+ countryCode,
+ setCountryCode,
+ onCompleteSignUp,
+}: CompleteSignUpProps) => {
+ const { t } = useTranslation('authentication');
+ const [error, setError] = useState();
+ const textFieldRef = useRef(null);
+ const inputRef = useRef(null);
+ const [touched, setTouched] = useState(false);
+ const inputError: boolean = touched && !!error;
+ const [sendOtpLoading, setSendOtpLoading] = useState(false);
+
+ const isPhoneValid = (code: string, phone: string) => {
+ const phoneNumber = parsePhoneNumberFromString(code + phone);
+
+ return phoneNumber && phoneNumber.isValid();
+ };
+
+ const handleBlur = () => {
+ setTouched(true);
+
+ handleValueError();
+ };
+
+ const handleValueError = () => {
+ if (!value) {
+ setError(t('loginForm.thisFieldIsRequired'));
+ }
+ if (!isPhoneValid(countryCode, value)) {
+ setError(t('loginForm.phoneNumberIsInvalid'));
+ } else {
+ setError(undefined);
+ }
+ };
+
+ const handleCompleteSignUp = async () => {
+ handleValueError();
+
+ if (!value || !isPhoneValid(countryCode, value)) {
+ inputRef.current?.focus();
+ } else {
+ setSendOtpLoading(true);
+
+ await sendSmsOtp({ phoneNumber: countryCode + value });
+ onCompleteSignUp(countryCode, value);
+
+ setSendOtpLoading(false);
+ }
+ };
+
+ return (
+
+
+ {t('completeSignUp.completeSignUp')}
+
+
+
+ {t(
+ 'completeSignUp.emailHasBeenSuccessfullyVerifiedPleaseEnterYourContactNumberToContinue',
+ { email },
+ )}
+
+
+ setValue(e.target.value)}
+ onBlur={handleBlur}
+ error={inputError}
+ helperText={inputError ? error : ''}
+ autoFocus
+ slotProps={{
+ htmlInput: { dir: 'auto', sx: { lineHeight: 1.5, paddingX: 0 } },
+ input: {
+ endAdornment: (
+
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx
new file mode 100644
index 0000000..d5b9b89
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/EnterPasswordForm.tsx
@@ -0,0 +1,190 @@
+import { useRef, useState } from 'react';
+import { AuthenticationCard } from '../AuthenticationCard';
+import { ArrowLeft, Edit2, Eye, EyeSlash } from 'iconsax-react';
+import {
+ Box,
+ Button,
+ IconButton,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { Toast } from '@/components/Toast';
+import { Link } from 'react-router-dom';
+import type { AuthType } from '../../types/authTypes';
+import type { CountryCode, GUID } from '@/types/commonTypes';
+import {
+ loginWithPassword,
+ sendEmailOtp,
+ sendSmsOtp,
+} from '../../api/authorizationAPI';
+import type { PasswordLoginRequest } from '../../types/userTypes';
+
+export interface EnterPasswordFormProps {
+ onEditValue: () => void;
+ onLoginWithOTP: () => void;
+ onLoggedIn: (userId: GUID) => void;
+ emailOrPhone: string;
+ authType: AuthType;
+ loginRegisterValue: string;
+ countryCode: CountryCode;
+ authReturnUrl: string;
+}
+
+export const EnterPasswordForm = ({
+ onEditValue,
+ onLoginWithOTP,
+ onLoggedIn,
+ emailOrPhone,
+ authType,
+ loginRegisterValue,
+ countryCode,
+ authReturnUrl,
+}: EnterPasswordFormProps) => {
+ const { t } = useTranslation('authentication');
+ const [passValue, setPassValue] = useState('');
+ const [inputTouched, setInputTouched] = useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const inputRef = useRef(null);
+ const [loginLoading, setLoginLoading] = useState(false);
+ const [isLoginStatusSuccess, setIsLoginStatusSuccess] = useState();
+ const [loginAlertOpen, setLoginAlertOpen] = useState(false);
+ const [loginFailedMessage, setLoginFailedMessage] = useState('');
+ const [sendOtpLoading, setSendOtpLoading] = useState(false);
+
+ const handleBlur = () => {
+ setInputTouched(true);
+ };
+
+ const handleSubmit = async () => {
+ if (!passValue) {
+ inputRef.current?.focus();
+ } else {
+ setLoginLoading(true);
+
+ const apiRequest: PasswordLoginRequest = {
+ phoneNumber:
+ authType === 'phone' ? countryCode + loginRegisterValue : undefined,
+ email: authType === 'email' ? loginRegisterValue : undefined,
+ password: passValue,
+ returnUrl: authReturnUrl,
+ };
+ const result = await loginWithPassword(apiRequest);
+ const jsonRes = await result.json();
+
+ if (jsonRes.success) {
+ setIsLoginStatusSuccess(true);
+ onLoggedIn(jsonRes.userId);
+ } else {
+ setIsLoginStatusSuccess(false);
+ setLoginFailedMessage(jsonRes.message);
+ }
+ setLoginAlertOpen(true);
+ setLoginLoading(false);
+ }
+ };
+
+ const handleLoginWithOtp = async () => {
+ setSendOtpLoading(true);
+
+ if (authType === 'phone') {
+ await sendSmsOtp({ phoneNumber: countryCode + loginRegisterValue });
+ } else {
+ await sendEmailOtp({ email: loginRegisterValue });
+ }
+
+ setSendOtpLoading(false);
+ onLoginWithOTP();
+ };
+
+ return (
+
+ setLoginAlertOpen(false)}
+ color={!isLoginStatusSuccess ? 'error' : 'success'}
+ >
+ {!isLoginStatusSuccess
+ ? loginFailedMessage
+ : t('verify.youHaveSuccessfullyLoggedIn')}
+
+
+
+
+ {t('enterPassword.loginWithPassword')}
+
+
+ }
+ onClick={onEditValue}
+ >
+ {emailOrPhone}
+
+
+
+
+ {t('enterPassword.enterThePasswordYouSetForYourAccount')}
+
+
+ setPassValue(e.target.value)}
+ onBlur={handleBlur}
+ error={!passValue && inputTouched}
+ helperText={
+ !passValue && inputTouched ? t('loginForm.thisFieldIsRequired') : ''
+ }
+ autoFocus
+ slotProps={{
+ htmlInput: { sx: { lineHeight: 1.5 } },
+ input: {
+ endAdornment: (
+ setShowPassword(!showPassword)}
+ >
+ {showPassword ? : }
+
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+ }
+ >
+ {t('enterPassword.loginWithOneTimeCode')}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx
new file mode 100644
index 0000000..9a74826
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/GoogleAuthentication.tsx
@@ -0,0 +1,81 @@
+import { Button } from '@mui/material';
+import { useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import type { GoogleCodeClientResponse } from '../../types/userTypes';
+import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI';
+import type { GUID } from '@/types/commonTypes';
+import { Google } from 'iconsax-react';
+
+export interface GoogleAuthenticationProps {
+ disabled: boolean;
+ authReturnUrl: string;
+ onGoogleAuthenticated: (userId: GUID) => void;
+}
+
+export const GoogleAuthentication = ({
+ disabled,
+ authReturnUrl,
+ onGoogleAuthenticated,
+}: GoogleAuthenticationProps) => {
+ const { t } = useTranslation('authentication');
+ const [loginWithGoogleLoading, setLoginWithGoogleLoading] =
+ useState(false);
+
+ const clientRef = useRef(null);
+
+ useEffect(() => {
+ const script = document.createElement('script');
+ script.src = 'https://accounts.google.com/gsi/client';
+ script.async = true;
+ script.defer = true;
+ document.body.appendChild(script);
+
+ script.onload = () => {
+ clientRef.current = google.accounts.oauth2.initCodeClient({
+ client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
+ scope: 'openid email profile',
+ ux_mode: 'popup',
+ response_type: 'id_token',
+ callback: async (resp: GoogleCodeClientResponse) => {
+ setLoginWithGoogleLoading(true);
+
+ const result = await loginOrSignUpWithGoogle({
+ idToken: resp.id_token,
+ returnUrl: authReturnUrl,
+ });
+ const jsonRes = await result.json();
+
+ if (jsonRes.success) {
+ onGoogleAuthenticated(jsonRes.userId);
+ } else {
+ // Todo: Add useToast to handle error
+ }
+
+ setLoginWithGoogleLoading(false);
+ },
+ });
+ };
+
+ return () => {
+ document.body.removeChild(script);
+ };
+ }, []);
+
+ const handleGoogleLogin = () => {
+ if (clientRef.current) {
+ clientRef.current.requestCode();
+ }
+ };
+
+ return (
+ }
+ >
+ {t('loginForm.loginWithGoogle')}
+
+ );
+};
diff --git a/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx
new file mode 100644
index 0000000..0b7e1aa
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/LoginRegiserForm.tsx
@@ -0,0 +1,170 @@
+import { Button, Stack, TextField, Typography } from '@mui/material';
+import { useRef, useState, type Dispatch } from 'react';
+import { useTranslation } from 'react-i18next';
+import { isNumeric } from '@/utils/regexes/isNumeric';
+import type { AuthType } from '../../types/authTypes';
+import { isEmail } from '@/utils/regexes/isEmail';
+import { AuthenticationCard } from '../AuthenticationCard';
+import { CountryCodeSelector } from '../CountryCodeSelector';
+import type { UserStatus } from '../../types/userTypes';
+import { getUserStatusByPhoneNumberOrEmail } from '../../api/authorizationAPI';
+import { Toast } from '@/components/Toast';
+import type { CountryCode, GUID } from '@/types/commonTypes';
+import { GoogleAuthentication } from './GoogleAuthentication';
+import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
+
+export interface LoginRegisterFormProps {
+ loginRegisterValue: string;
+ setLoginRegisterValue: Dispatch;
+ countryCode: CountryCode;
+ setCountryCode: Dispatch;
+ authType: AuthType;
+ setAuthType: Dispatch;
+ onLoginRegisterSubmit: (value: string, userStatus: UserStatus) => void;
+ authReturnUrl: string;
+ onGoogleAuthenticated: (userId: GUID) => void;
+}
+
+export function LoginRegisterForm({
+ loginRegisterValue,
+ setLoginRegisterValue,
+ countryCode,
+ setCountryCode,
+ authType,
+ setAuthType,
+ onLoginRegisterSubmit,
+ authReturnUrl,
+ onGoogleAuthenticated,
+}: LoginRegisterFormProps) {
+ const [checkStatusLoading, setCheckStatusLoading] = useState(false);
+ const { t } = useTranslation('authentication');
+ const textFieldRef = useRef(null);
+ const inputRef = useRef(null);
+ const [error, setError] = useState();
+ const [touched, setTouched] = useState(false);
+ const [errorMessage, setErrorMessage] = useState();
+ const inputError: boolean = touched && !!error;
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ const newValue = event.target.value;
+ setLoginRegisterValue(newValue);
+
+ // If the new value contains only digits (or is empty), it's a phone number
+ if (isNumeric(newValue)) {
+ setAuthType('phone');
+ } else {
+ setAuthType('email');
+ }
+ };
+
+ const handleBlur = () => {
+ setTouched(true);
+ validateInput(loginRegisterValue, authType);
+ };
+
+ const validateInput = (
+ value: string,
+ authType: AuthType,
+ setErrors: boolean = true,
+ ): boolean => {
+ if (!value) {
+ if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
+ return false;
+ }
+
+ if (authType === 'email' && !isEmail(value)) {
+ if (setErrors) setError(t('loginForm.emailIsInvalid'));
+ return false;
+ }
+
+ if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
+ if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
+ return false;
+ }
+
+ if (setErrors) setError(undefined);
+ return true;
+ };
+
+ const handleSubmit = async () => {
+ if (validateInput(loginRegisterValue, authType, false)) {
+ setCheckStatusLoading(true);
+ const result = await getUserStatusByPhoneNumberOrEmail({
+ phoneNumber:
+ authType === 'phone' ? countryCode + loginRegisterValue : undefined,
+ email: authType === 'email' ? loginRegisterValue : undefined,
+ });
+ const jsonResult = await result.json();
+
+ if (jsonResult.success) {
+ onLoginRegisterSubmit(loginRegisterValue, jsonResult.userStatus);
+ } else {
+ setErrorMessage(jsonResult.message);
+ }
+ setCheckStatusLoading(false);
+ } else {
+ inputRef.current?.focus();
+ validateInput(loginRegisterValue, authType);
+ }
+ };
+
+ const showAdornment = authType === 'phone' && loginRegisterValue.length > 0;
+
+ return (
+
+ setErrorMessage(undefined)}
+ open={!!errorMessage}
+ >
+ {errorMessage}
+
+
+
+ {t('loginForm.title')}
+
+ {t('loginForm.description')}
+
+
+
+
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx
new file mode 100644
index 0000000..25c0c26
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/OtpVerifyForm.tsx
@@ -0,0 +1,225 @@
+import { useTranslation } from 'react-i18next';
+import { Box, Button, Stack, Typography } from '@mui/material';
+import { Edit2 } from 'iconsax-react';
+import DigitInput from '@/components/components/DigitsInput';
+import type { AuthMode, AuthType } from '../../types/authTypes';
+import { useEffect, useState } from 'react';
+import { Toast } from '@/components/Toast';
+import { AuthenticationCard } from '../AuthenticationCard';
+import type { LoginRequest } from '../../types/userTypes';
+import {
+ loginOrSignUpWithOtp,
+ sendEmailOtp,
+ sendSmsOtp,
+} from '../../api/authorizationAPI';
+import type { CountryCode, GUID } from '@/types/commonTypes';
+
+interface OtpVerifyFormProps {
+ value: string;
+ countryCode: CountryCode;
+ authType: AuthType;
+ authMode: AuthMode;
+ onEditValue: () => void;
+ onOTPVerified: (userId: GUID) => void;
+ onVerifyPhoneNumber: (userId: GUID) => void;
+ authReturnUrl: string;
+}
+
+export function OtpVerifyForm({
+ value,
+ countryCode,
+ authType,
+ authMode,
+ onEditValue,
+ onOTPVerified,
+ onVerifyPhoneNumber,
+ authReturnUrl,
+}: OtpVerifyFormProps) {
+ const [otpCode, setOtpCode] = useState('');
+ const [otpDigitInvalid, setOtpDigitInvalid] = useState(false);
+ const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
+ const [errorMessage, setErrorMessage] = useState();
+ const [verifyStatusLoading, setVerifyStatusLoading] =
+ useState(false);
+ const [verifyAlertOpen, setVerifyAlertOpen] = useState(false);
+ const { t } = useTranslation('authentication');
+ const [resendTimer, setResendTimer] = useState(120);
+ const [canResend, setCanResend] = useState(false);
+ const [resendLoading, setResendLoading] = useState(false);
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+ if (resendTimer > 0) {
+ interval = setInterval(() => {
+ setResendTimer((prev) => prev - 1);
+ }, 1000);
+ } else {
+ setCanResend(true);
+ }
+
+ return () => clearInterval(interval);
+ }, [resendTimer]);
+
+ const handleResendOTPCode = async () => {
+ setResendLoading(true);
+
+ if (authType === 'phone') {
+ await sendSmsOtp({ phoneNumber: countryCode + value });
+ } else {
+ await sendEmailOtp({ email: value });
+ }
+
+ setResendTimer(120);
+ setCanResend(false);
+ setResendLoading(false);
+ };
+
+ const formatTime = (seconds: number) => {
+ const min = Math.floor(seconds / 60);
+ const sec = seconds % 60;
+ return `${min}:${sec.toString().padStart(2, '0')}`;
+ };
+
+ const handleVerifyOTP = () => {
+ if (!otpCode || otpCode.length < 4) {
+ setOtpDigitInvalid(true);
+ } else {
+ handleLoginOrSignUp();
+ }
+ };
+
+ const handleLoginOrSignUp = async () => {
+ setOtpDigitInvalid(false);
+ setVerifyStatusLoading(true);
+
+ const loginRequest: LoginRequest = {
+ otpCode: otpCode,
+ phoneNumber: authType === 'phone' ? countryCode + value : undefined,
+ email: authType === 'email' ? value : undefined,
+ returnUrl: authReturnUrl,
+ };
+ const result = await loginOrSignUpWithOtp(loginRequest);
+ const jsonRes = await result.json();
+
+ if (jsonRes.success) {
+ setVerifyStatus('success');
+
+ if (jsonRes.registeredWithOutPhoneNumber) {
+ onVerifyPhoneNumber(jsonRes.userId);
+ } else {
+ onOTPVerified(jsonRes.userId);
+ }
+ } else {
+ setVerifyStatus('failed');
+ setErrorMessage(jsonRes.message);
+ }
+
+ setVerifyAlertOpen(true);
+ setVerifyStatusLoading(false);
+ };
+
+ const otpMessage = (): string => {
+ if (authType === 'phone' && authMode === 'login') {
+ return t(
+ 'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
+ );
+ } else if (authType === 'phone' && authMode === 'register') {
+ return t(
+ 'verify.thereIsNoAccountWithThisNumberA4DigitVerificationCodeHasBeenSentToThisNumberToCreateANewAccount',
+ );
+ } else if (authType === 'email' && authMode === 'login') {
+ return t(
+ 'verify.a4digitVerificationCodeHasBeenSentToYourEmailAddressPleaseEnterIt',
+ );
+ } else if (authType === 'email' && authMode === 'register') {
+ return t(
+ 'verify.thereIsNoAccountWithThisEmailAddressA4DigitVerificationCodeHasBeenSentToThisEmailAddressToCreateANewAccount',
+ );
+ }
+
+ return '';
+ };
+
+ const toastMessage =
+ verifyStatus === 'failed'
+ ? (errorMessage ?? t('verify.theVerificationCodeIsIncorrect'))
+ : verifyStatus === 'success' && authMode === 'register'
+ ? t('verify.youHaveSuccessfullySignedIn')
+ : verifyStatus === 'success' && authMode === 'login'
+ ? t('verify.youHaveSuccessfullyLoggedIn')
+ : '';
+
+ return (
+
+
+ setVerifyAlertOpen(false)}
+ color={verifyStatus === 'failed' ? 'error' : 'success'}
+ >
+ {toastMessage}
+
+
+
+ {t('verify.verify')}
+
+ }
+ onClick={onEditValue}
+ >
+ {authType === 'phone' ? countryCode + value : value}
+
+
+
+
+ {otpMessage()}
+
+
+ setOtpCode(value)}
+ />
+
+
+
+
+
+ {t('verify.resendCodeIn')}
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx
new file mode 100644
index 0000000..ad937d4
--- /dev/null
+++ b/src/features/authorization/components/AuthenticationSteps/VerifyPhoneNumber.tsx
@@ -0,0 +1,174 @@
+import { useTranslation } from 'react-i18next';
+import { Box, Button, Stack, Typography } from '@mui/material';
+import { Edit2 } from 'iconsax-react';
+import DigitInput from '@/components/components/DigitsInput';
+import { useEffect, useState } from 'react';
+import { Toast } from '@/components/Toast';
+import { AuthenticationCard } from '../AuthenticationCard';
+import type { ConfirmSmsOtpRequest } from '../../types/userTypes';
+import { confirmSmsOtp, sendSmsOtp } from '../../api/authorizationAPI';
+import type { CountryCode } from '@/types/commonTypes';
+
+interface VerifyPhoneNumberProps {
+ value: string;
+ countryCode: CountryCode;
+ onEditValue: () => void;
+ onPhoneNumberVerified: () => void;
+}
+
+export function VerifyPhoneNumber({
+ value,
+ countryCode,
+ onEditValue,
+ onPhoneNumberVerified,
+}: VerifyPhoneNumberProps) {
+ const [otpCode, setOtpCode] = useState('');
+ const [otpDigitInvalid, setOtpDigitInvalid] = useState(false);
+ const [verifyStatus, setVerifyStatus] = useState<'success' | 'failed'>();
+ const [errorMessage, setErrorMessage] = useState();
+ const [verifyStatusLoading, setVerifyStatusLoading] =
+ useState(false);
+ const [verifyAlertOpen, setVerifyAlertOpen] = useState(false);
+ const { t } = useTranslation('authentication');
+ const [resendTimer, setResendTimer] = useState(120);
+ const [canResend, setCanResend] = useState(false);
+ const [resendLoading, setResendLoading] = useState(false);
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+ if (resendTimer > 0) {
+ interval = setInterval(() => {
+ setResendTimer((prev) => prev - 1);
+ }, 1000);
+ } else {
+ setCanResend(true);
+ }
+
+ return () => clearInterval(interval);
+ }, [resendTimer]);
+
+ const handleResendOTPCode = async () => {
+ setResendLoading(true);
+
+ await sendSmsOtp({ phoneNumber: countryCode + value });
+
+ setResendTimer(120);
+ setCanResend(false);
+ setResendLoading(false);
+ };
+
+ const formatTime = (seconds: number) => {
+ const min = Math.floor(seconds / 60);
+ const sec = seconds % 60;
+ return `${min}:${sec.toString().padStart(2, '0')}`;
+ };
+
+ const handleVerifyOTP = async () => {
+ if (!otpCode || otpCode.length < 4) {
+ setOtpDigitInvalid(true);
+ } else {
+ setOtpDigitInvalid(false);
+ setVerifyStatusLoading(true);
+
+ const confirmSmsOtpRequest: ConfirmSmsOtpRequest = {
+ otpCode: otpCode,
+ phoneNumber: countryCode + value,
+ };
+ const result = await confirmSmsOtp(confirmSmsOtpRequest);
+ const jsonRes = await result.json();
+
+ if (jsonRes.success) {
+ setVerifyStatus('success');
+ onPhoneNumberVerified();
+ } else {
+ setVerifyStatus('failed');
+ setErrorMessage(jsonRes.message);
+ }
+
+ setVerifyAlertOpen(true);
+ setVerifyStatusLoading(false);
+ }
+ };
+
+ const verifyAlertMessage = (): string => {
+ if (verifyStatus === 'failed') {
+ return errorMessage ?? t('verify.theVerificationCodeIsIncorrect');
+ } else {
+ return t('verify.youHaveSuccessfullyLoggedIn');
+ }
+ };
+
+ return (
+
+
+ setVerifyAlertOpen(false)}
+ color={verifyStatus === 'failed' ? 'error' : 'success'}
+ >
+ {verifyAlertMessage()}
+
+
+
+ {t('verify.verify')}
+
+ }
+ onClick={onEditValue}
+ >
+ {countryCode + value}
+
+
+
+
+ {t(
+ 'verify.a4DigitVerificationCodeHasBeenSentToYourBobileNumberPleaseEnterIt',
+ )}
+
+
+ setOtpCode(value)}
+ />
+
+
+
+
+
+ {t('verify.resendCodeIn')}
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/CountryCodeSelector.tsx b/src/features/authorization/components/CountryCodeSelector.tsx
new file mode 100644
index 0000000..a93168b
--- /dev/null
+++ b/src/features/authorization/components/CountryCodeSelector.tsx
@@ -0,0 +1,254 @@
+import {
+ Box,
+ InputAdornment,
+ ListItem,
+ ListItemIcon,
+ ListItemText,
+ Menu,
+ MenuItem,
+ TextField,
+ Typography,
+} from '@mui/material';
+import { useMemo, useRef, useState, type RefObject } from 'react';
+import { ArrowDown2 } from 'iconsax-react';
+import ReactCountryFlag from 'react-country-flag';
+import { useTranslation } from 'react-i18next';
+import { countries, type Country } from '../../../countries';
+import type { CountryCode } from '@/types/commonTypes';
+interface CountryCodeSelectorProps {
+ show: boolean;
+ value: CountryCode;
+ onChange: (newValue: CountryCode) => void;
+ menuAnchor: HTMLElement | null;
+ onCloseFocusRef: RefObject;
+}
+
+/**
+ * An animated country code adornment that fades and slides into view.
+ * Its visibility is controlled by the `show` prop.
+ */
+export function CountryCodeSelector({
+ show,
+ value,
+ onChange,
+ menuAnchor,
+ onCloseFocusRef,
+}: CountryCodeSelectorProps) {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const [searchTerm, setSearchTerm] = useState('');
+ const open = Boolean(anchorEl);
+ const searchInputRef = useRef(null);
+ const menuWidth = menuAnchor ? menuAnchor.clientWidth : 'auto';
+ const { t, i18n } = useTranslation();
+
+ const selectedCountry =
+ countries.find((c) => c.phone === value) || countries[0];
+
+ const handleClick = () => {
+ setAnchorEl(menuAnchor);
+ };
+
+ const handleClose = () => {
+ setTimeout(() => {
+ setAnchorEl(null);
+ }, 0);
+ setTimeout(() => {
+ onCloseFocusRef.current?.focus();
+ }, 100);
+ setSearchTerm(''); // Reset search on close
+ };
+
+ const handleSelect = (country: Country) => {
+ onChange(country.phone);
+ handleClose();
+ };
+
+ const handleMenuEntered = () => {
+ // Focus the input field after the menu has finished opening
+ searchInputRef.current?.focus();
+ };
+
+ const filteredCountries = useMemo(
+ () =>
+ countries.filter(
+ (country) =>
+ t(country.label).toLowerCase().includes(searchTerm.toLowerCase()) ||
+ country.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ country.phone.includes(searchTerm),
+ ),
+ [searchTerm],
+ );
+
+ return (
+
+
+ theme.transitions.create(['width', 'opacity'], {
+ duration: theme.transitions.duration.standard,
+ }),
+
+ // Prevent content from wrapping or spilling out during animation
+ overflow: 'hidden',
+ whiteSpace: 'nowrap',
+
+ // layout styles
+ height: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ gap: 0.25,
+ pl: show ? 0.25 : 0,
+
+ '&:hover': {
+ cursor: 'pointer',
+ },
+ }}
+ >
+ {/* This inner Box prevents the content from being squeezed during the transition */}
+
+
+
+ {value}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/ForgetPassword/ChangePassword.tsx b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx
new file mode 100644
index 0000000..6da9c4d
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ChangePassword.tsx
@@ -0,0 +1,254 @@
+import { useRef, useState } from 'react';
+import { AuthenticationCard } from '../AuthenticationCard';
+import { Edit2, Eye, EyeSlash, TickCircle } from 'iconsax-react';
+import {
+ Box,
+ Button,
+ IconButton,
+ Stack,
+ TextField,
+ Typography,
+ useTheme,
+} from '@mui/material';
+import { useTranslation } from 'react-i18next';
+import { Toast } from '@/components/Toast';
+import { containsNumber } from '@/utils/regexes/containsNumber';
+import { containsSymbol } from '@/utils/regexes/containsSymbol';
+import { least8Chars } from '@/utils/regexes/least8Chars';
+import { hasUpperAndLowerLetter } from '@/utils/regexes/hasUpperAndLowerLetter';
+import type { ResetPasswordRequest } from '../../types/userTypes';
+import type { AuthType } from '../../types/authTypes';
+import type { CountryCode } from '@/types/commonTypes';
+import { resetPassword } from '../../api/authorizationAPI';
+
+export interface ChangePasswordProps {
+ onEditInfo: () => void;
+ onPasswordChanged: () => void;
+ forgettedPasswordInfo: string;
+ infoType: AuthType;
+ countryCode: CountryCode;
+}
+
+export const ChangePassword = ({
+ onEditInfo,
+ onPasswordChanged,
+ forgettedPasswordInfo,
+ infoType,
+ countryCode,
+}: ChangePasswordProps) => {
+ const theme = useTheme();
+ const { t } = useTranslation('authentication');
+ const [passValue, setPassValue] = useState('');
+ const [confirmPassValue, setConfirmPassValue] = useState('');
+ const [inputTouched, setInputTouched] = useState(false);
+ const [confirmInputTouched, setConfirmInputTouched] =
+ useState(false);
+ const [showPassword, setShowPassword] = useState(false);
+ const [showConfirmPassword, setShowConfirmPassword] =
+ useState(false);
+ const inputRef = useRef(null);
+ const confirmInputRef = useRef(null);
+ const [changePasswordLoading, setChangePasswordLoading] =
+ useState(false);
+ const [changePasswordStatus, setChangePasswordStatus] = useState<
+ 'success' | 'failed'
+ >();
+ const [changePassAlertOpen, setChangePassAlertOpen] =
+ useState(false);
+ const [changePassFailedMessage, setChangePassFailedMessage] =
+ useState('');
+
+ const passwordValidationRules = [
+ { title: t('forgetPassword.includingANumber'), validator: containsNumber },
+ { title: t('forgetPassword.atLeast8Characters'), validator: least8Chars },
+ {
+ title: t('forgetPassword.containsAnUppercaseAndLowercaseLetter'),
+ validator: hasUpperAndLowerLetter,
+ },
+ { title: t('forgetPassword.ContainsASymbol'), validator: containsSymbol },
+ ];
+
+ const handleBlur = () => {
+ setInputTouched(true);
+ };
+
+ const handleConfirmPassBlur = () => {
+ setConfirmInputTouched(true);
+ };
+
+ const handleSubmit = async () => {
+ if (!passValue || !isValidPassword(passValue)) {
+ setInputTouched(true);
+ inputRef.current?.focus();
+ } else if (passValue !== confirmPassValue) {
+ setConfirmInputTouched(true);
+ confirmInputRef.current?.focus();
+ } else {
+ setChangePasswordLoading(true);
+
+ const apiRequest: ResetPasswordRequest = {
+ email: infoType === 'email' ? forgettedPasswordInfo : undefined,
+ phoneNumber:
+ infoType === 'phone'
+ ? countryCode + forgettedPasswordInfo
+ : undefined,
+ newPassword: passValue,
+ confirmNewPassword: confirmPassValue,
+ };
+
+ const result = await resetPassword(apiRequest);
+ const jsonRes = await result.json();
+
+ if (jsonRes.success) {
+ setChangePasswordStatus('success');
+ onPasswordChanged();
+ } else {
+ setChangePasswordStatus('failed');
+ setChangePassFailedMessage(jsonRes.message);
+ }
+ setChangePassAlertOpen(true);
+
+ setChangePasswordLoading(false);
+ }
+ };
+
+ const isValidPassword = (value: string) => {
+ return (
+ containsNumber(value) &&
+ containsSymbol(value) &&
+ least8Chars(value) &&
+ hasUpperAndLowerLetter(value)
+ );
+ };
+
+ return (
+
+ setChangePassAlertOpen(false)}
+ color={changePasswordStatus === 'failed' ? 'error' : 'success'}
+ >
+ {changePasswordStatus === 'failed'
+ ? changePassFailedMessage
+ : t('forgetPassword.passwordChangedSuccessfully')}
+
+
+
+
+ {t('forgetPassword.changePassword')}
+
+
+ }
+ onClick={onEditInfo}
+ >
+ {forgettedPasswordInfo}
+
+
+
+
+ {t('forgetPassword.createANewPassword')}
+
+
+ setPassValue(e.target.value)}
+ onBlur={handleBlur}
+ error={inputTouched && !isValidPassword(passValue)}
+ autoFocus
+ slotProps={{
+ htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
+ input: {
+ startAdornment: confirmPassValue &&
+ isValidPassword(passValue) &&
+ passValue === confirmPassValue && (
+
+ ),
+ endAdornment: passValue ? (
+ setShowPassword(!showPassword)}
+ >
+ {showPassword ? : }
+
+ ) : (
+ ''
+ ),
+ },
+ }}
+ sx={{ mt: 4 }}
+ />
+
+ {!isValidPassword(passValue) && (
+
+ {passwordValidationRules.map((rule) => (
+
+
+
+ {rule.title}
+
+ ))}
+
+ )}
+
+ setConfirmPassValue(e.target.value)}
+ onBlur={handleConfirmPassBlur}
+ error={confirmInputTouched && confirmPassValue !== passValue}
+ slotProps={{
+ htmlInput: { sx: { lineHeight: 1.5, paddingInlineStart: 1 } },
+ input: {
+ startAdornment: confirmPassValue &&
+ isValidPassword(passValue) &&
+ passValue === confirmPassValue && (
+
+ ),
+ endAdornment: confirmPassValue ? (
+ setShowConfirmPassword(!showConfirmPassword)}
+ >
+ {showConfirmPassword ? : }
+
+ ) : (
+ ''
+ ),
+ },
+ }}
+ sx={{ my: 4 }}
+ />
+
+
+
+
+
+ );
+};
diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx
new file mode 100644
index 0000000..137b413
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordContainer.tsx
@@ -0,0 +1,68 @@
+import { useState } from 'react';
+import type { AuthType } from '../../types/authTypes';
+import { ForgettedPasswordInfo } from './ForgettedPasswordInfo';
+import { ForgetPasswordOtp } from './ForgetPasswordOtp';
+import { ChangePassword } from './ChangePassword';
+import type { CountryCode } from '@/types/commonTypes';
+
+export const ForgetPasswordContainer = () => {
+ const [forgetPassCurrentStep, setForgetPassCurrentStep] = useState<
+ 'enterInfo' | 'verifyOtp' | 'setPassword'
+ >('enterInfo');
+ const [forgettedPasswordInfo, setForgettedPasswordInfo] =
+ useState('');
+ const [infoCountryCode, setInfoCountryCode] = useState('+98');
+ const [infoType, setInfoType] = useState('email');
+
+ const handleVerifyOtp = () => {
+ setForgetPassCurrentStep('verifyOtp');
+ };
+
+ const handleEditInfo = () => {
+ setForgetPassCurrentStep('enterInfo');
+ };
+
+ const handleOtpVerified = () => {
+ setForgetPassCurrentStep('setPassword');
+ };
+
+ const handlePasswordChanged = () => {
+ console.log('changingPasswordTo');
+ };
+
+ return (
+ <>
+ {forgetPassCurrentStep === 'enterInfo' && (
+
+ )}
+
+ {forgetPassCurrentStep === 'verifyOtp' && (
+
+ )}
+
+ {forgetPassCurrentStep === 'setPassword' && (
+
+ )}
+ >
+ );
+};
diff --git a/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx
new file mode 100644
index 0000000..021aa81
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ForgetPasswordOtp.tsx
@@ -0,0 +1,185 @@
+import { useTranslation } from 'react-i18next';
+import { Box, Button, Stack, Typography } from '@mui/material';
+import { Edit2 } from 'iconsax-react';
+import DigitInput from '@/components/components/DigitsInput';
+import type { AuthType } from '../../types/authTypes';
+import { useEffect, useState } from 'react';
+import { Toast } from '@/components/Toast';
+import { AuthenticationCard } from '../AuthenticationCard';
+import type { ConfirmForgetPassCodeRequest } from '../../types/userTypes';
+import type { CountryCode } from '@/types/commonTypes';
+import { confirmForgetPassCode } from '../../api/authorizationAPI';
+
+interface ForgetPasswordOtpProps {
+ forgettedPasswordInfo: string;
+ infoType: AuthType;
+ countryCode: CountryCode;
+ onEditInfo: () => void;
+ onOTPVerified: (otpCode: string) => void;
+}
+
+export function ForgetPasswordOtp({
+ forgettedPasswordInfo,
+ infoType,
+ countryCode,
+ onEditInfo,
+ onOTPVerified,
+}: ForgetPasswordOtpProps) {
+ const [otpCode, setOtpCode] = useState('');
+ const [otpDigitInvalid, setOtpDigitInvalid] = useState(false);
+ const [verifyStatus, setVerifyStatus] = useState<'failed' | 'success'>();
+ const [verifyStatusLoading, setVerifyStatusLoading] =
+ useState(false);
+ const [verifyAlertMessage, setVerifyAlertMessage] = useState();
+ const { t } = useTranslation('authentication');
+ const [resendTimer, setResendTimer] = useState(120);
+ const [canResend, setCanResend] = useState(false);
+ const [resendLoading, setResendLoading] = useState(false);
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+ if (resendTimer > 0) {
+ interval = setInterval(() => {
+ setResendTimer((prev) => prev - 1);
+ }, 1000);
+ } else {
+ setCanResend(true);
+ }
+
+ return () => clearInterval(interval);
+ }, [resendTimer]);
+
+ const handleResendOTPCode = () => {
+ setResendLoading(true);
+
+ // TODO: Call API here instead of settimeout
+
+ setTimeout(() => {
+ console.log('resended');
+
+ setResendTimer(120);
+ setCanResend(false);
+ setResendLoading(false);
+ }, 1000);
+ };
+
+ const formatTime = (seconds: number) => {
+ const min = Math.floor(seconds / 60);
+ const sec = seconds % 60;
+ return `${min}:${sec.toString().padStart(2, '0')}`;
+ };
+
+ const handleVerifyOTP = async () => {
+ if (!otpCode || otpCode.length < 4) {
+ setOtpDigitInvalid(true);
+ } else {
+ setOtpDigitInvalid(false);
+ setVerifyStatusLoading(true);
+
+ // Change setTimeout to api call
+ const apiRequest: ConfirmForgetPassCodeRequest = {
+ email: infoType === 'email' ? forgettedPasswordInfo : undefined,
+ phoneNumber:
+ infoType === 'phone'
+ ? countryCode + forgettedPasswordInfo
+ : undefined,
+ code: otpCode,
+ };
+
+ const result = await confirmForgetPassCode(apiRequest);
+ const jsonRes = await result.json();
+
+ if (jsonRes.success) {
+ setVerifyStatus('success');
+ onOTPVerified(otpCode);
+ } else {
+ setVerifyStatus('failed');
+ setVerifyAlertMessage(jsonRes.message);
+ }
+
+ setVerifyStatusLoading(false);
+ }
+ };
+
+ return (
+
+
+ setVerifyAlertMessage(undefined)}
+ color={'error'}
+ >
+ {verifyAlertMessage}
+
+
+
+
+ {t('forgetPassword.forgetPassword')}
+
+
+ }
+ onClick={onEditInfo}
+ >
+ {infoType === 'phone'
+ ? countryCode + forgettedPasswordInfo
+ : forgettedPasswordInfo}
+
+
+
+
+ {infoType === 'email'
+ ? t(
+ 'forgetPassword.anEmailContainingARecoveryCodeHasBeenSentToThisEmailAddress',
+ )
+ : t(
+ 'forgetPassword.anCodeContainingARecoveryCodeHasBeenSentToThisPhoneNumber',
+ )}
+
+
+ setOtpCode(value)}
+ />
+
+
+
+
+
+ {t('verify.resendCodeIn')}
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx
new file mode 100644
index 0000000..6d2cf54
--- /dev/null
+++ b/src/features/authorization/components/ForgetPassword/ForgettedPasswordInfo.tsx
@@ -0,0 +1,164 @@
+import { Button, Stack, TextField, Typography } from '@mui/material';
+import { useRef, useState, type Dispatch } from 'react';
+import { useTranslation } from 'react-i18next';
+import { isNumeric } from '@/utils/regexes/isNumeric';
+import type { AuthType } from '../../types/authTypes';
+import { isEmail } from '@/utils/regexes/isEmail';
+import { AuthenticationCard } from '../AuthenticationCard';
+import { CountryCodeSelector } from '../CountryCodeSelector';
+import type { CountryCode } from '@/types/commonTypes';
+import { sendForgetPassCode } from '../../api/authorizationAPI';
+import type { SendForgetPassCodeRequest } from '../../types/userTypes';
+import { Toast } from '@/components/Toast';
+import { isPhoneNumber } from '@/utils/regexes/isValidPhoneNumber';
+
+export interface ForgettedPasswordInfoProps {
+ forgettedPasswordInfo: string;
+ setForgettedPasswordInfo: Dispatch;
+ infoType: AuthType;
+ setInfoType: Dispatch;
+ onVerifyOtp: (value: string) => void;
+ countryCode: CountryCode;
+ setCountryCode: Dispatch;
+}
+
+export function ForgettedPasswordInfo({
+ forgettedPasswordInfo,
+ setForgettedPasswordInfo,
+ infoType,
+ setInfoType,
+ onVerifyOtp,
+ countryCode,
+ setCountryCode,
+}: ForgettedPasswordInfoProps) {
+ const { t } = useTranslation('authentication');
+ const textFieldRef = useRef(null);
+ const inputRef = useRef(null);
+ const [error, setError] = useState();
+ const [touched, setTouched] = useState(false);
+ const [errorMessage, setErrorMessage] = useState();
+ const [sendCodeLoading, setSendCodeLoading] = useState(false);
+ const inputError: boolean = touched && !!error;
+
+ const handleInputChange = (event: React.ChangeEvent) => {
+ const newValue = event.target.value;
+ setForgettedPasswordInfo(newValue);
+
+ // If the new value contains only digits (or is empty), it's a phone number
+ if (isNumeric(newValue)) {
+ setInfoType('phone');
+ } else {
+ setInfoType('email');
+ }
+ };
+
+ const handleBlur = () => {
+ setTouched(true);
+ validateInput(forgettedPasswordInfo, infoType);
+ };
+
+ const validateInput = (
+ value: string,
+ authType: AuthType,
+ setErrors: boolean = true,
+ ) => {
+ if (!value) {
+ if (setErrors) setError(t('loginForm.thisFieldIsRequired'));
+ return false;
+ } else if (authType === 'email' && !isEmail(value)) {
+ if (setErrors) setError(t('loginForm.emailIsInvalid'));
+ return false;
+ } else if (authType === 'phone' && !isPhoneNumber(countryCode, value)) {
+ if (setErrors) setError(t('loginForm.phoneNumberIsInvalid'));
+ return false;
+ } else {
+ if (setErrors) setError(undefined);
+ return true;
+ }
+ };
+
+ const handleSubmit = async () => {
+ if (validateInput(forgettedPasswordInfo, infoType, false)) {
+ setSendCodeLoading(true);
+
+ const sendCodeRequest: SendForgetPassCodeRequest = {
+ email: infoType === 'email' ? forgettedPasswordInfo : undefined,
+ phoneNumber:
+ infoType === 'phone'
+ ? countryCode + forgettedPasswordInfo
+ : undefined,
+ };
+ const result = await sendForgetPassCode(sendCodeRequest);
+ const jsonRes = await result.json();
+
+ if (!jsonRes.success) {
+ setErrorMessage(jsonRes.message);
+ }
+
+ setSendCodeLoading(false);
+ onVerifyOtp(forgettedPasswordInfo);
+ } else {
+ inputRef.current?.focus();
+ validateInput(forgettedPasswordInfo, infoType);
+ }
+ };
+
+ const showAdornment =
+ infoType === 'phone' && forgettedPasswordInfo.length > 0;
+
+ return (
+
+ setErrorMessage(undefined)}
+ open={!!errorMessage}
+ >
+ {errorMessage}
+
+
+
+
+ {t('forgetPassword.forgetPassword')}
+
+
+ {t(
+ 'forgetPassword.pleaseEnterYourMobileNumberEmailToRecoverYourPassword',
+ )}
+
+
+
+
+ ),
+ },
+ }}
+ sx={{ my: 4, mb: 8 }}
+ />
+
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/index.ts b/src/features/authorization/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/src/features/authorization/routes/AuthenticationPage.tsx b/src/features/authorization/routes/AuthenticationPage.tsx
new file mode 100644
index 0000000..47917cf
--- /dev/null
+++ b/src/features/authorization/routes/AuthenticationPage.tsx
@@ -0,0 +1,20 @@
+import { FlexBox } from '@/components/components/common/FlexBox';
+import Logo from '@/components/Logo';
+import { AuthenticationSteps } from '../components/AuthenticationSteps/AuthenticationSteps';
+
+export function AuthenticationPage() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/routes/ForgetPasswordPage.tsx b/src/features/authorization/routes/ForgetPasswordPage.tsx
new file mode 100644
index 0000000..f54d961
--- /dev/null
+++ b/src/features/authorization/routes/ForgetPasswordPage.tsx
@@ -0,0 +1,20 @@
+import { FlexBox } from '@/components/components/common/FlexBox';
+import Logo from '@/components/Logo';
+import { ForgetPasswordContainer } from '../components/ForgetPassword/ForgetPasswordContainer';
+
+export function ForgetPasswordPage() {
+ return (
+
+
+
+
+ );
+}
diff --git a/src/features/authorization/types/authTypes.ts b/src/features/authorization/types/authTypes.ts
new file mode 100644
index 0000000..91e45f5
--- /dev/null
+++ b/src/features/authorization/types/authTypes.ts
@@ -0,0 +1,10 @@
+export type AuthType = 'email' | 'phone';
+
+export type AuthMode = 'register' | 'login';
+
+export type AuthStep =
+ | 'emailOrPhone'
+ | 'verify'
+ | 'enterPassword'
+ | 'addPhoneNumber'
+ | 'addedPhoneNumberVerify';
diff --git a/src/features/authorization/types/userTypes.ts b/src/features/authorization/types/userTypes.ts
new file mode 100644
index 0000000..99eb998
--- /dev/null
+++ b/src/features/authorization/types/userTypes.ts
@@ -0,0 +1,138 @@
+// GetUserStatusByPhoneNumberOrEmail
+
+import type { ApiResponse } from '@/types/apiResponse';
+import type { GUID } from '@/types/commonTypes';
+
+export interface GetUserStatusByPhoneNumberOrEmailRequest {
+ phoneNumber?: string;
+ email?: string;
+}
+
+export interface GetUserStatusByPhoneNumberOrEmailResponse extends ApiResponse {
+ userStatus: UserStatus;
+}
+
+export enum UserStatus {
+ None = 0,
+ RegisteredWithPassword = 1,
+ RegisteredWithoutPassword = 2,
+ NotRegistered = 3,
+}
+
+// LoginOrSignUpWithOtp
+
+export interface LoginRequest {
+ otpCode: string;
+ phoneNumber?: string;
+ email?: string;
+ returnUrl: string;
+}
+
+export interface PasswordLoginRequest {
+ phoneNumber?: string;
+ email?: string;
+ password: string;
+ returnUrl: string;
+}
+
+export interface LoginResponse extends ApiResponse {
+ returnUrl: string;
+ userId: GUID;
+ registeredWithOutPhoneNumber: boolean;
+ completedUserInformation: boolean;
+}
+
+// SendSmsOtp
+
+export interface SendSmsOtpRequest {
+ phoneNumber: string;
+}
+
+// SendEmailOtp
+
+export interface SendEmailOtpRequest {
+ email: string;
+}
+
+// ConfirmOtp
+
+export interface ConfirmEmailOtpRequest {
+ email: string;
+ otpCode: string;
+}
+
+export interface ConfirmSmsOtpRequest {
+ phoneNumber: string;
+ otpCode: string;
+}
+
+export interface ConfirmOtpResponse extends ApiResponse {
+ confirm: boolean;
+}
+
+// ResetPassword
+
+export interface ResetPasswordRequest {
+ email?: string;
+ phoneNumber?: string;
+ newPassword: string;
+ confirmNewPassword: string;
+}
+
+export interface ResetPasswordResponse extends ApiResponse {
+ passwordChanged: boolean;
+}
+
+// SendForgetPassCode
+
+export interface SendForgetPassCodeRequest {
+ email?: string;
+ phoneNumber?: string;
+}
+
+// ConfirmForgetPassCode
+
+export interface ConfirmForgetPassCodeRequest {
+ email?: string;
+ phoneNumber?: string;
+ code: string;
+}
+
+// LoginOrSignUpWithGoogle
+
+export interface GoogleCodeClientResponse {
+ id_token: string;
+}
+
+export interface LoginOrSignUpWithGoogleRequest {
+ idToken: string;
+ returnUrl: string;
+}
+
+export interface LoginOrSignUpWithGoogleResponse extends ApiResponse {
+ userId: GUID;
+ registeredWithOutPhoneNumber: boolean;
+ completedUserInformation: boolean;
+ returnUrl: string;
+}
+
+// CompleteUserInformation
+
+export interface CompleteUserInformationRequest {
+ firstName?: string;
+ lastName?: string;
+ gender?: Gender;
+ nationalCode?: string;
+ savePassword?: boolean;
+ password?: string;
+ saveEmail?: boolean;
+ email?: string;
+ birthDate?: string;
+ countryCode?: string;
+ userId?: GUID;
+}
+
+export enum Gender {
+ Male = 1,
+ Female = 2,
+}
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..3a11320
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,12 @@
+import React from 'react';
+
+declare global {
+ namespace JSX {
+ interface Element extends React.ReactElement {}
+ }
+
+ interface Window {
+ google: typeof google;
+ }
+ const google: any;
+}
diff --git a/src/hooks/useApi.ts b/src/hooks/useApi.ts
index f25aa37..47ce02e 100644
--- a/src/hooks/useApi.ts
+++ b/src/hooks/useApi.ts
@@ -1,3 +1,4 @@
+
import { useState, useEffect, useCallback } from 'react';
import { type ApiResponse } from '@/types/apiResponse';
diff --git a/src/lib/apiClient.ts b/src/lib/apiClient.ts
index cbb855c..557ffe5 100644
--- a/src/lib/apiClient.ts
+++ b/src/lib/apiClient.ts
@@ -12,8 +12,8 @@ const apiClient = axios.create({
// Set default headers
headers: {
+ 'Content-Type': 'application/json',
Accept: 'application/json',
- Authorization: `Bearer ${localStorage.getItem('authToken')}`,
},
});
diff --git a/src/providers/CustomThemeProvider.tsx b/src/providers/CustomThemeProvider.tsx
index 6887573..f7faed6 100644
--- a/src/providers/CustomThemeProvider.tsx
+++ b/src/providers/CustomThemeProvider.tsx
@@ -37,13 +37,28 @@ export const CustomThemeProvider: React.FC<{ children: React.ReactNode }> = ({
cssVariables: {
colorSchemeSelector: 'class',
},
+ components: {
+ MuiTextField: {
+ defaultProps: {
+ variant: 'outlined',
+ fullWidth: true,
+ },
+ },
+ MuiButton: {
+ defaultProps: {
+ size: 'large',
+ fullWidth: true,
+ variant: 'contained',
+ },
+ },
+ },
spacing: 8,
typography: typography,
});
}, [i18n]);
return (
-
+
{children}
);
diff --git a/src/providers/RtlProvider.tsx b/src/providers/RtlProvider.tsx
index 7bbf52a..6550bb6 100644
--- a/src/providers/RtlProvider.tsx
+++ b/src/providers/RtlProvider.tsx
@@ -1,25 +1,23 @@
-import React, { useState, useEffect } from 'react';
+import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import rtlPlugin from 'stylis-plugin-rtl';
+import { prefixer } from 'stylis';
// This provider configures Emotion's cache to support RTL.
export const RtlProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { i18n } = useTranslation();
- const [cache, setCache] = useState(createCache({ key: 'css' }));
- useEffect(() => {
- const newDir = i18n.dir(i18n.language);
-
- const newCache = createCache({
- key: 'css',
- stylisPlugins: newDir === 'rtl' ? [rtlPlugin] : [],
+ const cacheRtl = useMemo(() => {
+ const isRtl = i18n.dir(i18n.language) === 'rtl';
+ return createCache({
+ key: isRtl ? 'muirtl' : 'muiltr',
+ stylisPlugins: isRtl ? [prefixer, rtlPlugin] : [],
});
- setCache(newCache);
- }, [i18n, i18n.language]);
+ }, [i18n]);
- return {children};
+ return {children};
};
diff --git a/src/routes/config.tsx b/src/routes/config.tsx
new file mode 100644
index 0000000..61f2f8d
--- /dev/null
+++ b/src/routes/config.tsx
@@ -0,0 +1,119 @@
+import { Layout } from '@/components/Layout/Layout';
+import {
+ Calendar,
+ Devices,
+ LocationTick,
+ Mobile,
+ PasswordCheck,
+ Personalcard,
+ ProfileCircle,
+ Setting,
+ Shield,
+ Sms,
+ type Icon,
+} from 'iconsax-react';
+import { type ReactNode } from 'react';
+import { Navigate } from 'react-router-dom';
+
+export interface RouteConfig {
+ path: string;
+ element?: ReactNode;
+ navConfig?: {
+ title: string; // Translation key
+ icon?: Icon;
+ };
+ children?: RouteConfig[];
+}
+
+// can lazy load component if needed (ex. lazy(() => import('@/features/home/routes/HomePage'));)
+export const appRoutes: RouteConfig[] = [
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/setting',
+ element: ,
+ children: [
+ // TODO: add route component to each route
+ {
+ path: '/setting/profile',
+ navConfig: {
+ // Profile component
+ title: 'side.account',
+ icon: ProfileCircle,
+ },
+ children: [
+ {
+ path: '/setting/profile#info',
+ navConfig: {
+ title: 'side.personalInfo',
+ icon: Personalcard,
+ },
+ },
+ {
+ path: '/setting/profile#contact-info',
+ navConfig: {
+ title: 'side.contactInfo',
+ icon: Mobile,
+ },
+ },
+ {
+ path: '/setting/profile#email',
+ navConfig: {
+ title: 'side.email',
+ icon: Sms,
+ },
+ },
+ ],
+ },
+ {
+ path: '/setting/security',
+ // security component
+ navConfig: {
+ title: 'side.security',
+ icon: Shield,
+ },
+ children: [
+ {
+ path: '/setting/security#password',
+ navConfig: {
+ title: 'side.password',
+ icon: PasswordCheck,
+ },
+ },
+ {
+ path: '/setting/security#confirmed-ips',
+ navConfig: {
+ title: 'side.confirmedIps',
+ icon: LocationTick,
+ },
+ },
+ {
+ path: '/setting/security#recent-sessions',
+ navConfig: {
+ title: 'side.recentSessions',
+ icon: Devices,
+ },
+ },
+ ],
+ },
+ {
+ path: '/setting/active-sessions',
+ // active session component
+ navConfig: {
+ title: 'side.activeSessions',
+ icon: Calendar,
+ },
+ },
+ {
+ path: '/setting/preferences',
+ // setting component
+ navConfig: {
+ title: 'side.setting',
+ icon: Setting,
+ },
+ },
+ ],
+ },
+];
diff --git a/src/routes/index.tsx b/src/routes/index.tsx
new file mode 100644
index 0000000..541a2fb
--- /dev/null
+++ b/src/routes/index.tsx
@@ -0,0 +1,34 @@
+import { Suspense, type ReactNode } from 'react';
+import { createBrowserRouter, type RouteObject } from 'react-router-dom';
+import { appRoutes, type RouteConfig } from './config';
+
+/**
+ * A recursive function to map our custom route config to the format
+ * that react-router-dom expects, applying layouts and guards.
+ */
+function mapRoutes(routes: RouteConfig[]): RouteObject[] {
+ return routes.map((route) => {
+ // Start with the base element, wrapped in Suspense for lazy loading
+ let element: ReactNode = (
+ Loading...}>{route.element}
+ );
+
+ // Conditionally wrap the element in the specified layout
+ // if (route.layout) {
+ // element = {element};
+ // }
+
+ // Conditionally wrap the element in the authentication guard
+ // if (route.authRequired) {
+ // element = {element};
+ // }
+
+ return {
+ path: route.path,
+ element: element,
+ ...(route.children && { children: mapRoutes(route.children) }),
+ };
+ });
+}
+
+export const router = createBrowserRouter(mapRoutes(appRoutes));
diff --git a/src/theme/colors.ts b/src/theme/colors.ts
index c3446ea..4c68db7 100644
--- a/src/theme/colors.ts
+++ b/src/theme/colors.ts
@@ -49,9 +49,9 @@ export const PALETTE: Palette = {
},
error: {
light: {
- main: '#E53935',
- dark: '#C62828',
- light: '#EF5350',
+ main: '#d32f2f',
+ dark: '#c62828',
+ light: '#ef5350',
contrastText: '#FFFFFF',
},
dark: {
@@ -91,9 +91,9 @@ export const PALETTE: Palette = {
},
success: {
light: {
- main: '#43A047',
- dark: '#1B5E20',
- light: '#81C784',
+ main: '#2e7d32',
+ dark: '#1b5e20',
+ light: '#4caf50',
contrastText: '#FFFFFF',
},
// TODO
diff --git a/src/types/commonTypes.ts b/src/types/commonTypes.ts
new file mode 100644
index 0000000..b6dee6b
--- /dev/null
+++ b/src/types/commonTypes.ts
@@ -0,0 +1,3 @@
+export type GUID = `${string}-${string}-${string}-${string}-${string}`;
+
+export type CountryCode = `+${number}`;
diff --git a/src/types/fetchPromise.ts b/src/types/fetchPromise.ts
new file mode 100644
index 0000000..84483d5
--- /dev/null
+++ b/src/types/fetchPromise.ts
@@ -0,0 +1,5 @@
+export type FetchPromise = Promise>;
+
+export interface FetchResponse extends Response {
+ json(): Promise;
+}
diff --git a/src/utils/regexes/containsNumber.tsx b/src/utils/regexes/containsNumber.tsx
new file mode 100644
index 0000000..5ccefc8
--- /dev/null
+++ b/src/utils/regexes/containsNumber.tsx
@@ -0,0 +1 @@
+export const containsNumber = (value: string) => /\d/.test(value);
diff --git a/src/utils/regexes/containsSymbol.tsx b/src/utils/regexes/containsSymbol.tsx
new file mode 100644
index 0000000..c3ac510
--- /dev/null
+++ b/src/utils/regexes/containsSymbol.tsx
@@ -0,0 +1 @@
+export const containsSymbol = (value: string) => /[!@#$%&*\^]/.test(value);
diff --git a/src/utils/regexes/hasUpperAndLowerLetter.tsx b/src/utils/regexes/hasUpperAndLowerLetter.tsx
new file mode 100644
index 0000000..a29c262
--- /dev/null
+++ b/src/utils/regexes/hasUpperAndLowerLetter.tsx
@@ -0,0 +1,5 @@
+export const hasUpperAndLowerLetter = (value: string) => {
+ const hasUpper = /[A-Z]/.test(value);
+ const hasLower = /[a-z]/.test(value);
+ return hasUpper && hasLower;
+};
diff --git a/src/utils/regexes/isEmail.tsx b/src/utils/regexes/isEmail.tsx
new file mode 100644
index 0000000..b50768d
--- /dev/null
+++ b/src/utils/regexes/isEmail.tsx
@@ -0,0 +1,2 @@
+export const isEmail = (value: string) =>
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(value);
diff --git a/src/utils/regexes/isNumeric.ts b/src/utils/regexes/isNumeric.ts
new file mode 100644
index 0000000..10e385d
--- /dev/null
+++ b/src/utils/regexes/isNumeric.ts
@@ -0,0 +1 @@
+export const isNumeric = (value: string) => /^\d*$/.test(value);
diff --git a/src/utils/regexes/isValidPhoneNumber.tsx b/src/utils/regexes/isValidPhoneNumber.tsx
new file mode 100644
index 0000000..c3d54cb
--- /dev/null
+++ b/src/utils/regexes/isValidPhoneNumber.tsx
@@ -0,0 +1,7 @@
+import parsePhoneNumberFromString from 'libphonenumber-js';
+
+export const isPhoneNumber = (code: string, phone: string) => {
+ const phoneNumber = parsePhoneNumberFromString(code + phone);
+
+ return phoneNumber && phoneNumber.isValid();
+};
diff --git a/src/utils/regexes/least8Chars.tsx b/src/utils/regexes/least8Chars.tsx
new file mode 100644
index 0000000..c4c4c8a
--- /dev/null
+++ b/src/utils/regexes/least8Chars.tsx
@@ -0,0 +1 @@
+export const least8Chars = (value: string) => value.length >= 8;
diff --git a/vite.config.d.ts b/vite.config.d.ts
new file mode 100644
index 0000000..2c646ae
--- /dev/null
+++ b/vite.config.d.ts
@@ -0,0 +1,2 @@
+declare const _default: import('vite').UserConfig;
+export default _default;
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..e7bba8d
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+import path from 'path';
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+});