0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-02-17 22:04:19 -05:00

refactor(ui): useSocial hooks refactor (#956)

* refactor(ui): useSocial hooks refactor

useSOcial hooks refactor

* fix(ui): remove unnecsessary loading on setting query

remove unnecessary loading
This commit is contained in:
simeng-li 2022-05-26 18:15:41 +08:00 committed by GitHub
parent 9d8ef7632b
commit ab7a35267a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 195 additions and 145 deletions

View file

@ -32,16 +32,12 @@ const App = () => {
}
(async () => {
setLoading(true);
const settings = await getSignInExperienceSettings();
// Note: i18n must be initialized ahead of global experience settings
await initI18n(settings.languageInfo);
setExperienceSettings(settings);
setLoading(false);
})();
}, [isPreview, setExperienceSettings, setLoading]);

View file

@ -1,5 +1,7 @@
import React from 'react';
import useSocialSignInListener from '@/hooks/use-social-signin-listener';
import SocialSignInList from '../SocialSignInList';
export const defaultSize = 3;
@ -9,6 +11,8 @@ type Props = {
};
const PrimarySocialSignIn = ({ className }: Props) => {
useSocialSignInListener();
return <SocialSignInList className={className} />;
};

View file

@ -2,6 +2,7 @@ import React, { useMemo, useState, useRef } from 'react';
import usePlatform from '@/hooks/use-platform';
import useSocial from '@/hooks/use-social';
import useSocialSignInListener from '@/hooks/use-social-signin-listener';
import SocialSignInDropdown from '../SocialSignInDropdown';
import SocialSignInIconList from '../SocialSignInIconList';
@ -19,6 +20,8 @@ const SecondarySocialSignIn = ({ className }: Props) => {
const moreButtonRef = useRef<HTMLButtonElement>(null);
const { isMobile } = usePlatform();
useSocialSignInListener();
const isCollapsed = socialConnectors.length > defaultSize;
const displayConnectors = useMemo(() => {

View file

@ -15,8 +15,8 @@ type UseApi<T extends any[], U> = {
export type ErrorHandlers = {
[key in LogtoErrorCode]?: (error: RequestErrorBody) => void;
} & {
global?: (error: RequestErrorBody) => void;
callback?: (error: RequestErrorBody) => void;
global?: (error: RequestErrorBody) => void; // Overwrite default global error handle logic
callback?: (error: RequestErrorBody) => void; // Callback method
};
function useApi<Args extends any[], Response>(

View file

@ -0,0 +1,31 @@
import { useEffect, useContext } from 'react';
import { PageContext } from './use-page-context';
import { isNativeWebview } from './utils';
const useNativeMessageListener = () => {
const { setToast } = useContext(PageContext);
// Monitor Native Error Message
useEffect(() => {
if (!isNativeWebview()) {
return;
}
const nativeMessageHandler = (event: MessageEvent) => {
if (event.origin === window.location.origin) {
try {
setToast(JSON.stringify(event.data));
} catch {}
}
};
window.addEventListener('message', nativeMessageHandler);
return () => {
window.removeEventListener('message', nativeMessageHandler);
};
}, [setToast]);
};
export default useNativeMessageListener;

View file

@ -0,0 +1,66 @@
import { useCallback, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom';
import { parseQueryParameters } from '@/utils';
import { PageContext } from './use-page-context';
import { decodeState } from './utils';
const useSocialCallbackHandler = () => {
const { setToast } = useContext(PageContext);
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const parameters = useParams();
const navigate = useNavigate();
const socialCallbackHandler = useCallback(() => {
const { state, code, error, error_description } = parseQueryParameters(window.location.search);
const connectorId = parameters.connector;
// Connector auth error
if (error) {
setToast(`${error}${error_description ? `: ${error_description}` : ''}`);
}
// Connector auth missing state
if (!state || !code || !connectorId) {
setToast(t('error.missing_auth_data'));
return;
}
const decodedState = decodeState(state);
// Invalid state
if (!decodedState) {
setToast(t('error.missing_auth_data'));
return;
}
const { platform, callbackLink } = decodedState;
// Web/Mobile-Web redirect to sign-in/callback page to login
if (platform === 'web') {
navigate(
new URL(`${location.origin}/sign-in/callback/${connectorId}/${window.location.search}`),
{
replace: true,
}
);
return;
}
// Native Webview redirect to native app
if (!callbackLink) {
throw new Error('CallbackLink is empty');
}
window.location.assign(new URL(`${callbackLink}${window.location.search}`));
}, [navigate, parameters.connector, setToast, t]);
return socialCallbackHandler;
};
export default useSocialCallbackHandler;

View file

@ -0,0 +1,81 @@
import { useEffect, useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom';
import { signInWithSocial } from '@/apis/social';
import { parseQueryParameters } from '@/utils';
import useApi, { ErrorHandlers } from './use-api';
import useNativeMessageListener from './use-native-message-listener';
import { PageContext } from './use-page-context';
import { stateValidation } from './utils';
const useSocialSignInListener = () => {
const { setToast } = useContext(PageContext);
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const parameters = useParams();
const navigate = useNavigate();
useNativeMessageListener();
const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.identity_not_exists': (error) => {
if (parameters.connector) {
navigate(`/social-register/${parameters.connector}`, {
replace: true,
state: {
...(error.data as Record<string, unknown> | undefined),
},
});
}
},
}),
[navigate, parameters.connector]
);
const { result, run: asyncSignInWithSocial } = useApi(
signInWithSocial,
signInWithSocialErrorHandlers
);
const signInWithSocialHandler = useCallback(
async (connectorId: string, state: string, code: string) => {
void asyncSignInWithSocial({
connectorId,
code,
redirectUri: `${origin}/callback/${connectorId}`, // For validation use only
});
},
[asyncSignInWithSocial]
);
useEffect(() => {
if (result?.redirectTo) {
window.location.assign(result.redirectTo);
}
}, [result]);
// Social Sign-In Callback Handler
useEffect(() => {
if (!location.pathname.includes('/sign-in/callback') || !parameters.connector) {
return;
}
const { state, code } = parseQueryParameters(window.location.search);
if (!state || !code) {
return;
}
if (!stateValidation(state, parameters.connector)) {
setToast(t('error.invalid_connector_auth'));
return;
}
void signInWithSocialHandler(parameters.connector, state, code);
}, [parameters.connector, setToast, signInWithSocialHandler, t]);
};
export default useSocialSignInListener;

View file

@ -1,49 +1,18 @@
import { useEffect, useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams, useNavigate } from 'react-router-dom';
import { useCallback, useContext } from 'react';
import { invokeSocialSignIn, signInWithSocial } from '@/apis/social';
import { parseQueryParameters } from '@/utils';
import { invokeSocialSignIn } from '@/apis/social';
import useApi, { ErrorHandlers } from './use-api';
import useApi from './use-api';
import { PageContext } from './use-page-context';
import useTerms from './use-terms';
import {
getLogtoNativeSdk,
isNativeWebview,
generateState,
decodeState,
stateValidation,
storeState,
} from './utils';
import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from './utils';
const useSocial = () => {
const { setToast, experienceSettings } = useContext(PageContext);
const { experienceSettings } = useContext(PageContext);
const { termsValidation } = useTerms();
const parameters = useParams();
const navigate = useNavigate();
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const signInWithSocialErrorHandlers: ErrorHandlers = useMemo(
() => ({
'user.identity_not_exists': (error) => {
if (parameters.connector) {
navigate(`/social-register/${parameters.connector}`, {
replace: true,
state: {
...(error.data as Record<string, unknown> | undefined),
},
});
}
},
}),
[navigate, parameters.connector]
);
const { run: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
const { run: asyncSignInWithSocial } = useApi(signInWithSocial, signInWithSocialErrorHandlers);
const invokeSocialSignInHandler = useCallback(
async (connectorId: string, callback?: () => void) => {
if (!(await termsValidation())) {
@ -84,110 +53,9 @@ const useSocial = () => {
[asyncInvokeSocialSignIn, termsValidation]
);
const signInWithSocialHandler = useCallback(
async (connectorId: string, state: string, code: string) => {
if (!stateValidation(state, connectorId)) {
setToast(t('error.invalid_connector_auth'));
return;
}
const result = await asyncSignInWithSocial({
connectorId,
code,
redirectUri: `${origin}/callback/${connectorId}`,
});
if (result?.redirectTo) {
window.location.assign(result.redirectTo);
}
},
[asyncSignInWithSocial, setToast, t]
);
const socialCallbackHandler = useCallback(() => {
const { state, code, error, error_description } = parseQueryParameters(window.location.search);
const connectorId = parameters.connector;
if (error) {
setToast(`${error}${error_description ? `: ${error_description}` : ''}`);
}
if (!state || !code || !connectorId) {
setToast(t('error.missing_auth_data'));
return;
}
const decodedState = decodeState(state);
if (!decodedState) {
setToast(t('error.missing_auth_data'));
return;
}
const { platform, callbackLink } = decodedState;
if (platform === 'web') {
navigate(
new URL(`${location.origin}/sign-in/callback/${connectorId}/${window.location.search}`),
{
replace: true,
}
);
return;
}
if (!callbackLink) {
// CallbackLink should not empty for native webview
throw new Error('CallbackLink is empty');
}
window.location.assign(new URL(`${callbackLink}${window.location.search}`));
}, [navigate, parameters.connector, setToast, t]);
// Social Sign-In Callback Handler
useEffect(() => {
if (!location.pathname.includes('/sign-in/callback') || !parameters.connector) {
return;
}
const { state, code } = parseQueryParameters(window.location.search);
if (!state || !code) {
return;
}
void signInWithSocialHandler(parameters.connector, state, code);
}, [parameters.connector, signInWithSocialHandler]);
// Monitor Native Error Message
useEffect(() => {
if (!isNativeWebview()) {
return;
}
const nativeMessageHandler = (event: MessageEvent) => {
if (event.origin === window.location.origin) {
try {
setToast(JSON.stringify(event.data));
} catch {}
}
};
window.addEventListener('message', nativeMessageHandler);
return () => {
window.removeEventListener('message', nativeMessageHandler);
};
}, [setToast]);
return {
socialConnectors: experienceSettings?.socialConnectors ?? [],
invokeSocialSignIn: invokeSocialSignInHandler,
socialCallbackHandler,
};
};

View file

@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom';
import Button from '@/components/Button';
import { PageContext } from '@/hooks/use-page-context';
import useSocial from '@/hooks/use-social';
import useSocialCallbackHandler from '@/hooks/use-social-callback-handler';
import * as styles from './index.module.scss';
@ -14,10 +14,11 @@ type Props = {
const Callback = () => {
const { connector: connectorId } = useParams<Props>();
const { socialCallbackHandler } = useSocial();
const { experienceSettings } = useContext(PageContext);
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
const socialCallbackHandler = useSocialCallbackHandler();
const connectorLabel = useMemo(() => {
const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId);