Skip to content

Commit

Permalink
country code selection dropdown for phone number added (#362)
Browse files Browse the repository at this point in the history
* 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 <adhiraj@zelthy.com>
Co-authored-by: Harsh Shah <shahharsh176@gmail.com>
  • Loading branch information
3 people authored Sep 25, 2024
1 parent 121fc45 commit 50af3b9
Show file tree
Hide file tree
Showing 10 changed files with 1,444 additions and 38 deletions.
7 changes: 7 additions & 0 deletions backend/src/zango/api/platform/tenancy/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
26 changes: 24 additions & 2 deletions backend/src/zango/api/platform/tenancy/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"] = [
Expand Down Expand Up @@ -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()
Expand All @@ -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"],
Expand Down Expand Up @@ -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
Expand All @@ -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"]
Expand Down
62 changes: 62 additions & 0 deletions backend/src/zango/core/utils.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
10 changes: 4 additions & 6 deletions backend/src/zango/middleware/tenant.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
70 changes: 70 additions & 0 deletions frontend/src/components/Form/CountryCodeSelector.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Menu as="div" className="relative inline-block text-left">
<Menu.Button
className="inline-flex w-full items-center justify-center gap-[2px] rounded-[4px] rounded-r-[0] bg-white px-3 py-2 text-sm leading-8 text-gray-900"
onClick={handleMenuClick}
>
{countryCode?.dial_code}
<DropdownIcon />
</Menu.Button>

<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
ref={targetElementRef}
>
<Menu.Items className="absolute right-[-198px] z-10 mt-2 h-[40vh] w-64 origin-top-right overflow-auto rounded-[4px] border border-[#D4D4D4] bg-white">
<div className="py-1">
{countryCodeList.map((item) => {
return (
<Menu.Item key={item.name}>
{({ active }) => (
<div
onClick={() => {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})
</div>
)}
</Menu.Item>
);
})}
</div>
</Menu.Items>
</Transition>
</Menu>
);
}
2 changes: 1 addition & 1 deletion frontend/src/metadata.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "buildMajor": 0, "buildMinor": 3, "buildPatch": 0, "buildTag": "" }
{"buildMajor":0,"buildMinor":3,"buildPatch":0,"buildTag":""}
1 change: 1 addition & 0 deletions frontend/src/mocks/appUsersManagementHandlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export const appUsersManagementHandlers = [
previous: null,
records: searchValue ? [] : slicedData,
},
pn_country_code:'+213',
dropdown_options: {
roles: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand All @@ -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'),
Expand All @@ -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 () => {
Expand All @@ -74,10 +86,12 @@ const AddNewUserForm = ({ closeModal }) => {
payload: dynamicFormData,
});

if (success && response) {
if (success) {
closeModal();
dispatch(toggleRerenderPage());
}
else{
}
};

makeApiCall();
Expand Down Expand Up @@ -123,8 +137,10 @@ const AddNewUserForm = ({ closeModal }) => {
>
Mobile
</label>
<div className="flex gap-[12px] rounded-[6px] border border-[#DDE2E5] px-[12px] py-[14px]">
<span className="font-lato text-[#6C747D]">+91</span>
<div className="flex gap-[12px] rounded-[6px] border border-[#DDE2E5] px-[12px]">
<span className="font-lato text-[#6C747D]">
<CountryCodeSelector countryCode={countryCode} setCountryCode={setCountryCode} />
</span>
<input
id="mobile"
name="mobile"
Expand Down
Loading

0 comments on commit 50af3b9

Please sign in to comment.