@better-auth/expo
Version:
This plugin integrates Better Auth with Expo, allowing you to easily add authentication to your Expo (React Native) applications. It supports both Expo native and web apps.
256 lines (249 loc) • 8.2 kB
JavaScript
;
const Browser = require('expo-web-browser');
const Linking = require('expo-linking');
const reactNative = require('react-native');
const Constants = require('expo-constants');
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
function _interopNamespaceCompat(e) {
if (e && typeof e === 'object' && 'default' in e) return e;
const n = Object.create(null);
if (e) {
for (const k in e) {
n[k] = e[k];
}
}
n.default = e;
return n;
}
const Browser__namespace = /*#__PURE__*/_interopNamespaceCompat(Browser);
const Linking__namespace = /*#__PURE__*/_interopNamespaceCompat(Linking);
const Constants__default = /*#__PURE__*/_interopDefaultCompat(Constants);
function parseSetCookieHeader(header) {
const cookieMap = /* @__PURE__ */ new Map();
const cookies = splitSetCookieHeader(header);
cookies.forEach((cookie) => {
const parts = cookie.split(";").map((p) => p.trim());
const [nameValue, ...attributes] = parts;
const [name, ...valueParts] = nameValue.split("=");
const value = valueParts.join("=");
const cookieObj = { value };
attributes.forEach((attr) => {
const [attrName, ...attrValueParts] = attr.split("=");
const attrValue = attrValueParts.join("=");
cookieObj[attrName.toLowerCase()] = attrValue;
});
cookieMap.set(name, cookieObj);
});
return cookieMap;
}
function splitSetCookieHeader(setCookie) {
const parts = [];
let buffer = "";
let i = 0;
while (i < setCookie.length) {
const char = setCookie[i];
if (char === ",") {
const recent = buffer.toLowerCase();
const hasExpires = recent.includes("expires=");
const hasGmt = /gmt/i.test(recent);
if (hasExpires && !hasGmt) {
buffer += char;
i += 1;
continue;
}
if (buffer.trim().length > 0) {
parts.push(buffer.trim());
buffer = "";
}
i += 1;
if (setCookie[i] === " ") i += 1;
continue;
}
buffer += char;
i += 1;
}
if (buffer.trim().length > 0) {
parts.push(buffer.trim());
}
return parts;
}
function getSetCookie(header, prevCookie) {
const parsed = parseSetCookieHeader(header);
let toSetCookie = {};
parsed.forEach((cookie, key) => {
const expiresAt = cookie["expires"];
const maxAge = cookie["max-age"];
const expires = maxAge ? new Date(Date.now() + Number(maxAge) * 1e3) : expiresAt ? new Date(String(expiresAt)) : null;
toSetCookie[key] = {
value: cookie["value"],
expires: expires ? expires.toISOString() : null
};
});
if (prevCookie) {
try {
const prevCookieParsed = JSON.parse(prevCookie);
toSetCookie = {
...prevCookieParsed,
...toSetCookie
};
} catch {
}
}
return JSON.stringify(toSetCookie);
}
function getCookie(cookie) {
let parsed = {};
try {
parsed = JSON.parse(cookie);
} catch (e) {
}
const toSend = Object.entries(parsed).reduce((acc, [key, value]) => {
if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) {
return acc;
}
return `${acc}; ${key}=${value.value}`;
}, "");
return toSend;
}
function getOrigin(scheme) {
const schemeURI = Linking__namespace.createURL("", { scheme });
return schemeURI;
}
const expoClient = (opts) => {
let store = null;
const cookieName = `${opts?.storagePrefix || "better-auth"}_cookie`;
const localCacheName = `${opts?.storagePrefix || "better-auth"}_session_data`;
const storage = opts?.storage;
const isWeb = reactNative.Platform.OS === "web";
const rawScheme = opts?.scheme || Constants__default.expoConfig?.scheme || Constants__default.platform?.scheme;
const scheme = Array.isArray(rawScheme) ? rawScheme[0] : rawScheme;
if (!scheme && !isWeb) {
throw new Error(
"Scheme not found in app.json. Please provide a scheme in the options."
);
}
return {
id: "expo",
getActions(_, $store) {
store = $store;
return {
/**
* Get the stored cookie.
*
* You can use this to get the cookie stored in the device and use it in your fetch
* requests.
*
* @example
* ```ts
* const cookie = client.getCookie();
* fetch("https://api.example.com", {
* headers: {
* cookie,
* },
* });
*/
getCookie: () => {
const cookie = storage.getItem(cookieName);
return getCookie(cookie || "{}");
}
};
},
fetchPlugins: [
{
id: "expo",
name: "Expo",
hooks: {
async onSuccess(context) {
if (isWeb) return;
const setCookie = context.response.headers.get("set-cookie");
if (setCookie) {
const prevCookie = await storage.getItem(cookieName);
const toSetCookie = getSetCookie(
setCookie || "",
prevCookie ?? void 0
);
await storage.setItem(cookieName, toSetCookie);
store?.notify("$sessionSignal");
}
if (context.request.url.toString().includes("/get-session") && !opts?.disableCache) {
const data = context.data;
storage.setItem(localCacheName, JSON.stringify(data));
}
if (context.data?.redirect && context.request.url.toString().includes("/sign-in") && !context.request?.body.includes("idToken")) {
const callbackURL = JSON.parse(context.request.body)?.callbackURL;
const to = callbackURL;
const signInURL = context.data?.url;
const result = await Browser__namespace.openAuthSessionAsync(signInURL, to);
if (result.type !== "success") return;
const url = new URL(result.url);
const cookie = String(url.searchParams.get("cookie"));
if (!cookie) return;
storage.setItem(cookieName, getSetCookie(cookie));
store?.notify("$sessionSignal");
}
}
},
async init(url, options) {
if (isWeb) {
return {
url,
options
};
}
options = options || {};
const storedCookie = storage.getItem(cookieName);
const cookie = getCookie(storedCookie || "{}");
options.credentials = "omit";
options.headers = {
...options.headers,
cookie,
"expo-origin": getOrigin(scheme),
"x-skip-oauth-proxy": "true"
// skip oauth proxy for expo
};
if (options.body?.callbackURL) {
if (options.body.callbackURL.startsWith("/")) {
const url2 = Linking__namespace.createURL(options.body.callbackURL, {
scheme
});
options.body.callbackURL = url2;
}
}
if (options.body?.newUserCallbackURL) {
if (options.body.newUserCallbackURL.startsWith("/")) {
const url2 = Linking__namespace.createURL(options.body.newUserCallbackURL, {
scheme
});
options.body.newUserCallbackURL = url2;
}
}
if (options.body?.errorCallbackURL) {
if (options.body.errorCallbackURL.startsWith("/")) {
const url2 = Linking__namespace.createURL(options.body.errorCallbackURL, {
scheme
});
options.body.errorCallbackURL = url2;
}
}
if (url.includes("/sign-out")) {
await storage.setItem(cookieName, "{}");
store?.atoms.session?.set({
data: null,
error: null,
isPending: false
});
storage.setItem(localCacheName, "{}");
}
return {
url,
options
};
}
}
]
};
};
exports.expoClient = expoClient;
exports.getCookie = getCookie;
exports.getSetCookie = getSetCookie;
exports.parseSetCookieHeader = parseSetCookieHeader;