mirror of
https://github.com/logto-io/logto.git
synced 2025-02-24 22:05:56 -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 () => {
|
(async () => {
|
||||||
setLoading(true);
|
|
||||||
|
|
||||||
const settings = await getSignInExperienceSettings();
|
const settings = await getSignInExperienceSettings();
|
||||||
|
|
||||||
// Note: i18n must be initialized ahead of global experience settings
|
// Note: i18n must be initialized ahead of global experience settings
|
||||||
await initI18n(settings.languageInfo);
|
await initI18n(settings.languageInfo);
|
||||||
|
|
||||||
setExperienceSettings(settings);
|
setExperienceSettings(settings);
|
||||||
|
|
||||||
setLoading(false);
|
|
||||||
})();
|
})();
|
||||||
}, [isPreview, setExperienceSettings, setLoading]);
|
}, [isPreview, setExperienceSettings, setLoading]);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import useSocialSignInListener from '@/hooks/use-social-signin-listener';
|
||||||
|
|
||||||
import SocialSignInList from '../SocialSignInList';
|
import SocialSignInList from '../SocialSignInList';
|
||||||
|
|
||||||
export const defaultSize = 3;
|
export const defaultSize = 3;
|
||||||
|
@ -9,6 +11,8 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const PrimarySocialSignIn = ({ className }: Props) => {
|
const PrimarySocialSignIn = ({ className }: Props) => {
|
||||||
|
useSocialSignInListener();
|
||||||
|
|
||||||
return <SocialSignInList className={className} />;
|
return <SocialSignInList className={className} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React, { useMemo, useState, useRef } from 'react';
|
||||||
|
|
||||||
import usePlatform from '@/hooks/use-platform';
|
import usePlatform from '@/hooks/use-platform';
|
||||||
import useSocial from '@/hooks/use-social';
|
import useSocial from '@/hooks/use-social';
|
||||||
|
import useSocialSignInListener from '@/hooks/use-social-signin-listener';
|
||||||
|
|
||||||
import SocialSignInDropdown from '../SocialSignInDropdown';
|
import SocialSignInDropdown from '../SocialSignInDropdown';
|
||||||
import SocialSignInIconList from '../SocialSignInIconList';
|
import SocialSignInIconList from '../SocialSignInIconList';
|
||||||
|
@ -19,6 +20,8 @@ const SecondarySocialSignIn = ({ className }: Props) => {
|
||||||
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
const moreButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
const { isMobile } = usePlatform();
|
const { isMobile } = usePlatform();
|
||||||
|
|
||||||
|
useSocialSignInListener();
|
||||||
|
|
||||||
const isCollapsed = socialConnectors.length > defaultSize;
|
const isCollapsed = socialConnectors.length > defaultSize;
|
||||||
|
|
||||||
const displayConnectors = useMemo(() => {
|
const displayConnectors = useMemo(() => {
|
||||||
|
|
|
@ -15,8 +15,8 @@ type UseApi<T extends any[], U> = {
|
||||||
export type ErrorHandlers = {
|
export type ErrorHandlers = {
|
||||||
[key in LogtoErrorCode]?: (error: RequestErrorBody) => void;
|
[key in LogtoErrorCode]?: (error: RequestErrorBody) => void;
|
||||||
} & {
|
} & {
|
||||||
global?: (error: RequestErrorBody) => void;
|
global?: (error: RequestErrorBody) => void; // Overwrite default global error handle logic
|
||||||
callback?: (error: RequestErrorBody) => void;
|
callback?: (error: RequestErrorBody) => void; // Callback method
|
||||||
};
|
};
|
||||||
|
|
||||||
function useApi<Args extends any[], Response>(
|
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 { useCallback, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { invokeSocialSignIn, signInWithSocial } from '@/apis/social';
|
import { invokeSocialSignIn } from '@/apis/social';
|
||||||
import { parseQueryParameters } from '@/utils';
|
|
||||||
|
|
||||||
import useApi, { ErrorHandlers } from './use-api';
|
import useApi from './use-api';
|
||||||
import { PageContext } from './use-page-context';
|
import { PageContext } from './use-page-context';
|
||||||
import useTerms from './use-terms';
|
import useTerms from './use-terms';
|
||||||
import {
|
import { getLogtoNativeSdk, isNativeWebview, generateState, storeState } from './utils';
|
||||||
getLogtoNativeSdk,
|
|
||||||
isNativeWebview,
|
|
||||||
generateState,
|
|
||||||
decodeState,
|
|
||||||
stateValidation,
|
|
||||||
storeState,
|
|
||||||
} from './utils';
|
|
||||||
|
|
||||||
const useSocial = () => {
|
const useSocial = () => {
|
||||||
const { setToast, experienceSettings } = useContext(PageContext);
|
const { experienceSettings } = useContext(PageContext);
|
||||||
const { termsValidation } = useTerms();
|
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: asyncInvokeSocialSignIn } = useApi(invokeSocialSignIn);
|
||||||
|
|
||||||
const { run: asyncSignInWithSocial } = useApi(signInWithSocial, signInWithSocialErrorHandlers);
|
|
||||||
|
|
||||||
const invokeSocialSignInHandler = useCallback(
|
const invokeSocialSignInHandler = useCallback(
|
||||||
async (connectorId: string, callback?: () => void) => {
|
async (connectorId: string, callback?: () => void) => {
|
||||||
if (!(await termsValidation())) {
|
if (!(await termsValidation())) {
|
||||||
|
@ -84,110 +53,9 @@ const useSocial = () => {
|
||||||
[asyncInvokeSocialSignIn, termsValidation]
|
[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 {
|
return {
|
||||||
socialConnectors: experienceSettings?.socialConnectors ?? [],
|
socialConnectors: experienceSettings?.socialConnectors ?? [],
|
||||||
invokeSocialSignIn: invokeSocialSignInHandler,
|
invokeSocialSignIn: invokeSocialSignInHandler,
|
||||||
socialCallbackHandler,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
import { PageContext } from '@/hooks/use-page-context';
|
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';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
@ -14,10 +14,11 @@ type Props = {
|
||||||
|
|
||||||
const Callback = () => {
|
const Callback = () => {
|
||||||
const { connector: connectorId } = useParams<Props>();
|
const { connector: connectorId } = useParams<Props>();
|
||||||
const { socialCallbackHandler } = useSocial();
|
|
||||||
const { experienceSettings } = useContext(PageContext);
|
const { experienceSettings } = useContext(PageContext);
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'main_flow' });
|
||||||
|
|
||||||
|
const socialCallbackHandler = useSocialCallbackHandler();
|
||||||
|
|
||||||
const connectorLabel = useMemo(() => {
|
const connectorLabel = useMemo(() => {
|
||||||
const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId);
|
const connector = experienceSettings?.socialConnectors.find(({ id }) => id === connectorId);
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue