diff --git a/README.md b/README.md index e5f62aa..53fbe8d 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,14 @@ put the following code. import GoogleSignInPlugin from "vue3-google-signin" +// for static clientId app.use(GoogleSignInPlugin, { clientId: 'CLIENT ID OBTAINED FROM GOOGLE API CONSOLE', }); +// for dynamic clientId +app.use(GoogleSignInPlugin); + // other config app.mount("#app"); @@ -75,6 +79,8 @@ pnpm add nuxt-vue3-google-signin Now you can add following entry to the `nuxt.config.ts`(or `nuxt.config.js`) +_The feature to dynamically add the `clientId` has not yet been implemented in this library._ + ```ts import { defineNuxtConfig } from 'nuxt/config' diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index c35d8b6..aefb968 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -58,6 +58,10 @@ const SideBar: DefaultTheme.Sidebar = [ }, ], }, + { + text: "Methods", + items: [{ text: "setGoogleClientId", link: "/methods/#setgoogleclientid" }], + }, { text: "Helpers", items: [ diff --git a/docs/guide/index.md b/docs/guide/index.md index 16009ab..09143ee 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -47,15 +47,23 @@ pnpm add vue3-google-signin Setting up the library is very simple. In your application entry point(`main.js` or `main.ts`) put the following code. +::: tip +:bulb: If you wish to dynamically add the `clientId`, you can use the `setGoogleClientId` method in your application. +::: + ```js // rest of the code import GoogleSignInPlugin from "vue3-google-signin" +// for static clientId app.use(GoogleSignInPlugin, { clientId: 'CLIENT ID OBTAINED FROM GOOGLE API CONSOLE', }); +// for dynamic clientId +app.use(GoogleSignInPlugin); + // other config app.mount("#app"); @@ -91,6 +99,10 @@ pnpm add nuxt-vue3-google-signin Now you can add following entry to the `nuxt.config.ts`(or `nuxt.config.js`) +::: warning +The feature to dynamically add the `clientId` has not yet been implemented in this library. +::: + ```ts import { defineNuxtConfig } from 'nuxt/config' diff --git a/docs/methods/index.md b/docs/methods/index.md new file mode 100644 index 0000000..b5f1114 --- /dev/null +++ b/docs/methods/index.md @@ -0,0 +1,31 @@ +--- +title: Methods +--- + +# Methods + +## setGoogleClientId() + +- **Type** + +```ts +function setGoogleClientId(id: string): void; +``` + +- **Details** + +This `setGoogleClientId` is used with the **vue3-google-signin** plugin to initialize the `clientId` dynamically, rather than at the time of installing the app. +_For example, it can be used when retrieving the clientId from a backend API._ + +:::tip +:bulb: While the `clientId` value is being loaded, the `isReady` value returned by the [**useCodeClient**](/composables/use-code-client), [**useTokenClient**](/composables/use-token-client), and [**useOneTap**](/composables/use-one-tap) hooks will be **false**. Additionally, the [**GoogleSignInButton**](/components/google-signin-button) component will have its `pointer-events` set to '**none**', and will become clickable after the loading process is complete. +::: + +- **Example** + +```ts +// some component +import { setGoogleClientId } from "vue3-google-signin"; + +setGoogleClientId("CLIENT ID OBTAINED FROM GOOGLE API CONSOLE"); +``` diff --git a/src/App.vue b/src/App.vue index 826b0ff..277f22d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,9 +5,16 @@ import useCodeClient from "./composables/useCodeClient"; import { ref } from "vue"; import useTokenClient from "./composables/useTokenClient"; import useOneTap from "./composables/useOneTap"; +import setGoogleClientId from "./methods/setGoogleClientId"; const scope = ref(""); +// Dynamic setting GoogleClientId +(async () => { + await new Promise((r) => setTimeout(r, 3000)); + setGoogleClientId(import.meta.env.VITE_GOOGLE_CLIENT_ID); +})(); + const onLoginSuccess = (resp: CredentialResponse) => { console.log("Login successful", resp); }; diff --git a/src/components/GoogleSignInButton.vue b/src/components/GoogleSignInButton.vue index 97fe982..ed51f7f 100644 --- a/src/components/GoogleSignInButton.vue +++ b/src/components/GoogleSignInButton.vue @@ -198,7 +198,7 @@ interface GoogleSignInButtonProps { locale?: string; } -const buttonContainerHeight = { large: 40, medium: 32, small: 20 }; +const buttonContainerHeight = { large: "40px", medium: "32px", small: "20px" }; const props = defineProps(); const emits = defineEmits<{ @@ -233,16 +233,18 @@ const emits = defineEmits<{ (e: "promptMomentNotification", notification: PromptMomentNotification): void; }>(); -const clientId = inject(GoogleClientIdKey); +const clientId = inject(GoogleClientIdKey); const targetElement = ref(null); +const isReady = ref(false); const { scriptLoaded } = useGsiScript(); watchEffect(() => { if (!scriptLoaded.value) return; + if (clientId?.value) isReady.value = true; window.google?.accounts.id.initialize({ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - client_id: clientId!, + client_id: clientId!.value, callback: (credentialResponse: CredentialResponse) => { if (!credentialResponse.credential) { emits("error"); @@ -294,8 +296,17 @@ onUnmounted(() => { }); const height = computed(() => buttonContainerHeight[props.size || "large"]); +const pointerEvents = computed(() => (isReady.value ? "initial" : "none")); diff --git a/src/composables/useCodeClient.ts b/src/composables/useCodeClient.ts index 27092e6..6ef2c7c 100644 --- a/src/composables/useCodeClient.ts +++ b/src/composables/useCodeClient.ts @@ -8,6 +8,7 @@ import { inject, unref, watchEffect, ref, readonly, type Ref } from "vue"; import { GoogleClientIdKey } from "@/utils/symbols"; import type { MaybeRef } from "@/utils/types"; import { buildCodeRequestRedirectUrl } from "../utils/oauth2"; +import { isClientIdValid, validateInitializeSetup } from "@/utils/validations"; /** * On success with implicit flow @@ -78,7 +79,7 @@ export interface UseCodeClientReturn { * * @memberof UseCodeClientReturn */ - login: () => void | undefined; + login: () => void; /** * Get a URL to perform code request without actually redirecting user. @@ -106,14 +107,20 @@ export default function useCodeClient( const { scope = "", onError, onSuccess, ...rest } = options; const { scriptLoaded } = useGsiScript(); - const clientId = inject(GoogleClientIdKey); + const clientId = inject(GoogleClientIdKey); const isReady = ref(false); const codeRequestRedirectUrl = ref(null); let client: CodeClient | undefined; + const login = () => { + if (!isClientIdValid(isReady.value, clientId?.value)) return; + + client?.requestCode(); + }; + watchEffect(() => { isReady.value = false; - if (!scriptLoaded.value) return; + if (!validateInitializeSetup(scriptLoaded.value, clientId?.value)) return; const scopeValue = unref(scope); const scopes = Array.isArray(scopeValue) @@ -122,15 +129,13 @@ export default function useCodeClient( const computedScopes = `openid email profile ${scopes}`; codeRequestRedirectUrl.value = buildCodeRequestRedirectUrl({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - client_id: clientId!, + client_id: clientId!.value, scope: computedScopes, ...rest, }); client = window.google?.accounts.oauth2.initCodeClient({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - client_id: clientId!, + client_id: clientId!.value, scope: computedScopes, callback: (response: CodeResponse) => { if (response.error) return onError?.(response); @@ -145,7 +150,7 @@ export default function useCodeClient( return { isReady: readonly(isReady), - login: () => client?.requestCode(), + login, codeRequestRedirectUrl: readonly(codeRequestRedirectUrl), }; } diff --git a/src/composables/useOneTap.ts b/src/composables/useOneTap.ts index 20aa092..80def39 100644 --- a/src/composables/useOneTap.ts +++ b/src/composables/useOneTap.ts @@ -8,6 +8,7 @@ import type { } from "@/interfaces/accounts"; import { inject, unref, watchEffect, ref, readonly, type Ref } from "vue"; import { GoogleClientIdKey } from "@/utils/symbols"; +import { isClientIdValid, validateInitializeSetup } from "@/utils/validations"; export interface UseGoogleOneTapLoginOptions { /** @@ -190,18 +191,20 @@ export default function useOneTap( } = options || {}; const { scriptLoaded } = useGsiScript(); - const clientId = inject(GoogleClientIdKey); + const clientId = inject(GoogleClientIdKey); const isReady = ref(false); - const login = () => - isReady.value && + const login = () => { + if (!isClientIdValid(isReady.value, clientId?.value)) return; + window.google?.accounts.id.prompt((notification) => onPromptMomentNotification?.(notification), ); + }; watchEffect((onCleanup) => { isReady.value = false; - if (!scriptLoaded.value) return; + if (!validateInitializeSetup(scriptLoaded.value, clientId?.value)) return; const shouldAutoLogin = !unref(disableAutomaticPrompt); @@ -216,8 +219,7 @@ export default function useOneTap( const cancel_on_tap_outside = unref(cancelOnTapOutside); window.google?.accounts.id.initialize({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - client_id: clientId!, + client_id: clientId!.value, callback: (credentialResponse: CredentialResponse) => { if (!credentialResponse.credential) { onError?.(); diff --git a/src/composables/useTokenClient.ts b/src/composables/useTokenClient.ts index 4d0715f..ef36153 100644 --- a/src/composables/useTokenClient.ts +++ b/src/composables/useTokenClient.ts @@ -6,8 +6,9 @@ import type { OverridableTokenClientConfig, } from "@/interfaces/oauth2"; import { inject, unref, watchEffect, ref, readonly, type Ref } from "vue"; -import { GoogleClientIdKey } from "../utils/symbols"; import type { MaybeRef } from "@/utils/types"; +import { GoogleClientIdKey } from "../utils/symbols"; +import { isClientIdValid, validateInitializeSetup } from "@/utils/validations"; /** * Success response @@ -78,7 +79,7 @@ export interface UseTokenClientReturn { * * @memberof UseTokenClientReturn */ - login: (overrideConfig?: OverridableTokenClientConfig) => void | undefined; + login: (overrideConfig?: OverridableTokenClientConfig) => void; } /** @@ -97,13 +98,19 @@ export default function useTokenClient( const { scope = "", onError, onSuccess, ...rest } = options; const { scriptLoaded } = useGsiScript(); - const clientId = inject(GoogleClientIdKey); + const clientId = inject(GoogleClientIdKey); const isReady = ref(false); let client: TokenClient | undefined; + const login = (overrideConfig?: OverridableTokenClientConfig) => { + if (!isClientIdValid(isReady.value, clientId?.value)) return; + + client?.requestAccessToken(overrideConfig); + }; + watchEffect(() => { isReady.value = false; - if (!scriptLoaded.value) return; + if (!validateInitializeSetup(scriptLoaded.value, clientId?.value)) return; const scopeValue = unref(scope); const scopes = Array.isArray(scopeValue) @@ -111,8 +118,7 @@ export default function useTokenClient( : scopeValue; client = window.google?.accounts.oauth2.initTokenClient({ - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - client_id: clientId!, + client_id: clientId!.value, scope: `openid email profile ${scopes}`, callback: (response: TokenResponse) => { if (response.error) return onError?.(response); @@ -127,7 +133,6 @@ export default function useTokenClient( return { isReady: readonly(isReady), - login: (overrideConfig?: OverridableTokenClientConfig) => - client?.requestAccessToken(overrideConfig), + login, }; } diff --git a/src/constant.ts b/src/constant.ts new file mode 100644 index 0000000..b816d3d --- /dev/null +++ b/src/constant.ts @@ -0,0 +1 @@ +export const PLUGIN_NAME = "GoogleSignInPlugin"; diff --git a/src/main.ts b/src/main.ts index 74934df..747f9ce 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,8 +4,12 @@ import GoogleOauth2Plugin from "./plugin"; const app = createApp(App); +// for static app.use(GoogleOauth2Plugin, { clientId: import.meta.env.VITE_GOOGLE_CLIENT_ID, }); +// for dynamic +// app.use(GoogleOauth2Plugin); + app.mount("#app"); diff --git a/src/methods/setGoogleClientId.ts b/src/methods/setGoogleClientId.ts new file mode 100644 index 0000000..2f5e366 --- /dev/null +++ b/src/methods/setGoogleClientId.ts @@ -0,0 +1,13 @@ +import { googleClientIdRef } from "@/store"; + +/** + * Dynamically set the Google OAuth client ID for the application. + * This method is primarily used with the `vue3-google-signin` plugin to configure the + * `clientId` after retrieving it dynamically. + * + * @export + * @param {string} id - The Google OAuth client ID to initialize. + */ +export default function setGoogleClientId(id: string) { + googleClientIdRef.value = id; +} diff --git a/src/plugin.ts b/src/plugin.ts index 434b481..473ef4e 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -5,6 +5,7 @@ import useGsiScript from "./composables/useGsiScript"; import useCodeClient from "./composables/useCodeClient"; import useOneTap from "./composables/useOneTap"; import useTokenClient from "./composables/useTokenClient"; +import setGoogleClientId from "./methods/setGoogleClientId"; import type { ImplicitFlowOptions, @@ -25,7 +26,7 @@ import type { AuthCodeFlowSuccessResponse, } from "./composables/useTokenClient"; -export const PLUGIN_NAME = "GoogleSignInPlugin"; +import { googleClientIdRef } from "./store"; export interface GoogleSignInPluginOptions { /** @@ -38,24 +39,10 @@ export interface GoogleSignInPluginOptions { clientId: string; } -const toPluginError = (err: string) => `[${PLUGIN_NAME}]: ${err}`; - const plugin: Plugin = { - install(app: App, options: GoogleSignInPluginOptions) { - if (!options) { - throw new Error( - toPluginError(`initialize plugin by passing an options object`), - ); - } - - if ( - !options.clientId || - (options.clientId && options.clientId.trim().length === 0) - ) { - throw new Error(toPluginError("clientId is required to initialize")); - } - - app.provide(GoogleClientIdKey, options.clientId); + install(app: App, options?: GoogleSignInPluginOptions) { + googleClientIdRef.value = options?.clientId; + app.provide(GoogleClientIdKey, googleClientIdRef); app.component("GoogleSignInButton", GoogleSignInButton); }, }; @@ -66,6 +53,7 @@ export { useGsiScript, useTokenClient, useOneTap, + setGoogleClientId, }; export * from "./interfaces"; diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..309d9b5 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,3 @@ +import { ref } from "vue"; + +export const googleClientIdRef = ref(); diff --git a/src/utils/logs.ts b/src/utils/logs.ts new file mode 100644 index 0000000..ea9dd30 --- /dev/null +++ b/src/utils/logs.ts @@ -0,0 +1,5 @@ +import { PLUGIN_NAME } from "@/constant"; + +export function toPluginError(err: string) { + return `[${PLUGIN_NAME}]: ${err}`; +} diff --git a/src/utils/symbols.ts b/src/utils/symbols.ts index c8847a7..2a3174c 100644 --- a/src/utils/symbols.ts +++ b/src/utils/symbols.ts @@ -1,3 +1,3 @@ -import type { InjectionKey } from "vue"; +import type { InjectionKey, Ref } from "vue"; -export const GoogleClientIdKey = Symbol() as InjectionKey; +export const GoogleClientIdKey = Symbol() as InjectionKey>; diff --git a/src/utils/validations.ts b/src/utils/validations.ts new file mode 100644 index 0000000..52664d3 --- /dev/null +++ b/src/utils/validations.ts @@ -0,0 +1,19 @@ +import { toPluginError } from "./logs"; + +export function isClientIdValid(isReady: boolean, clientId?: string) { + if (!clientId) + throw new Error( + toPluginError( + "Set clientId in options or use setClientId to initialize.", + ), + ); + + return isReady; +} + +export function validateInitializeSetup( + isScriptLoad: boolean, + clientId?: string, +) { + return isScriptLoad && !!clientId; +}