Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

country code selection dropdown for phone number added #362

Merged
merged 19 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading