import {
OAuthUserAgent,
configureOAuth,
createAuthorizationUrl,
deleteStoredSession,
finalizeAuthorization,
getSession,
listStoredSessions,
} from '@atcute/oauth-browser-client';
import type { ActorIdentifier, Did } from '@atcute/lexicons/syntax';
import type { Component, JSX } from 'solid-js';
import { createContext, createSignal, onMount, useContext } from 'solid-js';
import { TANGLED_OAUTH_SCOPE, identityResolver } from './api';
import { getErrorMessage } from './repo-utils';
const CURRENT_DID_KEY = 'untangled.currentDid';
configureOAuth({
metadata: {
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI,
},
identityResolver,
});
export interface AuthContextValue {
agent: () => OAuthUserAgent | null;
currentDid: () => Did | null;
error: () => string | null;
isLoading: () => boolean;
signIn: (identifier: string) => Promise<void>;
completeSignIn: () => Promise<string>;
signOut: () => Promise<void>;
refresh: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue>();
export const AuthProvider: Component<{ children: JSX.Element }> = (props) => {
const [agent, setAgent] = createSignal<OAuthUserAgent | null>(null);
const [currentDid, setCurrentDid] = createSignal<Did | null>(null);
const [isLoading, setIsLoading] = createSignal(true);
const [error, setError] = createSignal<string | null>(null);
const refresh = async () => {
setIsLoading(true);
setError(null);
const preferred = readStoredDid() ?? currentDid() ?? listStoredSessions()[0] ?? null;
if (!preferred) {
setAgent(null);
setCurrentDid(null);
setIsLoading(false);
return;
}
try {
const session = await getSession(preferred, {
allowStale: true,
});
const nextAgent = new OAuthUserAgent(session);
setAgent(nextAgent);
setCurrentDid(nextAgent.sub);
localStorage.setItem(CURRENT_DID_KEY, nextAgent.sub);
} catch (cause) {
deleteStoredSession(preferred);
localStorage.removeItem(CURRENT_DID_KEY);
setAgent(null);
setCurrentDid(null);
setError(getErrorMessage(cause));
} finally {
setIsLoading(false);
}
};
onMount(() => {
void refresh();
});
const signIn = async (identifier: string) => {
setError(null);
const trimmed = identifier.trim();
if (!trimmed) {
setError('Handle or DID required');
return;
}
try {
const url = await createAuthorizationUrl({
target: {
type: 'account',
identifier: trimmed as ActorIdentifier,
},
scope: TANGLED_OAUTH_SCOPE,
state: {
returnTo: `${location.pathname}${location.search}${location.hash}`,
},
});
window.location.assign(url.toString());
} catch (cause) {
setError(getErrorMessage(cause));
throw cause;
}
};
const completeSignIn = async (): Promise<string> => {
setIsLoading(true);
setError(null);
try {
const params = new URLSearchParams(location.hash.slice(1));
history.replaceState(null, '', location.pathname + location.search);
const { session, state } = await finalizeAuthorization(params);
const nextAgent = new OAuthUserAgent(session);
setAgent(nextAgent);
setCurrentDid(nextAgent.sub);
localStorage.setItem(CURRENT_DID_KEY, nextAgent.sub);
return typeof (state as { returnTo?: unknown } | null)?.returnTo === 'string'
? (state as { returnTo: string }).returnTo
: '/';
} catch (cause) {
setError(getErrorMessage(cause));
throw cause;
} finally {
setIsLoading(false);
}
};
const signOut = async () => {
setError(null);
const current = agent();
const did = currentDid();
try {
if (current) {
await current.signOut();
} else if (did) {
deleteStoredSession(did);
}
} catch {
if (did) {
deleteStoredSession(did);
}
} finally {
localStorage.removeItem(CURRENT_DID_KEY);
setAgent(null);
setCurrentDid(null);
}
};
const value: AuthContextValue = {
agent,
currentDid,
error,
isLoading,
signIn,
completeSignIn,
signOut,
refresh,
};
return <AuthContext.Provider value={value}>{props.children}</AuthContext.Provider>;
};
export const useAuth = (): AuthContextValue => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used inside AuthProvider');
}
return context;
};
const readStoredDid = (): Did | null => {
const value = localStorage.getItem(CURRENT_DID_KEY);
if (!value?.startsWith('did:')) {
return null;
}
return value as Did;
};