fix(SignInPage): Ensure click handler and login popup occur in same tick

The current SignIn implementation is broken on Safari iOS by default, due to the combination of useEffect and the extra asynchronous login check that occurs before the authentication routine that causes the popup (instantPopup: true). This change refactors the internals of SignIn page to avoid useEffect, which was arguably confusing for implementors, and also avoid overloading the use of the poorly scoped autoSignIn variable.

Signed-off-by: Chris Carpita <ccarpita@butterflynetinc.com>
This commit is contained in:
Chris Carpita
2021-09-20 21:07:18 -04:00
parent 64e3c8a228
commit 537bd04005
2 changed files with 51 additions and 45 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': minor
---
Fixed a popup-blocking bug affecting iOS Safari in SignInPage.tsx by ensuring that the popup occurs in the same tick as the tap/click
@@ -15,6 +15,7 @@
*/
import {
BackstageIdentity,
configApiRef,
SignInPageProps,
useApi,
@@ -91,62 +92,63 @@ export const SingleSignInPage = ({
const authApi = useApi(provider.apiRef);
const configApi = useApi(configApiRef);
const [autoShowPopup, setAutoShowPopup] = useState<boolean>(auto ?? false);
// Defaults to true so that an initial check for existing user session is made
const [retry, setRetry] = useState<{} | boolean | undefined>(undefined);
const [error, setError] = useState<Error>();
const [loginCount, setLoginCount] = useState(0);
// The SignIn component takes some time to decide whether the user is logged-in or not.
// showLoginPage is used to prevent a glitch-like experience where the sign-in page is
// displayed for a split second when the user is already logged-in.
const [showLoginPage, setShowLoginPage] = useState<boolean>(false);
useEffect(() => {
const login = async () => {
try {
let identity;
type LoginOpts = { checkExisting?: boolean; showPopup?: boolean };
const login = async ({ checkExisting, showPopup }: LoginOpts) => {
setLoginCount(prev => prev + 1);
try {
let identity: BackstageIdentity | undefined;
if (checkExisting) {
// Do an initial check if any logged-in session exists
identity = await authApi.getBackstageIdentity({
optional: true,
});
// If no session exists, show the sign-in page
if (!identity && autoShowPopup) {
// Unless auto is set to true, this step should not happen.
// When user intentionally clicks the Sign In button, autoShowPopup is set to true
setShowLoginPage(true);
identity = await authApi.getBackstageIdentity({
instantPopup: true,
});
}
if (!identity) {
setShowLoginPage(true);
return;
}
const profile = await authApi.getProfile();
onResult({
userId: identity!.id,
profile: profile!,
getIdToken: () => {
return authApi
.getBackstageIdentity()
.then(i => i!.token ?? i!.idToken);
},
signOut: async () => {
await authApi.signOut();
},
});
} catch (err) {
// User closed the sign-in modal
setError(err);
setShowLoginPage(true);
}
};
login();
}, [onResult, authApi, retry, autoShowPopup]);
// If no session exists, show the sign-in page
if (!identity && (showPopup || auto)) {
// Unless auto is set to true, this step should not happen.
// When user intentionally clicks the Sign In button, autoShowPopup is set to true
setShowLoginPage(true);
identity = await authApi.getBackstageIdentity({
instantPopup: true,
});
}
if (!identity) {
setShowLoginPage(true);
return;
}
const profile = await authApi.getProfile();
onResult({
userId: identity!.id,
profile: profile!,
getIdToken: () => {
return authApi
.getBackstageIdentity()
.then(i => i!.token ?? i!.idToken);
},
signOut: async () => {
await authApi.signOut();
},
});
} catch (err: any) {
// User closed the sign-in modal
setError(err);
setShowLoginPage(true);
}
};
if (loginCount === 0) {
login({ checkExisting: true });
}
return showLoginPage ? (
<Page themeId="home">
@@ -168,8 +170,7 @@ export const SingleSignInPage = ({
color="primary"
variant="outlined"
onClick={() => {
setRetry({});
setAutoShowPopup(true);
login({ showPopup: true });
}}
>
Sign In