@pradip1995/commerce-auth 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,12 +8,14 @@ import {
8
8
  LOGIN_VIEW,
9
9
  GoogleLoginButton,
10
10
  GoogleCallbackPage,
11
+ GoogleAuthShell,
12
+ GoogleIdentityServicesScript,
11
13
  } from "@pradip1995/commerce-auth"
12
14
  ```
13
15
 
14
- ### Google OAuth
16
+ ### Google OAuth (redirect flow)
15
17
 
16
- Use `GoogleLoginButton` anywhere you need a Google sign-in CTA (login, register, modals):
18
+ Use `GoogleLoginButton` for a "Continue with Google" CTA (login, register, modals):
17
19
 
18
20
  ```tsx
19
21
  import { GoogleLoginButton } from "@pradip1995/commerce-auth"
@@ -22,7 +24,7 @@ import { initiateGoogleAuth } from "@core/data/customer"
22
24
  <GoogleLoginButton countryCode={countryCode} initiateAuth={initiateGoogleAuth} />
23
25
  ```
24
26
 
25
- Wire the OAuth callback route to the shared page:
27
+ Wire the OAuth callback route:
26
28
 
27
29
  ```tsx
28
30
  import { GoogleCallbackPage } from "@pradip1995/commerce-auth"
@@ -38,18 +40,55 @@ export default function GoogleCallbackRoute() {
38
40
  }
39
41
  ```
40
42
 
41
- Pass your app's server actions via `initiateAuth`, `handleAuthCallback`, and `handleCustomerCallback` so client UI stays shared while data layer stays in each storefront.
43
+ ### Google One Tap (GIS SDK corner popup)
42
44
 
43
- ### Google One Tap (optional)
45
+ Uses the official [Google Identity Services](https://developers.google.com/identity/gsi/web/guides/overview) SDK (`accounts.google.com/gsi/client`):
44
46
 
45
- Set `NEXT_PUBLIC_GOOGLE_CLIENT_ID` (same OAuth client as the Medusa backend). Wrap the app layout:
47
+ 1. `google.accounts.id.initialize({ client_id, callback })`
48
+ 2. `google.accounts.id.prompt()` — shows the account picker on any page
49
+ 3. User clicks an account → JWT credential sent to your backend
50
+ 4. Medusa verifies the token via `@medusajs/auth-google`
51
+
52
+ **Google Cloud Console**
53
+
54
+ - OAuth 2.0 Client ID (Web application)
55
+ - Authorized JavaScript origins: `http://localhost:8000`, your production domain
56
+ - OAuth consent screen with app name and branding (controls "Sign in to … with Google")
57
+
58
+ **Root layout**
46
59
 
47
60
  ```tsx
48
- import { GoogleAuthProvider } from "@pradip1995/commerce-auth"
61
+ import { retrieveCustomer } from "@core/data/customer"
62
+ import {
63
+ GoogleAuthShell,
64
+ GoogleIdentityServicesScript,
65
+ } from "@pradip1995/commerce-auth"
66
+
67
+ export default async function RootLayout({ children }) {
68
+ const customer = await retrieveCustomer().catch(() => null)
69
+ const isAuthenticated = Boolean(customer && customer.id !== "pending_deletion")
49
70
 
50
- <GoogleAuthProvider>{children}</GoogleAuthProvider>
71
+ return (
72
+ <html>
73
+ <head>
74
+ {/* Step 2: Load GIS library (official docs) */}
75
+ <GoogleIdentityServicesScript />
76
+ </head>
77
+ <body>
78
+ {/* Step 3: Initialize + prompt on every page for signed-out users */}
79
+ <GoogleAuthShell isAuthenticated={isAuthenticated}>
80
+ {children}
81
+ </GoogleAuthShell>
82
+ </body>
83
+ </html>
84
+ )
85
+ }
51
86
  ```
52
87
 
53
- On login, `GoogleOneTapLogin` is enabled automatically when the client ID is present. Credential tokens are sent to Medusa's existing `/auth/customer/google/callback` endpoint (requires backend support for `credential` in the POST body).
88
+ Set `NEXT_PUBLIC_GOOGLE_CLIENT_ID` (same client ID as Medusa `GOOGLE_CLIENT_ID`).
89
+
90
+ **Backend:** `authenticateGoogleCredential` POSTs the JWT to Medusa `/auth/customer/google/callback` with `{ credential }`. Medusa validates signature, `aud`, and issuer before creating a session.
91
+
92
+ Optional: `autoSelectReturningUsers` on `GoogleAuthShell` enables GIS `auto_select` for users who already granted consent.
54
93
 
55
94
  Consuming apps must provide `@modules/*` shims for icons and modals, or replace theme slots to wrap headless exports.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pradip1995/commerce-auth",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Medusa storefront authentication — login, register, OTP, Google OAuth",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -30,11 +30,13 @@
30
30
  "./components/google-callback": "./src/components/google-callback/index.tsx",
31
31
  "./util/google-auth-client": "./src/util/google-auth-client.ts",
32
32
  "./components/google-oauth-provider": "./src/components/google-oauth-provider/index.tsx",
33
+ "./components/google-auth-shell": "./src/components/google-auth-shell/index.tsx",
34
+ "./components/google-identity-script": "./src/components/google-identity-script/index.tsx",
33
35
  "./components/google-one-tap": "./src/components/google-one-tap/index.tsx",
34
36
  "./components/google-credential-button": "./src/components/google-credential-button/index.tsx"
35
37
  },
36
38
  "peerDependencies": {
37
- "@pradip1995/commerce-core": "^2.0.0",
39
+ "@pradip1995/commerce-core": "^3.0.0",
38
40
  "@medusajs/ui": ">=4",
39
41
  "@react-oauth/google": ">=0.12",
40
42
  "next": ">=15",
@@ -47,7 +49,7 @@
47
49
  }
48
50
  },
49
51
  "devDependencies": {
50
- "@pradip1995/commerce-core": "^2.0.0",
52
+ "@pradip1995/commerce-core": "^3.0.0",
51
53
  "@react-oauth/google": "^0.12.2",
52
54
  "@medusajs/types": "latest",
53
55
  "@medusajs/ui": "latest",
@@ -0,0 +1,75 @@
1
+ "use client"
2
+
3
+ import {
4
+ authenticateGoogleCredential,
5
+ handleGoogleCallback,
6
+ } from "@core/data/customer"
7
+ import { usePathname } from "next/navigation"
8
+
9
+ import { GoogleOneTapLogin } from "../google-one-tap"
10
+ import { getGoogleClientId } from "../google-oauth-provider"
11
+
12
+ export type GoogleAuthShellProps = {
13
+ children: React.ReactNode
14
+ /** Pass from server layout via retrieveCustomer() */
15
+ isAuthenticated?: boolean
16
+ defaultCountryCode?: string
17
+ /** Skip One Tap UI for returning users who already consented (GIS auto_select) */
18
+ autoSelectReturningUsers?: boolean
19
+ }
20
+
21
+ function GoogleOneTapGate({
22
+ isAuthenticated,
23
+ defaultCountryCode,
24
+ autoSelectReturningUsers,
25
+ }: {
26
+ isAuthenticated: boolean
27
+ defaultCountryCode: string
28
+ autoSelectReturningUsers: boolean
29
+ }) {
30
+ const pathname = usePathname()
31
+ const clientId = getGoogleClientId()
32
+ const isCallbackRoute = pathname?.startsWith("/auth/customer/google/callback")
33
+ const disabled = !clientId || isAuthenticated || isCallbackRoute
34
+
35
+ if (disabled) {
36
+ return null
37
+ }
38
+
39
+ return (
40
+ <GoogleOneTapLogin
41
+ handlers={{
42
+ authenticateCredential: authenticateGoogleCredential,
43
+ handleCustomerCallback: handleGoogleCallback,
44
+ }}
45
+ defaultCountryCode={defaultCountryCode}
46
+ autoSelect={autoSelectReturningUsers}
47
+ useFedcmForPrompt={true}
48
+ />
49
+ )
50
+ }
51
+
52
+ export function GoogleAuthShell({
53
+ children,
54
+ isAuthenticated = false,
55
+ defaultCountryCode,
56
+ autoSelectReturningUsers = false,
57
+ }: GoogleAuthShellProps) {
58
+ const region =
59
+ defaultCountryCode ??
60
+ process.env.NEXT_PUBLIC_DEFAULT_REGION?.trim() ??
61
+ "us"
62
+
63
+ return (
64
+ <>
65
+ <GoogleOneTapGate
66
+ isAuthenticated={isAuthenticated}
67
+ defaultCountryCode={region}
68
+ autoSelectReturningUsers={autoSelectReturningUsers}
69
+ />
70
+ {children}
71
+ </>
72
+ )
73
+ }
74
+
75
+ export default GoogleAuthShell
@@ -0,0 +1,23 @@
1
+ import Script from "next/script"
2
+
3
+ /**
4
+ * Loads the official Google Identity Services (GIS) SDK in `<head>`.
5
+ * @see https://developers.google.com/identity/gsi/web/guides/overview
6
+ */
7
+ export function GoogleIdentityServicesScript() {
8
+ const clientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID?.trim()
9
+
10
+ if (!clientId) {
11
+ return null
12
+ }
13
+
14
+ return (
15
+ <Script
16
+ id="google-identity-services"
17
+ src="https://accounts.google.com/gsi/client"
18
+ strategy="beforeInteractive"
19
+ />
20
+ )
21
+ }
22
+
23
+ export default GoogleIdentityServicesScript
@@ -1,12 +1,15 @@
1
1
  "use client"
2
2
 
3
- import { useGoogleOneTapLogin } from "@react-oauth/google"
4
3
  import { useParams } from "next/navigation"
5
- import { useCallback } from "react"
4
+ import { useEffect, useRef } from "react"
6
5
  import {
7
6
  completeGoogleCredentialSignIn,
8
7
  type GoogleCredentialSignInHandlers,
9
8
  } from "../../util/google-auth-client"
9
+ import {
10
+ type GoogleCredentialResponse,
11
+ waitForGoogleIdentityServices,
12
+ } from "../../util/google-identity-services"
10
13
  import { getGoogleClientId } from "../google-oauth-provider"
11
14
 
12
15
  export type GoogleOneTapLoginProps = {
@@ -14,6 +17,9 @@ export type GoogleOneTapLoginProps = {
14
17
  countryCode?: string
15
18
  defaultCountryCode?: string
16
19
  disabled?: boolean
20
+ /** Enable automatic sign-in for returning users who already granted consent */
21
+ autoSelect?: boolean
22
+ useFedcmForPrompt?: boolean
17
23
  onError?: (message: string) => void
18
24
  }
19
25
 
@@ -22,41 +28,77 @@ export function GoogleOneTapLogin({
22
28
  countryCode: countryCodeProp,
23
29
  defaultCountryCode = "us",
24
30
  disabled = false,
31
+ autoSelect = false,
32
+ useFedcmForPrompt = true,
25
33
  onError,
26
34
  }: GoogleOneTapLoginProps) {
27
35
  const params = useParams()
28
36
  const clientId = getGoogleClientId()
37
+ const handlersRef = useRef(handlers)
38
+ const onErrorRef = useRef(onError)
29
39
 
30
- const handleCredential = useCallback(
31
- async (credential: string) => {
32
- const result = await completeGoogleCredentialSignIn(credential, handlers, {
33
- countryCode:
34
- countryCodeProp || (params?.countryCode as string | undefined),
35
- defaultCountryCode,
36
- })
40
+ handlersRef.current = handlers
41
+ onErrorRef.current = onError
42
+
43
+ useEffect(() => {
44
+ if (disabled || !clientId) {
45
+ return
46
+ }
47
+
48
+ let cancelled = false
37
49
 
38
- if (result.error) {
39
- onError?.(result.error)
40
- }
41
- },
42
- [countryCodeProp, defaultCountryCode, handlers, onError, params?.countryCode]
43
- )
50
+ waitForGoogleIdentityServices()
51
+ .then((idApi) => {
52
+ if (cancelled) {
53
+ return
54
+ }
44
55
 
45
- useGoogleOneTapLogin({
46
- disabled: disabled || !clientId,
47
- onSuccess: async (response) => {
48
- const credential = response.credential
49
- if (!credential) {
50
- onError?.("No Google credential received")
51
- return
52
- }
56
+ idApi.initialize({
57
+ client_id: clientId,
58
+ callback: (response: GoogleCredentialResponse) => {
59
+ const credential = response.credential
60
+ if (!credential) {
61
+ onErrorRef.current?.("No Google credential received")
62
+ return
63
+ }
64
+
65
+ completeGoogleCredentialSignIn(credential, handlersRef.current, {
66
+ countryCode:
67
+ countryCodeProp || (params?.countryCode as string | undefined),
68
+ defaultCountryCode,
69
+ }).then((result) => {
70
+ if (result.error) {
71
+ onErrorRef.current?.(result.error)
72
+ }
73
+ })
74
+ },
75
+ auto_select: autoSelect,
76
+ cancel_on_tap_outside: true,
77
+ use_fedcm_for_prompt: useFedcmForPrompt,
78
+ context: "signin",
79
+ })
80
+
81
+ idApi.prompt()
82
+ })
83
+ .catch((error: Error) => {
84
+ if (!cancelled) {
85
+ onErrorRef.current?.(error.message)
86
+ }
87
+ })
53
88
 
54
- await handleCredential(credential)
55
- },
56
- onError: () => {
57
- onError?.("Google One Tap was dismissed or failed")
58
- },
59
- })
89
+ return () => {
90
+ cancelled = true
91
+ window.google?.accounts?.id?.cancel()
92
+ }
93
+ }, [
94
+ clientId,
95
+ countryCodeProp,
96
+ defaultCountryCode,
97
+ disabled,
98
+ autoSelect,
99
+ useFedcmForPrompt,
100
+ params?.countryCode,
101
+ ])
60
102
 
61
103
  return null
62
104
  }
@@ -1,14 +1,7 @@
1
1
  "use client"
2
2
 
3
- import {
4
- authenticateGoogleCredential,
5
- handleGoogleCallback,
6
- initiateGoogleAuth,
7
- login,
8
- } from "@core/data/customer"
9
- import { GoogleLoginButton } from "../google-login"
10
- import { GoogleOneTapLogin } from "../google-one-tap"
11
- import { getGoogleClientId } from "../google-oauth-provider"
3
+ import { login } from "@core/data/customer"
4
+ import GoogleLogin from "@modules/account/components/google-login"
12
5
  import { LOGIN_VIEW } from "@core/types/account"
13
6
  import ErrorMessage from "@modules/checkout/components/error-message"
14
7
  import Envelope from "@modules/common/icons/envelope"
@@ -112,19 +105,8 @@ const Login = ({ setCurrentView }: Props) => {
112
105
  setShowGuestModal(true)
113
106
  }
114
107
 
115
- const googleCredentialHandlers = {
116
- authenticateCredential: authenticateGoogleCredential,
117
- handleCustomerCallback: handleGoogleCallback,
118
- }
119
-
120
108
  return (
121
109
  <div className="w-full flex flex-col" data-testid="login-page">
122
- {getGoogleClientId() ? (
123
- <GoogleOneTapLogin
124
- handlers={googleCredentialHandlers}
125
- countryCode={params?.countryCode as string | undefined}
126
- />
127
- ) : null}
128
110
  <h1 className="text-xl min-[340px]:text-2xl min-[550px]:text-3xl font-bold text-heading mb-1 min-[340px]:mb-2 text-center min-[550px]:text-left">
129
111
  Welcome Back
130
112
  </h1>
@@ -134,10 +116,7 @@ const Login = ({ setCurrentView }: Props) => {
134
116
 
135
117
  {/* Google Login at Top */}
136
118
  <div className="mb-6">
137
- <GoogleLoginButton
138
- countryCode={params?.countryCode as string | undefined}
139
- initiateAuth={initiateGoogleAuth}
140
- />
119
+ <GoogleLogin />
141
120
 
142
121
  {/* Quick Link at Top */}
143
122
  <div className="mt-4 text-center">
@@ -10,8 +10,7 @@ import User from "@modules/common/icons/user"
10
10
  import PhoneIcon from "@modules/common/icons/phone"
11
11
  import Eye from "@modules/common/icons/eye"
12
12
  import EyeOff from "@modules/common/icons/eye-off"
13
- import { initiateGoogleAuth } from "@core/data/customer"
14
- import { GoogleLoginButton } from "../google-login"
13
+ import GoogleLogin from "@modules/account/components/google-login"
15
14
  import {
16
15
  registerCustomer,
17
16
  sendCustomerOTP,
@@ -194,10 +193,7 @@ const Register = ({ setCurrentView }: Props) => {
194
193
 
195
194
  {/* Google Signup at Top */}
196
195
  <div className="mb-6">
197
- <GoogleLoginButton
198
- countryCode={params?.countryCode as string | undefined}
199
- initiateAuth={initiateGoogleAuth}
200
- />
196
+ <GoogleLogin />
201
197
 
202
198
  {/* Quick Link at Top */}
203
199
  <div className="mt-4 text-center">
package/src/index.ts CHANGED
@@ -39,6 +39,14 @@ export {
39
39
  } from "./components/google-oauth-provider"
40
40
  export type { GoogleAuthProviderProps } from "./components/google-oauth-provider"
41
41
 
42
+ export { GoogleAuthShell, default as GoogleAuthRoot } from "./components/google-auth-shell"
43
+ export type { GoogleAuthShellProps } from "./components/google-auth-shell"
44
+
45
+ export {
46
+ GoogleIdentityServicesScript,
47
+ default as GoogleIdentityScript,
48
+ } from "./components/google-identity-script"
49
+
42
50
  export { GoogleOneTapLogin, default as GoogleOneTap } from "./components/google-one-tap"
43
51
  export type { GoogleOneTapLoginProps } from "./components/google-one-tap"
44
52
 
@@ -6,8 +6,6 @@ import { LOGIN_VIEW } from "@core/types/account"
6
6
  import Login from "@modules/account/components/login"
7
7
  import Register from "@modules/account/components/register"
8
8
  import ForgotPassword from "@modules/account/components/forgot-password"
9
- import { GoogleAuthProvider } from "../components/google-oauth-provider"
10
-
11
9
  export { LOGIN_VIEW }
12
10
 
13
11
  const LoginTemplate = () => {
@@ -25,8 +23,7 @@ const LoginTemplate = () => {
25
23
  }
26
24
 
27
25
  return (
28
- <GoogleAuthProvider>
29
- <div className="min-h-screen w-full flex flex-col bg-page-bg">
26
+ <div className="min-h-screen w-full flex flex-col bg-page-bg">
30
27
  <div className="flex-1 flex flex-col items-center justify-start px-2 min-[340px]:px-3 min-[550px]:px-4 sm:px-6 md:px-8 pt-2 min-[340px]:pt-3 min-[550px]:pt-4 sm:pt-4 md:pt-6 pb-6 min-[340px]:pb-8 min-[550px]:pb-8 sm:pb-8 md:pb-8">
31
28
  <h1 className="text-2xl min-[340px]:text-3xl min-[550px]:text-3xl sm:text-4xl md:text-4xl font-bold text-heading mb-4 min-[340px]:mb-6 min-[550px]:mb-8 sm:mb-8 md:mb-8 text-center">
32
29
  My Account
@@ -40,7 +37,6 @@ const LoginTemplate = () => {
40
37
  </div>
41
38
  </div>
42
39
  </div>
43
- </GoogleAuthProvider>
44
40
  )
45
41
  }
46
42
 
@@ -0,0 +1,81 @@
1
+ export const GOOGLE_IDENTITY_SCRIPT_SRC = "https://accounts.google.com/gsi/client"
2
+
3
+ export type GoogleCredentialResponse = {
4
+ credential?: string
5
+ select_by?: string
6
+ }
7
+
8
+ declare global {
9
+ interface Window {
10
+ google?: {
11
+ accounts: {
12
+ id: {
13
+ initialize: (config: Record<string, unknown>) => void
14
+ prompt: (listener?: (notification: unknown) => void) => void
15
+ cancel: () => void
16
+ }
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+ export function getGoogleIdentityScript(): HTMLScriptElement | null {
23
+ if (typeof document === "undefined") {
24
+ return null
25
+ }
26
+
27
+ return document.querySelector(
28
+ `script[src="${GOOGLE_IDENTITY_SCRIPT_SRC}"]`
29
+ ) as HTMLScriptElement | null
30
+ }
31
+
32
+ /** Wait until the official GIS SDK (`google.accounts.id`) is ready. */
33
+ export function waitForGoogleIdentityServices(
34
+ timeoutMs = 10000
35
+ ): Promise<NonNullable<Window["google"]>["accounts"]["id"]> {
36
+ return new Promise((resolve, reject) => {
37
+ const getIdApi = () => window.google?.accounts?.id
38
+
39
+ if (getIdApi()) {
40
+ resolve(getIdApi()!)
41
+ return
42
+ }
43
+
44
+ const existingScript = getGoogleIdentityScript()
45
+ const onScriptLoad = () => {
46
+ const idApi = getIdApi()
47
+ if (idApi) {
48
+ resolve(idApi)
49
+ }
50
+ }
51
+
52
+ if (existingScript) {
53
+ existingScript.addEventListener("load", onScriptLoad)
54
+ } else {
55
+ const script = document.createElement("script")
56
+ script.src = GOOGLE_IDENTITY_SCRIPT_SRC
57
+ script.async = true
58
+ script.defer = true
59
+ script.addEventListener("load", onScriptLoad)
60
+ script.addEventListener("error", () =>
61
+ reject(new Error("Failed to load Google Identity Services"))
62
+ )
63
+ document.head.appendChild(script)
64
+ }
65
+
66
+ const started = Date.now()
67
+ const interval = window.setInterval(() => {
68
+ const idApi = getIdApi()
69
+ if (idApi) {
70
+ window.clearInterval(interval)
71
+ resolve(idApi)
72
+ return
73
+ }
74
+
75
+ if (Date.now() - started >= timeoutMs) {
76
+ window.clearInterval(interval)
77
+ reject(new Error("Google Identity Services timed out"))
78
+ }
79
+ }, 50)
80
+ })
81
+ }