Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions src/components/account/Account.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ const meta = {

const render = defineAuthStoryRender(() => html`<solid-ui-account></solid-ui-account>`)

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
10 changes: 9 additions & 1 deletion src/components/account/Account.styles.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
8 changes: 8 additions & 0 deletions src/components/account/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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,
}

Expand All @@ -52,6 +54,12 @@ export default class Account extends WebComponent {
}

protected render () {
if (!this.auth.initialized) {
return html`
<icon-svg-spinners-180-ring></icon-svg-spinners-180-ring>
`
}

if (!this.auth.account) {
return html`
<solid-ui-login-button>
Expand Down
2 changes: 2 additions & 0 deletions src/components/guard/Guard.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ const meta = {

const render = defineAuthStoryRender<typeof meta.argTypes>(() => html`
<solid-guard>
<span slot="initializing">Initializing content</span>
<span slot="guest">Guest content</span>
<span>Logged in content</span>
</solid-guard>
`)

export const Primary = { render }
export const Guest = { render, args: { user: 'Guest' } }
export const Initializing = { render, args: { user: 'Initializing' } }

export default meta
20 changes: 20 additions & 0 deletions src/components/guard/Guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
<slot name="initializing"></slot>
`
}

if (!this.auth.account) {
return html`
<slot name="guest"></slot>
Expand Down
6 changes: 6 additions & 0 deletions src/components/provider/Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>) {
super.willUpdate(changedProperties)

Expand Down
6 changes: 2 additions & 4 deletions src/lib/auth/NoopAuth.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
36 changes: 28 additions & 8 deletions src/lib/auth/SolidAuth.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/lib/auth/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import NoopAuth from './NoopAuth'
import Account from './Account'

export interface AuthContext {
initialized: boolean;
account: Account | null;
login(loginUrl?: string): Promise<void>;
signup(): Promise<void>;
Expand Down
1 change: 1 addition & 0 deletions src/storybook/auth/StorybookAuth.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions src/storybook/components/StorybookProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>) {
super.willUpdate(changedProperties)

this.auth.initialized = this.initialized

if (!this.webId) {
this.auth.account = null

Expand Down
13 changes: 11 additions & 2 deletions src/storybook/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ export type GetStoryArgs<T extends object> = {
[K in keyof T]: T[K] extends { options: ArrayLike<infer TValue> } ? TValue : T[K] extends { control: 'text' } ? string : never
}

function renderStorybook (content: TemplateResult, user: ReturnType<typeof USER_OPTIONS.resolve> = null) {
function renderStorybook (content: TemplateResult, user?: ReturnType<typeof USER_OPTIONS.resolve>) {
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`
<storybook-provider webId="${user?.webId}" avatarUrl="${user?.avatarUrl}">
<storybook-provider
webId=${attributes.webId}
avatarUrl=${attributes.avatarUrl}
.initialized=${attributes.initialized}
>
${content}
</storybook-provider>
`, container)
Expand Down
5 changes: 3 additions & 2 deletions src/storybook/stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }],
])
Loading