@pradip1995/commerce-auth 1.1.7 → 2.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 +47 -1
- package/package.json +17 -4
- package/src/components/google-callback/index.tsx +163 -0
- package/src/components/google-credential-button/index.tsx +85 -0
- package/src/components/google-login/index.tsx +91 -0
- package/src/components/google-oauth-provider/index.tsx +32 -0
- package/src/components/google-one-tap/index.tsx +64 -0
- package/src/components/login/index.tsx +24 -50
- package/src/components/register/index.tsx +5 -49
- package/src/data/customer.ts +42 -0
- package/src/index.ts +40 -0
- package/src/templates/login-template.tsx +4 -1
- package/src/util/google-auth-client.ts +132 -0
- package/src/util-google-oauth.ts +0 -28
package/README.md
CHANGED
|
@@ -3,7 +3,53 @@
|
|
|
3
3
|
Authentication UI and server actions: login, register, OTP, Google OAuth.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
Login,
|
|
8
|
+
LOGIN_VIEW,
|
|
9
|
+
GoogleLoginButton,
|
|
10
|
+
GoogleCallbackPage,
|
|
11
|
+
} from "@pradip1995/commerce-auth"
|
|
7
12
|
```
|
|
8
13
|
|
|
14
|
+
### Google OAuth
|
|
15
|
+
|
|
16
|
+
Use `GoogleLoginButton` anywhere you need a Google sign-in CTA (login, register, modals):
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { GoogleLoginButton } from "@pradip1995/commerce-auth"
|
|
20
|
+
import { initiateGoogleAuth } from "@core/data/customer"
|
|
21
|
+
|
|
22
|
+
<GoogleLoginButton countryCode={countryCode} initiateAuth={initiateGoogleAuth} />
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Wire the OAuth callback route to the shared page:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { GoogleCallbackPage } from "@pradip1995/commerce-auth"
|
|
29
|
+
import { handleGoogleAuthCallback, handleGoogleCallback } from "@core/data/customer"
|
|
30
|
+
|
|
31
|
+
export default function GoogleCallbackRoute() {
|
|
32
|
+
return (
|
|
33
|
+
<GoogleCallbackPage
|
|
34
|
+
handleAuthCallback={handleGoogleAuthCallback}
|
|
35
|
+
handleCustomerCallback={handleGoogleCallback}
|
|
36
|
+
/>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Pass your app's server actions via `initiateAuth`, `handleAuthCallback`, and `handleCustomerCallback` so client UI stays shared while data layer stays in each storefront.
|
|
42
|
+
|
|
43
|
+
### Google One Tap (optional)
|
|
44
|
+
|
|
45
|
+
Set `NEXT_PUBLIC_GOOGLE_CLIENT_ID` (same OAuth client as the Medusa backend). Wrap the app layout:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { GoogleAuthProvider } from "@pradip1995/commerce-auth"
|
|
49
|
+
|
|
50
|
+
<GoogleAuthProvider>{children}</GoogleAuthProvider>
|
|
51
|
+
```
|
|
52
|
+
|
|
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).
|
|
54
|
+
|
|
9
55
|
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": "2.0.0",
|
|
4
4
|
"description": "Medusa storefront authentication — login, register, OTP, Google OAuth",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -25,17 +25,30 @@
|
|
|
25
25
|
"./types/account": "./src/types/account.ts",
|
|
26
26
|
"./data/customer": "./src/data/customer.ts",
|
|
27
27
|
"./data/customer-registration": "./src/data/customer-registration.ts",
|
|
28
|
-
"./data/guest": "./src/data/guest.ts"
|
|
28
|
+
"./data/guest": "./src/data/guest.ts",
|
|
29
|
+
"./components/google-login": "./src/components/google-login/index.tsx",
|
|
30
|
+
"./components/google-callback": "./src/components/google-callback/index.tsx",
|
|
31
|
+
"./util/google-auth-client": "./src/util/google-auth-client.ts",
|
|
32
|
+
"./components/google-oauth-provider": "./src/components/google-oauth-provider/index.tsx",
|
|
33
|
+
"./components/google-one-tap": "./src/components/google-one-tap/index.tsx",
|
|
34
|
+
"./components/google-credential-button": "./src/components/google-credential-button/index.tsx"
|
|
29
35
|
},
|
|
30
36
|
"peerDependencies": {
|
|
31
|
-
"@pradip1995/commerce-core": "^
|
|
37
|
+
"@pradip1995/commerce-core": "^2.0.0",
|
|
32
38
|
"@medusajs/ui": ">=4",
|
|
39
|
+
"@react-oauth/google": ">=0.12",
|
|
33
40
|
"next": ">=15",
|
|
34
41
|
"react": ">=19",
|
|
35
42
|
"react-dom": ">=19"
|
|
36
43
|
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@react-oauth/google": {
|
|
46
|
+
"optional": true
|
|
47
|
+
}
|
|
48
|
+
},
|
|
37
49
|
"devDependencies": {
|
|
38
|
-
"@pradip1995/commerce-core": "^
|
|
50
|
+
"@pradip1995/commerce-core": "^2.0.0",
|
|
51
|
+
"@react-oauth/google": "^0.12.2",
|
|
39
52
|
"@medusajs/types": "latest",
|
|
40
53
|
"@medusajs/ui": "latest",
|
|
41
54
|
"@types/react": "^19",
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Suspense, useEffect, useRef, useState } from "react"
|
|
4
|
+
import { useRouter, useSearchParams } from "next/navigation"
|
|
5
|
+
import {
|
|
6
|
+
decodeJwtPayload,
|
|
7
|
+
GOOGLE_LOGIN_COUNTRY_CODE_KEY,
|
|
8
|
+
resolveCountryCode,
|
|
9
|
+
resolvePostGoogleAuthRedirect,
|
|
10
|
+
} from "../../util/google-auth-client"
|
|
11
|
+
|
|
12
|
+
export type GoogleAuthCallbackResult = {
|
|
13
|
+
error?: string
|
|
14
|
+
token?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type GoogleCustomerCallbackResult = {
|
|
18
|
+
error?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type GoogleCallbackContentProps = {
|
|
22
|
+
handleAuthCallback: (
|
|
23
|
+
params: Record<string, string>
|
|
24
|
+
) => Promise<GoogleAuthCallbackResult>
|
|
25
|
+
handleCustomerCallback: (
|
|
26
|
+
token: string,
|
|
27
|
+
email?: string,
|
|
28
|
+
first_name?: string,
|
|
29
|
+
last_name?: string
|
|
30
|
+
) => Promise<GoogleCustomerCallbackResult>
|
|
31
|
+
defaultCountryCode?: string
|
|
32
|
+
loginPath?: string
|
|
33
|
+
errorButtonClassName?: string
|
|
34
|
+
loadingMessage?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function GoogleCallbackContent({
|
|
38
|
+
defaultCountryCode = "us",
|
|
39
|
+
loginPath = "/account",
|
|
40
|
+
errorButtonClassName = "px-4 py-2 account-btn-primary rounded-lg",
|
|
41
|
+
loadingMessage = "Processing Google authentication...",
|
|
42
|
+
handleAuthCallback,
|
|
43
|
+
handleCustomerCallback,
|
|
44
|
+
}: GoogleCallbackContentProps) {
|
|
45
|
+
const searchParams = useSearchParams()
|
|
46
|
+
const router = useRouter()
|
|
47
|
+
const [error, setError] = useState<string>()
|
|
48
|
+
const handledRef = useRef(false)
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (handledRef.current) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
handledRef.current = true
|
|
55
|
+
|
|
56
|
+
const runCallback = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const params: Record<string, string> = {}
|
|
59
|
+
searchParams.forEach((value, key) => {
|
|
60
|
+
params[key] = value
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (!params.code || !params.state) {
|
|
64
|
+
throw new Error("Missing authentication parameters.")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const authResult = await handleAuthCallback(params)
|
|
68
|
+
|
|
69
|
+
if (authResult.error) {
|
|
70
|
+
throw new Error(authResult.error)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const token = authResult.token
|
|
74
|
+
if (!token || typeof token !== "string") {
|
|
75
|
+
throw new Error("No token received.")
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const decoded = decodeJwtPayload(token)
|
|
79
|
+
const userMetadata = decoded?.user_metadata as
|
|
80
|
+
| { name?: string; email?: string }
|
|
81
|
+
| undefined
|
|
82
|
+
const name = userMetadata?.name
|
|
83
|
+
const nameParts = name?.split(" ") || []
|
|
84
|
+
|
|
85
|
+
const callbackResult = await handleCustomerCallback(
|
|
86
|
+
token,
|
|
87
|
+
userMetadata?.email || (decoded?.email as string | undefined),
|
|
88
|
+
nameParts[0] || undefined,
|
|
89
|
+
nameParts.slice(1).join(" ") || undefined
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (callbackResult.error) {
|
|
93
|
+
throw new Error(callbackResult.error)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const savedCountryCode = localStorage.getItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY)
|
|
97
|
+
const countryCode = resolveCountryCode(savedCountryCode || undefined, defaultCountryCode)
|
|
98
|
+
|
|
99
|
+
if (savedCountryCode) {
|
|
100
|
+
localStorage.removeItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
window.location.href = resolvePostGoogleAuthRedirect(
|
|
104
|
+
countryCode,
|
|
105
|
+
defaultCountryCode
|
|
106
|
+
)
|
|
107
|
+
} catch (err) {
|
|
108
|
+
setError(err instanceof Error ? err.message : "Authentication failed.")
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
void runCallback()
|
|
113
|
+
}, [
|
|
114
|
+
defaultCountryCode,
|
|
115
|
+
handleAuthCallback,
|
|
116
|
+
handleCustomerCallback,
|
|
117
|
+
searchParams,
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
if (error) {
|
|
121
|
+
return (
|
|
122
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
123
|
+
<div className="text-center">
|
|
124
|
+
<p className="text-red-500 mb-4">{error}</p>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
onClick={() => router.push(loginPath)}
|
|
128
|
+
className={errorButtonClassName}
|
|
129
|
+
>
|
|
130
|
+
Go to Login
|
|
131
|
+
</button>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
139
|
+
<p>{loadingMessage}</p>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export type GoogleCallbackPageProps = GoogleCallbackContentProps & {
|
|
145
|
+
suspenseFallback?: React.ReactNode
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function GoogleCallbackPage({
|
|
149
|
+
suspenseFallback = (
|
|
150
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
151
|
+
<p>Loading...</p>
|
|
152
|
+
</div>
|
|
153
|
+
),
|
|
154
|
+
...contentProps
|
|
155
|
+
}: GoogleCallbackPageProps) {
|
|
156
|
+
return (
|
|
157
|
+
<Suspense fallback={suspenseFallback}>
|
|
158
|
+
<GoogleCallbackContent {...contentProps} />
|
|
159
|
+
</Suspense>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export default GoogleCallbackPage
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { CredentialResponse, GoogleLogin } from "@react-oauth/google"
|
|
4
|
+
import { useParams } from "next/navigation"
|
|
5
|
+
import { useCallback, useState } from "react"
|
|
6
|
+
import {
|
|
7
|
+
completeGoogleCredentialSignIn,
|
|
8
|
+
type GoogleCredentialSignInHandlers,
|
|
9
|
+
} from "../../util/google-auth-client"
|
|
10
|
+
import { getGoogleClientId } from "../google-oauth-provider"
|
|
11
|
+
|
|
12
|
+
export type GoogleCredentialButtonProps = {
|
|
13
|
+
handlers: GoogleCredentialSignInHandlers
|
|
14
|
+
countryCode?: string
|
|
15
|
+
defaultCountryCode?: string
|
|
16
|
+
className?: string
|
|
17
|
+
onError?: (message: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function GoogleCredentialButton({
|
|
21
|
+
handlers,
|
|
22
|
+
countryCode: countryCodeProp,
|
|
23
|
+
defaultCountryCode = "us",
|
|
24
|
+
className,
|
|
25
|
+
onError,
|
|
26
|
+
}: GoogleCredentialButtonProps) {
|
|
27
|
+
const params = useParams()
|
|
28
|
+
const [loading, setLoading] = useState(false)
|
|
29
|
+
const clientId = getGoogleClientId()
|
|
30
|
+
|
|
31
|
+
const handleSuccess = useCallback(
|
|
32
|
+
async (response: CredentialResponse) => {
|
|
33
|
+
const credential = response.credential
|
|
34
|
+
if (!credential) {
|
|
35
|
+
onError?.("No Google credential received")
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
setLoading(true)
|
|
41
|
+
const result = await completeGoogleCredentialSignIn(credential, handlers, {
|
|
42
|
+
countryCode:
|
|
43
|
+
countryCodeProp || (params?.countryCode as string | undefined),
|
|
44
|
+
defaultCountryCode,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
if (result.error) {
|
|
48
|
+
onError?.(result.error)
|
|
49
|
+
setLoading(false)
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
onError?.("Google sign-in failed")
|
|
53
|
+
setLoading(false)
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
[countryCodeProp, defaultCountryCode, handlers, onError, params?.countryCode]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (!clientId) {
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
className={className}
|
|
66
|
+
aria-busy={loading}
|
|
67
|
+
data-testid="google-credential-button"
|
|
68
|
+
>
|
|
69
|
+
<GoogleLogin
|
|
70
|
+
onSuccess={handleSuccess}
|
|
71
|
+
onError={() => {
|
|
72
|
+
onError?.("Google sign-in was cancelled")
|
|
73
|
+
setLoading(false)
|
|
74
|
+
}}
|
|
75
|
+
theme="outline"
|
|
76
|
+
size="large"
|
|
77
|
+
shape="pill"
|
|
78
|
+
width="100%"
|
|
79
|
+
text="continue_with"
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default GoogleCredentialButton
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import Image from "next/image"
|
|
4
|
+
import { useParams } from "next/navigation"
|
|
5
|
+
import { useState } from "react"
|
|
6
|
+
import {
|
|
7
|
+
clearMedusaAuthCookies,
|
|
8
|
+
GOOGLE_LOGIN_COUNTRY_CODE_KEY,
|
|
9
|
+
resolveCountryCode,
|
|
10
|
+
} from "../../util/google-auth-client"
|
|
11
|
+
|
|
12
|
+
export type GoogleAuthInitiateResult = {
|
|
13
|
+
error?: string
|
|
14
|
+
redirectUrl?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type GoogleLoginButtonProps = {
|
|
18
|
+
countryCode?: string
|
|
19
|
+
defaultCountryCode?: string
|
|
20
|
+
label?: string
|
|
21
|
+
loadingLabel?: string
|
|
22
|
+
googleIconSrc?: string
|
|
23
|
+
className?: string
|
|
24
|
+
disabled?: boolean
|
|
25
|
+
initiateAuth: (countryCode?: string) => Promise<GoogleAuthInitiateResult>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_BUTTON_CLASS =
|
|
29
|
+
"w-full flex items-center justify-center gap-2.5 py-3 px-4 border border-gray-200 bg-white hover:bg-gray-50 transition-all shadow-sm hover:shadow-md active:scale-[0.98]"
|
|
30
|
+
|
|
31
|
+
export function GoogleLoginButton({
|
|
32
|
+
countryCode: countryCodeProp,
|
|
33
|
+
defaultCountryCode = "us",
|
|
34
|
+
label = "Continue with Google",
|
|
35
|
+
loadingLabel = "Connecting...",
|
|
36
|
+
googleIconSrc = "/Google.svg",
|
|
37
|
+
className,
|
|
38
|
+
disabled = false,
|
|
39
|
+
initiateAuth,
|
|
40
|
+
}: GoogleLoginButtonProps) {
|
|
41
|
+
const params = useParams()
|
|
42
|
+
const [loading, setLoading] = useState(false)
|
|
43
|
+
|
|
44
|
+
const handleClick = async (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
45
|
+
event.preventDefault()
|
|
46
|
+
event.stopPropagation()
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
setLoading(true)
|
|
50
|
+
|
|
51
|
+
const countryCode = resolveCountryCode(
|
|
52
|
+
countryCodeProp || (params?.countryCode as string | undefined),
|
|
53
|
+
defaultCountryCode
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
localStorage.setItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY, countryCode)
|
|
57
|
+
clearMedusaAuthCookies()
|
|
58
|
+
|
|
59
|
+
const result = await initiateAuth(countryCode)
|
|
60
|
+
|
|
61
|
+
if (result.error) {
|
|
62
|
+
throw new Error(result.error)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (result.redirectUrl) {
|
|
66
|
+
window.location.href = result.redirectUrl
|
|
67
|
+
} else {
|
|
68
|
+
setLoading(false)
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
setLoading(false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={handleClick}
|
|
79
|
+
disabled={disabled || loading}
|
|
80
|
+
className={className ?? DEFAULT_BUTTON_CLASS}
|
|
81
|
+
style={{ borderRadius: "30px" }}
|
|
82
|
+
>
|
|
83
|
+
<Image src={googleIconSrc} alt="Google" width={20} height={20} />
|
|
84
|
+
<span className="text-sm text-gray-700 font-bold">
|
|
85
|
+
{loading ? loadingLabel : label}
|
|
86
|
+
</span>
|
|
87
|
+
</button>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default GoogleLoginButton
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { GoogleOAuthProvider } from "@react-oauth/google"
|
|
4
|
+
import { useEffect, useState } from "react"
|
|
5
|
+
|
|
6
|
+
export type GoogleAuthProviderProps = {
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
clientId?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getGoogleClientId(): string | undefined {
|
|
12
|
+
return process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID?.trim() || undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function GoogleAuthProvider({
|
|
16
|
+
children,
|
|
17
|
+
clientId = getGoogleClientId(),
|
|
18
|
+
}: GoogleAuthProviderProps) {
|
|
19
|
+
const [mounted, setMounted] = useState(false)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
setMounted(true)
|
|
23
|
+
}, [])
|
|
24
|
+
|
|
25
|
+
if (!clientId || !mounted) {
|
|
26
|
+
return <>{children}</>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return <GoogleOAuthProvider clientId={clientId}>{children}</GoogleOAuthProvider>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default GoogleAuthProvider
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useGoogleOneTapLogin } from "@react-oauth/google"
|
|
4
|
+
import { useParams } from "next/navigation"
|
|
5
|
+
import { useCallback } from "react"
|
|
6
|
+
import {
|
|
7
|
+
completeGoogleCredentialSignIn,
|
|
8
|
+
type GoogleCredentialSignInHandlers,
|
|
9
|
+
} from "../../util/google-auth-client"
|
|
10
|
+
import { getGoogleClientId } from "../google-oauth-provider"
|
|
11
|
+
|
|
12
|
+
export type GoogleOneTapLoginProps = {
|
|
13
|
+
handlers: GoogleCredentialSignInHandlers
|
|
14
|
+
countryCode?: string
|
|
15
|
+
defaultCountryCode?: string
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
onError?: (message: string) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function GoogleOneTapLogin({
|
|
21
|
+
handlers,
|
|
22
|
+
countryCode: countryCodeProp,
|
|
23
|
+
defaultCountryCode = "us",
|
|
24
|
+
disabled = false,
|
|
25
|
+
onError,
|
|
26
|
+
}: GoogleOneTapLoginProps) {
|
|
27
|
+
const params = useParams()
|
|
28
|
+
const clientId = getGoogleClientId()
|
|
29
|
+
|
|
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
|
+
})
|
|
37
|
+
|
|
38
|
+
if (result.error) {
|
|
39
|
+
onError?.(result.error)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[countryCodeProp, defaultCountryCode, handlers, onError, params?.countryCode]
|
|
43
|
+
)
|
|
44
|
+
|
|
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
|
+
}
|
|
53
|
+
|
|
54
|
+
await handleCredential(credential)
|
|
55
|
+
},
|
|
56
|
+
onError: () => {
|
|
57
|
+
onError?.("Google One Tap was dismissed or failed")
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default GoogleOneTapLogin
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
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"
|
|
4
12
|
import { LOGIN_VIEW } from "@core/types/account"
|
|
5
13
|
import ErrorMessage from "@modules/checkout/components/error-message"
|
|
6
14
|
import Envelope from "@modules/common/icons/envelope"
|
|
7
15
|
import Lock from "@modules/common/icons/lock"
|
|
8
16
|
import Eye from "@modules/common/icons/eye"
|
|
9
17
|
import EyeOff from "@modules/common/icons/eye-off"
|
|
10
|
-
import Image from "next/image"
|
|
11
18
|
import { useEffect, useRef, useState } from "react"
|
|
12
19
|
import { useRouter, useParams, useSearchParams } from "next/navigation"
|
|
13
20
|
import LocalizedClientLink from "@modules/common/components/localized-client-link"
|
|
@@ -39,7 +46,6 @@ const Login = ({ setCurrentView }: Props) => {
|
|
|
39
46
|
} = useServerAction((formData: FormData) => login(null, formData))
|
|
40
47
|
const [showPassword, setShowPassword] = useState(false)
|
|
41
48
|
const [loginMethod, setLoginMethod] = useState<"email" | "phone">("email")
|
|
42
|
-
const [googleLoading, setGoogleLoading] = useState(false)
|
|
43
49
|
const [showGuestModal, setShowGuestModal] = useState(false)
|
|
44
50
|
const [checkingGuest, setCheckingGuest] = useState(false)
|
|
45
51
|
const [showDeletionModal, setShowDeletionModal] = useState(false)
|
|
@@ -106,8 +112,19 @@ const Login = ({ setCurrentView }: Props) => {
|
|
|
106
112
|
setShowGuestModal(true)
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
const googleCredentialHandlers = {
|
|
116
|
+
authenticateCredential: authenticateGoogleCredential,
|
|
117
|
+
handleCustomerCallback: handleGoogleCallback,
|
|
118
|
+
}
|
|
119
|
+
|
|
109
120
|
return (
|
|
110
121
|
<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}
|
|
111
128
|
<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">
|
|
112
129
|
Welcome Back
|
|
113
130
|
</h1>
|
|
@@ -117,53 +134,10 @@ const Login = ({ setCurrentView }: Props) => {
|
|
|
117
134
|
|
|
118
135
|
{/* Google Login at Top */}
|
|
119
136
|
<div className="mb-6">
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
e.stopPropagation()
|
|
125
|
-
try {
|
|
126
|
-
setGoogleLoading(true)
|
|
127
|
-
|
|
128
|
-
// Save current country code before redirecting to Google OAuth
|
|
129
|
-
const countryCode =
|
|
130
|
-
(params?.countryCode as string) ||
|
|
131
|
-
window.location.pathname.split("/")[1] ||
|
|
132
|
-
"us"
|
|
133
|
-
localStorage.setItem("googleLoginCountryCode", countryCode)
|
|
134
|
-
|
|
135
|
-
document.cookie =
|
|
136
|
-
"_medusa_jwt=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
137
|
-
document.cookie =
|
|
138
|
-
"_medusa_cache_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
139
|
-
|
|
140
|
-
// Use server-side action instead of direct backend call
|
|
141
|
-
const result = await initiateGoogleAuth(countryCode)
|
|
142
|
-
|
|
143
|
-
if (result.error) {
|
|
144
|
-
throw new Error(result.error)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const redirectUrl = result.redirectUrl
|
|
148
|
-
|
|
149
|
-
if (redirectUrl) {
|
|
150
|
-
window.location.href = redirectUrl
|
|
151
|
-
} else {
|
|
152
|
-
setGoogleLoading(false)
|
|
153
|
-
}
|
|
154
|
-
} catch (error) {
|
|
155
|
-
setGoogleLoading(false)
|
|
156
|
-
}
|
|
157
|
-
}}
|
|
158
|
-
disabled={googleLoading}
|
|
159
|
-
className="w-full flex items-center justify-center gap-2.5 py-3 px-4 border border-gray-200 bg-white hover:bg-gray-50 transition-all shadow-sm hover:shadow-md active:scale-[0.98]"
|
|
160
|
-
style={{ borderRadius: "30px" }}
|
|
161
|
-
>
|
|
162
|
-
<Image src="/Google.svg" alt="Google" width={20} height={20} />
|
|
163
|
-
<span className="text-sm text-gray-700 font-bold">
|
|
164
|
-
{googleLoading ? "Connecting..." : "Continue with Google"}
|
|
165
|
-
</span>
|
|
166
|
-
</button>
|
|
137
|
+
<GoogleLoginButton
|
|
138
|
+
countryCode={params?.countryCode as string | undefined}
|
|
139
|
+
initiateAuth={initiateGoogleAuth}
|
|
140
|
+
/>
|
|
167
141
|
|
|
168
142
|
{/* Quick Link at Top */}
|
|
169
143
|
<div className="mt-4 text-center">
|
|
@@ -10,8 +10,8 @@ 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 Image from "next/image"
|
|
14
13
|
import { initiateGoogleAuth } from "@core/data/customer"
|
|
14
|
+
import { GoogleLoginButton } from "../google-login"
|
|
15
15
|
import {
|
|
16
16
|
registerCustomer,
|
|
17
17
|
sendCustomerOTP,
|
|
@@ -43,7 +43,6 @@ const Register = ({ setCurrentView }: Props) => {
|
|
|
43
43
|
const params = useParams()
|
|
44
44
|
const router = useRouter()
|
|
45
45
|
const [showPassword, setShowPassword] = useState(false)
|
|
46
|
-
const [googleLoading, setGoogleLoading] = useState(false)
|
|
47
46
|
const [error, setError] = useState<string | null>(null)
|
|
48
47
|
|
|
49
48
|
// Multi-step registration state
|
|
@@ -195,53 +194,10 @@ const Register = ({ setCurrentView }: Props) => {
|
|
|
195
194
|
|
|
196
195
|
{/* Google Signup at Top */}
|
|
197
196
|
<div className="mb-6">
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
e.stopPropagation()
|
|
203
|
-
try {
|
|
204
|
-
setGoogleLoading(true)
|
|
205
|
-
|
|
206
|
-
// Save current country code before redirecting to Google OAuth
|
|
207
|
-
const countryCode =
|
|
208
|
-
(params?.countryCode as string) ||
|
|
209
|
-
window.location.pathname.split("/")[1] ||
|
|
210
|
-
"us"
|
|
211
|
-
localStorage.setItem("googleLoginCountryCode", countryCode)
|
|
212
|
-
|
|
213
|
-
document.cookie =
|
|
214
|
-
"_medusa_jwt=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
215
|
-
document.cookie =
|
|
216
|
-
"_medusa_cache_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
217
|
-
|
|
218
|
-
// Use server-side action instead of direct backend call
|
|
219
|
-
const result = await initiateGoogleAuth(countryCode)
|
|
220
|
-
|
|
221
|
-
if (result.error) {
|
|
222
|
-
throw new Error(result.error)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const redirectUrl = result.redirectUrl
|
|
226
|
-
|
|
227
|
-
if (redirectUrl) {
|
|
228
|
-
window.location.href = redirectUrl
|
|
229
|
-
} else {
|
|
230
|
-
setGoogleLoading(false)
|
|
231
|
-
}
|
|
232
|
-
} catch (error) {
|
|
233
|
-
setGoogleLoading(false)
|
|
234
|
-
}
|
|
235
|
-
}}
|
|
236
|
-
disabled={googleLoading}
|
|
237
|
-
className="w-full flex items-center justify-center gap-2.5 py-3 px-4 border border-gray-200 bg-white hover:bg-gray-50 transition-all shadow-sm hover:shadow-md active:scale-[0.98]"
|
|
238
|
-
style={{ borderRadius: "30px" }}
|
|
239
|
-
>
|
|
240
|
-
<Image src="/Google.svg" alt="Google" width={20} height={20} />
|
|
241
|
-
<span className="text-sm text-gray-700 font-bold">
|
|
242
|
-
{googleLoading ? "Connecting..." : "Continue with Google"}
|
|
243
|
-
</span>
|
|
244
|
-
</button>
|
|
197
|
+
<GoogleLoginButton
|
|
198
|
+
countryCode={params?.countryCode as string | undefined}
|
|
199
|
+
initiateAuth={initiateGoogleAuth}
|
|
200
|
+
/>
|
|
245
201
|
|
|
246
202
|
{/* Quick Link at Top */}
|
|
247
203
|
<div className="mt-4 text-center">
|
package/src/data/customer.ts
CHANGED
|
@@ -462,6 +462,48 @@ export async function initiateGoogleAuth(countryCode?: string) {
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
+
export async function authenticateGoogleCredential(credential: string) {
|
|
466
|
+
try {
|
|
467
|
+
if (!credential?.trim()) {
|
|
468
|
+
return { error: "Missing Google credential" }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const backendUrl =
|
|
472
|
+
process.env.MEDUSA_BACKEND_URL || process.env.NEXT_PUBLIC_MEDUSA_BACKEND_URL
|
|
473
|
+
|
|
474
|
+
if (!backendUrl) {
|
|
475
|
+
return { error: "Backend URL not configured" }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const response = await fetch(`${backendUrl}/auth/customer/google/callback`, {
|
|
479
|
+
method: "POST",
|
|
480
|
+
headers: {
|
|
481
|
+
"Content-Type": "application/json",
|
|
482
|
+
},
|
|
483
|
+
body: JSON.stringify({ credential }),
|
|
484
|
+
cache: "no-store",
|
|
485
|
+
})
|
|
486
|
+
|
|
487
|
+
const data = await response.json().catch(() => ({}))
|
|
488
|
+
|
|
489
|
+
if (!response.ok) {
|
|
490
|
+
const message =
|
|
491
|
+
typeof data?.message === "string" ? data.message : "Authentication failed"
|
|
492
|
+
return { error: message }
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const token = data.token || data
|
|
496
|
+
|
|
497
|
+
if (!token || typeof token !== "string") {
|
|
498
|
+
return { error: "No token received" }
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return { token }
|
|
502
|
+
} catch (error: any) {
|
|
503
|
+
return { error: error.message || "Internal server error" }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
465
507
|
export async function handleGoogleAuthCallback(params: Record<string, string>) {
|
|
466
508
|
try {
|
|
467
509
|
if (!params.code || !params.state) {
|
package/src/index.ts
CHANGED
|
@@ -10,3 +10,43 @@ export { default as Login } from "./components/login"
|
|
|
10
10
|
export { default as Register } from "./components/register"
|
|
11
11
|
export { default as ForgotPassword } from "./components/forgot-password"
|
|
12
12
|
export { default as LoginTemplate } from "./templates/login-template"
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
GoogleLoginButton,
|
|
16
|
+
default as GoogleLogin,
|
|
17
|
+
} from "./components/google-login"
|
|
18
|
+
export type {
|
|
19
|
+
GoogleAuthInitiateResult,
|
|
20
|
+
GoogleLoginButtonProps,
|
|
21
|
+
} from "./components/google-login"
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
GoogleCallbackContent,
|
|
25
|
+
GoogleCallbackPage,
|
|
26
|
+
default as GoogleCallback,
|
|
27
|
+
} from "./components/google-callback"
|
|
28
|
+
export type {
|
|
29
|
+
GoogleAuthCallbackResult,
|
|
30
|
+
GoogleCallbackContentProps,
|
|
31
|
+
GoogleCallbackPageProps,
|
|
32
|
+
GoogleCustomerCallbackResult,
|
|
33
|
+
} from "./components/google-callback"
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
GoogleAuthProvider,
|
|
37
|
+
getGoogleClientId,
|
|
38
|
+
default as GoogleOAuthProvider,
|
|
39
|
+
} from "./components/google-oauth-provider"
|
|
40
|
+
export type { GoogleAuthProviderProps } from "./components/google-oauth-provider"
|
|
41
|
+
|
|
42
|
+
export { GoogleOneTapLogin, default as GoogleOneTap } from "./components/google-one-tap"
|
|
43
|
+
export type { GoogleOneTapLoginProps } from "./components/google-one-tap"
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
GoogleCredentialButton,
|
|
47
|
+
default as GoogleCredentialLogin,
|
|
48
|
+
} from "./components/google-credential-button"
|
|
49
|
+
export type { GoogleCredentialButtonProps } from "./components/google-credential-button"
|
|
50
|
+
|
|
51
|
+
export type { GoogleCredentialSignInHandlers } from "./util/google-auth-client"
|
|
52
|
+
export { completeGoogleCredentialSignIn } from "./util/google-auth-client"
|
|
@@ -6,6 +6,7 @@ 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"
|
|
9
10
|
|
|
10
11
|
export { LOGIN_VIEW }
|
|
11
12
|
|
|
@@ -24,7 +25,8 @@ const LoginTemplate = () => {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
return (
|
|
27
|
-
<
|
|
28
|
+
<GoogleAuthProvider>
|
|
29
|
+
<div className="min-h-screen w-full flex flex-col bg-page-bg">
|
|
28
30
|
<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">
|
|
29
31
|
<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">
|
|
30
32
|
My Account
|
|
@@ -38,6 +40,7 @@ const LoginTemplate = () => {
|
|
|
38
40
|
</div>
|
|
39
41
|
</div>
|
|
40
42
|
</div>
|
|
43
|
+
</GoogleAuthProvider>
|
|
41
44
|
)
|
|
42
45
|
}
|
|
43
46
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export const GOOGLE_LOGIN_COUNTRY_CODE_KEY = "googleLoginCountryCode"
|
|
2
|
+
export const LOGIN_REDIRECT_URL_KEY = "loginRedirectUrl"
|
|
3
|
+
|
|
4
|
+
export function resolveCountryCode(
|
|
5
|
+
countryCode?: string,
|
|
6
|
+
defaultCountryCode = "us"
|
|
7
|
+
): string {
|
|
8
|
+
if (countryCode?.trim()) {
|
|
9
|
+
return countryCode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof window !== "undefined") {
|
|
13
|
+
const fromPath = window.location.pathname.split("/")[1]
|
|
14
|
+
if (fromPath) {
|
|
15
|
+
return fromPath
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return defaultCountryCode
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function clearMedusaAuthCookies(): void {
|
|
23
|
+
if (typeof document === "undefined") {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
document.cookie = "_medusa_jwt=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
28
|
+
document.cookie = "_medusa_cache_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function decodeJwtPayload(token: string): Record<string, unknown> | null {
|
|
32
|
+
try {
|
|
33
|
+
const payload = token.split(".")[1]
|
|
34
|
+
const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"))
|
|
35
|
+
return JSON.parse(decoded) as Record<string, unknown>
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type GoogleCredentialSignInHandlers = {
|
|
42
|
+
authenticateCredential: (credential: string) => Promise<{
|
|
43
|
+
error?: string
|
|
44
|
+
token?: string
|
|
45
|
+
}>
|
|
46
|
+
handleCustomerCallback: (
|
|
47
|
+
token: string,
|
|
48
|
+
email?: string,
|
|
49
|
+
first_name?: string,
|
|
50
|
+
last_name?: string
|
|
51
|
+
) => Promise<{ error?: string }>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function completeGoogleCredentialSignIn(
|
|
55
|
+
credential: string,
|
|
56
|
+
handlers: GoogleCredentialSignInHandlers,
|
|
57
|
+
options?: {
|
|
58
|
+
countryCode?: string
|
|
59
|
+
defaultCountryCode?: string
|
|
60
|
+
}
|
|
61
|
+
): Promise<{ error?: string }> {
|
|
62
|
+
const defaultCountryCode = options?.defaultCountryCode ?? "us"
|
|
63
|
+
const countryCode = resolveCountryCode(options?.countryCode, defaultCountryCode)
|
|
64
|
+
|
|
65
|
+
localStorage.setItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY, countryCode)
|
|
66
|
+
clearMedusaAuthCookies()
|
|
67
|
+
|
|
68
|
+
const authResult = await handlers.authenticateCredential(credential)
|
|
69
|
+
|
|
70
|
+
if (authResult.error) {
|
|
71
|
+
return { error: authResult.error }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const token = authResult.token
|
|
75
|
+
if (!token) {
|
|
76
|
+
return { error: "No token received" }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const decoded = decodeJwtPayload(token)
|
|
80
|
+
const userMetadata = decoded?.user_metadata as
|
|
81
|
+
| { name?: string; email?: string }
|
|
82
|
+
| undefined
|
|
83
|
+
const name = userMetadata?.name
|
|
84
|
+
const nameParts = name?.split(" ") || []
|
|
85
|
+
|
|
86
|
+
const callbackResult = await handlers.handleCustomerCallback(
|
|
87
|
+
token,
|
|
88
|
+
userMetadata?.email || (decoded?.email as string | undefined),
|
|
89
|
+
nameParts[0] || undefined,
|
|
90
|
+
nameParts.slice(1).join(" ") || undefined
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if (callbackResult.error) {
|
|
94
|
+
return { error: callbackResult.error }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const savedCountryCode = localStorage.getItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY)
|
|
98
|
+
const resolvedCountryCode = resolveCountryCode(
|
|
99
|
+
savedCountryCode || countryCode,
|
|
100
|
+
defaultCountryCode
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if (savedCountryCode) {
|
|
104
|
+
localStorage.removeItem(GOOGLE_LOGIN_COUNTRY_CODE_KEY)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
window.location.href = resolvePostGoogleAuthRedirect(
|
|
108
|
+
resolvedCountryCode,
|
|
109
|
+
defaultCountryCode
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return {}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function resolvePostGoogleAuthRedirect(
|
|
116
|
+
countryCode: string,
|
|
117
|
+
defaultCountryCode = "us"
|
|
118
|
+
): string {
|
|
119
|
+
const redirectUrl = localStorage.getItem(LOGIN_REDIRECT_URL_KEY)
|
|
120
|
+
const hasKeepContext = redirectUrl?.includes("login_context=keep")
|
|
121
|
+
|
|
122
|
+
if (redirectUrl && hasKeepContext) {
|
|
123
|
+
localStorage.removeItem(LOGIN_REDIRECT_URL_KEY)
|
|
124
|
+
return redirectUrl
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (redirectUrl) {
|
|
128
|
+
localStorage.removeItem(LOGIN_REDIRECT_URL_KEY)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return `/${countryCode || defaultCountryCode}`
|
|
132
|
+
}
|
package/src/util-google-oauth.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OAuth redirect_uri must exactly match a URI registered in Google Cloud Console.
|
|
3
|
-
* That is the storefront callback path (often includes country code), NOT the backend API path.
|
|
4
|
-
*
|
|
5
|
-
* Set NEXT_PUBLIC_GOOGLE_OAUTH_CALLBACK_URL to override (must match Console exactly).
|
|
6
|
-
*/
|
|
7
|
-
export function getGoogleOAuthCallbackUrl(countryCode?: string): string {
|
|
8
|
-
const explicit = process.env.NEXT_PUBLIC_GOOGLE_OAUTH_CALLBACK_URL?.trim()
|
|
9
|
-
if (explicit) {
|
|
10
|
-
return explicit
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const base =
|
|
14
|
-
process.env.NEXT_PUBLIC_BASE_URL ||
|
|
15
|
-
process.env.STOREFRONT_URL ||
|
|
16
|
-
"http://localhost:8000"
|
|
17
|
-
const normalized = base.replace(/\/$/, "")
|
|
18
|
-
const region =
|
|
19
|
-
countryCode?.trim().toLowerCase() ||
|
|
20
|
-
process.env.NEXT_PUBLIC_DEFAULT_REGION?.trim().toLowerCase() ||
|
|
21
|
-
""
|
|
22
|
-
|
|
23
|
-
if (region && /^[a-z]{2,3}$/i.test(region)) {
|
|
24
|
-
return `${normalized}/${region}/auth/customer/google/callback`
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return `${normalized}/auth/customer/google/callback`
|
|
28
|
-
}
|