From 1ff7ef2bf82ec3d09cdde65c53c1bfdd558ed693 Mon Sep 17 00:00:00 2001 From: Bereket Engida Date: Wed, 30 Oct 2024 17:59:56 +0300 Subject: [PATCH] feat: init pkg --- packages/better-auth/package.json | 1 + .../better-auth/src/plugins/expo/client.ts | 95 +++++++++++ .../better-auth/src/plugins/expo/index.ts | 0 packages/expo/package.json | 8 +- packages/expo/src/index.ts | 98 +++++++++-- pnpm-lock.yaml | 159 +++++++++++++++++- 6 files changed, 340 insertions(+), 21 deletions(-) create mode 100644 packages/better-auth/src/plugins/expo/client.ts create mode 100644 packages/better-auth/src/plugins/expo/index.ts diff --git a/packages/better-auth/package.json b/packages/better-auth/package.json index cb095bfeee..2f62bf1db0 100644 --- a/packages/better-auth/package.json +++ b/packages/better-auth/package.json @@ -231,6 +231,7 @@ "pg": "^8.12.0", "prisma": "^5.19.1", "react": "^18.3.1", + "react-native": "~0.74.6", "solid-js": "^1.8.18", "tsup": "^8.2.4", "typescript": "5.6.1-rc", diff --git a/packages/better-auth/src/plugins/expo/client.ts b/packages/better-auth/src/plugins/expo/client.ts new file mode 100644 index 0000000000..eba13ef128 --- /dev/null +++ b/packages/better-auth/src/plugins/expo/client.ts @@ -0,0 +1,95 @@ +import { atom } from "nanostores"; +import type { BetterAuthClientPlugin } from "../../types"; +import { Linking } from "react-native"; + +interface ExpoClientOptions { + storage: { + getItem: (key: string) => string; + setItem: (key: string, value: string) => void; + deleteItem: (key: string) => void; + }; + scheme: string; +} + +export const expoClient = (options: ExpoClientOptions) => { + const { storage } = options; + let notify = () => {}; + const cookieName = "better-auth_cookie"; + const storeCookie = storage.getItem("cookie"); + const hasSessionCookie = storeCookie?.includes("session_token"); + const isAuthenticated = atom(!!hasSessionCookie); + function createURL(path: string) { + return `${options.scheme}/${path}`; + } + return { + id: "expo", + getActions(_, $store) { + notify = () => $store.notify("_sessionSignal"); + return {}; + }, + getAtoms() { + return { + isAuthenticated, + }; + }, + fetchPlugins: [ + { + id: "expo", + name: "Expo", + hooks: { + async onSuccess(context) { + const setCookie = context.response.headers.get("set-cookie"); + if (setCookie) { + await storage.setItem(cookieName, setCookie); + } + if ( + context.data.redirect && + context.request.url.toString().includes("/sign-in") + ) { + const callbackURL = context.request.body?.callbackURL; + const to = createURL(callbackURL); + const signInURL = context.data?.url; + const result = await Browser.openAuthSessionAsync(signInURL, to); + if (result.type !== "success") return; + const url = Linking.parse(result.url); + const cookie = String(url.queryParams?.cookie); + if (!cookie) return; + await SecureStore.setItemAsync(cookieName, cookie); + notify(); + } + }, + }, + async init(url, options) { + options = options || {}; + const cookie = await SecureStore.getItemAsync(cookieName); + const scheme = Constants.default.expoConfig?.scheme; + const schemeURL = typeof scheme === "string" ? scheme : scheme?.[0]; + if (!schemeURL) { + throw new Error("Scheme not found in app.json"); + } + options.credentials = "omit"; + options.headers = { + ...options.headers, + cookie: cookie || "", + origin: schemeURL, + }; + if (options.body?.callbackURL) { + if (options.body.callbackURL.startsWith("/")) { + const url = Linking.createURL(options.body.callbackURL); + options.body.callbackURL = url; + } + } + if (url.includes("/sign-out")) { + isAuthenticated.set(false); + storage.deleteItem(cookieName); + notify(); + } + return { + url, + options, + }; + }, + }, + ], + } satisfies BetterAuthClientPlugin; +}; diff --git a/packages/better-auth/src/plugins/expo/index.ts b/packages/better-auth/src/plugins/expo/index.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/expo/package.json b/packages/expo/package.json index 2005f35f1c..90e16d3ee3 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -5,6 +5,10 @@ "module": "dist/index.mjs", "devDependencies": { "better-auth": "workspace:^", + "expo-constants": "~16.0.2", + "expo-linking": "~6.3.1", + "expo-secure-store": "~13.0.2", + "expo-web-browser": "~13.0.3", "unbuild": "^2.0.0" }, "exports": { @@ -14,6 +18,8 @@ } }, "dependencies": { - "@better-fetch/fetch": "1.1.12" + "@better-fetch/fetch": "1.1.12", + "nanostores": "^0.11.2", + "react-native": "~0.74.6" } } \ No newline at end of file diff --git a/packages/expo/src/index.ts b/packages/expo/src/index.ts index 74775c3f77..aedd3d29c2 100644 --- a/packages/expo/src/index.ts +++ b/packages/expo/src/index.ts @@ -1,18 +1,86 @@ -import { type BetterFetchOption } from "@better-fetch/fetch"; +import { atom } from "nanostores"; import type { BetterAuthClientPlugin } from "better-auth"; -import { createAuthClient } from "../../better-auth/src/client"; +import * as SecureStore from "expo-secure-store"; +import * as Linking from "expo-linking"; +import * as Browser from "expo-web-browser"; +import * as Constants from "expo-constants"; -interface ExpoClientOptions { - baseURL: string; - fetchOptions?: BetterFetchOption; - plugins?: BetterAuthClientPlugin[]; -} - -export const createExpoClient = (options: O) => { - const baseClient = createAuthClient({ - disableDefaultFetchPlugins: true, - baseURL: options.baseURL, - plugins: options.plugins, - }); - return baseClient; +export const expoClient = () => { + let sessionNotify = () => {}; + const cookieName = "better-auth_cookie"; + const storeCookie = SecureStore.getItem("cookie"); + const hasSessionCookie = storeCookie?.includes("session_token"); + const isAuthenticated = atom(!!hasSessionCookie); + const storage = SecureStore; + return { + id: "expo", + getActions(_, $store) { + sessionNotify = () => $store.notify("$sessionSignal"); + return {}; + }, + getAtoms() { + return { + isAuthenticated, + }; + }, + fetchPlugins: [ + { + id: "expo", + name: "Expo", + hooks: { + async onSuccess(context) { + const setCookie = context.response.headers.get("set-cookie"); + if (setCookie) { + await storage.setItemAsync(cookieName, setCookie); + } + if ( + context.data.redirect && + context.request.url.toString().includes("/sign-in") + ) { + const callbackURL = context.request.body?.callbackURL; + const to = Linking.createURL(callbackURL); + const signInURL = context.data?.url; + const result = await Browser.openAuthSessionAsync(signInURL, to); + if (result.type !== "success") return; + const url = Linking.parse(result.url); + const cookie = String(url.queryParams?.cookie); + if (!cookie) return; + await storage.setItemAsync(cookieName, cookie); + sessionNotify(); + } + }, + }, + async init(url, options) { + options = options || {}; + const cookie = await storage.getItemAsync(cookieName); + const scheme = Constants.default.expoConfig?.scheme; + const schemeURL = typeof scheme === "string" ? scheme : scheme?.[0]; + if (!schemeURL) { + throw new Error("Scheme not found in app.json"); + } + options.credentials = "omit"; + options.headers = { + ...options.headers, + cookie: cookie || "", + origin: schemeURL, + }; + if (options.body?.callbackURL) { + if (options.body.callbackURL.startsWith("/")) { + const url = Linking.createURL(options.body.callbackURL); + options.body.callbackURL = url; + } + } + if (url.includes("/sign-out")) { + isAuthenticated.set(false); + await SecureStore.deleteItemAsync(cookieName); + sessionNotify(); + } + return { + url, + options, + }; + }, + }, + ], + } satisfies BetterAuthClientPlugin; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d2c57d694e..29680e92d5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1671,7 +1671,7 @@ importers: version: 3.11.3 next: specifier: ^14.2.8 - version: 14.2.13(react-dom@19.0.0-rc-cae764ce-20241025(react@18.3.1))(react@18.3.1) + version: 14.2.13(@babel/core@7.26.0)(react-dom@19.0.0-rc-cae764ce-20241025(react@18.3.1))(react@18.3.1) oauth2-mock-server: specifier: ^7.1.2 version: 7.1.2 @@ -1684,6 +1684,9 @@ importers: react: specifier: ^18.3.1 version: 18.3.1 + react-native: + specifier: ~0.74.6 + version: 0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.9)(encoding@0.1.13)(react@18.3.1) solid-js: specifier: ^1.8.18 version: 1.9.1 @@ -1772,10 +1775,28 @@ importers: '@better-fetch/fetch': specifier: 1.1.12 version: 1.1.12 + nanostores: + specifier: ^0.11.2 + version: 0.11.3 + react-native: + specifier: ~0.74.6 + version: 0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@19.0.0-rc-cae764ce-20241025) devDependencies: better-auth: specifier: workspace:^ version: link:../better-auth + expo-constants: + specifier: ~16.0.2 + version: 16.0.2(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13)) + expo-linking: + specifier: ~6.3.1 + version: 6.3.1(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13)) + expo-secure-store: + specifier: ~13.0.2 + version: 13.0.2(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13)) + expo-web-browser: + specifier: ~13.0.3 + version: 13.0.3(expo@51.0.38(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13)) unbuild: specifier: ^2.0.0 version: 2.0.0(typescript@5.6.3) @@ -25650,6 +25671,24 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@react-native/virtualized-lists@0.74.88(@types/react@18.3.12)(react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.0.0-rc-cae764ce-20241025 + react-native: 0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@19.0.0-rc-cae764ce-20241025) + optionalDependencies: + '@types/react': 18.3.12 + + '@react-native/virtualized-lists@0.74.88(@types/react@18.3.9)(react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.9)(encoding@0.1.13)(react@18.3.1))(react@18.3.1)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 18.3.1 + react-native: 0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.9)(encoding@0.1.13)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.9 + '@react-native/virtualized-lists@0.74.88(@types/react@18.3.9)(react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.9)(encoding@0.1.13)(react@19.0.0-rc-69d4b800-20241021))(react@19.0.0-rc-69d4b800-20241021)': dependencies: invariant: 2.2.4 @@ -35778,7 +35817,7 @@ snapshots: next-tick@1.1.0: {} - next@14.2.13(react-dom@19.0.0-rc-cae764ce-20241025(react@18.3.1))(react@18.3.1): + next@14.2.13(@babel/core@7.26.0)(react-dom@19.0.0-rc-cae764ce-20241025(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.13 '@swc/helpers': 0.5.5 @@ -35788,7 +35827,7 @@ snapshots: postcss: 8.4.31 react: 18.3.1 react-dom: 19.0.0-rc-cae764ce-20241025(react@18.3.1) - styled-jsx: 5.1.1(react@18.3.1) + styled-jsx: 5.1.1(@babel/core@7.26.0)(react@18.3.1) optionalDependencies: '@next/swc-darwin-arm64': 14.2.13 '@next/swc-darwin-x64': 14.2.13 @@ -37705,6 +37744,108 @@ snapshots: - supports-color - utf-8-validate + react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@19.0.0-rc-cae764ce-20241025): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native-community/cli': 13.6.9(encoding@0.1.13) + '@react-native-community/cli-platform-android': 13.6.9(encoding@0.1.13) + '@react-native-community/cli-platform-ios': 13.6.9(encoding@0.1.13) + '@react-native/assets-registry': 0.74.88 + '@react-native/codegen': 0.74.88(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + '@react-native/community-cli-plugin': 0.74.88(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13) + '@react-native/gradle-plugin': 0.74.88 + '@react-native/js-polyfills': 0.74.88 + '@react-native/normalize-colors': 0.74.88 + '@react-native/virtualized-lists': 0.74.88(@types/react@18.3.12)(react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.12)(encoding@0.1.13)(react@19.0.0-rc-cae764ce-20241025))(react@19.0.0-rc-cae764ce-20241025) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + base64-js: 1.5.1 + chalk: 4.1.2 + event-target-shim: 5.0.1 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + jsc-android: 250231.0.0 + memoize-one: 5.2.1 + metro-runtime: 0.80.12 + metro-source-map: 0.80.12 + mkdirp: 0.5.6 + nullthrows: 1.1.1 + pretty-format: 26.6.2 + promise: 8.3.0 + react: 19.0.0-rc-cae764ce-20241025 + react-devtools-core: 5.3.2 + react-refresh: 0.14.2 + react-shallow-renderer: 16.15.0(react@19.0.0-rc-cae764ce-20241025) + regenerator-runtime: 0.13.11 + scheduler: 0.24.0-canary-efb381bbf-20230505 + stacktrace-parser: 0.1.10 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 18.3.12 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - encoding + - supports-color + - utf-8-validate + + react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.9)(encoding@0.1.13)(react@18.3.1): + dependencies: + '@jest/create-cache-key-function': 29.7.0 + '@react-native-community/cli': 13.6.9(encoding@0.1.13) + '@react-native-community/cli-platform-android': 13.6.9(encoding@0.1.13) + '@react-native-community/cli-platform-ios': 13.6.9(encoding@0.1.13) + '@react-native/assets-registry': 0.74.88 + '@react-native/codegen': 0.74.88(@babel/preset-env@7.26.0(@babel/core@7.26.0)) + '@react-native/community-cli-plugin': 0.74.88(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(encoding@0.1.13) + '@react-native/gradle-plugin': 0.74.88 + '@react-native/js-polyfills': 0.74.88 + '@react-native/normalize-colors': 0.74.88 + '@react-native/virtualized-lists': 0.74.88(@types/react@18.3.9)(react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.9)(encoding@0.1.13)(react@18.3.1))(react@18.3.1) + abort-controller: 3.0.0 + anser: 1.4.10 + ansi-regex: 5.0.1 + base64-js: 1.5.1 + chalk: 4.1.2 + event-target-shim: 5.0.1 + flow-enums-runtime: 0.0.6 + glob: 7.2.3 + invariant: 2.2.4 + jest-environment-node: 29.7.0 + jsc-android: 250231.0.0 + memoize-one: 5.2.1 + metro-runtime: 0.80.12 + metro-source-map: 0.80.12 + mkdirp: 0.5.6 + nullthrows: 1.1.1 + pretty-format: 26.6.2 + promise: 8.3.0 + react: 18.3.1 + react-devtools-core: 5.3.2 + react-refresh: 0.14.2 + react-shallow-renderer: 16.15.0(react@18.3.1) + regenerator-runtime: 0.13.11 + scheduler: 0.24.0-canary-efb381bbf-20230505 + stacktrace-parser: 0.1.10 + whatwg-fetch: 3.6.20 + ws: 6.2.3 + yargs: 17.7.2 + optionalDependencies: + '@types/react': 18.3.9 + transitivePeerDependencies: + - '@babel/core' + - '@babel/preset-env' + - bufferutil + - encoding + - supports-color + - utf-8-validate + react-native@0.74.6(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(@types/react@18.3.9)(encoding@0.1.13)(react@19.0.0-rc-69d4b800-20241021): dependencies: '@jest/create-cache-key-function': 29.7.0 @@ -38036,6 +38177,12 @@ snapshots: react-is: 18.3.1 optional: true + react-shallow-renderer@16.15.0(react@19.0.0-rc-cae764ce-20241025): + dependencies: + object-assign: 4.1.1 + react: 19.0.0-rc-cae764ce-20241025 + react-is: 18.3.1 + react-smooth@4.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: fast-equals: 5.0.1 @@ -39394,10 +39541,12 @@ snapshots: dependencies: inline-style-parser: 0.2.4 - styled-jsx@5.1.1(react@18.3.1): + styled-jsx@5.1.1(@babel/core@7.26.0)(react@18.3.1): dependencies: client-only: 0.0.1 react: 18.3.1 + optionalDependencies: + '@babel/core': 7.26.0 styled-jsx@5.1.6(@babel/core@7.26.0)(react@19.0.0-rc-69d4b800-20241021): dependencies: @@ -39762,7 +39911,7 @@ snapshots: postcss: 8.4.47 postcss-import: 15.1.0(postcss@8.4.47) postcss-js: 4.0.1(postcss@8.4.47) - postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.1(@types/node@22.8.4)(typescript@5.6.3)) + postcss-load-config: 4.0.2(postcss@8.4.47)(ts-node@10.9.1(@types/node@22.8.4)(typescript@5.3.3)) postcss-nested: 6.2.0(postcss@8.4.47) postcss-selector-parser: 6.1.2 resolve: 1.22.8