Skip to content

Commit

Permalink
Working error messages for failure cases
Browse files Browse the repository at this point in the history
  • Loading branch information
robknight committed Sep 26, 2024
1 parent 6a0fc21 commit 29df6a8
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,8 @@ export function LoginInterstitialScreen(): JSX.Element {
const loadedIssuedPCDs = useLoadedIssuedPCDs();

useLayoutEffect(() => {
console.log("loadedIssuedPCDs", loadedIssuedPCDs);
if (loadedIssuedPCDs) {
const pendingRequest = getPendingRequest();
console.log("pendingRequest", pendingRequest);
if (pendingRequest) {
switch (pendingRequest.key) {
case "proof": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { pendingRequestKeys } from "../../../src/sessionStorage";
import { Spacer } from "../../core";
import { RippleLoader } from "../../core/RippleLoader";
import { AppContainer } from "../../shared/AppContainer";
import { AuthMessage } from "./ConnectPopupScreen";
import { IFrameAuthenticationMessage } from "./ConnectPopupScreen";

export function AuthenticateIFrameScreen(): ReactNode {
const encryptionKey = useSyncKey();
Expand All @@ -21,7 +21,6 @@ export function AuthenticateIFrameScreen(): ReactNode {
}, []);

useEffect(() => {
console.log("AuthenticateIFrameScreen", isLegitimateOpener);
if (isLegitimateOpener && encryptionKey) {
const chan = new MessageChannel();
chan.port1.onmessage = (): void => {
Expand All @@ -36,7 +35,7 @@ export function AuthenticateIFrameScreen(): ReactNode {
{
type: "auth",
encryptionKey: encryptionKey as string
} satisfies AuthMessage,
} satisfies IFrameAuthenticationMessage,
origin,
[chan.port2]
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { requestDownloadAndDecryptStorage } from "@pcd/passport-interface";
import { Button, Spacer } from "@pcd/passport-ui";
import { ReactNode, useCallback, useState } from "react";
import { ReactNode, useCallback, useEffect, useRef, useState } from "react";
import styled from "styled-components";
import urljoin from "url-join";
import * as v from "valibot";
import { appConfig } from "../../../src/appConfig";
import { useDispatch } from "../../../src/appHooks";
import { useSelector } from "../../../src/subscribe";
import { H1, TextCenter } from "../../core";
import { AppContainer } from "../../shared/AppContainer";
import { Spinner } from "../../shared/Spinner";

const AuthMessage = v.object({
const IFrameAuthenticationMessageSchema = v.object({
type: v.literal("auth"),
encryptionKey: v.string()
});

export type AuthMessage = v.InferOutput<typeof AuthMessage>;
export type IFrameAuthenticationMessage = v.InferOutput<
typeof IFrameAuthenticationMessageSchema
>;

enum PopupAuthenticationStatus {
Start,
PopupOpen,
PopupBlocked,
PopupClosed,
Authenticating,
AuthenticationError
Start = "Start",
PopupOpen = "PopupOpen",
PopupBlocked = "PopupBlocked",
PopupClosed = "PopupClosed",
Authenticating = "Authenticating",
AuthenticationError = "AuthenticationError"
}

/**
Expand All @@ -35,13 +39,12 @@ enum PopupAuthenticationStatus {
* {@link useZappServer} which will tell the Zapp to close the modal window.
*/
export function ConnectPopupScreen(): ReactNode {
const [error, setError] = useState<string | null>(null);
const [status, setStatus] = useState<PopupAuthenticationStatus>(
PopupAuthenticationStatus.Start
);
const dispatch = useDispatch();
const tryToLogin = useCallback(
async (encryptionKey: string) => {
async (encryptionKey: string): Promise<boolean> => {
// Try to download and decrypt the storage
const storageRequest = await requestDownloadAndDecryptStorage(
appConfig.zupassServer,
Expand All @@ -54,81 +57,156 @@ export function ConnectPopupScreen(): ReactNode {
storage: storageRequest.value,
encryptionKey
});
return true;
} else {
// Something unexpected went wrong
setError(
"Unable to log in automatically, please enter your email to log in"
);
return false;
}
},
[dispatch]
);

const messageListenerAbortRef = useRef<AbortController | null>(null);
const popupRef = useRef<Window | null>(null);

useEffect(() => {
let closeCheckInterval: number;
let closeNotifyTimeout: number;
if (status === PopupAuthenticationStatus.PopupOpen) {
closeCheckInterval = window.setInterval(() => {
// The user may close the popup without authenticating, in which case we
// need to show a message to the user telling them not to do this.
// There is no reliable event for detecting this, so we have to check
// using a timer.
if (popupRef.current && popupRef.current.closed) {
clearInterval(closeCheckInterval);
// Race conditions are a risk here, so we wait 250ms to ensure that
// the popup has had a chance to send a message.
closeNotifyTimeout = window.setTimeout(() => {
if (status === PopupAuthenticationStatus.PopupOpen) {
setStatus(PopupAuthenticationStatus.PopupClosed);
messageListenerAbortRef.current?.abort();
}
}, 250);
}
}, 250);
return () => {
if (closeCheckInterval) {
clearInterval(closeCheckInterval);
}
if (closeNotifyTimeout) {
clearTimeout(closeNotifyTimeout);
}
};
}
}, [status]);

useEffect(() => {
if (
status === PopupAuthenticationStatus.PopupOpen ||
status === PopupAuthenticationStatus.PopupBlocked
) {
// If the user closes the popup, we need to abort the message listener
const messageListenerAbort = new AbortController();

// If we already have a message listener, abort it
if (messageListenerAbortRef.current) {
messageListenerAbortRef.current.abort();
}
messageListenerAbortRef.current = messageListenerAbort;

window.addEventListener(
"message",
async (event) => {
if (event.origin !== window.origin) {
return;
}

// Check if the message is an authentication message
const parsed = v.safeParse(
IFrameAuthenticationMessageSchema,
event.data
);
if (
parsed.success &&
status === PopupAuthenticationStatus.PopupOpen
) {
setStatus(PopupAuthenticationStatus.Authenticating);
// Sending this message back to the iframe lets it know that
// we've received the authentication message and it's okay to
// close
event.ports[0].postMessage("ACK");
const loginResult = await tryToLogin(parsed.output.encryptionKey);
if (!loginResult) {
setStatus(PopupAuthenticationStatus.AuthenticationError);
}
}
},
{ signal: messageListenerAbort.signal }
);
}
}, [status, tryToLogin]);

const openPopup = useCallback(() => {
const popup = window.open(
urljoin(window.origin, "/#/authenticate-iframe"),
"_blank",
"width=500,height=500,popup=true"
);
if (!popup) {
// Although the popup was blocked, the user may cause it to open by
// allowing the browser to open it, so we should continue to set up
// the message listener.
setStatus(PopupAuthenticationStatus.PopupBlocked);
return;
} else {
setStatus(PopupAuthenticationStatus.PopupOpen);
popupRef.current = popup;
}

// If the user closes the popup, we need to abort the message listener
const abortReceiveMessage = new AbortController();

const interval = window.setInterval(() => {
// The user may close the popup without authenticating, in which case we
// need to show a message to the user telling them not to do this.
// There is no reliable event for detecting this, so we have to check
// using a timer.
if (popup.closed) {
setStatus(PopupAuthenticationStatus.PopupClosed);
clearInterval(interval);
// Race conditions are a risk here, so we wait 250ms to ensure that
// the popup has had a chance to send a message.
window.setTimeout(() => {
abortReceiveMessage.abort();
}, 250);
}
}, 250);

window.addEventListener(
"message",
(event) => {
console.log("message", event);
if (event.origin !== window.origin || event.source !== popup) {
return;
}

// Check if the message is an authentication message
const parsed = v.safeParse(AuthMessage, event.data);
if (parsed.success) {
event.ports[0].postMessage("ACK");
tryToLogin(parsed.output.encryptionKey);
}
},
{ signal: abortReceiveMessage.signal }
);
}, [tryToLogin]);
}, []);

const inProgress =
status === PopupAuthenticationStatus.PopupOpen ||
status === PopupAuthenticationStatus.Authenticating;

const zappName = useSelector((state) => state.connectedZapp?.name);
const zappOrigin = useSelector((state) => state.zappOrigin);

return (
<AppContainer bg="primary">
<Spacer h={64} />
<TextCenter>
<H1>ZUPASS</H1>
<Spacer h={24} />
<TextCenter>
<ZappName>{zappName}</ZappName> ({zappOrigin}) would like to connect
to your Zupass.
</TextCenter>
<Spacer h={24} />
<Button onClick={openPopup} disabled={inProgress}>
<Spinner show={inProgress} text="Connect" />
</Button>
<Spacer h={8} />
{error && <TextCenter>{error}</TextCenter>}
<Spacer h={24} />
{status === PopupAuthenticationStatus.PopupBlocked && (
<TextCenter>
Your browser may be configured to block popup windows. Please check
your browser settings and click the button above to try again.
</TextCenter>
)}
{status === PopupAuthenticationStatus.AuthenticationError && (
<TextCenter>
An unexpected error occurred. Please try again.
</TextCenter>
)}
{status === PopupAuthenticationStatus.PopupClosed && (
<TextCenter>
The popup window was closed before authentication could complete.
Please try again by clicking the button above.
</TextCenter>
)}
</TextCenter>
</AppContainer>
);
}

const ZappName = styled.span`
font-weight: bold;
`;
2 changes: 1 addition & 1 deletion examples/test-zapp/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export function cn(...classes: string[]): string {
}

export const DEFAULT_CONNECTION_INFO: ClientConnectionInfo = {
url: process.env.CLIENT_URL ?? "https://staging-rob.zupass.org",
url: import.meta.env.VITE_CLIENT_URL ?? "https://staging-rob.zupass.org",
type: "iframe"
};

Expand Down
9 changes: 9 additions & 0 deletions examples/test-zapp/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_CLIENT_URL: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
1 change: 0 additions & 1 deletion test-packaging/vite/src/vite-env.d.ts

This file was deleted.

2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@
"NODE_OPTIONS",
"ZAPP_RESTRICT_ORIGINS",
"ZAPP_ALLOWED_SIGNER_ORIGINS",
"CLIENT_URL",
"VITE_CLIENT_URL",
"EMBEDDED_ZAPPS",
"//// add env vars above this line ////"
],
Expand Down

0 comments on commit 29df6a8

Please sign in to comment.