0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

refactor(experience): add global loading status on page redirect (#5774)

* refactor(experience): add global loading status on page redirect

add global loading status on page redirect

* chore: add changeset
This commit is contained in:
simeng-li 2024-05-20 09:19:46 +08:00 committed by GitHub
parent e715049bae
commit cb1a38c405
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 128 additions and 50 deletions

View file

@ -0,0 +1,7 @@
---
"@logto/experience": patch
---
show global loading icon on page relocate
This is to address the issue where the user is redirected back to the client after a successful login, but the page is not yet fully loaded. This will show a global loading icon to indicate that the page is still loading. Preventing the user from interacting with the current sign-in page and avoid page idling confusion.

View file

@ -3,11 +3,13 @@ import { useCallback } from 'react';
import { bindSocialRelatedUser } from '@/apis/interaction';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
const useBindSocialRelatedUser = () => {
const handleError = useErrorHandler();
const preSignInErrorHandler = usePreSignInErrorHandler();
const redirectTo = useGlobalRedirectTo();
const asyncBindSocialRelatedUser = useApi(bindSocialRelatedUser);
@ -22,10 +24,10 @@ const useBindSocialRelatedUser = () => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[asyncBindSocialRelatedUser, handleError, preSignInErrorHandler]
[asyncBindSocialRelatedUser, handleError, preSignInErrorHandler, redirectTo]
);
};

View file

@ -1,12 +1,13 @@
import type { EmailVerificationCodePayload, PhoneVerificationCodePayload } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { addProfileWithVerificationCodeIdentifier } from '@/apis/interaction';
import useApi from '@/hooks/use-api';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
import type { VerificationCodeIdentifier } from '@/types';
import { SearchParameters } from '@/types';
@ -21,6 +22,7 @@ const useContinueFlowCodeVerification = (
errorCallback?: () => void
) => {
const [searchParameters] = useSearchParams();
const redirectTo = useGlobalRedirectTo();
const handleError = useErrorHandler();
const verifyVerificationCode = useApi(addProfileWithVerificationCodeIdentifier);
@ -76,10 +78,16 @@ const useContinueFlowCodeVerification = (
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[errorCallback, handleError, verifyVerificationCode, verifyVerificationCodeErrorHandlers]
[
errorCallback,
handleError,
redirectTo,
verifyVerificationCode,
verifyVerificationCodeErrorHandlers,
]
);
return {

View file

@ -1,6 +1,6 @@
import type { EmailVerificationCodePayload, PhoneVerificationCodePayload } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@ -12,6 +12,7 @@ import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
import { useSieMethods } from '@/hooks/use-sie';
import type { VerificationCodeIdentifier } from '@/types';
@ -28,6 +29,7 @@ const useRegisterFlowCodeVerification = (
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
const redirectTo = useGlobalRedirectTo();
const { signInMode } = useSieMethods();
@ -74,13 +76,14 @@ const useRegisterFlowCodeVerification = (
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
}, [
handleError,
method,
navigate,
preSignInErrorHandler,
redirectTo,
show,
showIdentifierErrorAlert,
signInMode,
@ -117,10 +120,10 @@ const useRegisterFlowCodeVerification = (
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[errorCallback, errorHandlers, handleError, verifyVerificationCode]
[errorCallback, errorHandlers, handleError, redirectTo, verifyVerificationCode]
);
return {

View file

@ -1,17 +1,18 @@
import type { EmailVerificationCodePayload, PhoneVerificationCodePayload } from '@logto/schemas';
import { SignInIdentifier, SignInMode } from '@logto/schemas';
import { useMemo, useCallback } from 'react';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import {
signInWithVerificationCodeIdentifier,
registerWithVerifiedIdentifier,
signInWithVerificationCodeIdentifier,
} from '@/apis/interaction';
import useApi from '@/hooks/use-api';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
import { useSieMethods } from '@/hooks/use-sie';
import type { VerificationCodeIdentifier } from '@/types';
@ -28,6 +29,7 @@ const useSignInFlowCodeVerification = (
const { t } = useTranslation();
const { show } = useConfirmModal();
const navigate = useNavigate();
const redirectTo = useGlobalRedirectTo();
const { signInMode } = useSieMethods();
const handleError = useErrorHandler();
@ -77,19 +79,20 @@ const useSignInFlowCodeVerification = (
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
}, [
handleError,
method,
navigate,
registerWithIdentifierAsync,
preSignInErrorHandler,
show,
showIdentifierErrorAlert,
signInMode,
show,
t,
method,
target,
registerWithIdentifierAsync,
showIdentifierErrorAlert,
navigate,
handleError,
preSignInErrorHandler,
redirectTo,
]);
const errorHandlers = useMemo<ErrorHandlers>(
@ -118,10 +121,10 @@ const useSignInFlowCodeVerification = (
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[asyncSignInWithVerificationCodeIdentifier, errorHandlers, handleError]
[asyncSignInWithVerificationCodeIdentifier, errorHandlers, handleError, redirectTo]
);
return {

View file

@ -0,0 +1,26 @@
import { useCallback, useContext } from 'react';
import PageContext from '@/Providers/PageContextProvider/PageContext';
/**
* This hook provides a function that process the app redirection after user successfully signs in.
* Use window.location.replace to handle the redirection.
* Set the global loading state to true before redirecting.
* This is to prevent the user from interacting with the app while the redirection is in progress.
*/
function useGlobalRedirectTo() {
const { setLoading } = useContext(PageContext);
const redirectTo = useCallback(
(url: string | URL) => {
setLoading(true);
window.location.replace(url);
},
[setLoading]
);
return redirectTo;
}
export default useGlobalRedirectTo;

View file

@ -1,4 +1,4 @@
import { useState, useMemo, useCallback } from 'react';
import { useCallback, useMemo, useState } from 'react';
import type { PasswordSignInPayload } from '@/apis/interaction';
import { signInWithPasswordIdentifier } from '@/apis/interaction';
@ -7,11 +7,13 @@ import useCheckSingleSignOn from '@/hooks/use-check-single-sign-on';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from './use-global-redirect-to';
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
const usePasswordSignIn = () => {
const [errorMessage, setErrorMessage] = useState<string>();
const { onSubmit: checkSingleSignOn } = useCheckSingleSignOn();
const redirectTo = useGlobalRedirectTo();
const clearErrorMessage = useCallback(() => {
setErrorMessage('');
@ -51,10 +53,10 @@ const usePasswordSignIn = () => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[asyncSignIn, checkSingleSignOn, errorHandlers, handleError]
[asyncSignIn, checkSingleSignOn, errorHandlers, handleError, redirectTo]
);
return {

View file

@ -6,6 +6,7 @@ import { UserMfaFlow } from '@/types';
import useApi from './use-api';
import useErrorHandler, { type ErrorHandlers } from './use-error-handler';
import useGlobalRedirectTo from './use-global-redirect-to';
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
export type SendMfaPayloadApiOptions =
@ -29,6 +30,7 @@ const useSendMfaPayload = () => {
const asyncSendMfaPayload = useApi(sendMfaPayloadApi);
const preSignInErrorHandler = usePreSignInErrorHandler({ replace: true });
const handleError = useErrorHandler();
const redirectTo = useGlobalRedirectTo();
return useCallback(
async (
@ -48,10 +50,10 @@ const useSendMfaPayload = () => {
}
if (result) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[asyncSendMfaPayload, handleError, preSignInErrorHandler]
[asyncSendMfaPayload, handleError, preSignInErrorHandler, redirectTo]
);
};

View file

@ -4,10 +4,12 @@ import { skipMfa } from '@/apis/interaction';
import useApi from './use-api';
import useErrorHandler from './use-error-handler';
import useGlobalRedirectTo from './use-global-redirect-to';
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
const useSkipMfa = () => {
const asyncSkipMfa = useApi(skipMfa);
const redirectTo = useGlobalRedirectTo();
const handleError = useErrorHandler();
const preSignInErrorHandler = usePreSignInErrorHandler({ replace: true });
@ -20,9 +22,9 @@ const useSkipMfa = () => {
}
if (result) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
}, [asyncSkipMfa, handleError, preSignInErrorHandler]);
}, [asyncSkipMfa, handleError, preSignInErrorHandler, redirectTo]);
};
export default useSkipMfa;

View file

@ -4,10 +4,12 @@ import { linkWithSocial } from '@/apis/interaction';
import useApi from '@/hooks/use-api';
import useErrorHandler from './use-error-handler';
import useGlobalRedirectTo from './use-global-redirect-to';
const useLinkSocial = () => {
const handleError = useErrorHandler();
const asyncLinkWithSocial = useApi(linkWithSocial);
const redirectTo = useGlobalRedirectTo();
return useCallback(
async (connectorId: string) => {
@ -20,10 +22,10 @@ const useLinkSocial = () => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[asyncLinkWithSocial, handleError]
[asyncLinkWithSocial, handleError, redirectTo]
);
};

View file

@ -4,11 +4,13 @@ import { registerWithVerifiedSocial } from '@/apis/interaction';
import useApi from './use-api';
import useErrorHandler from './use-error-handler';
import useGlobalRedirectTo from './use-global-redirect-to';
import usePreSignInErrorHandler from './use-pre-sign-in-error-handler';
const useSocialRegister = (connectorId?: string, replace?: boolean) => {
const handleError = useErrorHandler();
const asyncRegisterWithSocial = useApi(registerWithVerifiedSocial);
const redirectTo = useGlobalRedirectTo();
const preSignInErrorHandler = usePreSignInErrorHandler({ linkSocial: connectorId, replace });
@ -23,10 +25,10 @@ const useSocialRegister = (connectorId?: string, replace?: boolean) => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[asyncRegisterWithSocial, handleError, preSignInErrorHandler]
[asyncRegisterWithSocial, handleError, preSignInErrorHandler, redirectTo]
);
};

View file

@ -10,6 +10,7 @@ import TermsLinks from '@/components/TermsLinks';
import TextLink from '@/components/TextLink';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import OrganizationSelector, { type Organization } from './OrganizationSelector';
import ScopesListCard from './ScopesListCard';
@ -21,6 +22,7 @@ const Consent = () => {
const handleError = useErrorHandler();
const asyncConsent = useApi(consent);
const { t } = useTranslation();
const redirectTo = useGlobalRedirectTo();
const [consentData, setConsentData] = useState<ConsentInfoResponse>();
const [selectedOrganization, setSelectedOrganization] = useState<Organization>();
@ -37,9 +39,9 @@ const Consent = () => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
}, [asyncConsent, handleError, selectedOrganization?.id]);
}, [asyncConsent, handleError, redirectTo, selectedOrganization?.id]);
useEffect(() => {
const getConsentInfoHandler = async () => {

View file

@ -6,6 +6,7 @@ import { addProfile } from '@/apis/interaction';
import SetPasswordForm from '@/containers/SetPassword';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import usePasswordAction, { type SuccessHandler } from '@/hooks/use-password-action';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
import { usePasswordPolicy } from '@/hooks/use-sie';
@ -18,6 +19,7 @@ const SetPassword = () => {
const navigate = useNavigate();
const { show } = useConfirmModal();
const redirectTo = useGlobalRedirectTo();
const preSignInErrorHandler = usePreSignInErrorHandler();
@ -31,11 +33,15 @@ const SetPassword = () => {
}),
[navigate, preSignInErrorHandler, show]
);
const successHandler: SuccessHandler<typeof addProfile> = useCallback((result) => {
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
}
}, []);
const successHandler: SuccessHandler<typeof addProfile> = useCallback(
(result) => {
if (result?.redirectTo) {
redirectTo(result.redirectTo);
}
},
[redirectTo]
);
const [action] = usePasswordAction({
api: async (password) => addProfile({ password }),
setErrorMessage,

View file

@ -1,9 +1,10 @@
import { useState, useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { addProfile } from '@/apis/interaction';
import useApi from '@/hooks/use-api';
import type { ErrorHandlers } from '@/hooks/use-error-handler';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import usePreSignInErrorHandler from '@/hooks/use-pre-sign-in-error-handler';
const useSetUsername = () => {
@ -15,6 +16,7 @@ const useSetUsername = () => {
const asyncAddProfile = useApi(addProfile);
const handleError = useErrorHandler();
const redirectTo = useGlobalRedirectTo();
const preSignInErrorHandler = usePreSignInErrorHandler();
@ -39,10 +41,10 @@ const useSetUsername = () => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[asyncAddProfile, errorHandlers, handleError]
[asyncAddProfile, errorHandlers, handleError, redirectTo]
);
return { errorMessage, clearErrorMessage, onSubmit };

View file

@ -7,6 +7,7 @@ import { setUserPassword } from '@/apis/interaction';
import SetPassword from '@/containers/SetPassword';
import { useConfirmModal } from '@/hooks/use-confirm-modal';
import { type ErrorHandlers } from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import useMfaErrorHandler from '@/hooks/use-mfa-error-handler';
import usePasswordAction, { type SuccessHandler } from '@/hooks/use-password-action';
import { usePasswordPolicy, useSieMethods } from '@/hooks/use-sie';
@ -17,6 +18,7 @@ const RegisterPassword = () => {
const { signUpMethods } = useSieMethods();
const navigate = useNavigate();
const redirectTo = useGlobalRedirectTo();
const { show } = useConfirmModal();
const [errorMessage, setErrorMessage] = useState<string>();
const clearErrorMessage = useCallback(() => {
@ -37,11 +39,14 @@ const RegisterPassword = () => {
[navigate, mfaErrorHandler, show]
);
const successHandler: SuccessHandler<typeof setUserPassword> = useCallback((result) => {
if (result && 'redirectTo' in result) {
window.location.replace(result.redirectTo);
}
}, []);
const successHandler: SuccessHandler<typeof setUserPassword> = useCallback(
(result) => {
if (result && 'redirectTo' in result) {
redirectTo(result.redirectTo);
}
},
[redirectTo]
);
const [action] = usePasswordAction({
api: setUserPassword,

View file

@ -6,6 +6,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { singleSignOnAuthorization, singleSignOnRegistration } from '@/apis/single-sign-on';
import useApi from '@/hooks/use-api';
import useErrorHandler from '@/hooks/use-error-handler';
import useGlobalRedirectTo from '@/hooks/use-global-redirect-to';
import { useSieMethods } from '@/hooks/use-sie';
import useTerms from '@/hooks/use-terms';
import useToast from '@/hooks/use-toast';
@ -17,6 +18,7 @@ const useSingleSignOnRegister = () => {
const request = useApi(singleSignOnRegistration);
const { termsValidation } = useTerms();
const navigate = useNavigate();
const redirectTo = useGlobalRedirectTo();
return useCallback(
async (connectorId: string) => {
@ -35,10 +37,10 @@ const useSingleSignOnRegister = () => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[handleError, navigate, request, termsValidation]
[handleError, navigate, redirectTo, request, termsValidation]
);
};
@ -58,6 +60,7 @@ const useSingleSignOnListener = (connectorId: string) => {
const [isConsumed, setIsConsumed] = useState(false);
const [searchParameters, setSearchParameters] = useSearchParams();
const { setToast } = useToast();
const redirectTo = useGlobalRedirectTo();
const { signInMode } = useSieMethods();
const handleError = useErrorHandler();
@ -97,12 +100,13 @@ const useSingleSignOnListener = (connectorId: string) => {
}
if (result?.redirectTo) {
window.location.replace(result.redirectTo);
redirectTo(result.redirectTo);
}
},
[
handleError,
navigate,
redirectTo,
registerSingleSignOnIdentity,
setToast,
signInMode,