diff --git a/src/components/account/Account.stories.ts b/src/components/account/Account.stories.ts
index 7e0f2977b..78995baaf 100644
--- a/src/components/account/Account.stories.ts
+++ b/src/components/account/Account.stories.ts
@@ -15,12 +15,8 @@ const meta = {
const render = defineAuthStoryRender(() => html``)
-export default meta
-
export const Primary = { render }
-export const Guest = {
- render,
- args: {
- user: 'Guest',
- }
-}
+export const Guest = { render, args: { user: 'Guest' } }
+export const Initializing = { render, args: { user: 'Initializing' } }
+
+export default meta
diff --git a/src/components/account/Account.styles.css b/src/components/account/Account.styles.css
index 58f6d7cae..34ab7aa01 100644
--- a/src/components/account/Account.styles.css
+++ b/src/components/account/Account.styles.css
@@ -1,13 +1,21 @@
:host {
+ --image-size: 1.875rem; /* 30px */
+
display: inline-flex;
flex-direction: row;
gap: 10px;
}
+:host([data-state-initializing]) {
+ icon-svg-spinners-180-ring {
+ width: var(--image-size);
+ height: var(--image-size);
+ }
+}
+
:host([data-state-loggedIn]) {
--padding: 4px;
--border-width: 1px;
- --image-size: 1.875rem; /* 30px */
button {
display: inline-flex;
diff --git a/src/components/account/Account.ts b/src/components/account/Account.ts
index a2069f070..4e6c82611 100644
--- a/src/components/account/Account.ts
+++ b/src/components/account/Account.ts
@@ -15,6 +15,7 @@ import '~icons/lucide/chevron-down'
import '~icons/lucide/log-in'
import '~icons/lucide/log-out'
import '~icons/lucide/user'
+import '~icons/svg-spinners/180-ring'
import styles from './Account.styles.css'
@@ -28,6 +29,7 @@ export interface AccountMenuItem {
export default class Account extends WebComponent {
static styles = styles
static states = {
+ initializing: (component: Account) => !component.auth.initialized,
loggedIn: (component: Account) => !!component.auth.account,
}
@@ -52,6 +54,12 @@ export default class Account extends WebComponent {
}
protected render () {
+ if (!this.auth.initialized) {
+ return html`
+
+ `
+ }
+
if (!this.auth.account) {
return html`
diff --git a/src/components/guard/Guard.stories.ts b/src/components/guard/Guard.stories.ts
index 881fc8857..e09d3e9b0 100644
--- a/src/components/guard/Guard.stories.ts
+++ b/src/components/guard/Guard.stories.ts
@@ -16,6 +16,7 @@ const meta = {
const render = defineAuthStoryRender(() => html`
+ Initializing content
Guest content
Logged in content
@@ -23,5 +24,6 @@ const render = defineAuthStoryRender(() => html`
export const Primary = { render }
export const Guest = { render, args: { user: 'Guest' } }
+export const Initializing = { render, args: { user: 'Initializing' } }
export default meta
diff --git a/src/components/guard/Guard.ts b/src/components/guard/Guard.ts
index 4d29dec53..416dcb8e7 100644
--- a/src/components/guard/Guard.ts
+++ b/src/components/guard/Guard.ts
@@ -8,7 +8,27 @@ export default class Guard extends WebComponent {
@consume({ context: authContext, subscribe: true })
private accessor auth: AuthContext = DEFAULT_AUTH_CONTEXT
+ private unsubscribeSessionUpdated?: () => void
+
+ connectedCallback () {
+ super.connectedCallback()
+
+ this.unsubscribeSessionUpdated = this.auth.onSessionUpdated(() => this.requestUpdate())
+ }
+
+ disconnectedCallback () {
+ super.disconnectedCallback()
+
+ this.unsubscribeSessionUpdated?.()
+ }
+
protected render () {
+ if (!this.auth.initialized) {
+ return html`
+
+ `
+ }
+
if (!this.auth.account) {
return html`
diff --git a/src/components/provider/Provider.ts b/src/components/provider/Provider.ts
index 9786e4f72..8020c4f89 100644
--- a/src/components/provider/Provider.ts
+++ b/src/components/provider/Provider.ts
@@ -15,6 +15,12 @@ export default class Provider extends WebComponent {
@provide({ context: authContext })
private accessor auth = new SolidAuth()
+ async connectedCallback () {
+ super.connectedCallback()
+
+ await this.auth.initialize()
+ }
+
protected willUpdate (changedProperties: Map) {
super.willUpdate(changedProperties)
diff --git a/src/lib/auth/NoopAuth.ts b/src/lib/auth/NoopAuth.ts
index d8d852533..dc0c28fea 100644
--- a/src/lib/auth/NoopAuth.ts
+++ b/src/lib/auth/NoopAuth.ts
@@ -1,10 +1,8 @@
import { AuthContext } from './context'
-import Account from './Account'
export default class NoopAuth implements AuthContext {
- get account (): Account | null {
- return null
- }
+ public readonly initialized = false
+ public readonly account = null
async login () {
throw new Error('Can\'t use auth, missing context provider')
diff --git a/src/lib/auth/SolidAuth.ts b/src/lib/auth/SolidAuth.ts
index ef16e2c18..d69b32e1e 100644
--- a/src/lib/auth/SolidAuth.ts
+++ b/src/lib/auth/SolidAuth.ts
@@ -1,9 +1,9 @@
-import { authSession, solidLogicSingleton } from 'solid-logic'
import Account from '@/lib/auth/Account'
+import ns from '@/lib/ns'
+import { authn, authSession, solidLogicSingleton } from 'solid-logic'
import { AuthContext } from '@/lib/auth'
import { showDialog } from '@/lib/dialogs'
import { html } from 'lit'
-import ns from '@/lib/ns'
import '@/components/login-modal'
@@ -24,11 +24,26 @@ function findAccountImage (webId: string): string | undefined {
}
export default class SolidAuth implements AuthContext {
+ private _initialized = false
+ private listeners: (() => unknown)[] = []
+
constructor (public signupUrl: string = DEFAULT_SIGNUP_URL) {}
+ async initialize () {
+ await authn.checkUser()
+
+ this._initialized = true
+ this.listeners.forEach(listener => listener())
+ }
+
+ get initialized (): boolean {
+ return this._initialized
+ }
+
get account (): Account | null {
const webId: string | undefined = authSession.webId ?? authSession.info?.webId
const isActive: boolean = authSession.isActive ?? authSession.info?.isLoggedIn ?? Boolean(webId)
+
if (!isActive || !webId) {
return null
}
@@ -74,21 +89,26 @@ export default class SolidAuth implements AuthContext {
const listener = () => {
callback()
}
+
+ this.listeners.push(listener)
+
if (typeof sessionEventTarget.addEventListener === 'function') {
sessionEventTarget.addEventListener('sessionStateChange', listener)
} else {
- authSession.events.on('login', callback)
- authSession.events.on('logout', callback)
- authSession.events.on('sessionRestore', callback)
+ authSession.events.on('login', listener)
+ authSession.events.on('logout', listener)
+ authSession.events.on('sessionRestore', listener)
}
return () => {
+ this.listeners = this.listeners.filter(_listener => _listener !== listener)
+
if (typeof sessionEventTarget.removeEventListener === 'function') {
sessionEventTarget.removeEventListener('sessionStateChange', listener)
} else {
- authSession.events.off('login', callback)
- authSession.events.off('logout', callback)
- authSession.events.off('sessionRestore', callback)
+ authSession.events.off('login', listener)
+ authSession.events.off('logout', listener)
+ authSession.events.off('sessionRestore', listener)
}
}
}
diff --git a/src/lib/auth/context.ts b/src/lib/auth/context.ts
index 822bb6ad2..14fc82a7e 100644
--- a/src/lib/auth/context.ts
+++ b/src/lib/auth/context.ts
@@ -3,6 +3,7 @@ import NoopAuth from './NoopAuth'
import Account from './Account'
export interface AuthContext {
+ initialized: boolean;
account: Account | null;
login(loginUrl?: string): Promise;
signup(): Promise;
diff --git a/src/storybook/auth/StorybookAuth.ts b/src/storybook/auth/StorybookAuth.ts
index 291004bc6..110ce4aa6 100644
--- a/src/storybook/auth/StorybookAuth.ts
+++ b/src/storybook/auth/StorybookAuth.ts
@@ -1,6 +1,7 @@
import { Account, AuthContext } from '@/lib/auth'
export default class StorybookAuth implements AuthContext {
+ public initialized = true
public account: Account | null = null
async login (loginUrl?: string) {
diff --git a/src/storybook/components/StorybookProvider.ts b/src/storybook/components/StorybookProvider.ts
index b34d4391c..b7bfbc7a1 100644
--- a/src/storybook/components/StorybookProvider.ts
+++ b/src/storybook/components/StorybookProvider.ts
@@ -15,12 +15,17 @@ export class StorybookProvider extends WebComponent {
@property({ type: String, reflect: true })
accessor avatarUrl: string | undefined
+ @property({ type: Boolean, reflect: true })
+ accessor initialized = true
+
@provide({ context: authContext })
private accessor auth = new StorybookAuth()
willUpdate (changedProperties: Map) {
super.willUpdate(changedProperties)
+ this.auth.initialized = this.initialized
+
if (!this.webId) {
this.auth.account = null
diff --git a/src/storybook/helpers.ts b/src/storybook/helpers.ts
index c1fa21033..2606149fe 100644
--- a/src/storybook/helpers.ts
+++ b/src/storybook/helpers.ts
@@ -9,11 +9,20 @@ export type GetStoryArgs = {
[K in keyof T]: T[K] extends { options: ArrayLike } ? TValue : T[K] extends { control: 'text' } ? string : never
}
-function renderStorybook (content: TemplateResult, user: ReturnType = null) {
+function renderStorybook (content: TemplateResult, user?: ReturnType) {
+ user ??= USER_OPTIONS.resolve('Guest')
+
const container = document.createElement('div')
+ const attributes = 'initialized' in user
+ ? { initialized: user.initialized }
+ : { webId: user.webId, avatarUrl: user.avatarUrl, initialized: true }
render(html`
-
+
${content}
`, container)
diff --git a/src/storybook/stubs.ts b/src/storybook/stubs.ts
index 89bb914c4..f46beec9e 100644
--- a/src/storybook/stubs.ts
+++ b/src/storybook/stubs.ts
@@ -13,6 +13,7 @@ export const users = {
} as const
export const USER_OPTIONS = defineControlOptions([
- ...objectEntries(users).map(([label, value]) => [label, value]) as [keyof typeof users | 'Guest', typeof users[keyof typeof users] | null][],
- ['Guest', null],
+ ...objectEntries(users).map(([label, value]) => [label, value]) as [keyof typeof users | 'Guest', typeof users[keyof typeof users]][],
+ ['Guest', { initialized: true }],
+ ['Initializing', { initialized: false }],
])