Browse Source

feat: implement phone number formatting, dynamic placeholders, and validation for QuestionPhone component with multi-language support

master
mortezaei 5 days ago
parent
commit
879f864575
  1. 101
      src/components/Componentes/question-phone.test.tsx
  2. 174
      src/components/Componentes/question-phone.tsx
  3. 40
      src/components/Componentes/question-snap-list.tsx
  4. 10
      src/components/Componentes/question-viewport-coordinator.ts
  5. 3
      src/translations/locales/ar.json
  6. 3
      src/translations/locales/az.json
  7. 3
      src/translations/locales/bn.json
  8. 3
      src/translations/locales/da.json
  9. 3
      src/translations/locales/de.json
  10. 3
      src/translations/locales/en.json
  11. 3
      src/translations/locales/es.json
  12. 3
      src/translations/locales/fa.json
  13. 3
      src/translations/locales/fr.json
  14. 3
      src/translations/locales/gu.json
  15. 3
      src/translations/locales/ha.json
  16. 3
      src/translations/locales/he.json
  17. 3
      src/translations/locales/hi.json
  18. 3
      src/translations/locales/id.json
  19. 3
      src/translations/locales/ks.json
  20. 3
      src/translations/locales/pt.json
  21. 3
      src/translations/locales/ru.json
  22. 3
      src/translations/locales/sw.json
  23. 3
      src/translations/locales/tg.json
  24. 3
      src/translations/locales/tr.json
  25. 3
      src/translations/locales/ul.json
  26. 3
      src/translations/locales/ur.json
  27. 3
      src/translations/locales/uz.json
  28. 3
      src/translations/locales/zh.json

101
src/components/Componentes/question-phone.test.tsx

@ -30,6 +30,8 @@ vi.mock("@/translations/provider", () => ({
"Select country": "Select country", "Select country": "Select country",
Close: "Close", Close: "Close",
Confirm: "Confirm", Confirm: "Confirm",
"Enter a valid phone number with country code.":
"Enter a valid phone number with country code.",
}, },
}), }),
})); }));
@ -207,7 +209,7 @@ describe("QuestionPhone IP country detection and shimmer", () => {
expect(container.querySelectorAll(".shimmer-bg").length).toBe(0); expect(container.querySelectorAll(".shimmer-bg").length).toBe(0);
expect(screen.getByText("+1")).toBeDefined(); expect(screen.getByText("+1")).toBeDefined();
expect(screen.getByDisplayValue("2025550143")).toBeDefined();
expect(screen.getByDisplayValue("202-555-0143")).toBeDefined();
expect(httpMocks.get).not.toHaveBeenCalled(); expect(httpMocks.get).not.toHaveBeenCalled();
}); });
@ -219,10 +221,10 @@ describe("QuestionPhone IP country detection and shimmer", () => {
httpMocks.get.mockReturnValue(regionPromise); httpMocks.get.mockReturnValue(regionPromise);
render(<QuestionPhone question={phoneQuestion1} />);
render(<QuestionPhone question={phoneQuestion1} countryCode="+44" />);
const input = screen.getByRole("textbox"); const input = screen.getByRole("textbox");
fireEvent.change(input, { target: { value: "123456" } });
fireEvent.change(input, { target: { value: "7400123456" } });
await act(async () => { await act(async () => {
resolveRegion({ resolveRegion({
@ -233,6 +235,97 @@ describe("QuestionPhone IP country detection and shimmer", () => {
}); });
}); });
expect(screen.getByDisplayValue("123456")).toBeDefined();
expect(screen.getByDisplayValue("7400 123456")).toBeDefined();
});
it("formats Iranian phone numbers as 3-3-4 Telegram style as digits are typed", async () => {
httpMocks.get.mockResolvedValue({
data: { country: "Iran", country_code: "IR" },
});
render(<QuestionPhone question={phoneQuestion1} countryCode="+98" />);
const input = screen.getByRole("textbox") as HTMLInputElement;
// Type 3 digits
fireEvent.change(input, { target: { value: "912" } });
expect(input.value).toBe("912");
// Type 6 digits
fireEvent.change(input, { target: { value: "912345" } });
expect(input.value).toBe("912 345");
// Type 10 digits
fireEvent.change(input, { target: { value: "9123456789" } });
expect(input.value).toBe("912 345 6789");
});
it("shows dynamic placeholder based on resolved country code", async () => {
let resolveRegion!: (value: unknown) => void;
const regionPromise = new Promise((resolve) => {
resolveRegion = resolve;
});
httpMocks.get.mockReturnValue(regionPromise);
render(<QuestionPhone question={phoneQuestion1} countryCode="+44" />);
const input = screen.getByRole("textbox") as HTMLInputElement;
expect(input.placeholder).toBe("7400 123456");
await act(async () => {
resolveRegion({
data: {
country: "Iran",
country_code: "IR",
},
});
});
await waitFor(() => {
expect(input.placeholder).toBe("912 345 6789");
});
});
it("does not show validation error on active typing, only shows error on blur if incomplete", async () => {
httpMocks.get.mockResolvedValue({
data: { country: "Iran", country_code: "IR" },
});
const { container } = render(
<QuestionPhone question={phoneQuestion1} countryCode="+98" />,
);
const input = screen.getByRole("textbox") as HTMLInputElement;
// Focus and start typing incomplete number
fireEvent.focus(input);
fireEvent.change(input, { target: { value: "912" } });
// No error while focused and typing
expect(
screen.queryByText("Enter a valid phone number with country code."),
).toBeNull();
expect(container.querySelector(".border-\\[\\#F2465F\\]")).toBeNull();
// Blur input without completing
fireEvent.blur(input);
// Error appears after blur
expect(
screen.getByText("Enter a valid phone number with country code."),
).toBeDefined();
expect(container.querySelector(".border-\\[\\#F2465F\\]")).not.toBeNull();
// Focus back and complete valid number
fireEvent.focus(input);
fireEvent.change(input, { target: { value: "9123456789" } });
fireEvent.blur(input);
// Error is gone
expect(
screen.queryByText("Enter a valid phone number with country code."),
).toBeNull();
expect(container.querySelector(".border-\\[\\#F2465F\\]")).toBeNull();
}); });
}); });

174
src/components/Componentes/question-phone.tsx

@ -1,6 +1,10 @@
"use client"; "use client";
import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber";
import {
AsYouTypeFormatter,
PhoneNumberFormat,
PhoneNumberUtil,
} from "google-libphonenumber";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types"; import type { MarriagePhoneFieldValue } from "@/hooks/marriage/types";
@ -264,10 +268,11 @@ function readPhoneValue(value: unknown, fallbackCode: string): PhoneValueParts {
function getMaxLengthForCountry(codeValue: string): number { function getMaxLengthForCountry(codeValue: string): number {
try { try {
const cleanCode = codeValue.replace(/[^\d]/g, ""); const cleanCode = codeValue.replace(/[^\d]/g, "");
if (!cleanCode) return 15;
if (!cleanCode) return 18;
if (cleanCode === "98") return 12; // 3 + 1 + 3 + 1 + 4 = 12 chars
const countryCode = Number(cleanCode); const countryCode = Number(cleanCode);
const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode); const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode);
if (!regionCode || regionCode === "ZZ") return 15;
if (!regionCode || regionCode === "ZZ") return 18;
const exampleMobile = phoneUtil.getExampleNumberForType( const exampleMobile = phoneUtil.getExampleNumberForType(
regionCode, regionCode,
@ -283,9 +288,93 @@ function getMaxLengthForCountry(codeValue: string): number {
: 0; : 0;
const baseLen = Math.max(lenMobile, lenGeneral, 8); const baseLen = Math.max(lenMobile, lenGeneral, 8);
return baseLen + 1;
return baseLen + 6;
} catch { } catch {
return 15;
return 18;
}
}
function getCountryPlaceholder(
codeValue: string,
defaultPlaceholder?: string,
): string {
try {
const cleanCode = codeValue.replace(/[^\d]/g, "");
if (!cleanCode) {
return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789";
}
if (cleanCode === "98") {
return "912 345 6789";
}
const countryCode = Number(cleanCode);
const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode);
if (!regionCode || regionCode === "ZZ") {
return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789";
}
const example =
phoneUtil.getExampleNumberForType(
regionCode,
1, // MOBILE
) || phoneUtil.getExampleNumber(regionCode);
if (!example) {
return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789";
}
const intl = phoneUtil.format(example, PhoneNumberFormat.INTERNATIONAL);
const prefix = `+${cleanCode} `;
if (intl.startsWith(prefix)) {
return intl.slice(prefix.length);
}
return String(example.getNationalNumber());
} catch {
return defaultPlaceholder?.replace(/^\+\d+\s*/, "") || "912 345 6789";
}
}
function formatPhoneNumberAsYouType(
rawInput: string,
codeValue: string,
): string {
const cleanCode = codeValue.replace(/[^\d]/g, "");
const rawDigits = rawInput.replace(/\D/g, "");
if (!rawDigits) return "";
// Special telegram-style handling for Iran (+98): 3 - 3 - 4 (e.g. 912 345 6789)
if (cleanCode === "98") {
const digits = rawDigits.startsWith("0") ? rawDigits.slice(1) : rawDigits;
const max10 = digits.slice(0, 10);
if (max10.length <= 3) return max10;
if (max10.length <= 6) return `${max10.slice(0, 3)} ${max10.slice(3)}`;
return `${max10.slice(0, 3)} ${max10.slice(3, 6)} ${max10.slice(6)}`;
}
const countryCode = Number(cleanCode);
const regionCode = phoneUtil.getRegionCodeForCountryCode(countryCode);
if (!regionCode || regionCode === "ZZ") {
return rawDigits;
}
try {
const formatter = new AsYouTypeFormatter(regionCode);
let formatted = "";
const fullNumber = `+${cleanCode}${rawDigits}`;
for (const char of fullNumber) {
formatted = formatter.inputDigit(char);
}
const codePrefix = `+${cleanCode} `;
if (formatted.startsWith(codePrefix)) {
return formatted.slice(codePrefix.length);
}
if (formatted.startsWith(`+${cleanCode}`)) {
return formatted.slice(`+${cleanCode}`.length).trim();
}
return rawDigits;
} catch {
return rawDigits;
} }
} }
@ -433,13 +522,23 @@ export function QuestionPhone({
}, [value, defaultCodeValue]); }, [value, defaultCodeValue]);
const initialPhone = useMemo(() => { const initialPhone = useMemo(() => {
return readPhoneValue(value, defaultCodeValue).phoneValue;
}, [value, defaultCodeValue]);
const rawPhone = readPhoneValue(value, defaultCodeValue).phoneValue;
return formatPhoneNumberAsYouType(rawPhone, initialCode);
}, [value, defaultCodeValue, initialCode]);
const [codeValue, setCodeValue] = useState(initialCode); const [codeValue, setCodeValue] = useState(initialCode);
const [phoneValue, setPhoneValue] = useState(initialPhone); const [phoneValue, setPhoneValue] = useState(initialPhone);
const [isFocused, setIsFocused] = useState(false);
const [isTouched, setIsTouched] = useState(false);
const lastCommittedValueRef = useRef(value); const lastCommittedValueRef = useRef(value);
const dynamicPlaceholder = useMemo(() => {
return getCountryPlaceholder(
codeValue || defaultCodeValue,
question.extras?.placeHolder,
);
}, [codeValue, defaultCodeValue, question.extras?.placeHolder]);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false); const [isClosing, setIsClosing] = useState(false);
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
@ -471,6 +570,11 @@ export function QuestionPhone({
const onGeoCodeResolved = (resolvedCode: string) => { const onGeoCodeResolved = (resolvedCode: string) => {
if (!isMounted || userInteractedRef.current) return; if (!isMounted || userInteractedRef.current) return;
setCodeValue(resolvedCode); setCodeValue(resolvedCode);
if (phoneValue) {
setPhoneValue((prev) =>
formatPhoneNumberAsYouType(prev, resolvedCode),
);
}
setIsResolvingCountry(false); setIsResolvingCountry(false);
}; };
@ -480,7 +584,13 @@ export function QuestionPhone({
.then((resolvedCode) => { .then((resolvedCode) => {
if (!isMounted) return; if (!isMounted) return;
if (!userInteractedRef.current) { if (!userInteractedRef.current) {
setCodeValue(resolvedCode || defaultCodeValue);
const finalCode = resolvedCode || defaultCodeValue;
setCodeValue(finalCode);
if (phoneValue) {
setPhoneValue((prev) =>
formatPhoneNumberAsYouType(prev, finalCode),
);
}
} }
setIsResolvingCountry(false); setIsResolvingCountry(false);
}) })
@ -496,10 +606,12 @@ export function QuestionPhone({
isMounted = false; isMounted = false;
geoListeners.delete(onGeoCodeResolved); geoListeners.delete(onGeoCodeResolved);
}; };
}, [hasExplicitValue, isResolvingCountry, defaultCodeValue]);
}, [hasExplicitValue, isResolvingCountry, defaultCodeValue, phoneValue]);
const normalizedPhoneState = getNormalizedPhoneValue(codeValue, phoneValue); const normalizedPhoneState = getNormalizedPhoneValue(codeValue, phoneValue);
const showInvalidState = const showInvalidState =
isTouched &&
!isFocused &&
codeValue.trim().length > 0 && codeValue.trim().length > 0 &&
phoneValue.trim().length > 0 && phoneValue.trim().length > 0 &&
!normalizedPhoneState.isValid; !normalizedPhoneState.isValid;
@ -631,13 +743,19 @@ export function QuestionPhone({
if (explicit) { if (explicit) {
setIsResolvingCountry(false); setIsResolvingCountry(false);
setCodeValue(nextValue.codeValue); setCodeValue(nextValue.codeValue);
const maxLen = getMaxLengthForCountry(nextValue.codeValue);
setPhoneValue(nextValue.phoneValue.slice(0, maxLen));
const formatted = formatPhoneNumberAsYouType(
nextValue.phoneValue,
nextValue.codeValue,
);
setPhoneValue(formatted);
} else if (value !== null && value !== undefined) { } else if (value !== null && value !== undefined) {
const resolvedCode = cachedCode || defaultCodeValue; const resolvedCode = cachedCode || defaultCodeValue;
setCodeValue(resolvedCode); setCodeValue(resolvedCode);
const maxLen = getMaxLengthForCountry(resolvedCode);
setPhoneValue(nextValue.phoneValue.slice(0, maxLen));
const formatted = formatPhoneNumberAsYouType(
nextValue.phoneValue,
resolvedCode,
);
setPhoneValue(formatted);
} }
lastCommittedValueRef.current = value; lastCommittedValueRef.current = value;
@ -664,12 +782,11 @@ export function QuestionPhone({
userInteractedRef.current = true; userInteractedRef.current = true;
setIsResolvingCountry(false); setIsResolvingCountry(false);
setManuallySelectedGeoCode(selectedCode); setManuallySelectedGeoCode(selectedCode);
const maxLen = getMaxLengthForCountry(selectedCode);
const truncatedPhone = phoneValue.slice(0, maxLen);
const reformatted = formatPhoneNumberAsYouType(phoneValue, selectedCode);
setCodeValue(selectedCode); setCodeValue(selectedCode);
setPhoneValue(truncatedPhone);
updateStoredValue(selectedCode, truncatedPhone);
setPhoneValue(reformatted);
updateStoredValue(selectedCode, reformatted);
closeSheet(); closeSheet();
}; };
@ -768,19 +885,27 @@ export function QuestionPhone({
data-bwignore="true" data-bwignore="true"
data-form-type="other" data-form-type="other"
disabled={disabled} disabled={disabled}
placeholder={question.extras.placeHolder?.replace(/^\+\d+\s*/, "")}
placeholder={dynamicPlaceholder}
value={phoneValue} value={phoneValue}
maxLength={getMaxLengthForCountry(codeValue)} maxLength={getMaxLengthForCountry(codeValue)}
onFocus={() => {
setIsFocused(true);
}}
onBlur={() => {
setIsFocused(false);
setIsTouched(true);
}}
onChange={(event) => { onChange={(event) => {
userInteractedRef.current = true; userInteractedRef.current = true;
setIsResolvingCountry(false); setIsResolvingCountry(false);
setManuallySelectedGeoCode(codeValue); setManuallySelectedGeoCode(codeValue);
const nextPhoneValue = sanitizePhoneNumber(event.target.value);
const maxLen = getMaxLengthForCountry(codeValue);
const truncatedPhone = nextPhoneValue.slice(0, maxLen);
const formatted = formatPhoneNumberAsYouType(
event.target.value,
codeValue,
);
setPhoneValue(truncatedPhone);
updateStoredValue(codeValue, truncatedPhone);
setPhoneValue(formatted);
updateStoredValue(codeValue, formatted);
}} }}
dir="ltr" dir="ltr"
className="h-full w-full border-0 bg-transparent p-0 text-left text-[15px] font-medium leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#98A2B3]" className="h-full w-full border-0 bg-transparent p-0 text-left text-[15px] font-medium leading-none text-[#181818] tabular-nums outline-none placeholder:text-[#98A2B3]"
@ -789,7 +914,8 @@ export function QuestionPhone({
</div> </div>
{showInvalidState ? ( {showInvalidState ? (
<span className="block group-10 font-semibold text-[#F2465F]"> <span className="block group-10 font-semibold text-[#F2465F]">
Enter a valid phone number with country code.
{t["Enter a valid phone number with country code."] ||
"Enter a valid phone number with country code."}
</span> </span>
) : null} ) : null}

40
src/components/Componentes/question-snap-list.tsx

@ -14,20 +14,20 @@ import {
useQuestionViewportCoordinator, useQuestionViewportCoordinator,
} from "./question-viewport-coordinator"; } from "./question-viewport-coordinator";
const WHEEL_GESTURE_IDLE_MS = 320;
const BACKGROUND_DRAG_SLOP = 10;
const OPTION_CARD_DRAG_SLOP = 16;
const FAST_FLICK_MIN_DISTANCE = 36;
const DRAG_COMMIT_RATIO = 0.3;
const DRAG_FLICK_VELOCITY = 0.4;
const SNAP_ANIMATION_MS = 340;
const SNAP_EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
const RUBBER_BAND_RESISTANCE = 0.4;
const WHEEL_GESTURE_IDLE_MS = 280;
const BACKGROUND_DRAG_SLOP = 8;
const OPTION_CARD_DRAG_SLOP = 14;
const TEXT_INPUT_DRAG_SLOP = 16;
const FAST_FLICK_MIN_DISTANCE = 24;
const DRAG_COMMIT_RATIO = 0.15;
const DRAG_FLICK_VELOCITY = 0.22;
const SNAP_ANIMATION_MS = 240;
const SNAP_EASE = "cubic-bezier(0.16, 1, 0.3, 1)";
const RUBBER_BAND_RESISTANCE = 0.35;
/** /**
* Text inputs and direct embedded controls that MUST be completely isolated
* from the snap/drag gesture recognizer so first-tap focus, virtual keyboard,
* and caret manipulation work 100% reliably.
* Text inputs and direct embedded controls that MUST allow clean 1st-tap focus
* while still permitting intentional vertical swipe when dragged beyond slop.
*/ */
const TEXT_INPUT_SELECTOR = [ const TEXT_INPUT_SELECTOR = [
'input:not([type="radio"]):not([type="checkbox"])', 'input:not([type="radio"]):not([type="checkbox"])',
@ -58,6 +58,7 @@ const OPTION_CARD_SELECTOR = [
type SnapDragState = { type SnapDragState = {
pointerDown: boolean; pointerDown: boolean;
hardIgnored: boolean; hardIgnored: boolean;
isTextInput: boolean;
isOptionCard: boolean; isOptionCard: boolean;
engaged: boolean; engaged: boolean;
didDrag: boolean; didDrag: boolean;
@ -113,6 +114,7 @@ export function QuestionSnapList({
const dragRef = useRef<SnapDragState>({ const dragRef = useRef<SnapDragState>({
pointerDown: false, pointerDown: false,
hardIgnored: false, hardIgnored: false,
isTextInput: false,
isOptionCard: false, isOptionCard: false,
engaged: false, engaged: false,
didDrag: false, didDrag: false,
@ -397,14 +399,19 @@ export function QuestionSnapList({
const movedPastFlick = Math.abs(drag.offset) >= FAST_FLICK_MIN_DISTANCE; const movedPastFlick = Math.abs(drag.offset) >= FAST_FLICK_MIN_DISTANCE;
let direction: 0 | 1 | -1 = 0; let direction: 0 | 1 | -1 = 0;
let reason = "none";
if (!cancelled && (draggedFar || flicked || movedPastFlick)) { if (!cancelled && (draggedFar || flicked || movedPastFlick)) {
if ((drag.offset > 0 || drag.velocity > 0) && canNext) { if ((drag.offset > 0 || drag.velocity > 0) && canNext) {
direction = 1; direction = 1;
reason = flicked ? "flick_next" : (draggedFar ? "drag_far_next" : "moved_past_flick_next");
} else if ((drag.offset < 0 || drag.velocity < 0) && canPrev) { } else if ((drag.offset < 0 || drag.velocity < 0) && canPrev) {
direction = -1; direction = -1;
reason = flicked ? "flick_prev" : (draggedFar ? "drag_far_prev" : "moved_past_flick_prev");
} }
} }
console.log(`[Snap] FinishDrag -> dir: ${direction} (${reason}) | offset: ${drag.offset.toFixed(0)}px | vel: ${drag.velocity.toFixed(2)} | threshold: ${(drag.height * DRAG_COMMIT_RATIO).toFixed(0)}px`);
if (direction === 0) { if (direction === 0) {
snapPanelsTo(0); snapPanelsTo(0);
return; return;
@ -491,6 +498,8 @@ export function QuestionSnapList({
drag.startTime = performance.now(); drag.startTime = performance.now();
drag.lastMoveTime = drag.startTime; drag.lastMoveTime = drag.startTime;
touchStartYRef.current = drag.startY; touchStartYRef.current = drag.startY;
console.log(`[Snap] TouchStart at (${drag.startX.toFixed(0)}, ${drag.startY.toFixed(0)}) on <${target?.tagName?.toLowerCase()}> (isOption: ${isOptionCard})`);
}; };
const onTouchMove = (event: TouchEvent) => { const onTouchMove = (event: TouchEvent) => {
@ -526,7 +535,7 @@ export function QuestionSnapList({
} }
// Must be primarily vertical gesture (avoid hijacking horizontal swipe / scroll) // Must be primarily vertical gesture (avoid hijacking horizontal swipe / scroll)
if (absY < absX * 1.1) {
if (absY < absX * 0.9) {
return; return;
} }
@ -535,8 +544,13 @@ export function QuestionSnapList({
drag.didDrag = true; drag.didDrag = true;
suppressNextClickRef.current = true; suppressNextClickRef.current = true;
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
const elapsed = Math.max(1, now - drag.startTime); const elapsed = Math.max(1, now - drag.startTime);
drag.velocity = (drag.startY - currentY) / elapsed; drag.velocity = (drag.startY - currentY) / elapsed;
console.log(`[Snap] Drag Engaged: deltaY=${deltaY.toFixed(0)}px, slop=${slop}px`);
} }
// Non-passive preventDefault stops native pan and guarantees gesture ownership // Non-passive preventDefault stops native pan and guarantees gesture ownership

10
src/components/Componentes/question-viewport-coordinator.ts

@ -94,7 +94,15 @@ function setLift(nextLift: number) {
function getKeyboardTop() { function getKeyboardTop() {
if (!keyboardVisible || !activeQuestionInput) return null; if (!keyboardVisible || !activeQuestionInput) return null;
const flutterTop = window.innerHeight - keyboardHeight;
// When keyboard is opening/visible, use the projected full keyboard height
// (either current incoming height or last known full keyboard height)
// so the CSS transition animates in ONE single smooth, continuous curve concurrent with OS keyboard.
const targetHeight = Math.max(
keyboardHeight,
lastKeyboardHeight > 200 ? lastKeyboardHeight : 320,
);
const flutterTop = window.innerHeight - targetHeight;
const viewport = window.visualViewport; const viewport = window.visualViewport;
const viewportBottom = viewport const viewportBottom = viewport
? viewport.offsetTop + viewport.height ? viewport.offsetTop + viewport.height

3
src/translations/locales/ar.json

@ -785,5 +785,6 @@
"Voice Call": "مكالمة صوتية", "Voice Call": "مكالمة صوتية",
"Video Call": "مكالمة فيديو", "Video Call": "مكالمة فيديو",
"No public information is available to display.": "لا توجد معلومات عامة متاحة للعرض.", "No public information is available to display.": "لا توجد معلومات عامة متاحة للعرض.",
"General Information & Personal Details": "المعلومات العامة والتفاصيل الشخصية"
"General Information & Personal Details": "المعلومات العامة والتفاصيل الشخصية",
"Enter a valid phone number with country code.": "يرجى إدخال رقم هاتف صالح مع رمز الدولة."
} }

3
src/translations/locales/az.json

@ -785,5 +785,6 @@
"Voice Call": "Səsli Zəng", "Voice Call": "Səsli Zəng",
"Video Call": "Video Zəng", "Video Call": "Video Zəng",
"No public information is available to display.": "Göstəriləcək heç bir ictimai məlumat qeyd edilməyib.", "No public information is available to display.": "Göstəriləcək heç bir ictimai məlumat qeyd edilməyib.",
"General Information & Personal Details": "Ümumi məlumat və fərdi xüsusiyyətlər"
"General Information & Personal Details": "Ümumi məlumat və fərdi xüsusiyyətlər",
"Enter a valid phone number with country code.": "Zəhmət olmasa ölkə kodu ilə birlikdə etibarlı bir telefon nömrəsi daxil edin."
} }

3
src/translations/locales/bn.json

@ -785,5 +785,6 @@
"Voice Call": "ভয়েস কল", "Voice Call": "ভয়েস কল",
"Video Call": "ভিডিও কল", "Video Call": "ভিডিও কল",
"No public information is available to display.": "প্রদর্শনের জন্য কোনও সর্বজনীন তথ্য পাওয়া যায়নি।", "No public information is available to display.": "প্রদর্শনের জন্য কোনও সর্বজনীন তথ্য পাওয়া যায়নি।",
"General Information & Personal Details": "সাধারণ তথ্য এবং ব্যক্তিগত বিবরণ"
"General Information & Personal Details": "সাধারণ তথ্য এবং ব্যক্তিগত বিবরণ",
"Enter a valid phone number with country code.": "দেশের কোড সহ একটি বৈধ ফোন নম্বর লিখুন।"
} }

3
src/translations/locales/da.json

@ -785,5 +785,6 @@
"Voice Call": "Taleopkald", "Voice Call": "Taleopkald",
"Video Call": "Videoopkald", "Video Call": "Videoopkald",
"No public information is available to display.": "Ingen offentlige oplysninger er tilgængelige.", "No public information is available to display.": "Ingen offentlige oplysninger er tilgængelige.",
"General Information & Personal Details": "Generelle oplysninger og personlige detaljer"
"General Information & Personal Details": "Generelle oplysninger og personlige detaljer",
"Enter a valid phone number with country code.": "Indtast et gyldigt telefonnummer med landekode."
} }

3
src/translations/locales/de.json

@ -785,5 +785,6 @@
"Voice Call": "Sprachanruf", "Voice Call": "Sprachanruf",
"Video Call": "Videoanruf", "Video Call": "Videoanruf",
"No public information is available to display.": "Keine öffentlichen Informationen zur Anzeige verfügbar.", "No public information is available to display.": "Keine öffentlichen Informationen zur Anzeige verfügbar.",
"General Information & Personal Details": "Allgemeine Informationen und persönliche Details"
"General Information & Personal Details": "Allgemeine Informationen und persönliche Details",
"Enter a valid phone number with country code.": "Geben Sie eine gültige Telefonnummer mit Ländercode ein."
} }

3
src/translations/locales/en.json

@ -833,5 +833,6 @@
"Text Message": "Text Message", "Text Message": "Text Message",
"Video Call": "Video Call", "Video Call": "Video Call",
"No public information is available to display.": "No public information is available to display.", "No public information is available to display.": "No public information is available to display.",
"General Information & Personal Details": "General Information & Personal Details"
"General Information & Personal Details": "General Information & Personal Details",
"Enter a valid phone number with country code.": "Enter a valid phone number with country code."
} }

3
src/translations/locales/es.json

@ -785,5 +785,6 @@
"Voice Call": "Llamada de Voz", "Voice Call": "Llamada de Voz",
"Video Call": "Llamada de Video", "Video Call": "Llamada de Video",
"No public information is available to display.": "No hay información pública disponible para mostrar.", "No public information is available to display.": "No hay información pública disponible para mostrar.",
"General Information & Personal Details": "Información general y detalles personales"
"General Information & Personal Details": "Información general y detalles personales",
"Enter a valid phone number with country code.": "Introduce un número de teléfono válido con código de país."
} }

3
src/translations/locales/fa.json

@ -833,5 +833,6 @@
"Text Message": "پیام متنی", "Text Message": "پیام متنی",
"Video Call": "تماس تصویری", "Video Call": "تماس تصویری",
"No public information is available to display.": "اطلاعات عمومی قابل نمایشی ثبت نشده است.", "No public information is available to display.": "اطلاعات عمومی قابل نمایشی ثبت نشده است.",
"General Information & Personal Details": "اطلاعات عمومی و مشخصات فردی"
"General Information & Personal Details": "اطلاعات عمومی و مشخصات فردی",
"Enter a valid phone number with country code.": "لطفاً یک شماره تماس معتبر به همراه کد کشور وارد کنید."
} }

3
src/translations/locales/fr.json

@ -785,5 +785,6 @@
"Voice Call": "Appel vocal", "Voice Call": "Appel vocal",
"Video Call": "Appel vidéo", "Video Call": "Appel vidéo",
"No public information is available to display.": "Aucune information publique disponible à afficher.", "No public information is available to display.": "Aucune information publique disponible à afficher.",
"General Information & Personal Details": "Informations générales et détails personnels"
"General Information & Personal Details": "Informations générales et détails personnels",
"Enter a valid phone number with country code.": "Entrez un numéro de téléphone valide avec l'indicatif du pays."
} }

3
src/translations/locales/gu.json

@ -785,5 +785,6 @@
"Voice Call": "વૉઇસ કૉલ", "Voice Call": "વૉઇસ કૉલ",
"Video Call": "વિડિયો કૉલ", "Video Call": "વિડિયો કૉલ",
"No public information is available to display.": "પ્રદર્શિત કરવા માટે કોઈ જાહેર માહિતી ઉપલબ્ધ નથી.", "No public information is available to display.": "પ્રદર્શિત કરવા માટે કોઈ જાહેર માહિતી ઉપલબ્ધ નથી.",
"General Information & Personal Details": "સામાન્ય માહિતી અને વ્યક્તિગત વિગતો"
"General Information & Personal Details": "સામાન્ય માહિતી અને વ્યક્તિગત વિગતો",
"Enter a valid phone number with country code.": "દેશના કોડ સાથે માન્ય ફોન નંબર દાખલ કરો."
} }

3
src/translations/locales/ha.json

@ -785,5 +785,6 @@
"Voice Call": "Kiran Murya", "Voice Call": "Kiran Murya",
"Video Call": "Kiran Bidiyo", "Video Call": "Kiran Bidiyo",
"No public information is available to display.": "Babu bayanan jama'a da za a iya nunawa.", "No public information is available to display.": "Babu bayanan jama'a da za a iya nunawa.",
"General Information & Personal Details": "Bayanai na gama-gari da cikakkun bayanan sirri"
"General Information & Personal Details": "Bayanai na gama-gari da cikakkun bayanan sirri",
"Enter a valid phone number with country code.": "Shigar da lambar waya mai aiki tare da lambar ƙasa."
} }

3
src/translations/locales/he.json

@ -322,5 +322,6 @@
"Voice Call": "Voice Call", "Voice Call": "Voice Call",
"Video Call": "Video Call", "Video Call": "Video Call",
"No public information is available to display.": "אין מידע ציבורי זמין להצגה.", "No public information is available to display.": "אין מידע ציבורי זמין להצגה.",
"General Information & Personal Details": "מידע כללי ופרטים אישיים"
"General Information & Personal Details": "מידע כללי ופרטים אישיים",
"Enter a valid phone number with country code.": "הזן מספר טלפון חוקי עם קידומת מדינה."
} }

3
src/translations/locales/hi.json

@ -785,5 +785,6 @@
"Voice Call": "वॉइस कॉल", "Voice Call": "वॉइस कॉल",
"Video Call": "वीडियो कॉल", "Video Call": "वीडियो कॉल",
"No public information is available to display.": "प्रदर्शित करने के लिए कोई सार्वजनिक जानकारी उपलब्ध नहीं है।", "No public information is available to display.": "प्रदर्शित करने के लिए कोई सार्वजनिक जानकारी उपलब्ध नहीं है।",
"General Information & Personal Details": "सामान्य जानकारी और व्यक्तिगत विवरण"
"General Information & Personal Details": "सामान्य जानकारी और व्यक्तिगत विवरण",
"Enter a valid phone number with country code.": "देश कोड के साथ एक मान्य फ़ोन नंबर दर्ज करें।"
} }

3
src/translations/locales/id.json

@ -322,5 +322,6 @@
"Voice Call": "Panggilan Suara", "Voice Call": "Panggilan Suara",
"Video Call": "Panggilan Video", "Video Call": "Panggilan Video",
"No public information is available to display.": "Tidak ada informasi publik yang tersedia untuk ditampilkan.", "No public information is available to display.": "Tidak ada informasi publik yang tersedia untuk ditampilkan.",
"General Information & Personal Details": "Informasi Umum & Rincian Pribadi"
"General Information & Personal Details": "Informasi Umum & Rincian Pribadi",
"Enter a valid phone number with country code.": "Masukkan nomor telepon yang valid dengan kode negara."
} }

3
src/translations/locales/ks.json

@ -322,5 +322,6 @@
"Voice Call": "آوازی کال", "Voice Call": "آوازی کال",
"Video Call": "ویڈیو کال", "Video Call": "ویڈیو کال",
"No public information is available to display.": "ڈسپلے کرنہٕ خٲطرٕ کانہہ عام معلومات دٔستیاب چُھنہٕ۔", "No public information is available to display.": "ڈسپلے کرنہٕ خٲطرٕ کانہہ عام معلومات دٔستیاب چُھنہٕ۔",
"General Information & Personal Details": "عام معلومات تہٕ ذٲتی تفصیٖل"
"General Information & Personal Details": "عام معلومات تہٕ ذٲتی تفصیٖل",
"Enter a valid phone number with country code.": "ملک کے کوڈ کے ساتھ ایک درست فون نمبر درج کریں۔"
} }

3
src/translations/locales/pt.json

@ -322,5 +322,6 @@
"Voice Call": "Chamada de Voz", "Voice Call": "Chamada de Voz",
"Video Call": "Chamada de Vídeo", "Video Call": "Chamada de Vídeo",
"No public information is available to display.": "Nenhuma informação pública disponível para exibição.", "No public information is available to display.": "Nenhuma informação pública disponível para exibição.",
"General Information & Personal Details": "Informações Gerais e Detalhes Pessoais"
"General Information & Personal Details": "Informações Gerais e Detalhes Pessoais",
"Enter a valid phone number with country code.": "Introduza um número de telefone válido com o código do país."
} }

3
src/translations/locales/ru.json

@ -789,5 +789,6 @@
"Voice Call": "Голосовой звонок", "Voice Call": "Голосовой звонок",
"Video Call": "Видеозвонок", "Video Call": "Видеозвонок",
"No public information is available to display.": "Нет общедоступной информации для отображения.", "No public information is available to display.": "Нет общедоступной информации для отображения.",
"General Information & Personal Details": "Общая информация и личные данные"
"General Information & Personal Details": "Общая информация и личные данные",
"Enter a valid phone number with country code.": "Введите действительный номер телефона с кодом страны."
} }

3
src/translations/locales/sw.json

@ -322,5 +322,6 @@
"Voice Call": "Simu ya Sauti", "Voice Call": "Simu ya Sauti",
"Video Call": "Simu ya Video", "Video Call": "Simu ya Video",
"No public information is available to display.": "Hakuna taarifa za umma zinazoweza kuonyeshwa.", "No public information is available to display.": "Hakuna taarifa za umma zinazoweza kuonyeshwa.",
"General Information & Personal Details": "Taarifa za Jumla na Maelezo Binafsi"
"General Information & Personal Details": "Taarifa za Jumla na Maelezo Binafsi",
"Enter a valid phone number with country code.": "Weka nambari halali ya simu yenye msimbo wa nchi."
} }

3
src/translations/locales/tg.json

@ -322,5 +322,6 @@
"Voice Call": "Зангҳои овозӣ", "Voice Call": "Зангҳои овозӣ",
"Video Call": "Зангҳои видеоӣ", "Video Call": "Зангҳои видеоӣ",
"No public information is available to display.": "Маълумоти умумии дастрас барои намоиш нест.", "No public information is available to display.": "Маълумоти умумии дастрас барои намоиш нест.",
"General Information & Personal Details": "Маълумоти умумӣ ва мушаххасоти инфиродӣ"
"General Information & Personal Details": "Маълумоти умумӣ ва мушаххасоти инфиродӣ",
"Enter a valid phone number with country code.": "Рақами телефони дурустро бо рамзи кишвар ворид кунед."
} }

3
src/translations/locales/tr.json

@ -322,5 +322,6 @@
"Voice Call": "Sesli Arama", "Voice Call": "Sesli Arama",
"Video Call": "Görüntülü Arama", "Video Call": "Görüntülü Arama",
"No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.", "No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.",
"General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar"
"General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar",
"Enter a valid phone number with country code.": "Ülke kodu ile geçerli bir telefon numarası girin."
} }

3
src/translations/locales/ul.json

@ -322,5 +322,6 @@
"Voice Call": "Voice Call", "Voice Call": "Voice Call",
"Video Call": "Video Call", "Video Call": "Video Call",
"No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.", "No public information is available to display.": "Görüntülenecek genel bilgi bulunmamaktadır.",
"General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar"
"General Information & Personal Details": "Genel Bilgiler ve Kişisel Detaylar",
"Enter a valid phone number with country code.": "ملک کے کوڈ کے ساتھ ایک درست فون نمبر درج کریں۔"
} }

3
src/translations/locales/ur.json

@ -322,5 +322,6 @@
"Voice Call": "وائس کال", "Voice Call": "وائس کال",
"Video Call": "ویڈیو کال", "Video Call": "ویڈیو کال",
"No public information is available to display.": "دکھانے کے لیے کوئی عوامی معلومات دستیاب نہیں ہے۔", "No public information is available to display.": "دکھانے کے لیے کوئی عوامی معلومات دستیاب نہیں ہے۔",
"General Information & Personal Details": "عام معلومات اور ذاتی تفصیلات"
"General Information & Personal Details": "عام معلومات اور ذاتی تفصیلات",
"Enter a valid phone number with country code.": "ملک کے کوڈ کے ساتھ ایک درست فون نمبر درج کریں۔"
} }

3
src/translations/locales/uz.json

@ -322,5 +322,6 @@
"Voice Call": "Овозли қўнғироқ", "Voice Call": "Овозли қўнғироқ",
"Video Call": "Видео қўнғироқ", "Video Call": "Видео қўнғироқ",
"No public information is available to display.": "Ko'rsatish uchun umumiy ma'lumot mavjud emas.", "No public information is available to display.": "Ko'rsatish uchun umumiy ma'lumot mavjud emas.",
"General Information & Personal Details": "Umumiy ma'lumotlar va shaxsiy tafsilotlar"
"General Information & Personal Details": "Umumiy ma'lumotlar va shaxsiy tafsilotlar",
"Enter a valid phone number with country code.": "Mamlakat kodi bilan to'g'ri telefon raqamini kiriting."
} }

3
src/translations/locales/zh.json

@ -785,5 +785,6 @@
"Voice Call": "语音通话", "Voice Call": "语音通话",
"Video Call": "视频通话", "Video Call": "视频通话",
"No public information is available to display.": "没有可显示的公开信息。", "No public information is available to display.": "没有可显示的公开信息。",
"General Information & Personal Details": "一般信息和个人资料"
"General Information & Personal Details": "一般信息和个人资料",
"Enter a valid phone number with country code.": "请输入包含国家代码的有效电话号码。"
} }
Loading…
Cancel
Save