feat: lynx integration (#4470)

This commit is contained in:
Alex Yang
2025-09-05 13:21:25 -07:00
committed by GitHub
parent b771b13943
commit 9589cb7418
8 changed files with 445 additions and 1 deletions

View File

@@ -44,6 +44,27 @@ export const Icons = {
></path> ></path>
</svg> </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>) => ( solidStart: (props?: SVGProps<any>) => (
<svg <svg
className={props?.className} className={props?.className}

View File

@@ -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, icon: Icons.expo,
href: "/docs/integrations/expo", href: "/docs/integrations/expo",
}, },
{
title: "Lynx",
icon: Icons.lynx,
href: "/docs/integrations/lynx",
},
], ],
}, },
{ {

View 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.

View File

@@ -712,10 +712,14 @@
"zod": "^4.1.5" "zod": "^4.1.5"
}, },
"peerDependencies": { "peerDependencies": {
"@lynx-js/react": "*",
"react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0" "react-dom": "^18.0.0 || ^19.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@lynx-js/react": {
"optional": true
},
"react": { "react": {
"optional": true "optional": true
}, },
@@ -724,6 +728,7 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@lynx-js/react": "^0.112.5",
"@prisma/client": "^5.22.0", "@prisma/client": "^5.22.0",
"@tanstack/react-start": "^1.131.3", "@tanstack/react-start": "^1.131.3",
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",

View 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";

View 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);
}

View File

@@ -25,7 +25,7 @@ export interface UseStoreOptions<SomeStore> {
/** /**
* Subscribe to store changes and get store's value. * Subscribe to store changes and get store's value.
* *
* Can be user with store builder too. * Can be used with store builder too.
* *
* ```js * ```js
* import { useStore } from 'nanostores/react' * import { useStore } from 'nanostores/react'

22
pnpm-lock.yaml generated
View File

@@ -643,6 +643,9 @@ importers:
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.1.5 version: 4.1.5
devDependencies: devDependencies:
'@lynx-js/react':
specifier: ^0.112.5
version: 0.112.5(@types/react@18.3.23)
'@prisma/client': '@prisma/client':
specifier: ^5.22.0 specifier: ^5.22.0
version: 5.22.0(prisma@5.22.0) version: 5.22.0(prisma@5.22.0)
@@ -2459,6 +2462,9 @@ packages:
'@hexagon/base64@1.1.28': '@hexagon/base64@1.1.28':
resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} 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': '@hookform/resolvers@5.2.1':
resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==} resolution: {integrity: sha512-u0+6X58gkjMcxur1wRWokA7XsiiBJ6aK17aPZxhkoYiK5J+HcTx0Vhu9ovXe6H+dVpO6cjrn2FkJTryXEMlryQ==}
peerDependencies: peerDependencies:
@@ -2908,6 +2914,15 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] 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': '@mapbox/node-pre-gyp@2.0.0':
resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==} resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -14144,6 +14159,8 @@ snapshots:
'@hexagon/base64@1.1.28': {} '@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))': '@hookform/resolvers@5.2.1(react-hook-form@7.62.0(react@19.1.1))':
dependencies: dependencies:
'@standard-schema/utils': 0.3.0 '@standard-schema/utils': 0.3.0
@@ -14560,6 +14577,11 @@ snapshots:
'@libsql/win32-x64-msvc@0.5.20': '@libsql/win32-x64-msvc@0.5.20':
optional: true 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': '@mapbox/node-pre-gyp@2.0.0':
dependencies: dependencies:
consola: 3.4.2 consola: 3.4.2