From 50af3b983f384437a525fb786f4482346a40806d Mon Sep 17 00:00:00 2001 From: deepakdinesh1123 <52698821+deepakdinesh1123@users.noreply.github.com> Date: Wed, 25 Sep 2024 19:01:50 +0530 Subject: [PATCH] country code selection dropdown for phone number added (#362) * phone_number validation added * feat:country code * removed comments * region parameter and docstring added * fix lint errors * revert changes * revert changes * pn_country_code added and middleware changed * country code added to serializer * build deleted * country code for edit and add * fix: default country code * sync app_panel build * bug fixes * bug fixes * fix: invalid mobile number message in response * fix * fix * revert build changes --------- Co-authored-by: adhiraj23zelthy Co-authored-by: Harsh Shah --- .../api/platform/tenancy/v1/serializers.py | 7 + .../zango/api/platform/tenancy/v1/views.py | 26 +- backend/src/zango/core/utils.py | 62 + backend/src/zango/middleware/tenant.py | 10 +- .../components/Form/CountryCodeSelector.jsx | 70 + frontend/src/metadata.json | 2 +- .../src/mocks/appUsersManagementHandlers.js | 1 + .../Modals/AddNewUserModal/AddNewUserForm.jsx | 36 +- .../EditUserDetailsForm.jsx | 56 +- frontend/src/utils/countryCodes.js | 1212 +++++++++++++++++ 10 files changed, 1444 insertions(+), 38 deletions(-) create mode 100644 frontend/src/components/Form/CountryCodeSelector.jsx create mode 100644 frontend/src/utils/countryCodes.js diff --git a/backend/src/zango/api/platform/tenancy/v1/serializers.py b/backend/src/zango/api/platform/tenancy/v1/serializers.py index 4fc4d50bb..534f1ef22 100644 --- a/backend/src/zango/api/platform/tenancy/v1/serializers.py +++ b/backend/src/zango/api/platform/tenancy/v1/serializers.py @@ -92,6 +92,7 @@ def update(self, instance, validated_data): class AppUserModelSerializerModel(serializers.ModelSerializer): roles = UserRoleSerializerModel(many=True) + pn_country_code = serializers.SerializerMethodField() class Meta: model = AppUserModel @@ -104,8 +105,14 @@ class Meta: "is_active", "last_login", "created_at", + "pn_country_code", ] + def get_pn_country_code(self, obj): + if obj.mobile: + return f"+{obj.mobile.country_code}" + return None + class ThemeModelSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/src/zango/api/platform/tenancy/v1/views.py b/backend/src/zango/api/platform/tenancy/v1/views.py index 88d9bd1ef..6975ec606 100644 --- a/backend/src/zango/api/platform/tenancy/v1/views.py +++ b/backend/src/zango/api/platform/tenancy/v1/views.py @@ -17,7 +17,11 @@ from zango.core.api.utils import ZangoAPIPagination from zango.core.common_utils import set_app_schema_path from zango.core.permissions import IsPlatformUserAllowedApp -from zango.core.utils import get_search_columns +from zango.core.utils import ( + get_country_code_for_tenant, + get_search_columns, + validate_phone, +) from .serializers import ( AppUserModelSerializerModel, @@ -340,6 +344,10 @@ class UserViewAPIV1(ZangoGenericPlatformAPIView, ZangoAPIPagination): pagination_class = ZangoAPIPagination permission_classes = (IsPlatformUserAllowedApp,) + def get_app_tenant(self): + tenant_obj = TenantModel.objects.get(uuid=self.kwargs["app_uuid"]) + return tenant_obj + def get_dropdown_options(self): options = {} options["roles"] = [ @@ -386,10 +394,12 @@ def get(self, request, *args, **kwargs): app_users = self.paginate_queryset(app_users, request, view=self) serializer = AppUserModelSerializerModel(app_users, many=True) app_users_data = self.get_paginated_response_data(serializer.data) + app_tenant = self.get_app_tenant() success = True response = { "users": app_users_data, "message": "Users fetched successfully", + "pn_country_code": get_country_code_for_tenant(app_tenant), } if include_dropdown_options: response["dropdown_options"] = self.get_dropdown_options() @@ -406,6 +416,10 @@ def post(self, request, *args, **kwargs): data = request.data try: role_ids = data.getlist("roles") + if data.get("mobile"): + if not validate_phone(data["mobile"]): + result = {"message": "Invalid mobile number"} + return get_api_response(False, result, 400) creation_result = AppUserModel.create_user( name=data["name"], email=data["email"], @@ -437,7 +451,10 @@ def get(self, request, *args, **kwargs): obj = self.get_obj(**kwargs) serializer = AppUserModelSerializerModel(obj) success = True - response = {"user": serializer.data} + response = { + "user": serializer.data, + "pn_country_code": f"+{obj.mobile.country_code}", + } status = 200 except Exception as e: success = False @@ -447,7 +464,12 @@ def get(self, request, *args, **kwargs): return get_api_response(success, response, status) def put(self, request, *args, **kwargs): + data = request.data try: + if data.get("mobile"): + if not validate_phone(data["mobile"]): + result = {"message": "Invalid mobile number"} + return get_api_response(False, result, 400) obj = self.get_obj(**kwargs) update_result = AppUserModel.update_user(obj, request.data) success = update_result["success"] diff --git a/backend/src/zango/core/utils.py b/backend/src/zango/core/utils.py index b6108f274..b4e051c7a 100644 --- a/backend/src/zango/core/utils.py +++ b/backend/src/zango/core/utils.py @@ -1,7 +1,10 @@ import json +import phonenumbers import pytz +from phonenumbers.phonenumberutil import country_code_for_region + from django.conf import settings from django.db import connection from django.shortcuts import render @@ -120,3 +123,62 @@ def generate_lockout_response(request, credentials): {"logout_url": "/auth/logout", "cooloff_time": cooloff_time}, status=403, ) + + +def validate_phone(phone_number, region=None): + """ + Validates a phone number by parsing it and checking if it is a valid phone number for the given region. + + Args: + phone_number (str): The phone number to be validated. + region (str, optional): The region in which the phone number is valid. Defaults to None. + + Returns: + bool: True if the phone number is valid, False otherwise. + + Raises: + None + + """ + try: + region = region or settings.PHONENUMBER_DEFAULT_REGION + phone_number = phonenumbers.parse(phone_number, region=region) + if phonenumbers.is_valid_number(phone_number): + return True + except Exception: + return False + + +def get_region_from_timezone(tzname): + timezone_country = {} + for countrycode in pytz.country_timezones: + timezones = pytz.country_timezones[countrycode] + for tz in timezones: + timezone_country[tz] = countrycode + return timezone_country[tzname] + + +def get_country_code_for_tenant(tenant, with_plus_sign=True): + """ + Returns the country code for the given tenant. + + The region is first determined from the tenant's timezone. If no timezone is set, + the default region from `settings.PHONENUMBER_DEFAULT_REGION` is used. + + Args: + tenant: A TenantModel instance. + with_plus_sign (bool): Whether to prepend a "+" to the country code. Default is True. + + Returns: + str: The country code with or without "+" based on the region (e.g., "+1" for "US", "+91" for "IN"). + """ + default_region = settings.PHONENUMBER_DEFAULT_REGION + + if tenant.timezone: + try: + default_region = get_region_from_timezone(tenant.timezone) + except Exception: + pass + + country_code = country_code_for_region(default_region) + return f"+{country_code}" if with_plus_sign else country_code diff --git a/backend/src/zango/middleware/tenant.py b/backend/src/zango/middleware/tenant.py index 10574d861..09cab626a 100644 --- a/backend/src/zango/middleware/tenant.py +++ b/backend/src/zango/middleware/tenant.py @@ -18,6 +18,8 @@ from django.utils import timezone from django.utils.deprecation import MiddlewareMixin +from zango.core.utils import get_region_from_timezone + class ZangoTenantMainMiddleware(TenantMainMiddleware): TENANT_NOT_FOUND_EXCEPTION = Http404 @@ -146,12 +148,8 @@ def __call__(self, request): try: tzname = request.tenant.timezone timezone.activate(pytz.timezone(tzname)) - timezone_country = {} - for countrycode in pytz.country_timezones: - timezones = pytz.country_timezones[countrycode] - for tz in timezones: - timezone_country[tz] = countrycode - settings.PHONENUMBER_DEFAULT_REGION = timezone_country[tzname] + region = get_region_from_timezone(tzname) + settings.PHONENUMBER_DEFAULT_REGION = region except Exception: timezone.deactivate() return self.get_response(request) diff --git a/frontend/src/components/Form/CountryCodeSelector.jsx b/frontend/src/components/Form/CountryCodeSelector.jsx new file mode 100644 index 000000000..a1310c874 --- /dev/null +++ b/frontend/src/components/Form/CountryCodeSelector.jsx @@ -0,0 +1,70 @@ +import { Fragment, useState, useRef } from 'react'; +import { Menu, Transition } from '@headlessui/react'; +import { countryCodeList } from '../../utils/countryCodes'; +import { ReactComponent as DropdownIcon } from '../../assets/images/svg/down-arrow-icon.svg'; + +function classNames(...classes) { + return classes.filter(Boolean).join(' '); +} + +export default function CountryCodeSelector({ countryCode, setCountryCode }) { + + const targetElementRef = useRef(null); + + const handleMenuClick = () => { + setTimeout(() => { + if (targetElementRef.current) { + targetElementRef.current.scrollIntoView({ + behavior: 'smooth', // You can use 'auto' or 'smooth' for scrolling behavior + block: 'start', // You can use 'start', 'center', or 'end' for vertical alignment + inline: 'nearest', // You can use 'start', 'center', or 'end' for horizontal alignment + }); + } + }, 10); + }; + + return ( + + + {countryCode?.dial_code} + + + + + +
+ {countryCodeList.map((item) => { + return ( + + {({ active }) => ( +
{setCountryCode(item)}} + className={classNames( + active ? 'bg-gray-100 text-gray-900' : 'text-gray-700', + 'block cursor-pointer px-4 py-2 text-sm hover:bg-[#F0F3F4]' + )} + > + {item.name} ({item.dial_code}) +
+ )} +
+ ); + })} +
+
+
+
+ ); +} diff --git a/frontend/src/metadata.json b/frontend/src/metadata.json index 0ec5592b7..5014ed6da 100644 --- a/frontend/src/metadata.json +++ b/frontend/src/metadata.json @@ -1 +1 @@ -{ "buildMajor": 0, "buildMinor": 3, "buildPatch": 0, "buildTag": "" } \ No newline at end of file +{"buildMajor":0,"buildMinor":3,"buildPatch":0,"buildTag":""} diff --git a/frontend/src/mocks/appUsersManagementHandlers.js b/frontend/src/mocks/appUsersManagementHandlers.js index 4707dc6a1..dd7dbeb31 100644 --- a/frontend/src/mocks/appUsersManagementHandlers.js +++ b/frontend/src/mocks/appUsersManagementHandlers.js @@ -169,6 +169,7 @@ export const appUsersManagementHandlers = [ previous: null, records: searchValue ? [] : slicedData, }, + pn_country_code:'+213', dropdown_options: { roles: [ { diff --git a/frontend/src/pages/appUserManagement/components/Modals/AddNewUserModal/AddNewUserForm.jsx b/frontend/src/pages/appUserManagement/components/Modals/AddNewUserModal/AddNewUserForm.jsx index 77a241f68..9134feff4 100644 --- a/frontend/src/pages/appUserManagement/components/Modals/AddNewUserModal/AddNewUserForm.jsx +++ b/frontend/src/pages/appUserManagement/components/Modals/AddNewUserModal/AddNewUserForm.jsx @@ -12,13 +12,28 @@ import { selectAppUserManagementData, toggleRerenderPage, } from '../../../slice'; +import CountryCodeSelector from '../../../../../components/Form/CountryCodeSelector'; +import { useState , useLayoutEffect} from 'react'; +import { countryCodeList } from '../../../../../utils/countryCodes'; +import toast from 'react-hot-toast'; +import Notifications from '../../../../../components/Notifications'; const AddNewUserForm = ({ closeModal }) => { + const [countryCode,setCountryCode] = useState({ + name: 'India', + dial_code: '+91', + code: 'IN', + }) let { appId } = useParams(); const dispatch = useDispatch(); const appUserManagementData = useSelector(selectAppUserManagementData); const triggerApi = useApi(); + let pn_country_code = appUserManagementData?.pn_country_code ?? '+91' + useLayoutEffect(()=>{ + let countryCodeObj = countryCodeList.find((c)=>c.dial_code===pn_country_code) + setCountryCode(countryCodeObj) + },[]) let initialValues = { name: '', email: '', @@ -42,12 +57,7 @@ const AddNewUserForm = ({ closeModal }) => { if (!email) return true; }, then: Yup.string() - .min(10, 'Must be 10 digits') - .max(10, 'Must be 10 digits') .required('Required'), - otherwise: Yup.string() - .min(10, 'Must be 10 digits') - .max(10, 'Must be 10 digits'), }), password: Yup.string().required('Required'), roles: Yup.array().min(1, 'Minimun one is required').required('Required'), @@ -62,8 +72,10 @@ const AddNewUserForm = ({ closeModal }) => { ); let onSubmit = (values) => { - let tempValues = values; - + let tempValues = values + if(values.mobile){ + tempValues = {...values,mobile:countryCode?.dial_code+values.mobile} + } let dynamicFormData = transformToFormData(tempValues); const makeApiCall = async () => { @@ -74,10 +86,12 @@ const AddNewUserForm = ({ closeModal }) => { payload: dynamicFormData, }); - if (success && response) { + if (success) { closeModal(); dispatch(toggleRerenderPage()); } + else{ + } }; makeApiCall(); @@ -123,8 +137,10 @@ const AddNewUserForm = ({ closeModal }) => { > Mobile -
- +91 +
+ + + { let { appId } = useParams(); const dispatch = useDispatch(); + const [countryCode,setCountryCode] = useState({ + name: 'India', + dial_code: '+91', + code: 'IN', + }) const appUserManagementData = useSelector(selectAppUserManagementData); - const appUserManagementFormData = useSelector( + let appUserManagementFormData = useSelector( selectAppUserManagementFormData ); const triggerApi = useApi(); + let pn_country_code = appUserManagementFormData?.pn_country_code ?? '+91' + useLayoutEffect(()=>{ + if(appUserManagementFormData?.mobile=='' || appUserManagementFormData?.mobile==null){ + pn_country_code = appUserManagementData?.pn_country_code ?? '+91' + } + let countryCodeObj = countryCodeList.find((c)=>c.dial_code===pn_country_code) + setCountryCode(countryCodeObj) + },[]) + + let initialValues = { name: appUserManagementFormData?.name ?? '', email: appUserManagementFormData?.email ?? '', - mobile: appUserManagementFormData?.mobile - ? appUserManagementFormData?.mobile.indexOf('+91') > -1 - ? appUserManagementFormData?.mobile?.slice(3) ?? '' - : appUserManagementFormData?.mobile - : '', + mobile: (appUserManagementFormData?.mobile=='' || appUserManagementFormData?.mobile==null)? '': pn_country_code.length!=null?appUserManagementFormData?.mobile.slice(pn_country_code.length):appUserManagementFormData?.mobile, roles: appUserManagementFormData?.roles?.map((eachApp) => eachApp.id) ?? [], }; @@ -49,13 +65,7 @@ const EditUserDetailsForm = ({ closeModal }) => { is: (email) => { if (!email) return true; }, - then: Yup.string() - .min(10, 'Must be 10 digits') - .max(10, 'Must be 10 digits') - .required('Required'), - otherwise: Yup.string() - .min(10, 'Must be 10 digits') - .max(10, 'Must be 10 digits'), + then: Yup.string().required('Required'), }), roles: Yup.array().min(1, 'Minimun one is required').required('Required'), }, @@ -63,8 +73,10 @@ const EditUserDetailsForm = ({ closeModal }) => { ); let onSubmit = (values) => { - let tempValues = values; - + let tempValues = values + if(values.mobile){ + tempValues = {...values,mobile:countryCode?.dial_code+values.mobile} + } let dynamicFormData = transformToFormData(tempValues); const makeApiCall = async () => { @@ -75,9 +87,10 @@ const EditUserDetailsForm = ({ closeModal }) => { payload: dynamicFormData, }); - if (success && response) { + if (success) { closeModal(); dispatch(toggleRerenderPage()); + }else{ } }; @@ -124,9 +137,14 @@ const EditUserDetailsForm = ({ closeModal }) => { > Mobile -
- +91 - + + + +