2025-11-13 08:38:25 +00:00
|
|
|
|
// Google Identity Services (GIS) 配置
|
|
|
|
|
|
// const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!
|
|
|
|
|
|
const GOOGLE_CLIENT_ID = "606396962663-9pagar3g9vuhovi37vq9jqob6q1gngns.apps.googleusercontent.com"
|
|
|
|
|
|
|
|
|
|
|
|
// Google OAuth scopes
|
|
|
|
|
|
const GOOGLE_SCOPES = [
|
|
|
|
|
|
'https://www.googleapis.com/auth/userinfo.email',
|
|
|
|
|
|
'https://www.googleapis.com/auth/userinfo.profile'
|
|
|
|
|
|
].join(' ')
|
|
|
|
|
|
|
|
|
|
|
|
export interface GoogleUser {
|
|
|
|
|
|
id: string
|
|
|
|
|
|
email: string
|
|
|
|
|
|
verified_email: boolean
|
|
|
|
|
|
name: string
|
|
|
|
|
|
given_name: string
|
|
|
|
|
|
family_name: string
|
|
|
|
|
|
picture?: string
|
|
|
|
|
|
locale?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Google Identity Services 响应类型
|
|
|
|
|
|
export interface GoogleCredentialResponse {
|
|
|
|
|
|
credential: string // JWT ID token
|
|
|
|
|
|
select_by?: string
|
|
|
|
|
|
clientId?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Google OAuth Code Response (使用 Code Model)
|
|
|
|
|
|
export interface GoogleCodeResponse {
|
|
|
|
|
|
code: string // Authorization code
|
|
|
|
|
|
scope: string
|
|
|
|
|
|
authuser?: string
|
|
|
|
|
|
prompt?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 声明 Google Identity Services 全局对象
|
|
|
|
|
|
declare global {
|
|
|
|
|
|
interface Window {
|
|
|
|
|
|
google?: {
|
|
|
|
|
|
accounts: {
|
|
|
|
|
|
id: {
|
|
|
|
|
|
initialize: (config: GoogleIdConfiguration) => void
|
|
|
|
|
|
prompt: (momentListener?: (notification: PromptMomentNotification) => void) => void
|
|
|
|
|
|
renderButton: (parent: HTMLElement, options: GsiButtonConfiguration) => void
|
|
|
|
|
|
disableAutoSelect: () => void
|
|
|
|
|
|
cancel: () => void
|
|
|
|
|
|
}
|
|
|
|
|
|
oauth2: {
|
|
|
|
|
|
initCodeClient: (config: CodeClientConfig) => CodeClient
|
|
|
|
|
|
initTokenClient: (config: TokenClientConfig) => TokenClient
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface GoogleIdConfiguration {
|
|
|
|
|
|
client_id: string
|
|
|
|
|
|
callback?: (response: GoogleCredentialResponse) => void
|
|
|
|
|
|
auto_select?: boolean
|
|
|
|
|
|
cancel_on_tap_outside?: boolean
|
|
|
|
|
|
context?: 'signin' | 'signup' | 'use'
|
|
|
|
|
|
ux_mode?: 'popup' | 'redirect'
|
|
|
|
|
|
login_uri?: string
|
|
|
|
|
|
native_callback?: (response: GoogleCredentialResponse) => void
|
|
|
|
|
|
itp_support?: boolean
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface GsiButtonConfiguration {
|
|
|
|
|
|
type?: 'standard' | 'icon'
|
|
|
|
|
|
theme?: 'outline' | 'filled_blue' | 'filled_black'
|
|
|
|
|
|
size?: 'large' | 'medium' | 'small'
|
|
|
|
|
|
text?: 'signin_with' | 'signup_with' | 'continue_with' | 'signin'
|
|
|
|
|
|
shape?: 'rectangular' | 'pill' | 'circle' | 'square'
|
|
|
|
|
|
logo_alignment?: 'left' | 'center'
|
|
|
|
|
|
width?: string
|
|
|
|
|
|
locale?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface PromptMomentNotification {
|
|
|
|
|
|
isDisplayMoment: () => boolean
|
|
|
|
|
|
isDisplayed: () => boolean
|
|
|
|
|
|
isNotDisplayed: () => boolean
|
|
|
|
|
|
getNotDisplayedReason: () => string
|
|
|
|
|
|
isSkippedMoment: () => boolean
|
|
|
|
|
|
getSkippedReason: () => string
|
|
|
|
|
|
isDismissedMoment: () => boolean
|
|
|
|
|
|
getDismissedReason: () => string
|
|
|
|
|
|
getMomentType: () => string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface CodeClientConfig {
|
|
|
|
|
|
client_id: string
|
|
|
|
|
|
scope: string
|
|
|
|
|
|
callback: (response: GoogleCodeResponse) => void
|
|
|
|
|
|
error_callback?: (error: { type: string; message: string }) => void
|
|
|
|
|
|
ux_mode?: 'popup' | 'redirect'
|
|
|
|
|
|
redirect_uri?: string
|
|
|
|
|
|
state?: string
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface CodeClient {
|
|
|
|
|
|
requestCode: () => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TokenClientConfig {
|
|
|
|
|
|
client_id: string
|
|
|
|
|
|
scope: string
|
|
|
|
|
|
callback: (response: { access_token: string; expires_in: number; scope: string; token_type: string }) => void
|
|
|
|
|
|
error_callback?: (error: { type: string; message: string }) => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TokenClient {
|
|
|
|
|
|
requestAccessToken: () => void
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const googleOAuth = {
|
|
|
|
|
|
clientId: GOOGLE_CLIENT_ID,
|
|
|
|
|
|
scopes: GOOGLE_SCOPES,
|
|
|
|
|
|
|
|
|
|
|
|
// 加载 Google Identity Services SDK
|
|
|
|
|
|
loadScript: (): Promise<void> => {
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
// 检查是否已加载
|
|
|
|
|
|
if (window.google?.accounts) {
|
|
|
|
|
|
resolve()
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建 script 标签
|
|
|
|
|
|
const script = document.createElement('script')
|
|
|
|
|
|
script.src = 'https://accounts.google.com/gsi/client'
|
|
|
|
|
|
script.async = true
|
|
|
|
|
|
script.defer = true
|
|
|
|
|
|
script.onload = () => resolve()
|
|
|
|
|
|
script.onerror = () => reject(new Error('Failed to load Google Identity Services SDK'))
|
|
|
|
|
|
|
|
|
|
|
|
document.head.appendChild(script)
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化 Code Client(推荐方式,获取授权码)
|
|
|
|
|
|
initCodeClient: (callback: (response: GoogleCodeResponse) => void, errorCallback?: (error: any) => void) => {
|
|
|
|
|
|
if (!window.google?.accounts?.oauth2) {
|
|
|
|
|
|
throw new Error('Google Identity Services SDK not loaded')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return window.google.accounts.oauth2.initCodeClient({
|
|
|
|
|
|
client_id: GOOGLE_CLIENT_ID,
|
|
|
|
|
|
scope: GOOGLE_SCOPES,
|
|
|
|
|
|
ux_mode: 'popup', // 使用 popup 模式,不需要 redirect_uri
|
|
|
|
|
|
callback,
|
|
|
|
|
|
error_callback: errorCallback
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化 Token Client(直接获取 access token)
|
|
|
|
|
|
initTokenClient: (callback: (response: { access_token: string; expires_in: number; scope: string; token_type: string }) => void, errorCallback?: (error: any) => void) => {
|
|
|
|
|
|
if (!window.google?.accounts?.oauth2) {
|
|
|
|
|
|
throw new Error('Google Identity Services SDK not loaded')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return window.google.accounts.oauth2.initTokenClient({
|
|
|
|
|
|
client_id: GOOGLE_CLIENT_ID,
|
|
|
|
|
|
scope: GOOGLE_SCOPES,
|
|
|
|
|
|
callback,
|
|
|
|
|
|
error_callback: errorCallback
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化 Google Identity (获取 ID Token - JWT)
|
|
|
|
|
|
initGoogleId: (callback: (response: GoogleCredentialResponse) => void) => {
|
|
|
|
|
|
if (!window.google?.accounts?.id) {
|
|
|
|
|
|
throw new Error('Google Identity Services SDK not loaded')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
window.google.accounts.id.initialize({
|
|
|
|
|
|
client_id: GOOGLE_CLIENT_ID,
|
|
|
|
|
|
callback,
|
|
|
|
|
|
auto_select: false,
|
|
|
|
|
|
cancel_on_tap_outside: true,
|
|
|
|
|
|
ux_mode: 'popup'
|
|
|
|
|
|
})
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 触发 One Tap 流程
|
|
|
|
|
|
promptOneTap: (callback: (response: GoogleCredentialResponse) => void) => {
|
|
|
|
|
|
googleOAuth.initGoogleId(callback)
|
|
|
|
|
|
|
|
|
|
|
|
if (window.google?.accounts?.id) {
|
|
|
|
|
|
window.google.accounts.id.prompt((notification) => {
|
|
|
|
|
|
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
|
|
|
|
|
console.log('One Tap not displayed:', notification.getNotDisplayedReason() || notification.getSkippedReason())
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2025-11-24 03:47:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 FedCM (Federated Credential Management) 方式获取 ID Token
|
|
|
|
|
|
// 这种方式会弹出标准的 Google 登录窗口,无需用户预先登录
|
|
|
|
|
|
renderButton: (parent: HTMLElement, callback: (response: GoogleCredentialResponse) => void, options?: Partial<GsiButtonConfiguration>) => {
|
|
|
|
|
|
if (!window.google?.accounts?.id) {
|
|
|
|
|
|
throw new Error('Google Identity Services SDK not loaded')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 先初始化
|
|
|
|
|
|
window.google.accounts.id.initialize({
|
|
|
|
|
|
client_id: GOOGLE_CLIENT_ID,
|
|
|
|
|
|
callback,
|
|
|
|
|
|
auto_select: false,
|
|
|
|
|
|
cancel_on_tap_outside: true
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 渲染 Google 标准按钮
|
|
|
|
|
|
window.google.accounts.id.renderButton(parent, {
|
|
|
|
|
|
type: 'standard',
|
|
|
|
|
|
theme: 'outline',
|
|
|
|
|
|
size: 'large',
|
|
|
|
|
|
text: 'continue_with',
|
|
|
|
|
|
shape: 'rectangular',
|
|
|
|
|
|
logo_alignment: 'left',
|
|
|
|
|
|
...options
|
|
|
|
|
|
})
|
2025-11-13 08:38:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|