mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 04:19:32 +00:00
feat: lynx integration (#4470)
This commit is contained in:
@@ -44,6 +44,27 @@ export const Icons = {
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
lynx: (props?: SVGProps<any>) => (
|
||||
<svg
|
||||
className={props?.className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M5.19683 4.42475L2.58326 6.22507C2.29923 6.42072 2.10258 6.71472 2.0335 7.04698L1.77798 8.27604C1.76443 8.34121 1.73438 8.40204 1.69058 8.45301L0.504615 10.0077C0.344941 10.1935 0.348791 10.6871 0.786137 11.0031C0.953848 11.1493 1.17807 11.477 1.43312 11.8497L1.43312 11.8497C1.97313 12.639 2.65131 13.6302 3.22376 13.5272C4.03382 13.2426 5.02541 13.1516 5.78673 13.5272C7.2667 14.8049 6.90331 15.983 6.38234 17.672C6.17271 18.3516 5.93755 19.114 5.78673 19.9988C6.49755 17.4117 8.09592 14.4069 10.9781 13.3874C10.4588 12.9632 9.39906 12.5691 8.46129 12.4742C8.46129 12.4742 11.3423 10.0077 14.8957 8.87434C12.4151 2.97197 8.32052 0.151295 8.32052 0.151295C8.11187 -0.10677 7.69054 -0.0221092 7.6036 0.295351C7.53376 1.22845 7.41798 1.86295 7.22685 2.46105L5.78673 0.799291C5.6363 0.61794 5.33557 0.722655 5.33707 0.955861C5.5809 2.31136 5.54668 3.07222 5.19683 4.42475ZM6.21052 4.30085L6.21912 4.3003C6.23794 4.29909 6.25646 4.29664 6.27456 4.29302L6.21052 4.30085ZM8.15541 1.25793C9.1423 2.96321 9.58937 3.932 9.74102 5.73998C8.6912 5.14364 8.23382 4.99187 7.46183 4.99565C7.91215 3.62621 8.04976 2.8016 8.15541 1.25793Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M14.4988 13.9228C10.479 14.8427 8.19556 16.2278 6.44922 19.9994C9.58406 14.7399 19.0737 15.6805 19.0737 15.6805C18.8964 14.8214 17.0097 13.183 15.7688 12.1782C15.7688 12.1782 16.7699 10.9474 18.983 10.3244C18.983 10.3244 14.728 10.5709 12.2508 11.9084C13.0533 12.3319 14.0812 13.0467 14.4988 13.9228Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
solidStart: (props?: SVGProps<any>) => (
|
||||
<svg
|
||||
className={props?.className}
|
||||
|
||||
@@ -1402,6 +1402,11 @@ C0.7,239.6,62.1,0.5,62.2,0.4c0,0,54,13.8,119.9,30.8S302.1,62,302.2,62c0.2,0,0.2,
|
||||
icon: Icons.expo,
|
||||
href: "/docs/integrations/expo",
|
||||
},
|
||||
{
|
||||
title: "Lynx",
|
||||
icon: Icons.lynx,
|
||||
href: "/docs/integrations/lynx",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
212
docs/content/docs/integrations/lynx.mdx
Normal file
212
docs/content/docs/integrations/lynx.mdx
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
title: Lynx Integration
|
||||
description: Integrate Better Auth with Lynx cross-platform framework.
|
||||
---
|
||||
|
||||
This integration guide is for using Better Auth with [Lynx](https://lynxjs.org), a cross-platform rendering framework that enables developers to build applications for Android, iOS, and Web platforms with native rendering performance.
|
||||
|
||||
Before you start, make sure you have a Better Auth instance configured. If you haven't done that yet, check out the [installation](/docs/installation).
|
||||
|
||||
## Installation
|
||||
|
||||
Install Better Auth and the Lynx React dependency:
|
||||
|
||||
```package-install
|
||||
better-auth @lynx-js/react
|
||||
```
|
||||
|
||||
## Create Client Instance
|
||||
|
||||
Import `createAuthClient` from `better-auth/lynx` to create your client instance:
|
||||
|
||||
```ts title="lib/auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/lynx"
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: "http://localhost:3000" // The base URL of your auth server
|
||||
})
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The Lynx client provides the same API as other Better Auth clients, with optimized integration for Lynx's reactive system.
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
```ts
|
||||
import { authClient } from "./lib/auth-client"
|
||||
|
||||
// Sign in with email and password
|
||||
await authClient.signIn.email({
|
||||
email: "test@user.com",
|
||||
password: "password1234"
|
||||
})
|
||||
|
||||
// Sign up
|
||||
await authClient.signUp.email({
|
||||
email: "test@user.com",
|
||||
password: "password1234",
|
||||
name: "John Doe"
|
||||
})
|
||||
|
||||
// Sign out
|
||||
await authClient.signOut()
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
The Lynx client includes reactive hooks that integrate seamlessly with Lynx's component system:
|
||||
|
||||
#### useSession
|
||||
|
||||
```tsx title="components/user.tsx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function User() {
|
||||
const {
|
||||
data: session,
|
||||
isPending, // loading state
|
||||
error // error object
|
||||
} = authClient.useSession()
|
||||
|
||||
if (isPending) return <div>Loading...</div>
|
||||
if (error) return <div>Error: {error.message}</div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
{session ? (
|
||||
<div>
|
||||
<p>Welcome, {session.user.name}!</p>
|
||||
<button onClick={() => authClient.signOut()}>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button onClick={() => authClient.signIn.social({
|
||||
provider: 'github'
|
||||
})}>
|
||||
Sign In with GitHub
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Store Integration
|
||||
|
||||
The Lynx client uses [nanostores](https://github.com/nanostores/nanostores) for state management and provides a `useStore` hook for accessing reactive state:
|
||||
|
||||
```tsx title="components/session-info.tsx"
|
||||
import { useStore } from "better-auth/lynx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function SessionInfo() {
|
||||
// Access the session store directly
|
||||
const session = useStore(authClient.$store.session)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{session && (
|
||||
<pre>{JSON.stringify(session, null, 2)}</pre>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Advanced Store Usage
|
||||
|
||||
You can use the store with selective key watching for optimized re-renders:
|
||||
|
||||
```tsx title="components/optimized-user.tsx"
|
||||
import { useStore } from "better-auth/lynx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function OptimizedUser() {
|
||||
// Only re-render when specific keys change
|
||||
const session = useStore(authClient.$store.session, {
|
||||
keys: ['user.name', 'user.email'] // Only watch these specific keys
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
{session?.user && (
|
||||
<div>
|
||||
<h2>{session.user.name}</h2>
|
||||
<p>{session.user.email}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Plugin Support
|
||||
|
||||
The Lynx client supports all Better Auth plugins:
|
||||
|
||||
```ts title="lib/auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/lynx"
|
||||
import { magicLinkClient } from "better-auth/client/plugins"
|
||||
|
||||
const authClient = createAuthClient({
|
||||
plugins: [
|
||||
magicLinkClient()
|
||||
]
|
||||
})
|
||||
|
||||
// Use plugin methods
|
||||
await authClient.signIn.magicLink({
|
||||
email: "test@email.com"
|
||||
})
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Error handling works the same as other Better Auth clients:
|
||||
|
||||
```tsx title="components/login-form.tsx"
|
||||
import { authClient } from "../lib/auth-client"
|
||||
|
||||
export function LoginForm() {
|
||||
const signIn = async (email: string, password: string) => {
|
||||
const { data, error } = await authClient.signIn.email({
|
||||
email,
|
||||
password
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('Login failed:', error.message)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Login successful:', data)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
const formData = new FormData(e.target)
|
||||
signIn(formData.get('email'), formData.get('password'))
|
||||
}}>
|
||||
<input name="email" type="email" placeholder="Email" />
|
||||
<input name="password" type="password" placeholder="Password" />
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
The Lynx client provides:
|
||||
|
||||
- **Cross-Platform Support**: Works across Android, iOS, and Web platforms
|
||||
- **Optimized Performance**: Built specifically for Lynx's reactive system
|
||||
- **Nanostores Integration**: Uses nanostores for efficient state management
|
||||
- **Selective Re-rendering**: Watch specific store keys to minimize unnecessary updates
|
||||
- **Full API Compatibility**: All Better Auth methods and plugins work seamlessly
|
||||
- **TypeScript Support**: Full type safety with TypeScript inference
|
||||
|
||||
The Lynx integration maintains all the features and benefits of Better Auth while providing optimal performance and developer experience within Lynx's cross-platform ecosystem.
|
||||
@@ -712,10 +712,14 @@
|
||||
"zod": "^4.1.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@lynx-js/react": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@lynx-js/react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
@@ -724,6 +728,7 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lynx-js/react": "^0.112.5",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"@tanstack/react-start": "^1.131.3",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
|
||||
106
packages/better-auth/src/client/lynx/index.ts
Normal file
106
packages/better-auth/src/client/lynx/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { getClientConfig } from "../config";
|
||||
import type {
|
||||
BetterAuthClientPlugin,
|
||||
ClientOptions,
|
||||
InferActions,
|
||||
InferClientAPI,
|
||||
InferErrorCodes,
|
||||
IsSignal,
|
||||
} from "../types";
|
||||
import { createDynamicPathProxy } from "../proxy";
|
||||
import type { PrettifyDeep, UnionToIntersection } from "../../types/helper";
|
||||
import type {
|
||||
BetterFetchError,
|
||||
BetterFetchResponse,
|
||||
} from "@better-fetch/fetch";
|
||||
import { useStore } from "./lynx-store";
|
||||
import type { BASE_ERROR_CODES } from "../../error/codes";
|
||||
import type { SessionQueryParams } from "../types";
|
||||
|
||||
function getAtomKey(str: string) {
|
||||
return `use${capitalizeFirstLetter(str)}`;
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
type InferResolvedHooks<O extends ClientOptions> = O["plugins"] extends Array<
|
||||
infer Plugin
|
||||
>
|
||||
? Plugin extends BetterAuthClientPlugin
|
||||
? Plugin["getAtoms"] extends (fetch: any) => infer Atoms
|
||||
? Atoms extends Record<string, any>
|
||||
? {
|
||||
[key in keyof Atoms as IsSignal<key> extends true
|
||||
? never
|
||||
: key extends string
|
||||
? `use${Capitalize<key>}`
|
||||
: never]: () => ReturnType<Atoms[key]["get"]>;
|
||||
}
|
||||
: {}
|
||||
: {}
|
||||
: {}
|
||||
: {};
|
||||
|
||||
export function createAuthClient<Option extends ClientOptions>(
|
||||
options?: Option,
|
||||
) {
|
||||
const {
|
||||
pluginPathMethods,
|
||||
pluginsActions,
|
||||
pluginsAtoms,
|
||||
$fetch,
|
||||
$store,
|
||||
atomListeners,
|
||||
} = getClientConfig(options);
|
||||
let resolvedHooks: Record<string, any> = {};
|
||||
for (const [key, value] of Object.entries(pluginsAtoms)) {
|
||||
resolvedHooks[getAtomKey(key)] = () => useStore(value);
|
||||
}
|
||||
|
||||
const routes = {
|
||||
...pluginsActions,
|
||||
...resolvedHooks,
|
||||
$fetch,
|
||||
$store,
|
||||
};
|
||||
const proxy = createDynamicPathProxy(
|
||||
routes,
|
||||
$fetch,
|
||||
pluginPathMethods,
|
||||
pluginsAtoms,
|
||||
atomListeners,
|
||||
);
|
||||
|
||||
type ClientAPI = InferClientAPI<Option>;
|
||||
type Session = ClientAPI extends {
|
||||
getSession: () => Promise<infer Res>;
|
||||
}
|
||||
? Res extends BetterFetchResponse<infer S>
|
||||
? S
|
||||
: Res
|
||||
: never;
|
||||
return proxy as UnionToIntersection<InferResolvedHooks<Option>> &
|
||||
ClientAPI &
|
||||
InferActions<Option> & {
|
||||
useSession: () => {
|
||||
data: Session;
|
||||
isPending: boolean;
|
||||
error: BetterFetchError | null;
|
||||
refetch: (queryParams?: { query?: SessionQueryParams }) => void;
|
||||
};
|
||||
$Infer: {
|
||||
Session: NonNullable<Session>;
|
||||
};
|
||||
$fetch: typeof $fetch;
|
||||
$store: typeof $store;
|
||||
$ERROR_CODES: PrettifyDeep<
|
||||
InferErrorCodes<Option> & typeof BASE_ERROR_CODES
|
||||
>;
|
||||
};
|
||||
}
|
||||
|
||||
export { useStore };
|
||||
export type * from "@better-fetch/fetch";
|
||||
export type * from "nanostores";
|
||||
73
packages/better-auth/src/client/lynx/lynx-store.ts
Normal file
73
packages/better-auth/src/client/lynx/lynx-store.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { listenKeys } from "nanostores";
|
||||
import { useCallback, useRef, useSyncExternalStore } from "@lynx-js/react";
|
||||
import type { Store, StoreValue } from "nanostores";
|
||||
import type { DependencyList } from "@lynx-js/react";
|
||||
|
||||
type StoreKeys<T> = T extends { setKey: (k: infer K, v: any) => unknown }
|
||||
? K
|
||||
: never;
|
||||
|
||||
export interface UseStoreOptions<SomeStore> {
|
||||
/**
|
||||
* @default
|
||||
* ```ts
|
||||
* [store, options.keys]
|
||||
* ```
|
||||
*/
|
||||
deps?: DependencyList;
|
||||
|
||||
/**
|
||||
* Will re-render components only on specific key changes.
|
||||
*/
|
||||
keys?: StoreKeys<SomeStore>[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to store changes and get store's value.
|
||||
*
|
||||
* Can be used with store builder too.
|
||||
*
|
||||
* ```js
|
||||
* import { useStore } from 'nanostores/react'
|
||||
*
|
||||
* import { router } from '../store/router'
|
||||
*
|
||||
* export const Layout = () => {
|
||||
* let page = useStore(router)
|
||||
* if (page.route === 'home') {
|
||||
* return <HomePage />
|
||||
* } else {
|
||||
* return <Error404 />
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @param store Store instance.
|
||||
* @returns Store value.
|
||||
*/
|
||||
export function useStore<SomeStore extends Store>(
|
||||
store: SomeStore,
|
||||
options: UseStoreOptions<SomeStore> = {},
|
||||
): StoreValue<SomeStore> {
|
||||
let snapshotRef = useRef<StoreValue<SomeStore>>(store.get());
|
||||
|
||||
const { keys, deps = [store, keys] } = options;
|
||||
|
||||
let subscribe = useCallback((onChange: () => void) => {
|
||||
const emitChange = (value: StoreValue<SomeStore>) => {
|
||||
if (snapshotRef.current === value) return;
|
||||
snapshotRef.current = value;
|
||||
onChange();
|
||||
};
|
||||
|
||||
emitChange(store.value);
|
||||
if (keys?.length) {
|
||||
return listenKeys(store as any, keys, emitChange);
|
||||
}
|
||||
return store.listen(emitChange);
|
||||
}, deps);
|
||||
|
||||
let get = () => snapshotRef.current as StoreValue<SomeStore>;
|
||||
|
||||
return useSyncExternalStore(subscribe, get, get);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export interface UseStoreOptions<SomeStore> {
|
||||
/**
|
||||
* Subscribe to store changes and get store's value.
|
||||
*
|
||||
* Can be user with store builder too.
|
||||
* Can be used with store builder too.
|
||||
*
|
||||
* ```js
|
||||
* import { useStore } from 'nanostores/react'
|
||||
|
||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -643,6 +643,9 @@ importers:
|
||||
specifier: ^4.1.5
|
||||
version: 4.1.5
|
||||
devDependencies:
|
||||
'@lynx-js/react':
|
||||
specifier: ^0.112.5
|
||||
version: 0.112.5(@types/react@18.3.23)
|
||||
'@prisma/client':
|
||||
specifier: ^5.22.0
|
||||
version: 5.22.0(prisma@5.22.0)
|
||||
@@ -2459,6 +2462,9 @@ packages:
|
||||
'@hexagon/base64@1.1.28':
|
||||
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
|
||||
|
||||
'@hongzhiyuan/preact@10.24.0-00213bad':
|
||||
resolution: {integrity: sha512-bHWp4ZDK5ZimcY+bTWw3S3xGiB8eROpZj0RK3FClNIaTOajb0b11CsT3K+pdeakgPgq1jWN3T2e2rfrPm40JsQ==}
|
||||
|
||||
'@hookform/resolvers@5.2.1':
|
||||
resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==}
|
||||
peerDependencies:
|
||||
@@ -2908,6 +2914,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@lynx-js/react@0.112.5':
|
||||
resolution: {integrity: sha512-noFV05FjKsLtnTQLjEbjK4IPLPK+AEJ3r2klYjvLCPe9pQYXyHqoiN9bWwLFZDvcPGsXSdJ5TiahTTZuagGRjA==}
|
||||
peerDependencies:
|
||||
'@lynx-js/types': '*'
|
||||
'@types/react': ^18
|
||||
peerDependenciesMeta:
|
||||
'@lynx-js/types':
|
||||
optional: true
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.0':
|
||||
resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -14144,6 +14159,8 @@ snapshots:
|
||||
|
||||
'@hexagon/base64@1.1.28': {}
|
||||
|
||||
'@hongzhiyuan/preact@10.24.0-00213bad': {}
|
||||
|
||||
'@hookform/resolvers@5.2.1(react-hook-form@7.62.0(react@19.1.1))':
|
||||
dependencies:
|
||||
'@standard-schema/utils': 0.3.0
|
||||
@@ -14560,6 +14577,11 @@ snapshots:
|
||||
'@libsql/win32-x64-msvc@0.5.20':
|
||||
optional: true
|
||||
|
||||
'@lynx-js/react@0.112.5(@types/react@18.3.23)':
|
||||
dependencies:
|
||||
'@types/react': 18.3.23
|
||||
preact: '@hongzhiyuan/preact@10.24.0-00213bad'
|
||||
|
||||
'@mapbox/node-pre-gyp@2.0.0':
|
||||
dependencies:
|
||||
consola: 3.4.2
|
||||
|
||||
Reference in New Issue
Block a user