mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
fix(ui): fix no-restrict-syntax in ui (#1559)
* fix(ui): fix no-restrict-syntax in ui fix no-restricted-syntax in ui * fix(ui): cr update cr update * fix(ui): fix ts error fix ts error
This commit is contained in:
parent
4532081426
commit
816ce9f903
13 changed files with 70 additions and 59 deletions
|
@ -63,6 +63,7 @@
|
|||
"react-string-replace": "^1.0.0",
|
||||
"react-timer-hook": "^3.0.5",
|
||||
"stylelint": "^14.8.2",
|
||||
"superstruct": "^0.16.0",
|
||||
"typescript": "^4.6.2",
|
||||
"use-debounced-loader": "^0.1.1"
|
||||
},
|
||||
|
@ -72,8 +73,7 @@
|
|||
"eslintConfig": {
|
||||
"extends": "@silverhand/react",
|
||||
"rules": {
|
||||
"complexity": "off",
|
||||
"no-restricted-syntax": "off"
|
||||
"complexity": "off"
|
||||
}
|
||||
},
|
||||
"stylelint": {
|
||||
|
|
|
@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import Dropdown, { DropdownItem } from '@/components/Dropdown';
|
||||
import useSocial from '@/hooks/use-social';
|
||||
import { ConnectorData } from '@/types';
|
||||
import { isKeyOf } from '@/utils';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -47,8 +48,7 @@ const SocialSignInDropdown = ({ isOpen, onClose, connectors, anchorRef }: Props)
|
|||
>
|
||||
{connectors.map((connector) => {
|
||||
const { id, name, logo, logoDark } = connector;
|
||||
const languageKey = Object.keys(name).find((key) => key === language) ?? 'en';
|
||||
const localName = name[languageKey as Language];
|
||||
const localName = isKeyOf(language, name) ? name[language] : name[Language.English];
|
||||
|
||||
return (
|
||||
<DropdownItem
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
import { Optional } from '@silverhand/essentials';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useCallback, useEffect, useContext, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { is } from 'superstruct';
|
||||
|
||||
import { registerWithSocial, bindSocialRelatedUser } from '@/apis/social';
|
||||
import useApi from '@/hooks/use-api';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import { LocalSignInMethod } from '@/types';
|
||||
|
||||
type LocationState = {
|
||||
relatedUser?: string;
|
||||
};
|
||||
import { bindSocialStateGuard } from '@/types/guard';
|
||||
|
||||
const useBindSocial = () => {
|
||||
const state = useLocation().state as Optional<LocationState>;
|
||||
const { state } = useLocation();
|
||||
const { experienceSettings } = useContext(PageContext);
|
||||
const { result: registerResult, run: asyncRegisterWithSocial } = useApi(registerWithSocial);
|
||||
const { result: bindUserResult, run: asyncBindSocialRelatedUser } = useApi(bindSocialRelatedUser);
|
||||
|
@ -54,7 +52,7 @@ const useBindSocial = () => {
|
|||
|
||||
return {
|
||||
localSignInMethods,
|
||||
relatedUser: state?.relatedUser,
|
||||
relatedUser: conditional(is(state, bindSocialStateGuard) && state.relatedUser),
|
||||
registerWithSocial: createAccountHandler,
|
||||
bindSocialRelatedUser: bindRelatedUserHandler,
|
||||
};
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { useState, useCallback, useEffect, useRef, FormEvent } from 'react';
|
||||
|
||||
import { ErrorType } from '@/components/ErrorMessage';
|
||||
import { entries } from '@/utils';
|
||||
import { entries, fromEntries, Entries } from '@/utils';
|
||||
|
||||
const useForm = <T>(initialState: T) => {
|
||||
const useForm = <T extends Record<string, unknown>>(initialState: T) => {
|
||||
type ErrorState = {
|
||||
[key in keyof T]?: ErrorType;
|
||||
};
|
||||
|
@ -19,12 +19,12 @@ const useForm = <T>(initialState: T) => {
|
|||
const fieldValidationsRef = useRef<FieldValidations>({});
|
||||
|
||||
const validateForm = useCallback(() => {
|
||||
const errors = entries(fieldValue).map<[keyof T, ErrorType | undefined]>(([key, value]) => [
|
||||
const errors: Entries<ErrorState> = entries(fieldValue).map(([key, value]) => [
|
||||
key,
|
||||
fieldValidationsRef.current[key]?.(value),
|
||||
]);
|
||||
|
||||
setFieldErrors(Object.fromEntries(errors) as ErrorState);
|
||||
setFieldErrors(fromEntries(errors));
|
||||
|
||||
return errors.every(([, error]) => error === undefined);
|
||||
}, [fieldValidationsRef, fieldValue]);
|
||||
|
@ -37,8 +37,7 @@ const useForm = <T>(initialState: T) => {
|
|||
return {
|
||||
value: fieldValue[field],
|
||||
error: fieldErrors[field],
|
||||
onChange: ({ target }: FormEvent<HTMLInputElement>) => {
|
||||
const { value } = target as HTMLInputElement;
|
||||
onChange: ({ currentTarget: { value } }: FormEvent<HTMLInputElement>) => {
|
||||
setFieldValue((previous) => ({ ...previous, [field]: value }));
|
||||
},
|
||||
};
|
||||
|
@ -49,13 +48,13 @@ const useForm = <T>(initialState: T) => {
|
|||
// Revalidate on Input change
|
||||
useEffect(() => {
|
||||
setFieldErrors((previous) => {
|
||||
const errors = entries(fieldValue).map<[keyof T, ErrorType | undefined]>(([key, value]) => [
|
||||
const errors: Entries<ErrorState> = entries(fieldValue).map(([key, value]) => [
|
||||
key,
|
||||
// Only validate field with existing errors
|
||||
previous[key] && fieldValidationsRef.current[key]?.(value),
|
||||
]);
|
||||
|
||||
return Object.fromEntries(errors) as ErrorState;
|
||||
return fromEntries(errors);
|
||||
});
|
||||
}, [fieldValue, fieldValidationsRef]);
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Language } from '@logto/phrases-ui';
|
||||
import { AppearanceMode, ConnectorPlatform } from '@logto/schemas';
|
||||
import { ConnectorPlatform } from '@logto/schemas';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import i18next from 'i18next';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
@ -7,19 +6,11 @@ import { useEffect, useState } from 'react';
|
|||
import * as styles from '@/App.module.scss';
|
||||
import { Context } from '@/hooks/use-page-context';
|
||||
import initI18n from '@/i18n/init';
|
||||
import { SignInExperienceSettingsResponse, SignInExperienceSettings, Platform } from '@/types';
|
||||
import { SignInExperienceSettings, PreviewConfig } from '@/types';
|
||||
import { parseQueryParameters } from '@/utils';
|
||||
import { getPrimarySignInMethod, getSecondarySignInMethods } from '@/utils/sign-in-experience';
|
||||
import { filterPreviewSocialConnectors } from '@/utils/social-connectors';
|
||||
|
||||
type PreviewConfig = {
|
||||
signInExperience: SignInExperienceSettingsResponse;
|
||||
language: Language;
|
||||
mode: AppearanceMode.LightMode | AppearanceMode.DarkMode;
|
||||
platform: Platform;
|
||||
isNative: boolean;
|
||||
};
|
||||
|
||||
const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
||||
const [previewConfig, setPreviewConfig] = useState<PreviewConfig>();
|
||||
const { setTheme, setExperienceSettings, setPlatform } = context;
|
||||
|
@ -44,6 +35,8 @@ const usePreview = (context: Context): [boolean, PreviewConfig?] => {
|
|||
}
|
||||
|
||||
if (event.data.sender === 'ac_preview') {
|
||||
// #event.data should be guarded at the provider's side
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
setPreviewConfig(event.data.config as PreviewConfig);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -21,9 +21,7 @@ const useSocialSignInListener = () => {
|
|||
if (parameters.connector) {
|
||||
navigate(`/social/register/${parameters.connector}`, {
|
||||
replace: true,
|
||||
state: {
|
||||
...(error.data as Record<string, unknown> | undefined),
|
||||
},
|
||||
state: error.data,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { Nullable } from '@silverhand/essentials';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { is } from 'superstruct';
|
||||
|
||||
import NavBar from '@/components/NavBar';
|
||||
import PasscodeValidation from '@/containers/PasscodeValidation';
|
||||
import { PageContext } from '@/hooks/use-page-context';
|
||||
import ErrorPage from '@/pages/ErrorPage';
|
||||
import { UserFlow } from '@/types';
|
||||
import { passcodeStateGuard, passcodeMethodGuard } from '@/types/guard';
|
||||
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -16,30 +15,23 @@ type Parameters = {
|
|||
method: string;
|
||||
};
|
||||
|
||||
type StateType = Nullable<Record<string, string>>;
|
||||
|
||||
const Passcode = () => {
|
||||
const { t } = useTranslation();
|
||||
const { method, type } = useParams<Parameters>();
|
||||
const state = useLocation().state as StateType;
|
||||
const { state } = useLocation();
|
||||
const invalidType = type !== 'sign-in' && type !== 'register';
|
||||
const invalidMethod = method !== 'email' && method !== 'sms';
|
||||
const { setToast } = useContext(PageContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (method && !state?.[method]) {
|
||||
setToast(t(method === 'email' ? 'error.invalid_email' : 'error.invalid_phone'));
|
||||
}
|
||||
}, [method, setToast, state, t]);
|
||||
const invalidMethod = !is(method, passcodeMethodGuard);
|
||||
const invalidState = !is(state, passcodeStateGuard);
|
||||
|
||||
if (invalidType || invalidMethod) {
|
||||
return <ErrorPage />;
|
||||
}
|
||||
|
||||
const target = state?.[method];
|
||||
const target = !invalidState && state[method];
|
||||
|
||||
if (!target) {
|
||||
return <ErrorPage />;
|
||||
return <ErrorPage title={method === 'email' ? 'error.invalid_email' : 'error.invalid_phone'} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
12
packages/ui/src/types/guard.ts
Normal file
12
packages/ui/src/types/guard.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as s from 'superstruct';
|
||||
|
||||
export const bindSocialStateGuard = s.object({
|
||||
relatedUser: s.optional(s.string()),
|
||||
});
|
||||
|
||||
export const passcodeStateGuard = s.object({
|
||||
email: s.optional(s.string()),
|
||||
sms: s.optional(s.string()),
|
||||
});
|
||||
|
||||
export const passcodeMethodGuard = s.union([s.literal('email'), s.literal('sms')]);
|
|
@ -1,4 +1,5 @@
|
|||
import { SignInExperience, ConnectorMetadata } from '@logto/schemas';
|
||||
import { Language } from '@logto/phrases-ui';
|
||||
import { SignInExperience, ConnectorMetadata, AppearanceMode } from '@logto/schemas';
|
||||
|
||||
export type UserFlow = 'sign-in' | 'register';
|
||||
export type SignInMethod = 'username' | 'email' | 'sms' | 'social';
|
||||
|
@ -34,3 +35,11 @@ export type SignInExperienceSettings = Omit<
|
|||
export enum TermsOfUseModalMessage {
|
||||
SHOW_DETAIL_MODAL = 'SHOW_DETAIL_MODAL',
|
||||
}
|
||||
|
||||
export type PreviewConfig = {
|
||||
signInExperience: SignInExperienceSettingsResponse;
|
||||
language: Language;
|
||||
mode: AppearanceMode.LightMode | AppearanceMode.DarkMode;
|
||||
platform: Platform;
|
||||
isNative: boolean;
|
||||
};
|
||||
|
|
|
@ -18,6 +18,7 @@ export const countryCallingCodeMap: Record<string, CountryCode> = {
|
|||
export const isValidCountryCode = (countryCode: string): countryCode is CountryCode => {
|
||||
try {
|
||||
// Use getCountryCallingCode method to guard the input's value is in CountryCode union type, if type not match exceptions are expected
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getCountryCallingCode(countryCode as CountryCode);
|
||||
|
||||
return true;
|
||||
|
|
|
@ -26,16 +26,21 @@ export const getSearchParameters = (parameters: string | URLSearchParams, key: s
|
|||
return searchParameters.get(key) ?? undefined;
|
||||
};
|
||||
|
||||
type Entries<T> = Array<
|
||||
export type Entries<T> = Array<
|
||||
{
|
||||
[K in keyof T]: [K, T[K]];
|
||||
}[keyof T]
|
||||
>;
|
||||
|
||||
export const entries = <T>(object: T): Entries<T> => Object.entries(object) as Entries<T>;
|
||||
export const entries = <T extends Record<string, unknown>>(object: T): Entries<T> =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
Object.entries(object) as Entries<T>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export const inOperator = <K extends string, T extends object>(
|
||||
key: K,
|
||||
export const fromEntries = <T extends Record<string, unknown>>(entries: Entries<T>) =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
Object.fromEntries(entries) as T;
|
||||
|
||||
export const isKeyOf = <T extends Record<string, unknown>>(
|
||||
key: string | number | symbol,
|
||||
object: T
|
||||
): object is T & Record<K, unknown> => key in object;
|
||||
): key is keyof T => key in object;
|
||||
|
|
|
@ -9,10 +9,12 @@ import { getSignInExperience } from '@/apis/settings';
|
|||
import { SignInMethod, SignInExperienceSettingsResponse, SignInExperienceSettings } from '@/types';
|
||||
import { filterSocialConnectors } from '@/utils/social-connectors';
|
||||
|
||||
import { entries } from '.';
|
||||
|
||||
export const getPrimarySignInMethod = (signInMethods: SignInMethods) => {
|
||||
for (const [key, value] of Object.entries(signInMethods)) {
|
||||
for (const [key, value] of entries(signInMethods)) {
|
||||
if (value === 'primary') {
|
||||
return key as keyof SignInMethods;
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,9 +22,9 @@ export const getPrimarySignInMethod = (signInMethods: SignInMethods) => {
|
|||
};
|
||||
|
||||
export const getSecondarySignInMethods = (signInMethods: SignInMethods) =>
|
||||
Object.entries(signInMethods).reduce<SignInMethod[]>((methods, [key, value]) => {
|
||||
entries(signInMethods).reduce<SignInMethod[]>((methods, [key, value]) => {
|
||||
if (value === 'secondary') {
|
||||
return [...methods, key as SignInMethod];
|
||||
return [...methods, key];
|
||||
}
|
||||
|
||||
return methods;
|
||||
|
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
|
@ -1225,6 +1225,7 @@ importers:
|
|||
react-string-replace: ^1.0.0
|
||||
react-timer-hook: ^3.0.5
|
||||
stylelint: ^14.8.2
|
||||
superstruct: ^0.16.0
|
||||
typescript: ^4.6.2
|
||||
use-debounced-loader: ^0.1.1
|
||||
dependencies:
|
||||
|
@ -1277,6 +1278,7 @@ importers:
|
|||
react-string-replace: 1.0.0
|
||||
react-timer-hook: 3.0.5_sfoxds7t5ydpegc3knd667wn6m
|
||||
stylelint: 14.8.2
|
||||
superstruct: 0.16.0
|
||||
typescript: 4.6.2
|
||||
use-debounced-loader: 0.1.1_react@17.0.2
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue