Feature: Deep Link + Supabase OAuth + open extension in store with deep link (#16)

* feat(auth): add deep link and supabase auth

* fix(deep-link): fix some routing and reactive page rendering

* feat: implement supabase auth with pkce auth flow
This commit is contained in:
Huakun Shen 2024-11-05 09:27:52 -05:00 committed by GitHub
parent 2c99f231f7
commit 605a7844f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 422 additions and 78 deletions

View File

@ -1,10 +1,9 @@
import { appConfig, appState } from "@/stores"
import { appConfig, appState, auth } from "@/stores"
import { checkUpdateAndInstall } from "@/utils/updater"
import type { BuiltinCmd } from "@kksh/ui/types"
import { getVersion } from "@tauri-apps/api/app"
import { WebviewWindow } from "@tauri-apps/api/webviewWindow"
import { exit } from "@tauri-apps/plugin-process"
import { dev } from "$app/environment"
import { goto } from "$app/navigation"
import { toast } from "svelte-sonner"
import { v4 as uuidv4 } from "uuid"
@ -19,23 +18,25 @@ export const builtinCmds: BuiltinCmd[] = [
goto("/extension/store")
}
},
// {
// name: "Sign In",
// iconifyIcon: "mdi:login-variant",
// description: "",
// function: async () => {
// goto("/auth")
// }
// },
// {
// name: "Sign Out",
// iconifyIcon: "mdi:logout-variant",
// description: "",
// function: async () => {
// const supabase = useSupabaseClient()
// supabase.auth.signOut()
// }
// },
{
name: "Sign In",
iconifyIcon: "mdi:login-variant",
description: "",
function: async () => {
goto("/auth")
}
},
{
name: "Sign Out",
iconifyIcon: "mdi:logout-variant",
description: "",
function: async () => {
auth
.signOut()
.then(() => toast.success("Signed out"))
.catch((err) => toast.error("Failed to sign out: ", { description: err.message }))
}
},
{
name: "Show Draggable Area",
iconifyIcon: "mingcute:move-fill",

View File

@ -36,7 +36,7 @@ interface AppConfigAPI {
}
function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
const { subscribe, update, set } = writable<AppConfig>(defaultAppConfig)
const store = writable<AppConfig>(defaultAppConfig)
async function init() {
debug("Initializing app config")
@ -46,7 +46,7 @@ function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
if (parseRes.success) {
console.log("Parse Persisted App Config Success", parseRes.output)
const extensionsInstallDir = await getExtensionsFolder()
update((config) => ({
store.update((config) => ({
...config,
...parseRes.output,
isInitialized: true,
@ -60,7 +60,7 @@ function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
await persistStore.set("config", v.parse(PersistedAppConfig, defaultAppConfig))
}
subscribe(async (config) => {
store.subscribe(async (config) => {
console.log("Saving app config", config)
await persistStore.set("config", config)
updateTheme(config.theme)
@ -68,15 +68,13 @@ function createAppConfig(): Writable<AppConfig> & AppConfigAPI {
}
return {
setTheme: (theme: ThemeConfig) => update((config) => ({ ...config, theme })),
...store,
setTheme: (theme: ThemeConfig) => store.update((config) => ({ ...config, theme })),
setDevExtensionPath: (devExtensionPath: string | null) => {
console.log("setDevExtensionPath", devExtensionPath)
update((config) => ({ ...config, devExtensionPath }))
store.update((config) => ({ ...config, devExtensionPath }))
},
init,
subscribe,
update,
set
init
}
}

View File

@ -1,4 +1,4 @@
import type { AppState } from "@/types"
import type { AppState } from "@kksh/types"
import { get, writable, type Writable } from "svelte/store"
export const defaultAppState: AppState = {
@ -15,9 +15,7 @@ function createAppState(): Writable<AppState> & AppStateAPI {
const store = writable<AppState>(defaultAppState)
return {
subscribe: store.subscribe,
update: store.update,
set: store.set,
...store,
get: () => get(store),
clearSearchTerm: () => {
store.update((state) => ({ ...state, searchTerm: "" }))

View File

@ -0,0 +1,47 @@
import { supabase } from "@/supabase"
import type { AuthError, Session, User } from "@supabase/supabase-js"
import { get, writable, type Writable } from "svelte/store"
type State = { session: Session | null; user: User | null }
interface AuthAPI {
get: () => State
refresh: () => Promise<void>
signOut: () => Promise<{ error: AuthError | null }>
signInExchange: (code: string) => Promise<{ error: AuthError | null }>
}
function createAuth(): Writable<State> & AuthAPI {
const store = writable<State>({ session: null, user: null })
async function refresh() {
const {
data: { session },
error
} = await supabase.auth.getSession()
const {
data: { user }
} = await supabase.auth.getUser()
store.update((state) => ({ ...state, session, user }))
}
async function signOut() {
return supabase.auth.signOut().then((res) => {
refresh()
return res
})
}
async function signInExchange(code: string) {
return supabase.auth.exchangeCodeForSession(code).then((res) => {
refresh()
return res
})
}
return {
...store,
get: () => get(store),
refresh,
signOut,
signInExchange
}
}
export const auth = createAuth()

View File

@ -16,11 +16,11 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
uninstallStoreExtensionByIdentifier: (identifier: string) => Promise<ExtPackageJsonExtra>
upgradeStoreExtension: (identifier: string, tarballUrl: string) => Promise<ExtPackageJsonExtra>
} {
const { subscribe, update, set } = writable<ExtPackageJsonExtra[]>([])
const store = writable<ExtPackageJsonExtra[]>([])
function init() {
return extAPI.loadAllExtensionsFromDb().then((exts) => {
set(exts)
store.set(exts)
})
}
@ -43,7 +43,7 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
return extAPI
.loadExtensionManifestFromDisk(await path.join(extPath, "package.json"))
.then((ext) => {
update((exts) => {
store.update((exts) => {
const existingExt = exts.find((e) => e.extPath === ext.extPath)
if (existingExt) return exts
return [...exts, ext]
@ -69,7 +69,7 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
return extAPI
.uninstallExtensionByPath(targetPath)
.then(() => update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath)))
.then(() => store.update((exts) => exts.filter((ext) => ext.extPath !== targetExt.extPath)))
.then(() => targetExt)
}
@ -91,16 +91,14 @@ function createExtensionsStore(): Writable<ExtPackageJsonExtra[]> & {
}
return {
...store,
init,
getExtensionsFromStore,
findStoreExtensionByIdentifier,
registerNewExtensionByPath,
installFromTarballUrl,
uninstallStoreExtensionByIdentifier,
upgradeStoreExtension,
subscribe,
update,
set
upgradeStoreExtension
}
}

View File

@ -2,3 +2,4 @@ export * from "./appConfig"
export * from "./appState"
export * from "./winExtMap"
export * from "./extensions"
export * from "./auth"

View File

@ -40,6 +40,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
async function init() {}
return {
...store,
init,
registerExtensionWithWindow: async ({
extPath,
@ -114,10 +115,7 @@ function createWinExtMapStore(): Writable<WinExtMap> & API {
return unregisterExtensionSpawnedProcess(windowLabel, pid).then(() => {
ext.pids = ext.pids.filter((p) => p !== pid)
})
},
subscribe: store.subscribe,
update: store.update,
set: store.set
}
}
}

View File

@ -0,0 +1,115 @@
import { emitRefreshDevExt } from "@/utils/tauri-events"
import {
DEEP_LINK_PATH_AUTH_CONFIRM,
DEEP_LINK_PATH_OPEN,
DEEP_LINK_PATH_REFRESH_DEV_EXTENSION,
DEEP_LINK_PATH_STORE
} from "@kksh/api"
import type { UnlistenFn } from "@tauri-apps/api/event"
import { extname } from "@tauri-apps/api/path"
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"
import * as deepLink from "@tauri-apps/plugin-deep-link"
import { error } from "@tauri-apps/plugin-log"
import { goto } from "$app/navigation"
import { toast } from "svelte-sonner"
import * as v from "valibot"
import { isInMainWindow } from "./window"
const StorePathSearchParams = v.object({
identifier: v.optional(v.string())
})
export function initDeeplink(): Promise<UnlistenFn> {
console.log("init deeplink")
if (!isInMainWindow()) {
return Promise.resolve(() => {})
}
// deepLink.getCurrent()
return deepLink.onOpenUrl((urls) => {
console.log("deep link:", urls)
urls.forEach(handleDeepLink)
})
}
/**
* Show and focus on the main window
*/
function openMainWindow() {
const appWindow = getCurrentWebviewWindow()
return appWindow
.show()
.then(() => appWindow.setFocus())
.catch((err) => {
console.error(err)
error(`Failed to show window upon deep link: ${err.message}`)
toast.error("Failed to show window upon deep link", {
description: err.message
})
})
}
export async function handleKunkunProtocol(parsedUrl: URL) {
const params = Object.fromEntries(parsedUrl.searchParams)
const { host, pathname, href } = parsedUrl
if (href.startsWith(DEEP_LINK_PATH_OPEN)) {
openMainWindow()
} else if (href.startsWith(DEEP_LINK_PATH_STORE)) {
const parsed = v.parse(StorePathSearchParams, params)
openMainWindow()
if (parsed.identifier) {
goto(`/extension/store/${parsed.identifier}`)
} else {
goto("/extension/store")
}
} else if (href.startsWith(DEEP_LINK_PATH_REFRESH_DEV_EXTENSION)) {
emitRefreshDevExt()
} else if (href.startsWith(DEEP_LINK_PATH_AUTH_CONFIRM)) {
openMainWindow()
goto(`/auth/confirm?${parsedUrl.searchParams.toString()}`)
} else {
console.error("Invalid path:", pathname)
toast.error("Invalid path", {
description: parsedUrl.href
})
}
}
export async function handleFileProtocol(parsedUrl: URL) {
console.log("File protocol:", parsedUrl)
const filePath = parsedUrl.pathname // Remove the leading '//' kunkun://open?identifier=qrcode gives "open"
console.log("File path:", filePath)
// from file absolute path, get file extension
const fileExt = await extname(filePath)
console.log("File extension:", fileExt)
switch (fileExt) {
case "kunkun":
// TODO: Handle file protocol, install extension from file (essentially a .tgz file)
break
default:
console.error("Unknown file extension:", fileExt)
toast.error("Unknown file extension", {
description: fileExt
})
break
}
}
/**
*
* @param url Deep Link URl, e.g. kunkun://open
*/
export async function handleDeepLink(url: string) {
const parsedUrl = new URL(url)
switch (parsedUrl.protocol) {
case "kunkun:":
return handleKunkunProtocol(parsedUrl)
case "file:":
return handleFileProtocol(parsedUrl)
default:
console.error("Invalid Protocol:", parsedUrl.protocol)
toast.error("Invalid Protocol", {
description: parsedUrl.protocol
})
break
}
}

View File

@ -23,3 +23,13 @@ export function goBackOnEscapeClearSearchTerm(e: KeyboardEvent) {
}
}
}
export function goHomeOnEscapeClearSearchTerm(e: KeyboardEvent) {
if (e.key === "Escape") {
if (appState.get().searchTerm) {
appState.clearSearchTerm()
} else {
goHome()
}
}
}

View File

@ -1,4 +1,5 @@
<script lang="ts">
import { goHome } from "@/utils/route"
import { Error, Layouts } from "@kksh/ui"
import { page } from "$app/stores"
@ -11,12 +12,12 @@
<svelte:window on:keydown={handleKeyDown} />
<Layouts.Center class="h-screen">
<Layouts.Center class="min-h-screen py-5">
<Error.RawErrorJSONPreset
title="Unknown Error"
class="w-fit max-w-screen-sm"
title="Error"
class="w-fit max-w-screen-sm border-2 border-red-500"
message={$page.error?.message ?? "Unknown Error"}
onnGoBack={() => window.history.back()}
onGoBack={goHome}
rawJsonError={JSON.stringify($page, null, 2)}
/>
</Layouts.Center>

View File

@ -2,6 +2,7 @@
import AppContext from "@/components/context/AppContext.svelte"
import "../app.css"
import { appConfig, appState, extensions } from "@/stores"
import { initDeeplink } from "@/utils/deeplink"
import { isInMainWindow } from "@/utils/window"
import {
ModeWatcher,
@ -21,6 +22,7 @@
onMount(async () => {
unlisteners.push(await attachConsole())
unlisteners.push(await initDeeplink())
appConfig.init()
if (isInMainWindow()) {
extensions.init()

View File

@ -0,0 +1,62 @@
<script lang="ts">
import { auth } from "@/stores"
import { supabase } from "@/supabase"
import { goBackOnEscape } from "@/utils/key"
import { goBack, goHome } from "@/utils/route"
import Icon from "@iconify/svelte"
import { DEEP_LINK_PATH_AUTH_CONFIRM } from "@kksh/api"
import { Button, Card } from "@kksh/svelte5"
import { Layouts } from "@kksh/ui"
import { goto } from "$app/navigation"
import { ArrowLeft } from "lucide-svelte"
import { onMount } from "svelte"
import { toast } from "svelte-sonner"
import { open } from "tauri-plugin-shellx-api"
const redirectTo = DEEP_LINK_PATH_AUTH_CONFIRM
const signInWithOAuth = async (provider: "github" | "google") => {
console.log(`Login with ${provider} redirecting to ${redirectTo}`)
const { error, data } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo,
skipBrowserRedirect: true
}
})
if (error) {
toast.error("Failed to sign in with OAuth", { description: error.message })
} else {
data.url && open(data.url)
}
}
onMount(() => {
if ($auth.session) {
toast.success("Already Signed In")
goHome()
}
})
</script>
<svelte:window on:keydown={goBackOnEscape} />
<Button variant="outline" size="icon" onclick={goBack} class="absolute left-2 top-2 z-50">
<ArrowLeft class="size-4" />
</Button>
<div class="absolute h-10 w-full" data-tauri-drag-region></div>
<Layouts.Center class="h-screen w-screen" data-tauri-drag-region>
<Card.Root class="w-80">
<Card.Header class="flex flex-col items-center">
<img src="/favicon.png" alt="Kunkun" class="h-12 w-12 invert" />
<Card.Title class="text-xl">Sign In</Card.Title>
</Card.Header>
<Card.Content class="flex flex-col gap-2">
<Button variant="outline" size="lg" class="w-full" onclick={() => signInWithOAuth("github")}>
<Icon icon="fa6-brands:github" class="h-5 w-5" />
</Button>
<Button variant="outline" size="lg" class="w-full" onclick={() => signInWithOAuth("google")}>
<Icon icon="logos:google-icon" class="h-5 w-5" />
</Button>
</Card.Content>
</Card.Root>
</Layouts.Center>

View File

@ -0,0 +1,84 @@
<script lang="ts">
import { auth } from "@/stores"
import { supabase } from "@/supabase"
import { goHomeOnEscape } from "@/utils/key"
import { goBack, goHome } from "@/utils/route"
import { Avatar, Button } from "@kksh/svelte5"
import { goto } from "$app/navigation"
import { ArrowLeft } from "lucide-svelte"
import { onMount } from "svelte"
import { toast } from "svelte-sonner"
const { data } = $props()
async function authExchange() {
if (data.code) {
auth.signInExchange(data.code).then((res) => {
if (res.error) {
toast.error("Failed to sign in", { description: res.error.message })
} else {
toast.success("Signed In")
}
})
} else {
toast.error("No code found")
}
}
const avatarFallback = $derived.by(() => {
if (!$auth.session) return "?"
const nameSplit = $auth.session.user.user_metadata.name.split(" ").filter(Boolean)
if (nameSplit.length > 1) {
return nameSplit[0][0] + nameSplit.at(-1)[0]
} else if (nameSplit.length === 1) {
return nameSplit[0][0]
} else {
return "?"
}
})
onMount(() => {
authExchange()
})
function onSignOut() {
auth
.signOut()
.then(() => goto("/auth"))
.catch((err) => toast.error("Failed to sign out", { description: err.message }))
}
</script>
<svelte:window on:keydown={goHomeOnEscape} />
<Button
class="absolute left-2 top-2 z-50"
variant="outline"
size="icon"
onclick={() => {
console.log("go Home")
goto("/")
}}
>
<ArrowLeft class="size-4" />
</Button>
<div class="h-10 w-full" data-tauri-drag-region></div>
<main class="container pt-10">
<div class="flex grow items-center justify-center pt-16">
<div class="flex flex-col items-center gap-4">
{#if $auth.session}
<span class="font-mono text-4xl font-bold">Welcome, You are Logged In</span>
{:else}
<span class="font-mono text-4xl font-bold">You Are Not Logged In</span>
{/if}
<span class="flex flex-col items-center gap-5 text-xl">
{#if $auth.session}
<Avatar.Root class="h-32 w-32 border">
<Avatar.Image src={$auth.session?.user.user_metadata.avatar_url} alt="avatar" />
<Avatar.Fallback>{avatarFallback}</Avatar.Fallback>
</Avatar.Root>
{/if}
<Button variant="outline" onclick={onSignOut}>Sign Out</Button>
</span>
</div>
</div>
</main>

View File

@ -0,0 +1,10 @@
import { error } from "@sveltejs/kit"
import type { PageLoad } from "./$types"
export const load: PageLoad = async ({ params, url }) => {
const code = url.searchParams.get("code")
if (!code) {
throw error(400, "Auth Exchange Code is Required")
}
return { params, code }
}

View File

@ -2,8 +2,8 @@
import { getExtensionsFolder } from "@/constants"
import { appState, extensions } from "@/stores"
import { supabaseAPI } from "@/supabase"
import { goBackOnEscapeClearSearchTerm } from "@/utils/key"
import { goBack } from "@/utils/route"
import { goBackOnEscapeClearSearchTerm, goHomeOnEscapeClearSearchTerm } from "@/utils/key"
import { goBack, goHome } from "@/utils/route"
import { SBExt } from "@kksh/api/supabase"
import { isUpgradable } from "@kksh/extension"
import { Button, Command } from "@kksh/svelte5"
@ -64,12 +64,12 @@
}
</script>
<svelte:window on:keydown={goBackOnEscapeClearSearchTerm} />
<svelte:window on:keydown={goHomeOnEscapeClearSearchTerm} />
{#snippet leftSlot()}
<Button
variant="outline"
size="icon"
onclick={goBack}
onclick={goHome}
class={Constants.CLASSNAMES.BACK_BUTTON}
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
>

View File

@ -12,12 +12,12 @@
<svelte:window on:keydown={handleKeyDown} />
<Layouts.Center class="h-screen">
<Layouts.Center class="min-h-screen py-5">
<Error.RawErrorJSONPreset
title="Fail to Load Extension"
class="w-fit max-w-screen-sm"
class="w-fit max-w-screen-sm border-2 border-red-500"
message={$page.error?.message ?? "Unknown Error"}
onnGoBack={() => goto("/")}
onGoBack={() => goto("/")}
rawJsonError={JSON.stringify($page, null, 2)}
/>
</Layouts.Center>

View File

@ -9,13 +9,16 @@
import { StoreExtDetail } from "@kksh/ui/extension"
import { greaterThan, parse as parseSemver } from "@std/semver"
import { error } from "@tauri-apps/plugin-log"
import { goto } from "$app/navigation"
import { ArrowLeftIcon } from "lucide-svelte"
import { onMount } from "svelte"
import { toast } from "svelte-sonner"
import { get, derived as storeDerived } from "svelte/store"
const { data } = $props()
let { ext, manifest } = data
// let { ext, manifest } = data
const ext = $derived(data.ext)
const manifest = $derived(data.manifest)
const installedExt = storeDerived(installedStoreExts, ($e) => {
return $e.find((e) => e.kunkun.identifier === ext.identifier)
})
@ -133,7 +136,7 @@
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
if (!delayedImageDialogOpen) {
goBack()
goto("/extension/store")
}
}
}
@ -145,7 +148,7 @@
size="icon"
class={cn("fixed left-3 top-3", Constants.CLASSNAMES.BACK_BUTTON)}
data-flip-id={Constants.CLASSNAMES.BACK_BUTTON}
onclick={goBack}
onclick={() => goto("/extension/store")}
>
<ArrowLeftIcon />
</Button>

View File

@ -13,7 +13,12 @@ export const load: PageLoad = async ({
}): Promise<{
ext: Tables<"ext_publish">
manifest: KunkunExtManifest
params: {
identifier: string
}
}> => {
console.log("store[identifier] params", params)
const { error: dbError, data: ext } = await supabaseAPI.getLatestExtPublish(params.identifier)
if (dbError) {
return error(400, {
@ -30,6 +35,7 @@ export const load: PageLoad = async ({
return {
ext,
params,
manifest: parseManifest.output
}
}

View File

@ -15,8 +15,7 @@
},
"devDependencies": {
"@gcornut/valibot-json-schema": "^0.42.0",
"@types/bun": "latest",
"supabase": ">=1.8.1"
"@types/bun": "latest"
},
"peerDependencies": {
"@kksh/supabase": "workspace:*",
@ -25,7 +24,6 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.583.0",
"@kksh/api": "workspace:*",
"@supabase/supabase-js": "^2.43.4",
"valibot": "^0.40.0"
}
}

View File

@ -1,10 +1,9 @@
import { ExtPackageJson } from "@kksh/api/models"
import { type Database } from "@kksh/supabase"
import { createClient } from "@supabase/supabase-js"
import { createSB } from "@kksh/supabase"
import { parse, string } from "valibot"
import { getJsonSchema } from "../src"
const supabase = createClient<Database>(
const supabase = createSB(
parse(string(), process.env.SUPABASE_URL),
parse(string(), process.env.SUPABASE_SERVICE_ROLE_KEY)
)

View File

@ -2,7 +2,11 @@ import type { Database } from "@kksh/api/supabase/types"
import { createClient } from "@supabase/supabase-js"
export function createSB(supabaseUrl: string, supabaseAnonKey: string) {
return createClient<Database>(supabaseUrl, supabaseAnonKey)
return createClient<Database>(supabaseUrl, supabaseAnonKey, {
auth: {
flowType: "pkce"
}
})
}
export { SupabaseAPI } from "./api"

View File

@ -1,22 +1,25 @@
<script lang="ts">
import autoAnimate from "@formkit/auto-animate"
import Icon from "@iconify/svelte"
import { Button, ScrollArea } from "@kksh/svelte5"
import { Button, buttonVariants, Collapsible, ScrollArea } from "@kksh/svelte5"
import { Error, Layouts, Shiki } from "@kksh/ui"
import { ChevronsUpDown } from "lucide-svelte"
import { type Snippet } from "svelte"
import { fade, slide } from "svelte/transition"
const {
title,
message,
class: className,
rawJsonError,
onnGoBack,
onGoBack,
footer
}: {
title: string
message: string
class?: string
rawJsonError: string
onnGoBack?: () => void
onGoBack?: () => void
footer?: Snippet
} = $props()
@ -38,15 +41,27 @@
<svelte:window on:keydown={handleKeyDown} on:keyup={handleKeyUp} />
<Error.General {title} {message} class={className}>
<ScrollArea class="" orientation="both">
<Shiki class="" code={rawJsonError} lang="json" />
</ScrollArea>
<Collapsible.Root class="w-full space-y-2">
<div class="flex items-center justify-between space-x-4 px-4">
<h4 class="text-sm font-semibold">Raw Error JSON</h4>
<Collapsible.Trigger
class={buttonVariants({ variant: "ghost", size: "sm", class: "w-9 p-0" })}
>
<ChevronsUpDown class="size-4" />
</Collapsible.Trigger>
</div>
<Collapsible.Content class="space-y-2">
<ScrollArea class="h-64" orientation="both">
<Shiki class="" code={rawJsonError} lang="json" />
</ScrollArea>
</Collapsible.Content>
</Collapsible.Root>
<br />
{#snippet footer()}
{#if footer}
{@render footer()}
{:else}
<Button variant="default" class="w-full" onclick={onnGoBack} disabled={enterDown}>
<Button variant="default" class="w-full" onclick={onGoBack} disabled={enterDown}>
Go Back
<Icon icon="mi:enter" />
</Button>

6
pnpm-lock.yaml generated
View File

@ -415,9 +415,6 @@ importers:
'@kksh/supabase':
specifier: workspace:*
version: link:../supabase
'@supabase/supabase-js':
specifier: ^2.43.4
version: 2.46.1
typescript:
specifier: ^5.0.0
version: 5.5.4
@ -431,9 +428,6 @@ importers:
'@types/bun':
specifier: latest
version: 1.1.13
supabase:
specifier: '>=1.8.1'
version: 1.207.9
packages/supabase:
dependencies: