paskia-js: profile() dialog, auth-logout message, host color-scheme adoption
New profile() function opens the minimal profile in a compact dialog
iframe and always resolves ('logout' | 'back'), keeping the auth flow's
resolve/reject contract separate and unchanged. The overlay now injects
the host page's computed color-scheme into the iframe URL theme param
when the server has not provided one.
This commit is contained in:
@@ -20,6 +20,7 @@ export {
|
||||
isAuthIframeOpen,
|
||||
hideAuthIframe,
|
||||
showAuthIframe,
|
||||
profile,
|
||||
} from './overlay'
|
||||
|
||||
export { SessionValidator } from './validate'
|
||||
|
||||
@@ -32,12 +32,24 @@ body.paskia-backdrop {
|
||||
color-scheme: auto;
|
||||
background: transparent;
|
||||
}
|
||||
#${AUTH_IFRAME_ID}.paskia-dialog {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: min(36rem, 100%);
|
||||
height: min(42rem, 100%);
|
||||
}
|
||||
`
|
||||
|
||||
type DialogResult = 'logout' | 'back'
|
||||
|
||||
let authIframe: HTMLIFrameElement | null = null
|
||||
let authPromise: Promise<void> | null = null
|
||||
let authResolve: (() => void) | null = null
|
||||
let authPromise: Promise<DialogResult | undefined> | null = null
|
||||
let authResolve: ((result?: DialogResult) => void) | null = null
|
||||
let authReject: ((error: Error) => void) | null = null
|
||||
// Auth flows reject AuthCancelledError on auth-back (callers rely on it to
|
||||
// abort request retries); the profile dialog resolves 'back' instead.
|
||||
let cancelAsError = true
|
||||
let messageListenerInstalled = false
|
||||
let backdropHolders = 0
|
||||
|
||||
@@ -98,8 +110,20 @@ function handleAuthMessage(event: MessageEvent): void {
|
||||
|
||||
case 'auth-back':
|
||||
hideAuthIframe()
|
||||
if (authReject) {
|
||||
if (cancelAsError && authReject) {
|
||||
authReject(new AuthCancelledError())
|
||||
} else if (authResolve) {
|
||||
authResolve('back')
|
||||
}
|
||||
authPromise = null
|
||||
authResolve = null
|
||||
authReject = null
|
||||
break
|
||||
|
||||
case 'auth-logout':
|
||||
hideAuthIframe()
|
||||
if (authResolve) {
|
||||
authResolve('logout')
|
||||
authPromise = null
|
||||
authResolve = null
|
||||
authReject = null
|
||||
@@ -116,12 +140,15 @@ function ensureMessageListener(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Promise<void> {
|
||||
function openIframe(iframeUrl: string, title: string, dialog: boolean, cancelError: boolean): Promise<DialogResult | undefined> {
|
||||
injectStyles()
|
||||
ensureMessageListener()
|
||||
|
||||
if (authPromise) return authPromise
|
||||
|
||||
cancelAsError = cancelError
|
||||
iframeUrl = withAppTheme(iframeUrl)
|
||||
|
||||
if (document.getElementById(AUTH_IFRAME_ID)) {
|
||||
authPromise = new Promise((resolve, reject) => {
|
||||
authResolve = resolve
|
||||
@@ -140,6 +167,7 @@ export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Pro
|
||||
|
||||
authIframe = document.createElement('iframe')
|
||||
authIframe.id = AUTH_IFRAME_ID
|
||||
if (dialog) authIframe.classList.add('paskia-dialog')
|
||||
authIframe.title = title
|
||||
authIframe.src = iframeUrl
|
||||
document.body.appendChild(authIframe)
|
||||
@@ -147,6 +175,48 @@ export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Pro
|
||||
return authPromise
|
||||
}
|
||||
|
||||
// Detect the host page's own color scheme (CSS color-scheme on body) as an
|
||||
// implicit app-level default. Only an unambiguous 'light' or 'dark' counts;
|
||||
// 'normal', 'light dark' etc. mean the page adapts, so no override is needed.
|
||||
function detectColorScheme(): string {
|
||||
if (typeof window === 'undefined' || !document.body) return ''
|
||||
const scheme = getComputedStyle(document.body).colorScheme
|
||||
return scheme === 'light' || scheme === 'dark' ? scheme : ''
|
||||
}
|
||||
|
||||
// Apply the host page's own color scheme to the iframe URL hash — only when
|
||||
// the URL has no theme parameter yet (a server-provided user theme override
|
||||
// is authoritative). The restricted UI's precedence is: URL parameter (user
|
||||
// override from the server, else host color scheme) > cached profile theme
|
||||
// (localStorage) > browser/desktop default.
|
||||
function withAppTheme(iframeUrl: string): string {
|
||||
const theme = detectColorScheme()
|
||||
if (!theme) return iframeUrl
|
||||
const hashIndex = iframeUrl.indexOf('#')
|
||||
const base = hashIndex === -1 ? iframeUrl : iframeUrl.slice(0, hashIndex)
|
||||
const params = new URLSearchParams(hashIndex === -1 ? '' : iframeUrl.slice(hashIndex + 1))
|
||||
if (params.has('theme')) return iframeUrl
|
||||
params.set('theme', theme)
|
||||
return `${base}#${params}`
|
||||
}
|
||||
|
||||
export function showAuthIframe(iframeUrl: string, title = 'Authentication'): Promise<void> {
|
||||
return openIframe(iframeUrl, title, false, true).then(() => undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* signed out inside the frame, 'back' when they closed it without action.
|
||||
* The caller decides from context how to react to each (e.g. whether to
|
||||
* start a new login attempt with showAuthIframe).
|
||||
*/
|
||||
export function profile(): Promise<DialogResult> {
|
||||
return openIframe('/auth/restricted/iframe#mode=profile', 'Profile', true, false)
|
||||
.then((result) => result ?? 'back')
|
||||
}
|
||||
|
||||
export function createAuthIframe(iframeUrl: string, title = 'Authentication'): HTMLIFrameElement {
|
||||
injectStyles()
|
||||
const existing = document.getElementById(AUTH_IFRAME_ID)
|
||||
|
||||
Reference in New Issue
Block a user