Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove Chrome local storage access request #1906

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 25 additions & 169 deletions apps/passport-client/components/screens/LoginScreens/LoginScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
requestDownloadAndDecryptStorage,
requestLogToServer
} from "@pcd/passport-interface";
import { TextButton } from "@pcd/passport-ui";
import { requestLogToServer } from "@pcd/passport-interface";
import { validateEmail } from "@pcd/util";
import {
ChangeEvent,
Expand All @@ -11,7 +7,6 @@ import {
useEffect,
useState
} from "react";
import { UAParser } from "ua-parser-js";
import { appConfig } from "../../../src/appConfig";
import {
useDispatch,
Expand All @@ -30,7 +25,6 @@ import {
setPendingViewFrogCryptoRequest,
setPendingViewSubscriptionsRequest
} from "../../../src/sessionStorage";
import { useSelector } from "../../../src/subscribe";
import {
BigInput,
Button,
Expand All @@ -44,23 +38,12 @@ import { RippleLoader } from "../../core/RippleLoader";
import { AppContainer } from "../../shared/AppContainer";
import { InlineError } from "../../shared/InlineError";

enum StorageAccessStatus {
None, // Default status
CanRequest, // Suitable browser, show the option to request
Requesting, // Request dialog visible
Granted, // Access granted
NoLocalStorage, // Access granted but no relevant storage values found
Denied // Access denied
}

export function LoginScreen(): JSX.Element {
const dispatch = useDispatch();
const state = useStateContext().getState();
const [error, setError] = useState<string | undefined>();
const query = useQuery();
const redirectedFromAction = query?.get("redirectedFromAction") === "true";
const connectedZapp = useSelector((state) => state.connectedZapp);
const zappOrigin = useSelector((state) => state.zappOrigin);

const pendingGetWithoutProvingRequest = query?.get(
pendingRequestKeys.getWithoutProving
Expand Down Expand Up @@ -152,104 +135,6 @@ export function LoginScreen(): JSX.Element {
[dispatch, email]
);

const [storageAccessStatus, setStorageAccessStatus] = useState(
StorageAccessStatus.None
);

/**
* Assuming we're in Chrome and an iframe, and we've successfully loaded an
* encryption key from local storage, try to use it to log in.
*/
const tryToLogin = useCallback(
async (encryptionKey: string) => {
// Try to download and decrypt the storage
const storageRequest = await requestDownloadAndDecryptStorage(
appConfig.zupassServer,
encryptionKey
);
if (storageRequest.success) {
// Success, log in
dispatch({
type: "load-after-login",
storage: storageRequest.value,
encryptionKey
});
} else {
// Something unexpected went wrong
setError(
"Unable to log in automatically, please enter your email to log in"
);
setStorageAccessStatus(StorageAccessStatus.Denied);
}
},
[dispatch]
);

/**
* This will only be called if we're in an iframe and Chrome.
*/
const requestStorageAndLogIn = useCallback(async () => {
try {
setStorageAccessStatus(StorageAccessStatus.Requesting);
// @ts-expect-error Chrome-only API
const handle: { localStorage: Storage } =
// @ts-expect-error Chrome-only API
await document.requestStorageAccess({ localStorage: true });

setStorageAccessStatus(StorageAccessStatus.Granted);
// Access granted, try reading the local storage
const encryptionKey = handle.localStorage.getItem("encryption_key");
if (encryptionKey) {
await tryToLogin(encryptionKey);
} else {
setStorageAccessStatus(StorageAccessStatus.NoLocalStorage);
}
} catch (_e) {
// If the user rejected the storage access request, set an error message.
// The finally block will return the user to the regular login flow.
setError(
"Unable to log in automatically, please enter your email to log in"
);
setStorageAccessStatus(StorageAccessStatus.Denied);
}
}, [tryToLogin]);

useEffect(() => {
(async (): Promise<void> => {
// Are we in an iframe? If so, we might be able to skip requesting the
// user's email and password by retrieving their encryption key from the
// first-party local storage. Currently this only works on Chrome 125+.
const parser = new UAParser();
const browserName = parser.getBrowser().name;
const browserVersion = parser.getBrowser().version;
const isChrome125OrAbove =
browserName === "Chrome" &&
browserVersion &&
parseInt(browserVersion) >= 125;

if (window.parent !== window && isChrome125OrAbove) {
// Do we already have access?
const hasAccess = await document.hasStorageAccess();
if (!hasAccess) {
// No access, try requesting it interactively
// Setting this state will trigger the UI to show the "Connect to
// Zupass" button. To request storage access, the user must click
// the button and approve the dialog.
// Storage access requests must occur in response to a user action,
// so we can't request it automatically here and must wait for the
// user to click the button.
setStorageAccessStatus(StorageAccessStatus.CanRequest);
} else {
// Access is allowed in principle, now we can request storage
// Show a spinner:
setStorageAccessStatus(StorageAccessStatus.Requesting);
// Try to read from storage and log in
requestStorageAndLogIn();
}
}
})();
}, [dispatch, requestStorageAndLogIn, tryToLogin]);

useEffect(() => {
// Redirect to home if already logged in
if (self) {
Expand Down Expand Up @@ -290,61 +175,32 @@ export function LoginScreen(): JSX.Element {
</>
)}

{storageAccessStatus === StorageAccessStatus.CanRequest && (
<TextCenter>
<Spacer h={24} />
Do you want to allow <em>{connectedZapp?.name}</em> ({zappOrigin}) to
connect to Zupass?
<Spacer h={24} />
<Button onClick={requestStorageAndLogIn}>
Connect to Zupass
</Button>{" "}
<Spacer h={24} />
<TextButton
onClick={() => setStorageAccessStatus(StorageAccessStatus.Denied)}
>
Log in manually
</TextButton>
</TextCenter>
)}

{(storageAccessStatus === StorageAccessStatus.Requesting ||
storageAccessStatus === StorageAccessStatus.Granted) && (
<TextCenter>
{!state.loggingOut && (
<>
<Spacer h={24} />
<RippleLoader />
</TextCenter>
<CenterColumn>
<form onSubmit={onGenPass}>
<BigInput
autoCapitalize="off"
autoCorrect="off"
type="text"
autoFocus
placeholder="email address"
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEmail(e.target.value)
}
/>
<InlineError error={error} />
<Spacer h={8} />
<Button style="primary" type="submit">
Continue
</Button>
</form>
</CenterColumn>
<Spacer h={64} />
</>
)}

{(storageAccessStatus === StorageAccessStatus.None ||
storageAccessStatus === StorageAccessStatus.Denied ||
storageAccessStatus === StorageAccessStatus.NoLocalStorage) &&
!state.loggingOut && (
<>
<Spacer h={24} />
<CenterColumn>
<form onSubmit={onGenPass}>
<BigInput
autoCapitalize="off"
autoCorrect="off"
type="text"
autoFocus
placeholder="email address"
value={email}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEmail(e.target.value)
}
/>
<InlineError error={error} />
<Spacer h={8} />
<Button style="primary" type="submit">
Continue
</Button>
</form>
</CenterColumn>
<Spacer h={64} />
</>
)}
</AppContainer>
);
}
Loading