Skip to content

Instantly share code, notes, and snippets.

@arisris
Last active May 18, 2025 03:52
Show Gist options
  • Select an option

  • Save arisris/74cce117c4c7617348ff0d5fb4e170e3 to your computer and use it in GitHub Desktop.

Select an option

Save arisris/74cce117c4c7617348ff0d5fb4e170e3 to your computer and use it in GitHub Desktop.
Hanko auth provider for sveltekit, it use tailwindcss for styling
<script module lang="ts">
import { onMount, type Snippet } from "svelte";
import { register } from "@teamhanko/hanko-elements";
import type { StateName, Hanko, User } from "@teamhanko/hanko-elements";
import { PUBLIC_HANKO_API_URL } from "$env/static/public";
import { scale } from "svelte/transition";
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { browser } from "$app/environment";
let hanko = $state<Hanko>();
let user = $state<User | null>(null);
let loading = $state<boolean>(true);
let authDialogShow = $state<boolean>(false);
let elementLoaded = $state<boolean>(false);
const INIT_STATE_NAMES: StateName[] = [
"login_init",
"profile_init",
"registration_init",
];
const getAuth = () => {
if (!browser)
throw new Error("Hanko auth is only available in the browser context");
return hanko;
};
const toggleAuthDialog = () => {
if (elementLoaded) {
elementLoaded = false;
}
authDialogShow = !authDialogShow;
};
const signOut = (to?: string) => {
const auth = getAuth();
if (!auth) return;
auth.logout().then(() => {
if (authDialogShow) authDialogShow = false;
goto(to || "/");
});
};
const revalidate = async () => {
const auth = getAuth();
if (!auth) return;
try {
loading = true;
const token = auth.getSessionToken() as string | undefined;
if (token && token.length > 0) {
user = await auth.getUser();
} else {
user = null;
}
} catch (e) {
console.error(e);
user = null;
} finally {
loading = false;
}
};
export {
authButton,
revalidate,
getAuth,
signOut,
toggleAuthDialog,
hankoAuthProfileSnippet,
hankoAuthSnippet,
};
</script>
<script lang="ts">
let {
children,
}: {
children: Snippet;
} = $props();
const afterStateChange = (state: { state: { name: StateName } }) => {
if (INIT_STATE_NAMES.includes(state.state.name)) {
elementLoaded = true;
}
};
$effect(() => {
if (hanko) {
const u1 = hanko.onSessionCreated(revalidate);
const u2 = hanko.onUserLoggedOut(revalidate);
const u3 = hanko.onSessionExpired(revalidate);
const u4 = hanko.onUserDeleted(revalidate);
const u5 = hanko.onAfterStateChange(afterStateChange);
return () => {
u1();
u2();
u3();
u4();
u5();
};
}
});
onMount(() => {
register(PUBLIC_HANKO_API_URL, {
shadow: true,
sessionCheckInterval: 1000 * 60,
})
.then((e) => (hanko = e.hanko))
.then(revalidate)
.catch((e) => {
console.error(e);
hanko = undefined;
});
});
</script>
{#snippet hankoAuthSnippet()}
<hanko-auth></hanko-auth>
{/snippet}
{#snippet hankoAuthProfileSnippet()}
<hanko-profile></hanko-profile>
{/snippet}
{#snippet authButton()}
{#if loading}
<button type="button" class="btn btn-primary btn-block btn-sm" disabled>
<span class="loading loading-xs loading-bars"></span>
<span class="sr-only">Auth Loading</span>
</button>
{:else if user}
<button
type="button"
title="Profile"
class="group btn btn-secondary btn-block btn-sm"
onclick={toggleAuthDialog}
>
<span>Profile</span>
<span class="iconify ri--user-line size-4 group-hover:scale-105"></span>
</button>
{:else}
<button
type="button"
title="Sign In"
class="group btn btn-primary btn-block btn-sm"
onclick={toggleAuthDialog}
>
<span>Sign In</span>
<span class="iconify ri--login-box-line size-4 group-hover:scale-105"
></span>
</button>
{/if}
{/snippet}
{@render children()}
{#if hanko && authDialogShow}
<dialog out:scale={{ duration: 200 }} class="modal modal-open">
<div
class="relative flex flex-col items-center justify-center p-4 w-full max-w-[410px] bg-base-100 rounded-md"
>
{#if user}
{@render hankoAuthProfileSnippet()}
{#if elementLoaded}
<div class="flex flex-col gap-y-1 mt-auto w-full">
{#if !page.url.pathname.startsWith("/manage")}
<button
type="button"
class="btn btn-primary btn-block btn-sm"
onclick={() => {
authDialogShow = false;
setTimeout(() => goto("/manage"), 300);
}}
>
<span class="iconify ri--dashboard-line size-4"></span>
<span>Manage</span>
</button>
{/if}
<button
type="button"
title="Sign Out"
class="group btn btn-secondary btn-block btn-sm"
onclick={() => signOut()}
>
<span
class="iconify ri--logout-box-line size-4 group-hover:scale-105"
></span>
<span>Sign Out</span>
</button>
</div>
{/if}
{:else}
{@render hankoAuthSnippet()}
{/if}
{#if !elementLoaded}
<spam class="text-sm">Loading...</spam>
{/if}
<form method="dialog" class="absolute top-1 right-1">
<button
class="tooltip tooltip-bottom btn btn-ghost btn-circle"
tabindex="-1"
data-tip="Close"
onclick={toggleAuthDialog}
>
<span class="iconify ri--close-line size-6"></span>
<span class="sr-only">Close</span>
</button>
</form>
</div>
</dialog>
{/if}
<style lang="postcss">
@reference "$lib/app.css";
hanko-auth,
hanko-profile {
--background-color: var(--color-base-100);
--color: var(--color-base-content);
--color-shade-1: color-mix(
in oklab,
var(--color) 50%,
var(--color-base-content) 10%
);
--color-shade-2: var(--color-base-300);
--brand-color: var(--color-primary);
--brand-color-shade-1: color-mix(
in oklab,
var(--brand-color),
var(--color-primary-content) 5%
);
--brand-contrast-color: color-mix(
in oklab,
var(--color),
var(--color-primary-content) 5%
);
--link-color: var(--color-primary);
--error-color: var(--color-error);
}
hanko-profile::part(container) {
--container-padding: 24px 6px;
}
hanko-profile::part(headline1) {
--headline1-font-size: 18px;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment