better-auth
Version:
The most comprehensive authentication framework for TypeScript.
209 lines (208 loc) • 6.95 kB
JavaScript
import { PACKAGE_VERSION } from "../../version.mjs";
import { isSafeUrlScheme } from "@better-auth/core/utils/url";
//#region src/plugins/one-tap/client.ts
let isRequestInProgress = false;
function isFedCMSupported() {
return typeof window !== "undefined" && "IdentityCredential" in window;
}
/**
* Reasons that should NOT trigger a retry.
* @see https://developers.google.com/identity/gsi/web/reference/js-reference
*/
const noRetryReasons = {
dismissed: ["credential_returned", "cancel_called"],
skipped: ["user_cancel", "tap_outside"]
};
const oneTapClient = (options) => {
return {
id: "one-tap",
version: PACKAGE_VERSION,
fetchPlugins: [{
id: "fedcm-signout-handle",
name: "FedCM Sign-Out Handler",
hooks: { async onResponse(ctx) {
if (!ctx.request.url.toString().includes("/sign-out")) return;
if (options.promptOptions?.fedCM === false || !isFedCMSupported()) return;
navigator.credentials.preventSilentAccess();
} }
}],
getActions: ($fetch, _) => {
return { oneTap: async (opts, fetchOptions) => {
if (isRequestInProgress) {
console.warn("A Google One Tap request is already in progress. Please wait.");
return;
}
if (typeof window === "undefined" || !window.document) {
console.warn("Google One Tap is only available in browser environments");
return;
}
if (opts?.button) {
await loadGoogleScript();
const container = typeof opts.button.container === "string" ? document.querySelector(opts.button.container) : opts.button.container;
if (!container) {
console.error("Google One Tap: Button container not found", opts.button.container);
return;
}
async function callback(idToken) {
if ((await $fetch("/one-tap/callback", {
method: "POST",
body: {
idToken,
callbackURL: opts?.callbackURL
},
...opts?.fetchOptions,
...fetchOptions
}))?.error) return;
if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
const target = opts?.callbackURL ?? "/";
if (isSafeUrlScheme(target)) window.location.href = target;
}
}
const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
const contextValue = context ?? options.context ?? "signin";
const useFedCM = options.promptOptions?.fedCM !== false;
window.google?.accounts.id.initialize({
client_id: options.clientId,
callback: async (response) => {
try {
await callback(response.credential);
} catch (error) {
console.error("Error during button callback:", error);
}
},
auto_select: autoSelect,
cancel_on_tap_outside: cancelOnTapOutside,
context: contextValue,
ux_mode: opts?.uxMode || "popup",
nonce: opts?.nonce,
itp_support: true,
use_fedcm_for_prompt: useFedCM,
...options.additionalOptions
});
window.google?.accounts.id.renderButton(container, opts.button.config ?? { type: "icon" });
return;
}
async function callback(idToken) {
if ((await $fetch("/one-tap/callback", {
method: "POST",
body: {
idToken,
callbackURL: opts?.callbackURL
},
...opts?.fetchOptions,
...fetchOptions
}))?.error) return;
if (!opts?.fetchOptions && !fetchOptions || opts?.callbackURL) {
const target = opts?.callbackURL ?? "/";
if (isSafeUrlScheme(target)) window.location.href = target;
}
}
const { autoSelect, cancelOnTapOutside, context } = opts ?? {};
const contextValue = context ?? options.context ?? "signin";
isRequestInProgress = true;
try {
await loadGoogleScript();
await new Promise((resolve, reject) => {
let isResolved = false;
const baseDelay = options.promptOptions?.baseDelay ?? 1e3;
const maxAttempts = options.promptOptions?.maxAttempts ?? 5;
const useFedCM = options.promptOptions?.fedCM !== false;
window.google?.accounts.id.initialize({
client_id: options.clientId,
callback: async (response) => {
isResolved = true;
try {
await callback(response.credential);
resolve();
} catch (error) {
console.error("Error during One Tap callback:", error);
reject(error);
}
},
auto_select: autoSelect,
cancel_on_tap_outside: cancelOnTapOutside,
context: contextValue,
ux_mode: opts?.uxMode || "popup",
nonce: opts?.nonce,
/**
* @see {@link https://developers.google.com/identity/gsi/web/guides/overview}
*/
itp_support: true,
use_fedcm_for_prompt: useFedCM,
...options.additionalOptions
});
const handlePrompt = (attempt) => {
if (isResolved) return;
window.google?.accounts.id.prompt((notification) => {
if (isResolved) return;
if (notification.isDismissedMoment?.()) {
const reason = notification.getDismissedReason?.();
if (noRetryReasons.dismissed.includes(reason)) {
opts?.onPromptNotification?.(notification);
resolve();
return;
}
if (attempt < maxAttempts) {
const delay = Math.pow(2, attempt) * baseDelay;
setTimeout(() => handlePrompt(attempt + 1), delay);
} else {
opts?.onPromptNotification?.(notification);
resolve();
}
} else if (notification.isSkippedMoment?.()) {
const reason = notification.getSkippedReason?.();
if (!reason || noRetryReasons.skipped.includes(reason)) {
opts?.onPromptNotification?.(notification);
resolve();
return;
}
if (attempt < maxAttempts) {
const delay = Math.pow(2, attempt) * baseDelay;
setTimeout(() => handlePrompt(attempt + 1), delay);
} else {
opts?.onPromptNotification?.(notification);
resolve();
}
} else if (notification.isNotDisplayed?.()) {
opts?.onPromptNotification?.(notification);
resolve();
}
});
};
handlePrompt(0);
});
} catch (error) {
console.error("Error during Google One Tap flow:", error);
throw error;
} finally {
isRequestInProgress = false;
}
} };
},
getAtoms($fetch) {
return {};
}
};
};
const loadGoogleScript = () => {
return new Promise((resolve, reject) => {
if (window.googleScriptInitialized) {
resolve();
return;
}
const script = document.createElement("script");
script.src = "https://accounts.google.com/gsi/client";
script.async = true;
script.defer = true;
script.onload = () => {
window.googleScriptInitialized = true;
resolve();
};
script.onerror = () => {
reject(/* @__PURE__ */ new Error("Failed to load Google Identity Services script"));
};
document.head.appendChild(script);
});
};
//#endregion
export { oneTapClient };