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:
parent
9d8ef7632b
commit
ab7a35267a
9 changed files with 195 additions and 145 deletions
|
@ -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]);
|
||||
|
||||
|
|
|
@ -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} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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>(
|
||||
|
|
31
packages/ui/src/hooks/use-native-message-listener.ts
Normal file
31
packages/ui/src/hooks/use-native-message-listener.ts
Normal 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;
|
66
packages/ui/src/hooks/use-social-callback-handler.ts
Normal file
66
packages/ui/src/hooks/use-social-callback-handler.ts
Normal 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;
|
81
packages/ui/src/hooks/use-social-signin-listener.ts
Normal file
81
packages/ui/src/hooks/use-social-signin-listener.ts
Normal 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;
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue