chore: misc
7
demo/expo-example/.env.example
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
GITHUB_CLIENT_ID=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
BETTER_AUTH_SECRET=
|
||||||
|
DATABASE_URL=
|
||||||
|
BETTER_AUTH_URL=http://localohst:8081
|
||||||
20
demo/expo-example/.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
node_modules/
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
npm-debug.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
*.orig.*
|
||||||
|
web-build/
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
|
||||||
|
# The following patterns were generated by expo-cli
|
||||||
|
|
||||||
|
expo-env.d.ts
|
||||||
|
# @end expo-cli
|
||||||
15
demo/expo-example/README.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Better Auth Expo Example
|
||||||
|
|
||||||
|
This is an example of how to use Better Auth with Expo. It uses Expo's new API Router to host the auth server.
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
1. Clone the code sandbox (or the repo) and open it in your code editor
|
||||||
|
2. Move and Provide environment variable
|
||||||
|
3. Run the following commands
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm start
|
||||||
|
```s
|
||||||
|
|
||||||
|
Checkout the [expo guide](https://www.better-auth.com/docs/integrations/expo) to learn more.
|
||||||
55
demo/expo-example/app.config.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import type { ConfigContext, ExpoConfig } from "expo/config";
|
||||||
|
|
||||||
|
export default ({ config }: ConfigContext): ExpoConfig => ({
|
||||||
|
...config,
|
||||||
|
name: "Better Auth",
|
||||||
|
slug: "better-auth",
|
||||||
|
scheme: "better-auth",
|
||||||
|
version: "0.1.0",
|
||||||
|
orientation: "portrait",
|
||||||
|
icon: "./assets/icon.png",
|
||||||
|
userInterfaceStyle: "automatic",
|
||||||
|
splash: {
|
||||||
|
image: "./assets/icon.png",
|
||||||
|
resizeMode: "contain",
|
||||||
|
backgroundColor: "#1F104A",
|
||||||
|
},
|
||||||
|
web: {
|
||||||
|
bundler: "metro",
|
||||||
|
output: "server",
|
||||||
|
},
|
||||||
|
updates: {
|
||||||
|
fallbackToCacheTimeout: 0,
|
||||||
|
},
|
||||||
|
assetBundlePatterns: ["**/*"],
|
||||||
|
ios: {
|
||||||
|
bundleIdentifier: "your.bundle.identifier",
|
||||||
|
supportsTablet: true,
|
||||||
|
},
|
||||||
|
android: {
|
||||||
|
package: "your.bundle.identifier",
|
||||||
|
adaptiveIcon: {
|
||||||
|
foregroundImage: "./assets/icon.png",
|
||||||
|
backgroundColor: "#1F104A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// extra: {
|
||||||
|
// eas: {
|
||||||
|
// projectId: "your-eas-project-id",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
experiments: {
|
||||||
|
tsconfigPaths: true,
|
||||||
|
typedRoutes: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
"expo-router",
|
||||||
|
{
|
||||||
|
origin: "http://localhost:8081",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"expo-secure-store",
|
||||||
|
"expo-font",
|
||||||
|
],
|
||||||
|
});
|
||||||
BIN
demo/expo-example/assets/bg-image.jpeg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
demo/expo-example/assets/fonts/SpaceMono-Regular.ttf
Executable file
BIN
demo/expo-example/assets/icon.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
demo/expo-example/assets/images/adaptive-icon.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
demo/expo-example/assets/images/favicon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
demo/expo-example/assets/images/logo.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
demo/expo-example/assets/images/partial-react-logo.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
demo/expo-example/assets/images/react-logo.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
demo/expo-example/assets/images/react-logo@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
demo/expo-example/assets/images/react-logo@3x.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
demo/expo-example/assets/images/splash.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
9
demo/expo-example/babel.config.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
module.exports = function (api) {
|
||||||
|
api.cache(true);
|
||||||
|
return {
|
||||||
|
presets: [
|
||||||
|
["babel-preset-expo", { jsxImportSource: "nativewind" }],
|
||||||
|
"nativewind/babel",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
6
demo/expo-example/components.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"lib": "@/lib"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
demo/expo-example/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import "expo-router/entry";
|
||||||
39
demo/expo-example/metro.config.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
// Learn more: https://docs.expo.dev/guides/monorepos/
|
||||||
|
const { getDefaultConfig } = require("expo/metro-config");
|
||||||
|
const { FileStore } = require("metro-cache");
|
||||||
|
const { withNativeWind } = require("nativewind/metro");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const config = withMonorepoPaths(
|
||||||
|
withNativeWind(getDefaultConfig(__dirname), { input: "./src/global.css" }),
|
||||||
|
);
|
||||||
|
|
||||||
|
// XXX: Resolve our exports in workspace packages
|
||||||
|
// https://github.com/expo/expo/issues/26926
|
||||||
|
config.resolver.unstable_enablePackageExports = true;
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the monorepo paths to the Metro config.
|
||||||
|
* This allows Metro to resolve modules from the monorepo.
|
||||||
|
*
|
||||||
|
* @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config
|
||||||
|
* @param {import('expo/metro-config').MetroConfig} config
|
||||||
|
* @returns {import('expo/metro-config').MetroConfig}
|
||||||
|
*/
|
||||||
|
function withMonorepoPaths(config) {
|
||||||
|
const projectRoot = __dirname;
|
||||||
|
const workspaceRoot = path.resolve(projectRoot, "../..");
|
||||||
|
|
||||||
|
// #1 - Watch all files in the monorepo
|
||||||
|
config.watchFolders = [workspaceRoot];
|
||||||
|
|
||||||
|
// #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules
|
||||||
|
config.resolver.nodeModulesPaths = [
|
||||||
|
path.resolve(projectRoot, "node_modules"),
|
||||||
|
path.resolve(workspaceRoot, "node_modules"),
|
||||||
|
];
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
3
demo/expo-example/nativewind-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/// <reference types="nativewind/types" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
|
||||||
68
demo/expo-example/package.json
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "expo-example",
|
||||||
|
"main": "index.ts",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "git clean -xdf .cache .expo .turbo android ios node_modules",
|
||||||
|
"start": "expo start",
|
||||||
|
"dev": "expo run:android",
|
||||||
|
"ios": "expo run:ios",
|
||||||
|
"web": "expo start --web",
|
||||||
|
"test": "jest --watchAll",
|
||||||
|
"lint": "expo lint",
|
||||||
|
"android": "expo run:android"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@better-auth/expo": "workspace:*",
|
||||||
|
"@expo/metro-runtime": "^6.1.2",
|
||||||
|
"@expo/vector-icons": "^15.0.2",
|
||||||
|
"@nanostores/react": "^0.8.4",
|
||||||
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
|
"@react-navigation/native": "^7.1.17",
|
||||||
|
"@rn-primitives/avatar": "^1.1.0",
|
||||||
|
"@rn-primitives/separator": "^1.1.0",
|
||||||
|
"@rn-primitives/slot": "^1.1.0",
|
||||||
|
"@rn-primitives/types": "^1.1.0",
|
||||||
|
"@types/better-sqlite3": "^7.6.12",
|
||||||
|
"babel-plugin-transform-import-meta": "^2.2.1",
|
||||||
|
"better-auth": "workspace:*",
|
||||||
|
"better-sqlite3": "^11.6.0",
|
||||||
|
"expo": "~54.0.10",
|
||||||
|
"expo-constants": "~18.0.9",
|
||||||
|
"expo-crypto": "^15.0.7",
|
||||||
|
"expo-font": "~14.0.8",
|
||||||
|
"expo-linking": "~8.0.8",
|
||||||
|
"expo-router": "~6.0.8",
|
||||||
|
"expo-secure-store": "~15.0.7",
|
||||||
|
"expo-splash-screen": "~31.0.10",
|
||||||
|
"expo-status-bar": "~3.0.8",
|
||||||
|
"expo-system-ui": "~6.0.7",
|
||||||
|
"expo-web-browser": "~15.0.7",
|
||||||
|
"nanostores": "^0.11.3",
|
||||||
|
"nativewind": "^4.1.23",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"react-native": "~0.81.4",
|
||||||
|
"react-native-css-interop": "^0.2.1",
|
||||||
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
"react-native-reanimated": "~4.1.2",
|
||||||
|
"react-native-safe-area-context": "5.6.1",
|
||||||
|
"react-native-screens": "4.16.0",
|
||||||
|
"react-native-svg": "^15.12.1",
|
||||||
|
"react-native-web": "~0.21.1",
|
||||||
|
"react-native-worklets": "^0.5.1",
|
||||||
|
"tailwindcss": "^3.4.16"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.26.0",
|
||||||
|
"@babel/preset-env": "^7.26.0",
|
||||||
|
"@babel/runtime": "^7.26.0",
|
||||||
|
"@types/babel__core": "^7.20.5",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
|
"@types/react": "^19.1.12",
|
||||||
|
"@types/react-test-renderer": "^18.3.1",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
demo/expo-example/src/app/_layout.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Slot } from "expo-router";
|
||||||
|
import "../global.css";
|
||||||
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
|
import { ImageBackground, View } from "react-native";
|
||||||
|
import { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
return (
|
||||||
|
<SafeAreaProvider>
|
||||||
|
<ImageBackground
|
||||||
|
className="z-0 flex items-center justify-center"
|
||||||
|
source={require("../../assets/bg-image.jpeg")}
|
||||||
|
resizeMode="cover"
|
||||||
|
style={{
|
||||||
|
...(StyleSheet.absoluteFill as any),
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: "black",
|
||||||
|
opacity: 0.2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Slot />
|
||||||
|
</ImageBackground>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
demo/expo-example/src/app/api/auth/[...route]+api.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
|
||||||
|
export const GET = (request: Request) => {
|
||||||
|
return auth.handler(request);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const POST = (request: Request) => {
|
||||||
|
return auth.handler(request);
|
||||||
|
};
|
||||||
68
demo/expo-example/src/app/dashboard.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardFooter, CardHeader } from "@/components/ui/card";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import Ionicons from "@expo/vector-icons/AntDesign";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useStore } from "@nanostores/react";
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const { data: session, isPending } = useStore(authClient.useSession);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!session && !isPending) {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}, [session, isPending]);
|
||||||
|
return (
|
||||||
|
<Card className="w-10/12">
|
||||||
|
<CardHeader>
|
||||||
|
<View className="flex-row items-center gap-2">
|
||||||
|
<Avatar alt="user-image">
|
||||||
|
<AvatarImage
|
||||||
|
source={{
|
||||||
|
uri: session?.user?.image || "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AvatarFallback>
|
||||||
|
<Text>{session?.user?.name[0]}</Text>
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<View>
|
||||||
|
<Text className="font-bold">{session?.user?.name}</Text>
|
||||||
|
<Text className="text-sm">{session?.user?.email}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</CardHeader>
|
||||||
|
<CardFooter className="justify-between">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
className="flex-row items-center gap-2 "
|
||||||
|
>
|
||||||
|
<Ionicons name="edit" size={16} color="white" />
|
||||||
|
<Text>Edit User</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="flex-row items-center gap-2"
|
||||||
|
size="sm"
|
||||||
|
onPress={async () => {
|
||||||
|
await authClient.signOut({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push("/");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="logout" size={14} color="black" />
|
||||||
|
<Text>Sign Out</Text>
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
demo/expo-example/src/app/forget-password.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { View } from "react-native";
|
||||||
|
import Icons from "@expo/vector-icons/AntDesign";
|
||||||
|
import { router } from "expo-router";
|
||||||
|
|
||||||
|
export default function ForgetPassword() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
return (
|
||||||
|
<Card className="w-10/12 ">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Forget Password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email to reset your password
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<View className="px-6 mb-2">
|
||||||
|
<Input
|
||||||
|
autoCapitalize="none"
|
||||||
|
placeholder="Email"
|
||||||
|
value={email}
|
||||||
|
onChangeText={(text) => setEmail(text)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<CardFooter>
|
||||||
|
<View className="w-full gap-2">
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
authClient.forgetPassword({
|
||||||
|
email,
|
||||||
|
redirectTo: "/reset-password",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="w-full"
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<Text>Send Email</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/");
|
||||||
|
}}
|
||||||
|
className="w-full flex-row gap-4 items-center"
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
<Icons name="arrowleft" size={18} />
|
||||||
|
<Text>Back to Sign In</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
133
demo/expo-example/src/app/index.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import Ionicons from "@expo/vector-icons/AntDesign";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { Image, View } from "react-native";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { router, useNavigationContainerRef } from "expo-router";
|
||||||
|
import { useStore } from "@nanostores/react";
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const { data: isAuthenticated } = useStore(authClient.useSession);
|
||||||
|
const navContainerRef = useNavigationContainerRef();
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
if (navContainerRef.isReady()) {
|
||||||
|
router.push("/dashboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isAuthenticated, navContainerRef.isReady()]);
|
||||||
|
return (
|
||||||
|
<Card className="z-50 mx-6 backdrop-blur-lg bg-gray-200/70">
|
||||||
|
<CardHeader className="flex items-center justify-center gap-8">
|
||||||
|
<Image
|
||||||
|
source={require("../../assets/images/logo.png")}
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardTitle>Sign In to your account</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<View className="px-6 flex gap-2">
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
authClient.signIn.social({
|
||||||
|
provider: "google",
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
className="flex flex-row gap-2 items-center bg-white/50"
|
||||||
|
>
|
||||||
|
<Ionicons name="google" size={16} />
|
||||||
|
<Text>Sign In with Google</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="flex flex-row gap-2 items-center bg-white/50"
|
||||||
|
onPress={() => {
|
||||||
|
authClient.signIn.social({
|
||||||
|
provider: "github",
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="github" size={16} />
|
||||||
|
<Text>Sign In with GitHub</Text>
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<View className="flex-row gap-2 w-full items-center px-6 my-4">
|
||||||
|
<Separator className="flex-grow w-3/12" />
|
||||||
|
<Text>or continue with</Text>
|
||||||
|
<Separator className="flex-grow w-3/12" />
|
||||||
|
</View>
|
||||||
|
<View className="px-6">
|
||||||
|
<Input
|
||||||
|
placeholder="Email Address"
|
||||||
|
className="rounded-b-none border-b-0"
|
||||||
|
value={email}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setEmail(text);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
className="rounded-t-none"
|
||||||
|
secureTextEntry
|
||||||
|
value={password}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setPassword(text);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<CardFooter>
|
||||||
|
<View className="w-full">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
className="w-full"
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/forget-password");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className="underline text-center">Forget Password?</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
authClient.signIn.email(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (ctx) => {
|
||||||
|
alert(ctx.error.message);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>Continue</Text>
|
||||||
|
</Button>
|
||||||
|
<Text className="text-center mt-2">
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Text
|
||||||
|
className="underline"
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/sign-up");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
demo/expo-example/src/app/sign-up.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Text } from "@/components/ui/text";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import { KeyboardAvoidingView, View } from "react-native";
|
||||||
|
import { Image } from "react-native";
|
||||||
|
import { useRouter } from "expo-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export default function SignUp() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
return (
|
||||||
|
<Card className="z-50 mx-6">
|
||||||
|
<CardHeader className="flex items-center justify-center gap-8">
|
||||||
|
<Image
|
||||||
|
source={require("../../assets/images/logo.png")}
|
||||||
|
style={{
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CardTitle>Create new Account</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<View className="px-6">
|
||||||
|
<KeyboardAvoidingView>
|
||||||
|
<Input
|
||||||
|
placeholder="Name"
|
||||||
|
className="rounded-b-none border-b-0"
|
||||||
|
value={name}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setName(text);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
<KeyboardAvoidingView>
|
||||||
|
<Input
|
||||||
|
placeholder="Email"
|
||||||
|
className="rounded-b-none border-b-0"
|
||||||
|
value={email}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setEmail(text);
|
||||||
|
}}
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
|
||||||
|
<KeyboardAvoidingView>
|
||||||
|
<Input
|
||||||
|
placeholder="Password"
|
||||||
|
secureTextEntry
|
||||||
|
className="rounded-t-none"
|
||||||
|
value={password}
|
||||||
|
onChangeText={(text) => {
|
||||||
|
setPassword(text);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</View>
|
||||||
|
<CardFooter>
|
||||||
|
<View className="w-full mt-2">
|
||||||
|
<Button
|
||||||
|
onPress={async () => {
|
||||||
|
const res = await authClient.signUp.email(
|
||||||
|
{
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onError: (ctx) => {
|
||||||
|
alert(ctx.error.message);
|
||||||
|
},
|
||||||
|
onSuccess: (ctx) => {
|
||||||
|
router.push("/dashboard");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log(res);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text>Sign Up</Text>
|
||||||
|
</Button>
|
||||||
|
<Text className="text-center mt-2">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Text
|
||||||
|
className="underline"
|
||||||
|
onPress={() => {
|
||||||
|
router.push("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sign In
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
28
demo/expo-example/src/components/icons/google.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import Svg, { Path, SvgProps } from "react-native-svg";
|
||||||
|
|
||||||
|
export function GoogleIcon(props: SvgProps) {
|
||||||
|
return (
|
||||||
|
<Svg width="1em" height="1em" viewBox="0 0 128 128">
|
||||||
|
<Path
|
||||||
|
fill="#fff"
|
||||||
|
d="M44.59 4.21a63.28 63.28 0 0 0 4.33 120.9a67.6 67.6 0 0 0 32.36.35a57.13 57.13 0 0 0 25.9-13.46a57.44 57.44 0 0 0 16-26.26a74.3 74.3 0 0 0 1.61-33.58H65.27v24.69h34.47a29.72 29.72 0 0 1-12.66 19.52a36.2 36.2 0 0 1-13.93 5.5a41.3 41.3 0 0 1-15.1 0A37.2 37.2 0 0 1 44 95.74a39.3 39.3 0 0 1-14.5-19.42a38.3 38.3 0 0 1 0-24.63a39.25 39.25 0 0 1 9.18-14.91A37.17 37.17 0 0 1 76.13 27a34.3 34.3 0 0 1 13.64 8q5.83-5.8 11.64-11.63c2-2.09 4.18-4.08 6.15-6.22A61.2 61.2 0 0 0 87.2 4.59a64 64 0 0 0-42.61-.38"
|
||||||
|
></Path>
|
||||||
|
<Path
|
||||||
|
fill="#e33629"
|
||||||
|
d="M44.59 4.21a64 64 0 0 1 42.61.37a61.2 61.2 0 0 1 20.35 12.62c-2 2.14-4.11 4.14-6.15 6.22Q95.58 29.23 89.77 35a34.3 34.3 0 0 0-13.64-8a37.17 37.17 0 0 0-37.46 9.74a39.25 39.25 0 0 0-9.18 14.91L8.76 35.6A63.53 63.53 0 0 1 44.59 4.21"
|
||||||
|
></Path>
|
||||||
|
<Path
|
||||||
|
fill="#f8bd00"
|
||||||
|
d="M3.26 51.5a63 63 0 0 1 5.5-15.9l20.73 16.09a38.3 38.3 0 0 0 0 24.63q-10.36 8-20.73 16.08a63.33 63.33 0 0 1-5.5-40.9"
|
||||||
|
></Path>
|
||||||
|
<Path
|
||||||
|
fill="#587dbd"
|
||||||
|
d="M65.27 52.15h59.52a74.3 74.3 0 0 1-1.61 33.58a57.44 57.44 0 0 1-16 26.26c-6.69-5.22-13.41-10.4-20.1-15.62a29.72 29.72 0 0 0 12.66-19.54H65.27c-.01-8.22 0-16.45 0-24.68"
|
||||||
|
></Path>
|
||||||
|
<Path
|
||||||
|
fill="#319f43"
|
||||||
|
d="M8.75 92.4q10.37-8 20.73-16.08A39.3 39.3 0 0 0 44 95.74a37.2 37.2 0 0 0 14.08 6.08a41.3 41.3 0 0 0 15.1 0a36.2 36.2 0 0 0 13.93-5.5c6.69 5.22 13.41 10.4 20.1 15.62a57.13 57.13 0 0 1-25.9 13.47a67.6 67.6 0 0 1-32.36-.35a63 63 0 0 1-23-11.59A63.7 63.7 0 0 1 8.75 92.4"
|
||||||
|
></Path>
|
||||||
|
</Svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
47
demo/expo-example/src/components/ui/avatar.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import * as AvatarPrimitive from "@rn-primitives/avatar";
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
AvatarPrimitive.RootRef,
|
||||||
|
AvatarPrimitive.RootProps
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
AvatarPrimitive.ImageRef,
|
||||||
|
AvatarPrimitive.ImageProps
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={cn("aspect-square h-full w-full", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<
|
||||||
|
AvatarPrimitive.FallbackRef,
|
||||||
|
AvatarPrimitive.FallbackProps
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
||||||
|
|
||||||
|
export { Avatar, AvatarFallback, AvatarImage };
|
||||||
92
demo/expo-example/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Pressable } from "react-native";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { TextClassContext } from "@/components/ui/text";
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"group flex items-center justify-center rounded-md web:ring-offset-background web:transition-colors web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary web:hover:opacity-90 active:opacity-90",
|
||||||
|
destructive: "bg-destructive web:hover:opacity-90 active:opacity-90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
||||||
|
secondary: "bg-secondary web:hover:opacity-80 active:opacity-80",
|
||||||
|
ghost:
|
||||||
|
"web:hover:bg-accent web:hover:text-accent-foreground active:bg-accent",
|
||||||
|
link: "web:underline-offset-4 web:hover:underline web:focus:underline ",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2 native:h-12 native:px-5 native:py-3",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8 native:h-14",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonTextVariants = cva(
|
||||||
|
"web:whitespace-nowrap text-sm native:text-base font-medium text-foreground web:transition-colors",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "text-primary-foreground",
|
||||||
|
destructive: "text-destructive-foreground",
|
||||||
|
outline: "group-active:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"text-secondary-foreground group-active:text-secondary-foreground",
|
||||||
|
ghost: "group-active:text-accent-foreground",
|
||||||
|
link: "text-primary group-active:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "",
|
||||||
|
sm: "",
|
||||||
|
lg: "native:text-lg",
|
||||||
|
icon: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
type ButtonProps = React.ComponentPropsWithoutRef<typeof Pressable> &
|
||||||
|
VariantProps<typeof buttonVariants>;
|
||||||
|
|
||||||
|
const Button = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Pressable>,
|
||||||
|
ButtonProps
|
||||||
|
>(({ className, variant, size, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<TextClassContext.Provider
|
||||||
|
value={buttonTextVariants({
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
className: "web:pointer-events-none",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
className={cn(
|
||||||
|
props.disabled && "opacity-50 web:pointer-events-none",
|
||||||
|
buttonVariants({ variant, size, className }),
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
role="button"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</TextClassContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Button.displayName = "Button";
|
||||||
|
|
||||||
|
export { Button, buttonTextVariants, buttonVariants };
|
||||||
|
export type { ButtonProps };
|
||||||
86
demo/expo-example/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { TextRef, ViewRef } from "@rn-primitives/types";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Text, type TextProps, View, type ViewProps } from "react-native";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { TextClassContext } from "@/components/ui/text";
|
||||||
|
|
||||||
|
const Card = React.forwardRef<ViewRef, ViewProps>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<View
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border border-border bg-card shadow-sm shadow-foreground/10",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Card.displayName = "Card";
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<ViewRef, ViewProps>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<View
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardHeader.displayName = "CardHeader";
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<TextRef, TextProps>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<Text
|
||||||
|
role="heading"
|
||||||
|
aria-level={3}
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl text-card-foreground font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardTitle.displayName = "CardTitle";
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<TextRef, TextProps>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<Text
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardDescription.displayName = "CardDescription";
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<ViewRef, ViewProps>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<TextClassContext.Provider value="text-card-foreground">
|
||||||
|
<View ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
</TextClassContext.Provider>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardContent.displayName = "CardContent";
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<ViewRef, ViewProps>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<View
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-row items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
CardFooter.displayName = "CardFooter";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
};
|
||||||
166
demo/expo-example/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import * as DialogPrimitive from "@rn-primitives/dialog";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Platform, StyleSheet, View, type ViewProps } from "react-native";
|
||||||
|
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||||
|
import { X } from "@/lib/icons/X";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root;
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger;
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal;
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close;
|
||||||
|
|
||||||
|
const DialogOverlayWeb = React.forwardRef<
|
||||||
|
DialogPrimitive.OverlayRef,
|
||||||
|
DialogPrimitive.OverlayProps
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
const { open } = DialogPrimitive.useRootContext();
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"bg-black/80 flex justify-center items-center p-2 absolute top-0 right-0 bottom-0 left-0",
|
||||||
|
open
|
||||||
|
? "web:animate-in web:fade-in-0"
|
||||||
|
: "web:animate-out web:fade-out-0",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DialogOverlayWeb.displayName = "DialogOverlayWeb";
|
||||||
|
|
||||||
|
const DialogOverlayNative = React.forwardRef<
|
||||||
|
DialogPrimitive.OverlayRef,
|
||||||
|
DialogPrimitive.OverlayProps
|
||||||
|
>(({ className, children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
style={StyleSheet.absoluteFill}
|
||||||
|
className={cn(
|
||||||
|
"flex bg-black/80 justify-center items-center p-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeIn.duration(150)}
|
||||||
|
exiting={FadeOut.duration(150)}
|
||||||
|
>
|
||||||
|
<>{children}</>
|
||||||
|
</Animated.View>
|
||||||
|
</DialogPrimitive.Overlay>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
DialogOverlayNative.displayName = "DialogOverlayNative";
|
||||||
|
|
||||||
|
const DialogOverlay = Platform.select({
|
||||||
|
web: DialogOverlayWeb,
|
||||||
|
default: DialogOverlayNative,
|
||||||
|
});
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
DialogPrimitive.ContentRef,
|
||||||
|
DialogPrimitive.ContentProps & { portalHost?: string }
|
||||||
|
>(({ className, children, portalHost, ...props }, ref) => {
|
||||||
|
const { open } = DialogPrimitive.useRootContext();
|
||||||
|
return (
|
||||||
|
<DialogPortal hostName={portalHost}>
|
||||||
|
<DialogOverlay>
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"max-w-lg gap-4 border border-border web:cursor-default bg-background p-6 shadow-lg web:duration-200 rounded-lg",
|
||||||
|
open
|
||||||
|
? "web:animate-in web:fade-in-0 web:zoom-in-95"
|
||||||
|
: "web:animate-out web:fade-out-0 web:zoom-out-95",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
className={
|
||||||
|
"absolute right-4 top-4 p-0.5 web:group rounded-sm opacity-70 web:ring-offset-background web:transition-opacity web:hover:opacity-100 web:focus:outline-none web:focus:ring-2 web:focus:ring-ring web:focus:ring-offset-2 web:disabled:pointer-events-none"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<X
|
||||||
|
size={Platform.OS === "web" ? 16 : 18}
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground",
|
||||||
|
open && "text-accent-foreground",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogOverlay>
|
||||||
|
</DialogPortal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DialogHeader = ({ className, ...props }: ViewProps) => (
|
||||||
|
<View
|
||||||
|
className={cn("flex flex-col gap-1.5 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogHeader.displayName = "DialogHeader";
|
||||||
|
|
||||||
|
const DialogFooter = ({ className, ...props }: ViewProps) => (
|
||||||
|
<View
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end gap-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
DialogFooter.displayName = "DialogFooter";
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
DialogPrimitive.TitleRef,
|
||||||
|
DialogPrimitive.TitleProps
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg native:text-xl text-foreground font-semibold leading-none tracking-tight",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
DialogPrimitive.DescriptionRef,
|
||||||
|
DialogPrimitive.DescriptionProps
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm native:text-base text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
};
|
||||||
25
demo/expo-example/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { TextInput, type TextInputProps } from "react-native";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Input = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TextInput>,
|
||||||
|
TextInputProps
|
||||||
|
>(({ className, placeholderClassName, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<TextInput
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"web:flex h-10 native:h-12 web:w-full rounded-md border border-input bg-background px-3 web:py-2 text-base lg:text-sm native:text-lg native:leading-[1.25] text-foreground placeholder:text-muted-foreground web:ring-offset-background file:border-0 file:bg-transparent file:font-medium web:focus-visible:outline-none web:focus-visible:ring-2 web:focus-visible:ring-ring web:focus-visible:ring-offset-2",
|
||||||
|
props.editable === false && "opacity-50 web:cursor-not-allowed",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
placeholderClassName={cn("text-muted-foreground", placeholderClassName)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
export { Input };
|
||||||
28
demo/expo-example/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as SeparatorPrimitive from "@rn-primitives/separator";
|
||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Separator = React.forwardRef<
|
||||||
|
SeparatorPrimitive.RootRef,
|
||||||
|
SeparatorPrimitive.RootProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||||
|
ref,
|
||||||
|
) => (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 bg-border",
|
||||||
|
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||||
|
|
||||||
|
export { Separator };
|
||||||
28
demo/expo-example/src/components/ui/text.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import * as Slot from "@rn-primitives/slot";
|
||||||
|
import { SlottableTextProps, TextRef } from "@rn-primitives/types";
|
||||||
|
import * as React from "react";
|
||||||
|
import { Text as RNText } from "react-native";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const TextClassContext = React.createContext<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const Text = React.forwardRef<TextRef, SlottableTextProps>(
|
||||||
|
({ className, asChild = false, ...props }, ref) => {
|
||||||
|
const textClass = React.useContext(TextClassContext);
|
||||||
|
const Component = asChild ? Slot.Text : RNText;
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={cn(
|
||||||
|
"text-base text-foreground web:select-text",
|
||||||
|
textClass,
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Text.displayName = "Text";
|
||||||
|
|
||||||
|
export { Text, TextClassContext };
|
||||||
69
demo/expo-example/src/global.css
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 224 71.4% 4.1%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 224 71.4% 4.1%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 224 71.4% 4.1%;
|
||||||
|
--primary: 220.9 39.3% 11%;
|
||||||
|
--primary-foreground: 210 20% 98%;
|
||||||
|
--secondary: 220 14.3% 95.9%;
|
||||||
|
--secondary-foreground: 220.9 39.3% 11%;
|
||||||
|
--muted: 220 14.3% 95.9%;
|
||||||
|
--muted-foreground: 220 8.9% 46.1%;
|
||||||
|
--accent: 220 14.3% 95.9%;
|
||||||
|
--accent-foreground: 220.9 39.3% 11%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 220 13% 91%;
|
||||||
|
--input: 220 13% 91%;
|
||||||
|
--ring: 224 71.4% 4.1%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 224 71.4% 4.1%;
|
||||||
|
--foreground: 210 20% 98%;
|
||||||
|
--card: 224 71.4% 4.1%;
|
||||||
|
--card-foreground: 210 20% 98%;
|
||||||
|
--popover: 224 71.4% 4.1%;
|
||||||
|
--popover-foreground: 210 20% 98%;
|
||||||
|
--primary: 210 20% 98%;
|
||||||
|
--primary-foreground: 220.9 39.3% 11%;
|
||||||
|
--secondary: 215 27.9% 16.9%;
|
||||||
|
--secondary-foreground: 210 20% 98%;
|
||||||
|
--muted: 215 27.9% 16.9%;
|
||||||
|
--muted-foreground: 217.9 10.6% 64.9%;
|
||||||
|
--accent: 215 27.9% 16.9%;
|
||||||
|
--accent-foreground: 210 20% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 20% 98%;
|
||||||
|
--border: 215 27.9% 16.9%;
|
||||||
|
--input: 215 27.9% 16.9%;
|
||||||
|
--ring: 216 12.2% 83.9%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
demo/expo-example/src/lib/auth-client.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createAuthClient } from "better-auth/client";
|
||||||
|
import { expoClient } from "@better-auth/expo/client";
|
||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
baseURL: "http://localhost:8081",
|
||||||
|
disableDefaultFetchPlugins: true,
|
||||||
|
plugins: [
|
||||||
|
expoClient({
|
||||||
|
scheme: "better-auth",
|
||||||
|
storage: SecureStore,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
24
demo/expo-example/src/lib/auth.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { expo } from "@better-auth/expo";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
database: new Pool({
|
||||||
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
}),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
plugins: [expo()],
|
||||||
|
socialProviders: {
|
||||||
|
github: {
|
||||||
|
clientId: process.env.GITHUB_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
clientId: process.env.GOOGLE_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trustedOrigins: ["exp://"],
|
||||||
|
});
|
||||||
4
demo/expo-example/src/lib/icons/X.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { X } from "lucide-react-native";
|
||||||
|
import { iconWithClassName } from "./iconWithClassName";
|
||||||
|
iconWithClassName(X);
|
||||||
|
export { X };
|
||||||
14
demo/expo-example/src/lib/icons/iconWithClassName.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { LucideIcon } from "lucide-react-native";
|
||||||
|
import { cssInterop } from "nativewind";
|
||||||
|
|
||||||
|
export function iconWithClassName(icon: LucideIcon) {
|
||||||
|
cssInterop(icon, {
|
||||||
|
className: {
|
||||||
|
target: "style",
|
||||||
|
nativeStyleToProp: {
|
||||||
|
color: true,
|
||||||
|
opacity: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
16
demo/expo-example/src/lib/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { PressableStateCallbackType } from "react-native";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
export function isTextChildren(
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| ((state: PressableStateCallbackType) => React.ReactNode),
|
||||||
|
) {
|
||||||
|
return Array.isArray(children)
|
||||||
|
? children.every((child) => typeof child === "string")
|
||||||
|
: typeof children === "string";
|
||||||
|
}
|
||||||
75
demo/expo-example/tailwind.config.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
// NOTE: Update this to include the paths to all of your component files.
|
||||||
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
presets: [require("nativewind/preset")],
|
||||||
|
theme: {
|
||||||
|
container: {
|
||||||
|
center: true,
|
||||||
|
padding: "2rem",
|
||||||
|
screens: {
|
||||||
|
"2xl": "1400px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: "0" },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: "0" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
},
|
||||||
|
boxShadow: {
|
||||||
|
input: `0px 2px 3px -1px rgba(0,0,0,0.1), 0px 1px 0px 0px rgba(25,28,33,0.02), 0px 0px 0px 1px rgba(25,28,33,0.08)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
35
demo/expo-example/tsconfig.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"incremental": true,
|
||||||
|
"disableSourceOfProjectReferenceRedirect": true,
|
||||||
|
"tsBuildInfoFile": "${configDir}/.cache/tsbuildinfo.json",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"checkJs": false,
|
||||||
|
"types": ["nativewind"],
|
||||||
|
"module": "es2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-native",
|
||||||
|
"moduleSuffixes": [".ios", ".android", ".native", ""],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src",
|
||||||
|
".expo/types/**/*.ts",
|
||||||
|
"expo-env.d.ts",
|
||||||
|
"nativewind-env.d.ts"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules", "build", "dist", ".next", ".expo"],
|
||||||
|
"extends": "expo/tsconfig.base"
|
||||||
|
}
|
||||||
@@ -34,24 +34,22 @@ export default async function HomePage() {
|
|||||||
<div className="flex flex-col md:flex-row items-center justify-center h-12">
|
<div className="flex flex-col md:flex-row items-center justify-center h-12">
|
||||||
<span className="font-medium flex gap-2 text-sm text-zinc-700 dark:text-zinc-300">
|
<span className="font-medium flex gap-2 text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
<span className=" text-zinc-900 dark:text-white/90 hover:text-zinc-950 text-xs md:text-sm dark:hover:text-zinc-100 transition-colors">
|
<span className=" text-zinc-900 dark:text-white/90 hover:text-zinc-950 text-xs md:text-sm dark:hover:text-zinc-100 transition-colors">
|
||||||
Introducing{" "}
|
Auth.js (formerly NextAuth.js) is now part of{" "}
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">Better Auth</span>
|
||||||
Better Auth Infrastructure
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span className=" text-zinc-400 hidden md:block">|</span>
|
<span className=" text-zinc-400 hidden md:block">|</span>
|
||||||
<Link
|
<Link
|
||||||
href="https://better-auth.build"
|
href="/blog/authjs-joins-better-auth"
|
||||||
className="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 hidden dark:hover:text-blue-300 transition-colors md:block"
|
className="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 hidden dark:hover:text-blue-300 transition-colors md:block"
|
||||||
>
|
>
|
||||||
Join the waitlist →
|
Read the announcement →
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
href="https://better-auth.build"
|
href="/blog/authjs-joins-better-auth"
|
||||||
className="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 text-xs dark:hover:text-blue-300 transition-colors md:hidden"
|
className="font-semibold text-blue-600 dark:text-blue-400 hover:text-blue-700 text-xs dark:hover:text-blue-300 transition-colors md:hidden"
|
||||||
>
|
>
|
||||||
Join the waitlist →
|
Read the announcement →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ We are deeply grateful to the Auth.js community who have carried the project to
|
|||||||
|
|
||||||
Better Auth beginning was inspired by Auth.js, and now, together, the two projects can carry the ecosystem further. The end goal remains unchanged: you should own your auth!
|
Better Auth beginning was inspired by Auth.js, and now, together, the two projects can carry the ecosystem further. The end goal remains unchanged: you should own your auth!
|
||||||
|
|
||||||
|
|
||||||
|
<Callout type="none">
|
||||||
|
For the Auth.js team's announcement, see [GitHub discussion](https://github.com/nextauthjs/next-auth/discussions/13252).
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
|
||||||
### Learn More
|
### Learn More
|
||||||
|
|
||||||
<Cards>
|
<Cards>
|
||||||
|
|||||||
@@ -706,7 +706,7 @@
|
|||||||
"nanostores": "^1.0.1",
|
"nanostores": "^1.0.1",
|
||||||
"zod": "^4.1.5"
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependenciesOptional": {
|
||||||
"@lynx-js/react": "*",
|
"@lynx-js/react": "*",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"next": "^14.0.0 || ^15.0.0",
|
"next": "^14.0.0 || ^15.0.0",
|
||||||
|
|||||||
@@ -16,8 +16,18 @@ export async function generateState(
|
|||||||
message: "callbackURL is required",
|
message: "callbackURL is required",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeVerifier = generateRandomString(128);
|
const codeVerifier = generateRandomString(128);
|
||||||
const state = generateRandomString(32);
|
const state = generateRandomString(32);
|
||||||
|
const stateCookie = c.context.createAuthCookie("state", {
|
||||||
|
maxAge: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
await c.setSignedCookie(
|
||||||
|
stateCookie.name,
|
||||||
|
state,
|
||||||
|
c.context.secret,
|
||||||
|
stateCookie.attributes,
|
||||||
|
);
|
||||||
const data = JSON.stringify({
|
const data = JSON.stringify({
|
||||||
callbackURL,
|
callbackURL,
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
@@ -65,6 +75,7 @@ export async function parseState(c: GenericEndpointContext) {
|
|||||||
c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
|
c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
|
||||||
throw c.redirect(`${errorURL}?error=please_restart_the_process`);
|
throw c.redirect(`${errorURL}?error=please_restart_the_process`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedData = z
|
const parsedData = z
|
||||||
.object({
|
.object({
|
||||||
callbackURL: z.string(),
|
callbackURL: z.string(),
|
||||||
@@ -85,6 +96,19 @@ export async function parseState(c: GenericEndpointContext) {
|
|||||||
if (!parsedData.errorURL) {
|
if (!parsedData.errorURL) {
|
||||||
parsedData.errorURL = `${c.context.baseURL}/error`;
|
parsedData.errorURL = `${c.context.baseURL}/error`;
|
||||||
}
|
}
|
||||||
|
const stateCookie = c.context.createAuthCookie("state");
|
||||||
|
const stateCookieValue = await c.getSignedCookie(
|
||||||
|
stateCookie.name,
|
||||||
|
c.context.secret,
|
||||||
|
);
|
||||||
|
if (!stateCookieValue || stateCookieValue !== state) {
|
||||||
|
const errorURL =
|
||||||
|
c.context.options.onAPIError?.errorURL || `${c.context.baseURL}/error`;
|
||||||
|
throw c.redirect(`${errorURL}?error=state_mismatch`);
|
||||||
|
}
|
||||||
|
c.setCookie(stateCookie.name, "", {
|
||||||
|
maxAge: 0,
|
||||||
|
});
|
||||||
if (parsedData.expiresAt < Date.now()) {
|
if (parsedData.expiresAt < Date.now()) {
|
||||||
await c.context.internalAdapter.deleteVerificationValue(data.id);
|
await c.context.internalAdapter.deleteVerificationValue(data.id);
|
||||||
const errorURL =
|
const errorURL =
|
||||||
|
|||||||
@@ -49,13 +49,7 @@ export type InferSessionAPI<API> = API extends {
|
|||||||
headers: Headers;
|
headers: Headers;
|
||||||
response: PrettifyDeep<Awaited<ReturnType<E>>> | null;
|
response: PrettifyDeep<Awaited<ReturnType<E>>> | null;
|
||||||
}>
|
}>
|
||||||
: Promise<
|
: Promise<PrettifyDeep<Awaited<ReturnType<E>>> | null>
|
||||||
| (PrettifyDeep<Awaited<ReturnType<E>>> & {
|
|
||||||
options: E["options"];
|
|
||||||
path: E["path"];
|
|
||||||
})
|
|
||||||
| null
|
|
||||||
>
|
|
||||||
: Promise<Response>;
|
: Promise<Response>;
|
||||||
}
|
}
|
||||||
: never
|
: never
|
||||||
|
|||||||
@@ -72,7 +72,8 @@
|
|||||||
"expo-web-browser": ">=14.0.0"
|
"expo-web-browser": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@better-fetch/fetch": "catalog:"
|
"@better-fetch/fetch": "catalog:",
|
||||||
|
"zod": "^4.1.5"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
|
|||||||
@@ -221,7 +221,8 @@ export const expoClient = (opts: ExpoClientOptions) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const result = await Browser.openAuthSessionAsync(signInURL, to);
|
const proxyURL = `${context.request.baseURL}/expo-authorization-proxy?authorizationURL=${encodeURIComponent(signInURL)}`;
|
||||||
|
const result = await Browser.openAuthSessionAsync(proxyURL, to);
|
||||||
if (result.type !== "success") return;
|
if (result.type !== "success") return;
|
||||||
const url = new URL(result.url);
|
const url = new URL(result.url);
|
||||||
const cookie = String(url.searchParams.get("cookie"));
|
const cookie = String(url.searchParams.get("cookie"));
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import type { BetterAuthPlugin } from "better-auth/types";
|
import type { BetterAuthPlugin } from "better-auth/types";
|
||||||
import { createAuthMiddleware } from "better-auth/api";
|
import {
|
||||||
|
APIError,
|
||||||
|
createAuthEndpoint,
|
||||||
|
createAuthMiddleware,
|
||||||
|
} from "better-auth/api";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export interface ExpoOptions {
|
export interface ExpoOptions {
|
||||||
/**
|
/**
|
||||||
@@ -77,5 +82,39 @@ export const expo = (options?: ExpoOptions) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
endpoints: {
|
||||||
|
expoAuthorizationProxy: createAuthEndpoint(
|
||||||
|
"/expo-authorization-proxy",
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
query: z.object({
|
||||||
|
authorizationURL: z.string(),
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
isAction: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (ctx) => {
|
||||||
|
const { authorizationURL } = ctx.query;
|
||||||
|
const url = new URL(authorizationURL);
|
||||||
|
const state = url.searchParams.get("state");
|
||||||
|
if (!state) {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "Unexpected error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const stateCookie = ctx.context.createAuthCookie("state", {
|
||||||
|
maxAge: 5 * 60 * 1000, // 5 minutes
|
||||||
|
});
|
||||||
|
await ctx.setSignedCookie(
|
||||||
|
stateCookie.name,
|
||||||
|
state,
|
||||||
|
ctx.context.secret,
|
||||||
|
stateCookie.attributes,
|
||||||
|
);
|
||||||
|
return ctx.redirect(ctx.query.authorizationURL);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
} satisfies BetterAuthPlugin;
|
} satisfies BetterAuthPlugin;
|
||||||
};
|
};
|
||||||
|
|||||||