Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/ui/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
{ "path": "./dist/op-plans-page*.js", "maxSize": "3KB" },
{ "path": "./dist/statement-page*.js", "maxSize": "5KB" },
{ "path": "./dist/payment-attempt-page*.js", "maxSize": "4KB" },
{ "path": "./dist/web3-solana-wallet-buttons*.js", "maxSize": "79KB" }
{ "path": "./dist/web3-solana-wallet-buttons*.js", "maxSize": "79KB" },
{ "path": "./dist/phone-country-data*.js", "maxSize": "10KB" }
]
}
17 changes: 16 additions & 1 deletion packages/ui/rspack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,20 @@ const common = ({ mode, variant }) => {
module.resource.includes('/components/SignUp')
),
},
/**
* Phone country code data is lazy-loaded via dynamic import
* and excluded from ui-common to keep it in its own async chunk.
*/
phoneCountryData: {
name: 'phone-country-data',
test: module =>
!!(
module instanceof rspack.NormalModule &&
module.resource &&
module.resource.includes('/countryCodeData.')
),
enforce: true,
},
common: {
minChunks: 1,
name: 'ui-common',
Expand All @@ -107,7 +121,8 @@ const common = ({ mode, variant }) => {
module instanceof rspack.NormalModule &&
module.resource &&
!module.resource.includes('/components') &&
!module.resource.includes('node_modules')
!module.resource.includes('node_modules') &&
!module.resource.includes('/countryCodeData.')
),
},
defaultVendors: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { ClerkAPIResponseError, parseError } from '@clerk/shared/error';
import type { SignInResource } from '@clerk/shared/types';
import { waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { beforeAll, describe, expect, it, vi } from 'vitest';

import { bindCreateFixtures } from '@/test/create-fixtures';
import { act, mockWebAuthn, render, screen } from '@/test/utils';
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';

import { SignInFactorOne } from '../SignInFactorOne';

const { createFixtures } = bindCreateFixtures('SignIn');

beforeAll(async () => {
await loadCountryCodeData();
});

describe('SignInFactorOne', () => {
it('renders the component', async () => {
const { wrapper, fixtures } = await createFixtures(f => {
Expand Down
7 changes: 6 additions & 1 deletion packages/ui/src/components/SignIn/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { SignInResource } from '@clerk/shared/types';
import { describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';

import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';
import type { FormControlState } from '@/ui/utils/useFormControl';

import {
Expand All @@ -10,6 +11,10 @@ import {
getPreferredAlternativePhoneChannelForCombinedFlow,
} from '../utils';

beforeAll(async () => {
await loadCountryCodeData();
});

describe('determineStrategy(signIn, displayConfig)', () => {
describe('with password as the preferred sign in strategy', () => {
it('selects password if available', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';

import { bindCreateFixtures } from '@/test/create-fixtures';
import { render, screen } from '@/test/utils';
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';

import { SignUpVerifyPhone } from '../SignUpVerifyPhone';

const { createFixtures } = bindCreateFixtures('SignUp');

beforeAll(async () => {
await loadCountryCodeData();
});

describe('SignUpVerifyPhone', () => {
it('renders the component', async () => {
const { wrapper } = await createFixtures();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@ import type {
VerificationJSON,
} from '@clerk/shared/types';
import { act, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';

import { bindCreateFixtures } from '@/test/create-fixtures';
import { render } from '@/test/utils';
import { CardStateProvider } from '@/ui/elements/contexts';
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';

import { MfaSection } from '../MfaSection';

beforeAll(async () => {
await loadCountryCodeData();
});

const { createFixtures } = bindCreateFixtures('UserProfile');

const initConfig = createFixtures.config(f => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { act } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';

import { bindCreateFixtures } from '@/test/create-fixtures';
import { render, screen } from '@/test/utils';
import { CardStateProvider } from '@/ui/elements/contexts';
import { loadCountryCodeData } from '@/ui/elements/PhoneInput/countryCodeDataLoader';

import { PhoneSection } from '../PhoneSection';

const { createFixtures } = bindCreateFixtures('UserProfile');

beforeAll(async () => {
await loadCountryCodeData();
});

const initConfig = createFixtures.config(f => {
f.withPhoneNumber();
f.withUser({ email_addresses: ['[email protected]'] });
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { renderHook, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';

import { loadCountryCodeData } from '../countryCodeDataLoader';
import { useFormattedPhoneNumber } from '../useFormattedPhoneNumber';

beforeAll(async () => {
await loadCountryCodeData();
});

describe('useFormattedPhoneNumber', () => {
afterEach(() => {
// Empty the localStorage used within the hook
Expand Down
51 changes: 51 additions & 0 deletions packages/ui/src/elements/PhoneInput/countryCodeDataLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type {
CodeToCountryIsoMapType,
CountryEntry,
CountryIso,
IsoToCountryMapType,
} from './countryCodeData';

// Hardcoded US fallback for use before data loads
export const US_FALLBACK_ENTRY: CountryEntry = {
name: 'United States' as CountryEntry['name'],
iso: 'us' as CountryIso,
code: '1' as CountryEntry['code'],
pattern: '(...) ...-....' as CountryEntry['pattern'],
priority: 100,
};

// Module-level cache
let isoToCountryMap: IsoToCountryMapType | undefined;
let codeToCountriesMap: CodeToCountryIsoMapType | undefined;
let subAreaCodeSets: { us: ReadonlySet<string>; ca: ReadonlySet<string> } | undefined;
let loadPromise: Promise<void> | undefined;

export function loadCountryCodeData(): Promise<void> {
if (!loadPromise) {
loadPromise = import(/* webpackChunkName: "phone-country-data" */ './countryCodeData').then(mod => {
isoToCountryMap = mod.IsoToCountryMap;
codeToCountriesMap = mod.CodeToCountriesMap;
subAreaCodeSets = mod.SubAreaCodeSets;
});
}
return loadPromise;
}

export function isCountryCodeDataLoaded(): boolean {
return isoToCountryMap !== undefined;
}

export function getIsoToCountryMap(): IsoToCountryMapType | undefined {
return isoToCountryMap;
}

export function getCodeToCountriesMap(): CodeToCountryIsoMapType | undefined {
return codeToCountriesMap;
}

export function getSubAreaCodeSets() {
return subAreaCodeSets;
}

export type { CountryEntry, CountryIso, IsoToCountryMapType, CodeToCountryIsoMapType };
export type { CountryName, DialingCode, PhonePattern } from './countryCodeData';
28 changes: 24 additions & 4 deletions packages/ui/src/elements/PhoneInput/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useClerk } from '@clerk/shared/react';
import React, { forwardRef, memo, useEffect, useMemo, useRef } from 'react';
import React, { forwardRef, memo, useEffect, useMemo, useRef, useState } from 'react';

import { mergeRefs } from '@/ui/utils/mergeRefs';
import type { FeedbackType } from '@/ui/utils/useFormControl';
Expand All @@ -9,7 +9,7 @@ import { Check, ChevronUpDown } from '../../icons';
import { common, type PropsOfComponent } from '../../styledSystem';
import { Select, SelectButton, SelectOptionList } from '../Select';
import type { CountryEntry, CountryIso } from './countryCodeData';
import { IsoToCountryMap } from './countryCodeData';
import { getIsoToCountryMap, isCountryCodeDataLoaded, loadCountryCodeData } from './countryCodeDataLoader';
import { useFormattedPhoneNumber } from './useFormattedPhoneNumber';

const createSelectOption = (country: CountryEntry) => {
Expand All @@ -21,7 +21,17 @@ const createSelectOption = (country: CountryEntry) => {
};
};

const countryOptions = [...IsoToCountryMap.values()].map(createSelectOption);
function useCountryCodeData() {
const [loaded, setLoaded] = useState(isCountryCodeDataLoaded);

useEffect(() => {
if (!loaded) {
void loadCountryCodeData().then(() => setLoaded(true));
}
}, [loaded]);

return loaded;
}

type PhoneInputProps = PropsOfComponent<typeof Input> & { locationBasedCountryIso?: CountryIso };

Expand All @@ -34,6 +44,11 @@ const PhoneInputBase = forwardRef<HTMLInputElement, PhoneInputProps & { feedback
locationBasedCountryIso,
});

const countryOptions = useMemo(() => {
const map = getIsoToCountryMap();
return map ? [...map.values()].map(createSelectOption) : [];
}, []);

const callOnChangeProp = () => {
// Quick and dirty way to match this component's public API
// with every other Input component, so we can use the same helpers
Expand All @@ -43,7 +58,7 @@ const PhoneInputBase = forwardRef<HTMLInputElement, PhoneInputProps & { feedback

const selectedCountryOption = useMemo(() => {
return countryOptions.find(o => o.country.iso === iso) || countryOptions[0];
}, [iso]);
}, [countryOptions, iso]);

useEffect(callOnChangeProp, [numberWithCode]);

Expand Down Expand Up @@ -248,6 +263,11 @@ const CountryCodeListItem = memo((props: CountryCodeListItemProps) => {
export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps & { feedbackType?: FeedbackType }>(
(props, ref) => {
const { __internal_country } = useClerk();
const dataLoaded = useCountryCodeData();

if (!dataLoaded) {
return null;
}

return (
<PhoneInputBase
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import React from 'react';
import { extractDigits, formatPhoneNumber, parsePhoneString } from '@/ui/utils/phoneUtils';

import type { CountryIso } from './countryCodeData';
import { IsoToCountryMap } from './countryCodeData';
import { getIsoToCountryMap } from './countryCodeDataLoader';

type UseFormattedPhoneNumberProps = { initPhoneWithCode: string; locationBasedCountryIso?: CountryIso };

const format = (str: string, iso: CountryIso) => {
if (!str) {
return '';
}
const country = IsoToCountryMap.get(iso);
const country = getIsoToCountryMap()?.get(iso);
return formatPhoneNumber(str, country?.pattern, country?.code);
};

Expand All @@ -35,7 +35,7 @@ export const useFormattedPhoneNumber = (props: UseFormattedPhoneNumberProps) =>
if (!number) {
return '';
}
const dialCode = IsoToCountryMap.get(iso)?.code || '1';
const dialCode = getIsoToCountryMap()?.get(iso)?.code || '1';
return '+' + extractDigits(`${dialCode}${number}`);
}, [iso, number]);

Expand Down
7 changes: 6 additions & 1 deletion packages/ui/src/utils/__tests__/formatSafeIdentifier.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';

import { loadCountryCodeData } from '../../elements/PhoneInput/countryCodeDataLoader';
import { formatSafeIdentifier } from '../formatSafeIdentifier';

beforeAll(async () => {
await loadCountryCodeData();
});

describe('formatSafeIdentifier', () => {
const cases = [
['[email protected]', '[email protected]'],
Expand Down
7 changes: 6 additions & 1 deletion packages/ui/src/utils/__tests__/phoneUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { PhoneCodeChannel } from '@clerk/shared/types';
import { describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';

import { loadCountryCodeData } from '../../elements/PhoneInput/countryCodeDataLoader';
import {
extractDigits,
formatPhoneNumber,
Expand All @@ -10,6 +11,10 @@ import {
getPreferredPhoneCodeChannelByCountry,
} from '../phoneUtils';

beforeAll(async () => {
await loadCountryCodeData();
});

describe('phoneUtils', () => {
describe('countryIsoToFlagEmoji(iso)', () => {
it('handles undefined', () => {
Expand Down
Loading
Loading