diff --git a/packages/phrases/src/locales/en.ts b/packages/phrases/src/locales/en.ts
index c9fca1a8c..75a610d81 100644
--- a/packages/phrases/src/locales/en.ts
+++ b/packages/phrases/src/locales/en.ts
@@ -9,13 +9,17 @@ const translation = {
cancel: 'Cancel',
done: 'Done',
search: 'Search',
+ continue: 'Continue',
},
sign_in: {
+ sign_in: 'Sign In',
action: 'Sign In',
loading: 'Signing in...',
error: 'Username or password is invalid.',
username: 'Username',
password: 'Password',
+ email: 'Email',
+ phone_number: 'Phone Number',
terms_of_use: 'Terms of Use',
terms_agreement_prefix: 'I agree with ',
continue_with: 'Continue With',
diff --git a/packages/phrases/src/locales/zh-cn.ts b/packages/phrases/src/locales/zh-cn.ts
index b439df992..94b19a29d 100644
--- a/packages/phrases/src/locales/zh-cn.ts
+++ b/packages/phrases/src/locales/zh-cn.ts
@@ -11,13 +11,17 @@ const translation = {
cancel: '取消',
done: '完成',
search: '搜索',
+ continue: '继续',
},
sign_in: {
+ sign_in: '登录',
action: '登录',
loading: '登录中...',
error: '用户名或密码错误。',
username: '用户名',
password: '密码',
+ email: '邮箱',
+ phone_number: '手机',
terms_of_use: '用户协议',
terms_agreement_prefix: '登录即表明您已经同意',
continue_with: '更多',
diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx
index 4b88c1007..84643163f 100644
--- a/packages/ui/src/App.tsx
+++ b/packages/ui/src/App.tsx
@@ -6,6 +6,7 @@ import useTheme from './hooks/use-theme';
import initI18n from './i18n/init';
import Consent from './pages/Consent';
import Register from './pages/Register';
+import SecondarySignIn from './pages/SecondarySignIn';
import SignIn from './pages/SignIn';
import './scss/normalized.scss';
@@ -21,6 +22,7 @@ const App = () => {
+
diff --git a/packages/ui/src/components/Input/PhoneInput.tsx b/packages/ui/src/components/Input/PhoneInput.tsx
index 874d0ee4a..049c54ae6 100644
--- a/packages/ui/src/components/Input/PhoneInput.tsx
+++ b/packages/ui/src/components/Input/PhoneInput.tsx
@@ -3,6 +3,7 @@ import React, { useState, useMemo, useRef } from 'react';
import { CountryCallingCode, CountryMetaData } from '@/hooks/use-phone-number';
+import ErrorMessage, { ErrorType } from '../ErrorMessage';
import { ClearIcon, DownArrowIcon } from '../Icons';
import * as styles from './index.module.scss';
import * as phoneInputStyles from './phoneInput.module.scss';
@@ -12,26 +13,24 @@ type Value = { countryCallingCode?: CountryCallingCode; nationalNumber?: string
export type Props = {
name: string;
autoComplete?: AutoCompleteType;
- isDisabled?: boolean;
className?: string;
placeholder?: string;
countryCallingCode?: CountryCallingCode;
nationalNumber: string;
countryList?: CountryMetaData[];
- hasError?: boolean;
+ error?: ErrorType;
onChange: (value: Value) => void;
};
const PhoneInput = ({
name,
autoComplete,
- isDisabled,
className,
placeholder,
countryCallingCode,
nationalNumber,
countryList,
- hasError = false,
+ error,
onChange,
}: Props) => {
const [onFocus, setOnFocus] = useState(false);
@@ -69,43 +68,38 @@ const PhoneInput = ({
}, [countryCallingCode, countryList, onChange]);
return (
-
- {countrySelector}
-
{
- setOnFocus(true);
- }}
- onBlur={() => {
- setOnFocus(false);
- }}
- onChange={({ target: { value } }) => {
- onChange({ nationalNumber: value });
- }}
- />
- {nationalNumber && onFocus && (
-
{
- event.preventDefault();
- onChange({ nationalNumber: '' });
+
+
+ {countrySelector}
+ {
+ setOnFocus(true);
+ }}
+ onBlur={() => {
+ setOnFocus(false);
+ }}
+ onChange={({ target: { value } }) => {
+ onChange({ nationalNumber: value.replaceAll(/\D/g, '') });
}}
/>
- )}
+ {nationalNumber && onFocus && (
+ {
+ event.preventDefault();
+ onChange({ nationalNumber: '' });
+ }}
+ />
+ )}
+
+ {error &&
}
);
};
diff --git a/packages/ui/src/containers/Passwordless/EmailPasswordless.test.tsx b/packages/ui/src/containers/Passwordless/EmailPasswordless.test.tsx
new file mode 100644
index 000000000..a07057844
--- /dev/null
+++ b/packages/ui/src/containers/Passwordless/EmailPasswordless.test.tsx
@@ -0,0 +1,89 @@
+import { fireEvent, render, waitFor } from '@testing-library/react';
+import React from 'react';
+
+import { sendEmailPasscode as sendRegisterEmailPasscode } from '@/apis/register';
+import { sendEmailPasscode as sendSignInEmailPasscode } from '@/apis/sign-in';
+
+import EmailPasswordless from './EmailPasswordless';
+
+jest.mock('@/apis/sign-in', () => ({ sendEmailPasscode: jest.fn(async () => Promise.resolve()) }));
+jest.mock('@/apis/register', () => ({ sendEmailPasscode: jest.fn(async () => Promise.resolve()) }));
+
+describe('', () => {
+ test('render', () => {
+ const { queryByText, container } = render();
+ expect(container.querySelector('input[name="email"]')).not.toBeNull();
+ expect(queryByText('general.continue')).not.toBeNull();
+ expect(queryByText('sign_in.terms_of_use')).not.toBeNull();
+ });
+
+ test('required email with error message', () => {
+ const { queryByText, container, getByText } = render();
+ const submitButton = getByText('general.continue');
+
+ fireEvent.click(submitButton);
+ expect(queryByText('errors:user.invalid_email')).not.toBeNull();
+ expect(sendSignInEmailPasscode).not.toBeCalled();
+
+ const emailInput = container.querySelector('input[name="email"]');
+
+ if (emailInput) {
+ fireEvent.change(emailInput, { target: { value: 'foo' } });
+ expect(queryByText('errors:user.invalid_email')).not.toBeNull();
+
+ fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } });
+ expect(queryByText('errors:user.invalid_email')).toBeNull();
+ }
+ });
+
+ test('required terms of agreement with error message', () => {
+ const { queryByText, container, getByText } = render();
+ const submitButton = getByText('general.continue');
+ const emailInput = container.querySelector('input[name="email"]');
+
+ if (emailInput) {
+ fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } });
+ }
+
+ fireEvent.click(submitButton);
+ expect(queryByText('errors:form.terms_required')).not.toBeNull();
+ });
+
+ test('signin method properly', async () => {
+ const { container, getByText } = render();
+ const emailInput = container.querySelector('input[name="email"]');
+
+ if (emailInput) {
+ fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } });
+ }
+ const termsButton = getByText('sign_in.terms_agreement_prefix');
+ fireEvent.click(termsButton);
+
+ const submitButton = getByText('general.continue');
+
+ await waitFor(() => {
+ fireEvent.click(submitButton);
+ });
+
+ expect(sendSignInEmailPasscode).toBeCalledWith('foo@logto.io');
+ });
+
+ test('register method properly', async () => {
+ const { container, getByText } = render();
+ const emailInput = container.querySelector('input[name="email"]');
+
+ if (emailInput) {
+ fireEvent.change(emailInput, { target: { value: 'foo@logto.io' } });
+ }
+ const termsButton = getByText('sign_in.terms_agreement_prefix');
+ fireEvent.click(termsButton);
+
+ const submitButton = getByText('general.continue');
+
+ await waitFor(() => {
+ fireEvent.click(submitButton);
+ });
+
+ expect(sendRegisterEmailPasscode).toBeCalledWith('foo@logto.io');
+ });
+});
diff --git a/packages/ui/src/containers/Passwordless/EmailPasswordless.tsx b/packages/ui/src/containers/Passwordless/EmailPasswordless.tsx
new file mode 100644
index 000000000..d41aced7c
--- /dev/null
+++ b/packages/ui/src/containers/Passwordless/EmailPasswordless.tsx
@@ -0,0 +1,157 @@
+/**
+ * TODO:
+ * 1. API redesign handle api error and loading status globally in PageContext
+ * 2. Input field validation, should move the validation rule to the input field scope
+ * 4. Read terms of use settings from SignInExperience Settings
+ */
+import { LogtoErrorI18nKey } from '@logto/phrases';
+import classNames from 'classnames';
+import React, { useState, useCallback, useMemo, useEffect, useContext } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { sendEmailPasscode as sendRegisterEmailPasscode } from '@/apis/register';
+import { sendEmailPasscode as sendSignInEmailPasscode } from '@/apis/sign-in';
+import Button from '@/components/Button';
+import { ErrorType } from '@/components/ErrorMessage';
+import Input from '@/components/Input';
+import TermsOfUse from '@/components/TermsOfUse';
+import PageContext from '@/hooks/page-context';
+import useApi from '@/hooks/use-api';
+
+import * as styles from './index.module.scss';
+
+type Props = {
+ type: 'sign-in' | 'register';
+};
+
+type FieldState = {
+ email: string;
+ termsAgreement: boolean;
+};
+
+type ErrorState = {
+ [key in keyof FieldState]?: ErrorType;
+};
+
+type FieldValidations = {
+ [key in keyof FieldState]: (state: FieldState) => ErrorType | undefined;
+};
+
+const defaultState: FieldState = { email: '', termsAgreement: false };
+
+const emailRegEx = /^\S+@\S+\.\S+$/;
+
+const EmailPasswordless = ({ type }: Props) => {
+ const { t, i18n } = useTranslation();
+ const [fieldState, setFieldState] = useState(defaultState);
+ const [fieldErrors, setFieldErrors] = useState({});
+ const { setToast } = useContext(PageContext);
+
+ const sendPasscode = type === 'sign-in' ? sendSignInEmailPasscode : sendRegisterEmailPasscode;
+
+ const { loading, error, result, run: asyncSendPasscode } = useApi(sendPasscode);
+
+ const validations = useMemo(
+ () => ({
+ email: ({ email }) => {
+ if (!emailRegEx.test(email)) {
+ return 'user.invalid_email';
+ }
+ },
+ termsAgreement: ({ termsAgreement }) => {
+ if (!termsAgreement) {
+ return 'form.terms_required';
+ }
+ },
+ }),
+ []
+ );
+
+ const onSubmitHandler = useCallback(() => {
+ // Should be removed after api redesign
+ if (loading) {
+ return;
+ }
+
+ const emailError = validations.email(fieldState);
+
+ if (emailError) {
+ setFieldErrors((previous) => ({ ...previous, email: emailError }));
+
+ return;
+ }
+
+ const termsAgreementError = validations.termsAgreement(fieldState);
+
+ if (termsAgreementError) {
+ setFieldErrors((previous) => ({ ...previous, termsAgreement: termsAgreementError }));
+
+ return;
+ }
+
+ void asyncSendPasscode(fieldState.email);
+ }, [loading, validations, fieldState, asyncSendPasscode]);
+
+ useEffect(() => {
+ // TODO: navigate to the passcode page
+ console.log(result);
+ }, [result]);
+
+ useEffect(() => {
+ // Clear errors
+ for (const key of Object.keys(fieldState) as [keyof FieldState]) {
+ if (fieldState[key]) {
+ setFieldErrors((previous) => {
+ if (!previous[key]) {
+ return previous;
+ }
+
+ return { ...previous, [key]: validations[key](fieldState) };
+ });
+ }
+ }
+ }, [fieldState, validations]);
+
+ useEffect(() => {
+ if (error) {
+ setToast(i18n.t(`errors:${error.code}`));
+ }
+ }, [error, i18n, setToast]);
+
+ return (
+
+ );
+};
+
+export default EmailPasswordless;
diff --git a/packages/ui/src/containers/Passwordless/PhonePasswordless.test.tsx b/packages/ui/src/containers/Passwordless/PhonePasswordless.test.tsx
new file mode 100644
index 000000000..7377f4a7d
--- /dev/null
+++ b/packages/ui/src/containers/Passwordless/PhonePasswordless.test.tsx
@@ -0,0 +1,92 @@
+import { fireEvent, render, waitFor } from '@testing-library/react';
+import React from 'react';
+
+import { sendPhonePasscode as sendRegisterPhonePasscode } from '@/apis/register';
+import { sendPhonePasscode as sendSignInPhonePasscode } from '@/apis/sign-in';
+import { defaultCountryCallingCode } from '@/hooks/use-phone-number';
+
+import PhonePasswordless from './PhonePasswordless';
+
+jest.mock('@/apis/sign-in', () => ({ sendPhonePasscode: jest.fn(async () => Promise.resolve()) }));
+jest.mock('@/apis/register', () => ({ sendPhonePasscode: jest.fn(async () => Promise.resolve()) }));
+
+describe('', () => {
+ const phoneNumber = '18888888888';
+
+ test('render', () => {
+ const { queryByText, container } = render();
+ expect(container.querySelector('input[name="phone"]')).not.toBeNull();
+ expect(queryByText('general.continue')).not.toBeNull();
+ expect(queryByText('sign_in.terms_of_use')).not.toBeNull();
+ });
+
+ test('required phone with error message', () => {
+ const { queryByText, container, getByText } = render();
+ const submitButton = getByText('general.continue');
+
+ fireEvent.click(submitButton);
+ expect(queryByText('errors:user.invalid_phone')).not.toBeNull();
+ expect(sendSignInPhonePasscode).not.toBeCalled();
+
+ const phoneInput = container.querySelector('input[name="phone"]');
+
+ if (phoneInput) {
+ fireEvent.change(phoneInput, { target: { value: '1113' } });
+ expect(queryByText('errors:user.invalid_phone')).not.toBeNull();
+
+ fireEvent.change(phoneInput, { target: { value: phoneNumber } });
+ expect(queryByText('errors:user.invalid_phone')).toBeNull();
+ }
+ });
+
+ test('required terms of agreement with error message', () => {
+ const { queryByText, container, getByText } = render();
+ const submitButton = getByText('general.continue');
+ const phoneInput = container.querySelector('input[name="phone"]');
+
+ if (phoneInput) {
+ fireEvent.change(phoneInput, { target: { value: phoneNumber } });
+ }
+
+ fireEvent.click(submitButton);
+ expect(queryByText('errors:form.terms_required')).not.toBeNull();
+ });
+
+ test('signin method properly', async () => {
+ const { container, getByText } = render();
+ const phoneInput = container.querySelector('input[name="phone"]');
+
+ if (phoneInput) {
+ fireEvent.change(phoneInput, { target: { value: phoneNumber } });
+ }
+ const termsButton = getByText('sign_in.terms_agreement_prefix');
+ fireEvent.click(termsButton);
+
+ const submitButton = getByText('general.continue');
+
+ await waitFor(() => {
+ fireEvent.click(submitButton);
+ });
+
+ expect(sendSignInPhonePasscode).toBeCalledWith(`${defaultCountryCallingCode}${phoneNumber}`);
+ });
+
+ test('register method properly', async () => {
+ const { container, getByText } = render();
+ const phoneInput = container.querySelector('input[name="phone"]');
+
+ if (phoneInput) {
+ fireEvent.change(phoneInput, { target: { value: phoneNumber } });
+ }
+ const termsButton = getByText('sign_in.terms_agreement_prefix');
+ fireEvent.click(termsButton);
+
+ const submitButton = getByText('general.continue');
+
+ await waitFor(() => {
+ fireEvent.click(submitButton);
+ });
+
+ expect(sendRegisterPhonePasscode).toBeCalledWith(`${defaultCountryCallingCode}${phoneNumber}`);
+ });
+});
diff --git a/packages/ui/src/containers/Passwordless/PhonePasswordless.tsx b/packages/ui/src/containers/Passwordless/PhonePasswordless.tsx
new file mode 100644
index 000000000..28b94509a
--- /dev/null
+++ b/packages/ui/src/containers/Passwordless/PhonePasswordless.tsx
@@ -0,0 +1,159 @@
+/**
+ * TODO:
+ * 1. API redesign handle api error and loading status globally in PageContext
+ * 2. Input field validation, should move the validation rule to the input field scope
+ * 4. Read terms of use settings from SignInExperience Settings
+ */
+import { LogtoErrorI18nKey } from '@logto/phrases';
+import classNames from 'classnames';
+import React, { useState, useCallback, useMemo, useEffect, useContext } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { sendPhonePasscode as sendRegisterPhonePasscode } from '@/apis/register';
+import { sendPhonePasscode as sendSignInPhonePasscode } from '@/apis/sign-in';
+import Button from '@/components/Button';
+import { ErrorType } from '@/components/ErrorMessage';
+import PhoneInput from '@/components/Input/PhoneInput';
+import TermsOfUse from '@/components/TermsOfUse';
+import PageContext from '@/hooks/page-context';
+import useApi from '@/hooks/use-api';
+import usePhoneNumber, { countryList } from '@/hooks/use-phone-number';
+
+import * as styles from './index.module.scss';
+
+type Props = {
+ type: 'sign-in' | 'register';
+};
+
+type FieldState = {
+ phone: string;
+ termsAgreement: boolean;
+};
+
+type ErrorState = {
+ [key in keyof FieldState]?: ErrorType;
+};
+
+type FieldValidations = {
+ [key in keyof FieldState]: (state: FieldState) => ErrorType | undefined;
+};
+
+const defaultState: FieldState = { phone: '', termsAgreement: false };
+
+const PhonePasswordless = ({ type }: Props) => {
+ const { t, i18n } = useTranslation();
+ const [fieldState, setFieldState] = useState(defaultState);
+ const [fieldErrors, setFieldErrors] = useState({});
+ const { setToast } = useContext(PageContext);
+
+ const { phoneNumber, setPhoneNumber, isValidPhoneNumber } = usePhoneNumber();
+
+ const sendPasscode = type === 'sign-in' ? sendSignInPhonePasscode : sendRegisterPhonePasscode;
+ const { loading, error, result, run: asyncSendPasscode } = useApi(sendPasscode);
+
+ const validations = useMemo(
+ () => ({
+ phone: ({ phone }) => {
+ if (!isValidPhoneNumber(phone)) {
+ return 'user.invalid_phone';
+ }
+ },
+ termsAgreement: ({ termsAgreement }) => {
+ if (!termsAgreement) {
+ return 'form.terms_required';
+ }
+ },
+ }),
+ [isValidPhoneNumber]
+ );
+
+ const onSubmitHandler = useCallback(() => {
+ // Should be removed after api redesign
+ if (loading) {
+ return;
+ }
+
+ const phoneError = validations.phone(fieldState);
+
+ if (phoneError) {
+ setFieldErrors((previous) => ({ ...previous, phone: phoneError }));
+
+ return;
+ }
+
+ const termsAgreementError = validations.termsAgreement(fieldState);
+
+ if (termsAgreementError) {
+ setFieldErrors((previous) => ({ ...previous, termsAgreement: termsAgreementError }));
+
+ return;
+ }
+
+ void asyncSendPasscode(fieldState.phone);
+ }, [loading, validations, fieldState, asyncSendPasscode]);
+
+ useEffect(() => {
+ setFieldState((previous) => ({
+ ...previous,
+ phone: `${phoneNumber.countryCallingCode}${phoneNumber.nationalNumber}`,
+ }));
+ }, [phoneNumber]);
+
+ useEffect(() => {
+ // TODO: navigate to the passcode page
+ console.log(result);
+ }, [result]);
+
+ useEffect(() => {
+ // Clear errors
+ for (const key of Object.keys(fieldState) as [keyof FieldState]) {
+ if (fieldState[key]) {
+ setFieldErrors((previous) => {
+ if (!previous[key]) {
+ return previous;
+ }
+
+ return { ...previous, [key]: validations[key](fieldState) };
+ });
+ }
+ }
+ }, [fieldState, validations]);
+
+ useEffect(() => {
+ if (error) {
+ setToast(i18n.t(`errors:${error.code}`));
+ }
+ }, [error, i18n, setToast]);
+
+ return (
+
+ );
+};
+
+export default PhonePasswordless;
diff --git a/packages/ui/src/containers/Passwordless/index.module.scss b/packages/ui/src/containers/Passwordless/index.module.scss
new file mode 100644
index 000000000..3a5811973
--- /dev/null
+++ b/packages/ui/src/containers/Passwordless/index.module.scss
@@ -0,0 +1,27 @@
+@use '@/scss/underscore' as _;
+
+.form {
+ width: 100%;
+ max-width: 320px;
+ @include _.flex-column;
+
+ > * {
+ width: 100%;
+ }
+
+ .inputField {
+ margin-bottom: _.unit(11);
+
+ &.withError {
+ margin-bottom: _.unit(9);
+ }
+ }
+
+ .terms {
+ margin-bottom: _.unit(6);
+
+ &.withError {
+ margin-bottom: _.unit(5);
+ }
+ }
+}
diff --git a/packages/ui/src/containers/Passwordless/index.tsx b/packages/ui/src/containers/Passwordless/index.tsx
new file mode 100644
index 000000000..877bd6119
--- /dev/null
+++ b/packages/ui/src/containers/Passwordless/index.tsx
@@ -0,0 +1,2 @@
+export { default as EmailPasswordless } from './EmailPasswordless';
+export { default as PhonePasswordless } from './PhonePasswordless';
diff --git a/packages/ui/src/containers/PhoneInputProvider/index.test.tsx b/packages/ui/src/containers/PhoneInputProvider/index.test.tsx
deleted file mode 100644
index f6d77b589..000000000
--- a/packages/ui/src/containers/PhoneInputProvider/index.test.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import { render, fireEvent } from '@testing-library/react';
-import React from 'react';
-
-import { defaultCountryCallingCode } from '@/hooks/use-phone-number';
-
-import PhoneInputProvider from '.';
-
-describe('Phone Input Provider', () => {
- const onChange = jest.fn();
-
- beforeEach(() => {
- onChange.mockClear();
- });
-
- it('render with empty input', () => {
- const { queryByText } = render(
-
- );
-
- expect(queryByText(`+${defaultCountryCallingCode}`)).not.toBeNull();
- });
-
- it('render with input', () => {
- const { queryByText, container } = render(
-
- );
-
- expect(queryByText('+1')).not.toBeNull();
- expect(container.querySelector('input')?.value).toEqual('911');
- });
-
- it('update country code', () => {
- const { container } = render(
-
- );
-
- const selector = container.querySelector('select');
-
- if (selector) {
- fireEvent.change(selector, { target: { value: '86' } });
- expect(onChange).toBeCalledWith('+86911');
- }
- });
-
- it('update national code', () => {
- const { container } = render(
-
- );
-
- const input = container.querySelector('input');
-
- if (input) {
- fireEvent.change(input, { target: { value: '119' } });
- expect(onChange).toBeCalledWith('+1119');
- }
- });
-
- it('clear national code', () => {
- const { container } = render(
-
- );
-
- const input = container.querySelector('input');
-
- if (!input) {
- return;
- }
-
- fireEvent.focus(input);
-
- const clearButton = container.querySelectorAll('svg');
- expect(clearButton).toHaveLength(2);
-
- if (clearButton[1]) {
- fireEvent.mouseDown(clearButton[1]);
- expect(onChange).toBeCalledWith('+1');
- }
- });
-});
diff --git a/packages/ui/src/containers/PhoneInputProvider/index.tsx b/packages/ui/src/containers/PhoneInputProvider/index.tsx
deleted file mode 100644
index 4869a43b7..000000000
--- a/packages/ui/src/containers/PhoneInputProvider/index.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-
-import PhoneInput from '@/components/Input/PhoneInput';
-import usePhoneNumber, { countryList } from '@/hooks/use-phone-number';
-
-export type Props = {
- name: string;
- autoComplete?: AutoCompleteType;
- isDisabled?: boolean;
- className?: string;
- placeholder?: string;
- value: string;
- onChange: (value: string) => void;
-};
-
-const PhoneInputProvider = ({ value, onChange, ...inputProps }: Props) => {
- // TODO: error message
- const {
- error,
- phoneNumber: { countryCallingCode, nationalNumber, interacted },
- setPhoneNumber,
- } = usePhoneNumber(value, onChange);
-
- return (
- {
- setPhoneNumber((phoneNumber) => ({ ...phoneNumber, ...data, interacted: true }));
- }}
- />
- );
-};
-
-export default PhoneInputProvider;
diff --git a/packages/ui/src/hooks/use-phone-number.ts b/packages/ui/src/hooks/use-phone-number.ts
index 7a9a09729..d1cc3b3a1 100644
--- a/packages/ui/src/hooks/use-phone-number.ts
+++ b/packages/ui/src/hooks/use-phone-number.ts
@@ -4,7 +4,7 @@
*/
import {
- parsePhoneNumber as _parsePhoneNumber,
+ parsePhoneNumberWithError,
getCountries,
getCountryCallingCode,
CountryCallingCode,
@@ -12,20 +12,12 @@ import {
E164Number,
ParseError,
} from 'libphonenumber-js';
-import { useState, useEffect } from 'react';
+import { useState } from 'react';
// Should not need the react-phone-number-input package, but we use its locale country name for now
import en from 'react-phone-number-input/locale/en.json';
export type { CountryCallingCode } from 'libphonenumber-js';
-/**
- * TODO: Get Default Country Code
- */
-const defaultCountryCode: CountryCode = 'CN';
-
-export const defaultCountryCallingCode: CountryCallingCode =
- getCountryCallingCode(defaultCountryCode);
-
/**
* Provide Country Code Options
* TODO: Country Name i18n
@@ -52,9 +44,6 @@ type PhoneNumberData = {
nationalNumber: string;
};
-// Add interact status to prevent the initial onUpdate useEffect call
-type PhoneNumberState = PhoneNumberData & { interacted: boolean };
-
const parseE164Number = (value: string): E164Number | '' => {
if (!value || value.startsWith('+')) {
return value;
@@ -63,65 +52,33 @@ const parseE164Number = (value: string): E164Number | '' => {
return `+${value}`;
};
-export const parsePhoneNumber = (value: string): [ParseError?, PhoneNumberData?] => {
+const isValidPhoneNumber = (value: string): boolean => {
try {
- const phoneNumber = _parsePhoneNumber(parseE164Number(value));
- const { countryCallingCode, nationalNumber } = phoneNumber;
+ const phoneNumber = parsePhoneNumberWithError(parseE164Number(value));
- return [undefined, { countryCallingCode, nationalNumber }];
+ return phoneNumber.isValid();
} catch (error: unknown) {
if (error instanceof ParseError) {
- return [error];
+ return false;
}
throw error;
}
};
-const usePhoneNumber = (value: string, onChangeCallback: (value: string) => void) => {
- // TODO: phoneNumber format based on country
+export const defaultCountryCode: CountryCode = 'CN';
+export const defaultCountryCallingCode = getCountryCallingCode(defaultCountryCode);
- const [phoneNumber, setPhoneNumber] = useState({
+const usePhoneNumber = () => {
+ // TODO: Get Default Country Code
+ const [phoneNumber, setPhoneNumber] = useState({
countryCallingCode: defaultCountryCallingCode,
nationalNumber: '',
- interacted: false,
});
- const [error, setError] = useState();
-
- useEffect(() => {
- // Only run on data initialization
- if (phoneNumber.interacted) {
- return;
- }
-
- const [parseError, result] = parsePhoneNumber(value);
- setError(parseError);
-
- if (result) {
- const { countryCallingCode, nationalNumber } = result;
- setPhoneNumber((previous) => ({
- ...previous,
- countryCallingCode,
- nationalNumber,
- }));
- }
- }, [phoneNumber.interacted, value]);
-
- useEffect(() => {
- // Only run after data initialization
- if (!phoneNumber.interacted) {
- return;
- }
-
- const { countryCallingCode, nationalNumber } = phoneNumber;
- const [parseError] = parsePhoneNumber(`${countryCallingCode}${nationalNumber}`);
- setError(parseError);
- onChangeCallback(`+${countryCallingCode}${nationalNumber}`);
- }, [onChangeCallback, phoneNumber]);
return {
- error,
phoneNumber,
setPhoneNumber,
+ isValidPhoneNumber,
};
};
diff --git a/packages/ui/src/pages/SecondarySignIn/index.module.scss b/packages/ui/src/pages/SecondarySignIn/index.module.scss
new file mode 100644
index 000000000..4364e12e6
--- /dev/null
+++ b/packages/ui/src/pages/SecondarySignIn/index.module.scss
@@ -0,0 +1,22 @@
+@use '@/scss/underscore' as _;
+
+.wrapper {
+ position: relative;
+ padding: _.unit(8);
+ @include _.flex-column;
+}
+
+.navBar {
+ width: 100%;
+ margin-bottom: _.unit(6);
+
+ svg {
+ margin-left: _.unit(-2);
+ }
+}
+
+.title {
+ width: 100%;
+ @include _.title;
+ margin-bottom: _.unit(9);
+}
diff --git a/packages/ui/src/pages/SecondarySignIn/index.tsx b/packages/ui/src/pages/SecondarySignIn/index.tsx
new file mode 100644
index 000000000..1d5b82b53
--- /dev/null
+++ b/packages/ui/src/pages/SecondarySignIn/index.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { useHistory } from 'react-router-dom';
+
+import NavArrowIcon from '@/components/Icons/NavArrowIcon';
+import { PhonePasswordless } from '@/containers/Passwordless';
+
+import * as styles from './index.module.scss';
+
+const SecondarySignIn = () => {
+ const { t } = useTranslation();
+ const history = useHistory();
+
+ return (
+
+
+ {
+ history.goBack();
+ }}
+ />
+
+
{t('sign_in.sign_in')}
+
+
+ );
+};
+
+export default SecondarySignIn;