-
Notifications
You must be signed in to change notification settings - Fork 76
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
Iframe login via popup #1905
Merged
Merged
Iframe login via popup #1905
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
6a0fc21
Basic popup login working
robknight 29df6a8
Working error messages for failure cases
robknight ae1152d
Remove Chrome local storage access request (#1906)
robknight b7d8b4f
Merge branch 'main' into robknight/0xp-1226-popup-based-login
robknight File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 52 additions & 0 deletions
52
apps/passport-client/components/screens/ZappScreens/AuthenticateIFrameScreen.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { ReactNode, useEffect, useMemo } from "react"; | ||
import { useLoginIfNoSelf, useSyncKey } from "../../../src/appHooks"; | ||
import { pendingRequestKeys } from "../../../src/sessionStorage"; | ||
import { Spacer } from "../../core"; | ||
import { RippleLoader } from "../../core/RippleLoader"; | ||
import { AppContainer } from "../../shared/AppContainer"; | ||
import { IFrameAuthenticationMessage } from "./ConnectPopupScreen"; | ||
|
||
export function AuthenticateIFrameScreen(): ReactNode { | ||
const encryptionKey = useSyncKey(); | ||
|
||
useLoginIfNoSelf(pendingRequestKeys.authenticateIFrame, "true"); | ||
|
||
// We should only process Zapp approval requests if this window was opened | ||
// by a Zupass iframe. | ||
const isLegitimateOpener = useMemo(() => { | ||
return ( | ||
!!window.opener && | ||
new URL(window.opener.location.href).origin === window.location.origin | ||
); | ||
}, []); | ||
|
||
useEffect(() => { | ||
if (isLegitimateOpener && encryptionKey) { | ||
const chan = new MessageChannel(); | ||
chan.port1.onmessage = (): void => { | ||
// We're going to send the encryption key to the iframe | ||
// When the iframe gets the encryption key, it will | ||
// send a message back to this port, and we can close | ||
// this window | ||
window.close(); | ||
}; | ||
// Zapp is already approved, return to the Zupass iframe | ||
window.opener.postMessage( | ||
{ | ||
type: "auth", | ||
encryptionKey: encryptionKey as string | ||
} satisfies IFrameAuthenticationMessage, | ||
origin, | ||
[chan.port2] | ||
); | ||
window.close(); | ||
} | ||
}, [isLegitimateOpener, encryptionKey]); | ||
|
||
return ( | ||
<AppContainer bg="gray"> | ||
<Spacer h={64} /> | ||
<RippleLoader /> | ||
</AppContainer> | ||
); | ||
} |
212 changes: 212 additions & 0 deletions
212
apps/passport-client/components/screens/ZappScreens/ConnectPopupScreen.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import { requestDownloadAndDecryptStorage } from "@pcd/passport-interface"; | ||
import { Button, Spacer } from "@pcd/passport-ui"; | ||
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 IFrameAuthenticationMessageSchema = v.object({ | ||
type: v.literal("auth"), | ||
encryptionKey: v.string() | ||
}); | ||
|
||
export type IFrameAuthenticationMessage = v.InferOutput< | ||
typeof IFrameAuthenticationMessageSchema | ||
>; | ||
|
||
enum PopupAuthenticationStatus { | ||
Start = "Start", | ||
PopupOpen = "PopupOpen", | ||
PopupBlocked = "PopupBlocked", | ||
PopupClosed = "PopupClosed", | ||
Authenticating = "Authenticating", | ||
AuthenticationError = "AuthenticationError" | ||
} | ||
|
||
/** | ||
* This screen is only ever shown in a popup modal. It is used when Zupass is | ||
* embedded in an iframe but has not been authenticated yet, and it opens a | ||
* popup window which will handle authentication and post an encryption key | ||
* back to the iframe. | ||
* | ||
* After we get the encryption key, we log in. This will trigger an event in | ||
* {@link useZappServer} which will tell the Zapp to close the modal window. | ||
*/ | ||
export function ConnectPopupScreen(): ReactNode { | ||
const [status, setStatus] = useState<PopupAuthenticationStatus>( | ||
PopupAuthenticationStatus.Start | ||
); | ||
const dispatch = useDispatch(); | ||
const tryToLogin = useCallback( | ||
async (encryptionKey: string): Promise<boolean> => { | ||
// 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 | ||
}); | ||
return true; | ||
} else { | ||
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); | ||
} else { | ||
setStatus(PopupAuthenticationStatus.PopupOpen); | ||
popupRef.current = popup; | ||
} | ||
}, []); | ||
|
||
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={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; | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i like the migration from zod -- we are the only
z
that matter 😆There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valibot also produces much smaller bundles!