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

add InputPhone component #148

Merged
merged 6 commits into from
Feb 20, 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
17 changes: 17 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
## Summary

...

## Checklist before requesting a review

- [ ] Working on iOS
- [ ] Working on Android
- [ ] Integration tests added
- [ ] Visual regressions screenshots are up to date
- [ ] Design validated

## Screenshots

| iOS | Android |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <img src="https://github.com/GetLuko/streamline/blob/INSERT_YOUR_BRANCH_NAME/sandbox/e2e/ios/screenshots/INSERT_YOUR_COMPONENT_NAME.png?raw=true" width="300" /> | <img src="https://github.com/GetLuko/streamline/blob/INSERT_YOUR_BRANCH_NAME/sandbox/e2e/android/screenshots/INSERT_YOUR_COMPONENT_NAME.png?raw=true" width="300" /> |
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"eslint-plugin-react-native": "^4.1.0",
"expo-linear-gradient": "~12.3.0",
"jest": "^28.1.1",
"libphonenumber-js": "^1.10.55",
"prettier": "^2.0.5",
"react": "18.2.0",
"react-native": "0.72.10",
Expand Down Expand Up @@ -75,6 +76,7 @@
"@shopify/restyle": "2.0.0",
"dayjs": "1.10.6",
"expo-linear-gradient": "~12.3.0",
"libphonenumber-js": "^1.10.55",
"react": "18.2.0",
"react-native": "0.72.10",
"react-native-ama": "0.6.20",
Expand Down
4 changes: 3 additions & 1 deletion sandbox/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
dist/
dist/
e2e/android/tempScreenshots/
e2e/ios/tempScreenshots/
Binary file added sandbox/e2e/android/screenshots/InputPhone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added sandbox/e2e/ios/screenshots/InputPhone.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions sandbox/e2e/visual_regressions_flow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ appId: ${APPID}
COMPONENT: InputSearch
PLACEHOLDER: Search
TEXT: Write some text
- runFlow:
file: subflow/takeScreenshot.yaml
env:
COMPONENT: InputPhone
- runFlow:
file: subflow/takeScreenshot.yaml
env:
Expand Down
3 changes: 2 additions & 1 deletion sandbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "sandbox",
"version": "0.0.1",
"scripts": {
"start:e2e": "EXPO_PUBLIC_E2E=true expo start --clear --port 19000",
"start:e2e": "EXPO_PUBLIC_E2E=true expo start --clear --port 19000 --minify",
"start": "expo start --clear",
"test:visual:ios": "maestro test e2e/visual_regressions_flow.yml -e APPID=host.exp.Exponent -e PLATFORM=ios",
"test:visual:ios:compare": "reg-cli e2e/ios/screenshots e2e/ios/tempScreenshots e2e/ios/diffScreenshots -T 0.0001 -R e2e/ios/diffScreenshots/report.html -J e2e/ios/diffScreenshots/result.json",
Expand All @@ -18,6 +18,7 @@
"expo-font": "~11.10.2",
"expo-linear-gradient": "~12.7.1",
"expo-updates": "~0.24.10",
"libphonenumber-js": "^1.10.55",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.73.4",
Expand Down
29 changes: 29 additions & 0 deletions sandbox/src/app/sandbox/docs/input-phone.doc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { InputPhone } from '@getluko/streamline';

import { DocList } from '../components/DocList';

const docs: JSX.Element[] = [
<InputPhone countryCode="FR" label="Default" placeholder="0606060606" />,
<InputPhone
countryCode="US"
isFocused
label="Focus"
placeholder="0606060606"
/>,
<InputPhone
countryCode="GB"
isError
label="Error"
description="Error message"
placeholder="0606060606"
/>,
<InputPhone
countryCode="DE"
isDisabled
label="Disabled"
description="Description"
placeholder="0606060606"
/>,
];

export const InputPhoneSandbox = () => <DocList docs={docs} />;
2 changes: 2 additions & 0 deletions sandbox/src/app/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { DialogSandbox } from './docs/dialog.doc';
import { IconsSandbox } from './docs/icon.doc';
import { InputCodeSandbox } from './docs/input-code.doc';
import { InputDatePickerSandbox } from './docs/input-date-picker.doc';
import { InputPhoneSandbox } from './docs/input-phone.doc';
import { InputSearchSandbox } from './docs/input-search.doc';
import { InputSelectSandbox } from './docs/input-select.doc';
import { InputTextAreaSandbox } from './docs/input-text-area.doc';
Expand Down Expand Up @@ -95,6 +96,7 @@ export const sandboxItems: SandBoxSectionType[] = [
{ title: 'InputSelect', SandBox: InputSelectSandbox },
{ title: 'InputSearch', SandBox: InputSearchSandbox },
{ title: 'InputCode', SandBox: InputCodeSandbox },
{ title: 'InputPhone', SandBox: InputPhoneSandbox },
],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';

import CountryPicker from './country-picker';
import { renderWithProvider } from '../../../../testing/render-with-provider';

describe('CountryPicker', () => {
it('renders correctly with default props', () => {
const { getByText } = renderWithProvider(
<CountryPicker onCountryPickerPress={() => {}} />
);

// Check if the default country code (FR) is displayed
expect(getByText('+33')).toBeTruthy();
});

it('calls the onCountryPickerPress prop when pressed', () => {
const handlePress = jest.fn();
const { getByText } = renderWithProvider(
<CountryPicker onCountryPickerPress={handlePress} />
);

fireEvent.press(getByText('+33'));

expect(handlePress).toHaveBeenCalled();
});

it('displays the correct country code when the countryCode prop is provided', () => {
const { getByText } = renderWithProvider(
<CountryPicker countryCode="US" onCountryPickerPress={() => {}} />
);

// Check if the provided country code (US) is displayed
expect(getByText('+1')).toBeTruthy();
});
});
44 changes: 44 additions & 0 deletions src/components/inputs/input-phone/components/country-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { getCountryCallingCode } from 'libphonenumber-js';
import { Pressable } from 'react-native';

import { getFlagEmoji } from './country-picker.utils';
import { Box } from '../../../../primitives/box/box';
import { Icon } from '../../../../primitives/icon/icon';
import { Text } from '../../../../primitives/text/text';
import { CountryPickerProps } from '../input-phone.types';

const CountryPicker = ({
countryCode = 'FR',
isDisabled,
isError,
onCountryPickerPress,
}: CountryPickerProps) => {
const backgroundColor = isDisabled ? 'GREY_25' : 'PURE_WHITE_1000';
const borderColor = isError ? 'TERRA_500' : 'GREY_100';
const textColor = isDisabled ? 'GREY_500' : 'GREY_1000';

return (
<Pressable onPress={onCountryPickerPress} disabled={isDisabled}>
<Box
flexDirection="row"
alignItems="center"
alignSelf="flex-start"
height={48}
paddingHorizontal="md"
backgroundColor={backgroundColor}
borderRadius="lg"
borderColor={borderColor}
borderWidth={2}
>
<Text variant="body">{getFlagEmoji(countryCode)}</Text>

<Text variant="body" color={textColor} paddingHorizontal="xs">
{`+${getCountryCallingCode(countryCode)}`}
</Text>
<Icon iconName="ChevronDown" size="regular" color="GREY_400" />
</Box>
</Pressable>
);
};

export default CountryPicker;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getFlagEmoji } from './country-picker.utils';

describe('getFlagEmoji', () => {
it('returns the correct flag emoji for a given country code', () => {
expect(getFlagEmoji('US')).toBe('🇺🇸');
expect(getFlagEmoji('GB')).toBe('🇬🇧');
expect(getFlagEmoji('FR')).toBe('🇫🇷');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CountryCode } from 'libphonenumber-js';

export const getFlagEmoji = (countryCode: CountryCode): string => {
return [...countryCode.toUpperCase()]
.map((char) => String.fromCodePoint(127397 + char.charCodeAt(0)))
.reduce((a, b) => `${a}${b}`);
};
54 changes: 54 additions & 0 deletions src/components/inputs/input-phone/input-phone.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import React from 'react';
import { fireEvent } from '@testing-library/react-native';

import { InputPhone } from './input-phone';
import { renderWithProvider } from '../../../testing/render-with-provider';

describe('InputPhone', () => {
it('renders correctly with default props', () => {
const { getByText } = renderWithProvider(
<InputPhone onChangePhoneNumber={() => {}} />
);

// Check if the default country code (FR) is displayed
expect(getByText('+33')).toBeTruthy();
});

it('calls the onChangePhoneNumber prop when the phone number is valid', () => {
const handleChangePhoneNumber = jest.fn();
const { getByTestId } = renderWithProvider(
<InputPhone
onChangePhoneNumber={handleChangePhoneNumber}
testID="phone-input"
/>
);

fireEvent.changeText(getByTestId('phone-input'), '0123456789');

expect(handleChangePhoneNumber).toHaveBeenCalled();
});

it('calls the onError prop when the phone number is invalid', () => {
const handleError = jest.fn();
const { getByTestId } = renderWithProvider(
<InputPhone
onChangePhoneNumber={() => {}}
onError={handleError}
testID="phone-input"
/>
);

fireEvent.changeText(getByTestId('phone-input'), 'invalid');

expect(handleError).toHaveBeenCalled();
});

it('displays the correct country code when the countryCode prop is provided', () => {
const { getByText } = renderWithProvider(
<InputPhone countryCode="US" onChangePhoneNumber={() => {}} />
);

// Check if the provided country code (US) is displayed
expect(getByText('+1')).toBeTruthy();
});
});
85 changes: 85 additions & 0 deletions src/components/inputs/input-phone/input-phone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ParseError, parsePhoneNumberWithError } from 'libphonenumber-js';
import { FC, useState } from 'react';

import CountryPicker from './components/country-picker';
import { InputPhoneProps } from './input-phone.types';
import { Box } from '../../../primitives/box/box';
import { Text } from '../../../primitives/text/text';
import { InputTextLabel } from '../input-text/components/input-text-label';
import { InputText } from '../input-text/input-text';

export const InputPhone: FC<InputPhoneProps> = ({
onChangePhoneNumber,
onCountryPickerPress,
countryCode = 'FR',
isError,
isDisabled,
isFocused,
description,
label,
onError,
...props
}) => {
const [inputValue, setInputValue] = useState('');

const showDescription =
(description && !isError) || (isError && inputValue.length > 2);

const handleOnChangeText = (value: string) => {
try {
const phoneNumber = parsePhoneNumberWithError(value, countryCode);
setInputValue(phoneNumber.formatNational());
onChangePhoneNumber?.(phoneNumber);
hcourthias marked this conversation as resolved.
Show resolved Hide resolved
} catch (error) {
if (error instanceof ParseError) {
onError?.(error);
}
setInputValue(value);
}
};

return (
<Box>
{label ? (
<Box paddingBottom="xs">
<InputTextLabel
label={label}
active={isFocused}
disabled={isDisabled}
/>
</Box>
) : null}
<Box flexDirection="row">
<CountryPicker
countryCode={countryCode}
isDisabled={isDisabled}
isError={isError}
onCountryPickerPress={onCountryPickerPress}
/>
<Box flex={1} marginLeft="xs">
<InputText
{...props}
isDisabled={isDisabled}
isError={isError}
isFocused={isFocused}
keyboardType="phone-pad"
onChangeText={handleOnChangeText}
value={inputValue}
inputType="PHONE"
/>
</Box>
</Box>
{showDescription ? (
<Text
color={isError ? 'TERRA_500' : 'GREY_500'}
variant="caption"
marginTop="xs"
>
{description}
</Text>
) : null}
</Box>
);
};

export default InputPhone;
15 changes: 15 additions & 0 deletions src/components/inputs/input-phone/input-phone.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { CountryCode, ParseError, PhoneNumber } from 'libphonenumber-js';

import { InputTextProps } from '../input-text/types';

export type InputPhoneProps = InputTextProps & {
countryCode?: CountryCode;
onCountryPickerPress?: () => void;
onChangePhoneNumber?: (phoneNumber: PhoneNumber) => void;
onError?: (error: ParseError) => void;
};

export type CountryPickerProps = Pick<
InputPhoneProps,
'countryCode' | 'isDisabled' | 'isError' | 'onCountryPickerPress'
>;
2 changes: 1 addition & 1 deletion src/components/inputs/input-search/input-search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export function InputSearch({ placeholder, onChangeText }: InputSearchProps) {
<InputText
label=""
placeholder={placeholder}
isSearchInput
inputType="SEARCH"
onChangeText={onChangeText}
/>
);
Expand Down
Loading
Loading