profile(): resolve 'login' when login flow completes inside the dialog

The overlay now tracks which dialog kind is open: auth-success from a
profile dialog resolves 'login', auth-back only rejects
AuthCancelledError for auth dialogs.
This commit is contained in:
2026-09-18 00:27:21 +00:00
parent 0ca4e07e23
commit c5efa03908
+16 -12
View File
@@ -41,15 +41,18 @@ body.paskia-backdrop {
} }
` `
type DialogResult = 'logout' | 'back' type DialogResult = 'login' | 'logout' | 'back'
type DialogKind = 'auth' | 'profile'
let authIframe: HTMLIFrameElement | null = null let authIframe: HTMLIFrameElement | null = null
let authPromise: Promise<DialogResult | undefined> | null = null let authPromise: Promise<DialogResult | undefined> | null = null
let authResolve: ((result?: DialogResult) => void) | null = null let authResolve: ((result?: DialogResult) => void) | null = null
let authReject: ((error: Error) => void) | null = null let authReject: ((error: Error) => void) | null = null
// Auth flows reject AuthCancelledError on auth-back (callers rely on it to // Auth flows reject AuthCancelledError on auth-back (callers rely on it to
// abort request retries); the profile dialog resolves 'back' instead. // abort request retries) and resolve void on auth-success. The profile dialog
let cancelAsError = true // never rejects: auth-back resolves 'back', and auth-success (the user logged
// in while the profile dialog was open) resolves 'login'.
let dialogKind: DialogKind = 'auth'
let messageListenerInstalled = false let messageListenerInstalled = false
let backdropHolders = 0 let backdropHolders = 0
@@ -101,7 +104,7 @@ function handleAuthMessage(event: MessageEvent): void {
case 'auth-success': case 'auth-success':
hideAuthIframe() hideAuthIframe()
if (authResolve) { if (authResolve) {
authResolve() authResolve(dialogKind === 'profile' ? 'login' : undefined)
authPromise = null authPromise = null
authResolve = null authResolve = null
authReject = null authReject = null
@@ -110,7 +113,7 @@ function handleAuthMessage(event: MessageEvent): void {
case 'auth-back': case 'auth-back':
hideAuthIframe() hideAuthIframe()
if (cancelAsError && authReject) { if (dialogKind === 'auth' && authReject) {
authReject(new AuthCancelledError()) authReject(new AuthCancelledError())
} else if (authResolve) { } else if (authResolve) {
authResolve('back') authResolve('back')
@@ -140,13 +143,13 @@ function ensureMessageListener(): void {
} }
} }
function openIframe(iframeUrl: string, title: string, dialog: boolean, cancelError: boolean): Promise<DialogResult | undefined> { function openIframe(iframeUrl: string, title: string, kind: DialogKind): Promise<DialogResult | undefined> {
injectStyles() injectStyles()
ensureMessageListener() ensureMessageListener()
if (authPromise) return authPromise if (authPromise) return authPromise
cancelAsError = cancelError dialogKind = kind
iframeUrl = withAppTheme(iframeUrl) iframeUrl = withAppTheme(iframeUrl)
if (document.getElementById(AUTH_IFRAME_ID)) { if (document.getElementById(AUTH_IFRAME_ID)) {
@@ -167,7 +170,7 @@ function openIframe(iframeUrl: string, title: string, dialog: boolean, cancelErr
authIframe = document.createElement('iframe') authIframe = document.createElement('iframe')
authIframe.id = AUTH_IFRAME_ID authIframe.id = AUTH_IFRAME_ID
if (dialog) authIframe.classList.add('paskia-dialog') if (kind === 'profile') authIframe.classList.add('paskia-dialog')
authIframe.title = title authIframe.title = title
authIframe.src = iframeUrl authIframe.src = iframeUrl
document.body.appendChild(authIframe) document.body.appendChild(authIframe)
@@ -201,19 +204,20 @@ function withAppTheme(iframeUrl: string): string {
} }
export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Promise<void> { export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Promise<void> {
return openIframe(iframeUrl, title, false, true).then(() => undefined) return openIframe(iframeUrl, title, 'auth').then(() => undefined)
} }
/** /**
* Show the minimal profile of the logged-in user in a compact dialog iframe. * Show the minimal profile of the logged-in user in a compact dialog iframe.
* *
* Unlike the auth flows, this always resolves — 'logout' when the user * Unlike the auth flows, this always resolves — 'login' when the user was
* signed out inside the frame, 'back' when they closed it without action. * signed out and completed the login flow inside the frame, 'logout' when
* they signed out inside the frame, 'back' when they closed it otherwise.
* The caller decides from context how to react to each (e.g. whether to * The caller decides from context how to react to each (e.g. whether to
* start a new login attempt with showAuthIframe). * start a new login attempt with showAuthIframe).
*/ */
export function profile(): Promise<DialogResult> { export function profile(): Promise<DialogResult> {
return openIframe('/auth/restricted/iframe#mode=profile', 'Profile', true, false) return openIframe('/auth/restricted/iframe#mode=profile', 'Profile', 'profile')
.then((result) => result ?? 'back') .then((result) => result ?? 'back')
} }