@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 +48 -9
- package/package.json +5 -3
- package/src/components/google-auth-shell/index.tsx +75 -0
- package/src/components/google-identity-script/index.tsx +23 -0
- package/src/components/google-one-tap/index.tsx +71 -29
- package/src/components/login/index.tsx +3 -24
- package/src/components/register/index.tsx +2 -6
- package/src/index.ts +8 -0
- package/src/templates/login-template.tsx +1 -5
- package/src/util/google-identity-services.ts +81 -0
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`
|
|
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
|
|
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
|
-
|
|
43
|
+
### Google One Tap (GIS SDK — corner popup)
|
|
42
44
|
|
|
43
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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": "^
|
|
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": "^
|
|
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 {
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
50
|
+
waitForGoogleIdentityServices()
|
|
51
|
+
.then((idApi) => {
|
|
52
|
+
if (cancelled) {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
44
55
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
+
}
|