mirror of https://github.com/kortix-ai/suna.git
Merge pull request #1044 from escapade-mckv/fix-google-oauth
attempt to fix google oauth
This commit is contained in:
commit
9ec1a4e5ae
|
@ -0,0 +1,156 @@
|
||||||
|
# Google OAuth Setup Guide
|
||||||
|
|
||||||
|
This guide will help you configure Google Sign-In for your Suna application to avoid common errors like "Access blocked: This app's request is invalid".
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- A Google Cloud Console account
|
||||||
|
- Your Supabase project URL and anon key
|
||||||
|
|
||||||
|
## Step 1: Create a Google Cloud Project
|
||||||
|
|
||||||
|
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||||
|
2. Click "Select a project" → "New Project"
|
||||||
|
3. Enter a project name (e.g., "Suna App")
|
||||||
|
4. Click "Create"
|
||||||
|
|
||||||
|
## Step 2: Configure OAuth Consent Screen
|
||||||
|
|
||||||
|
1. In the Google Cloud Console, go to "APIs & Services" → "OAuth consent screen"
|
||||||
|
2. Select "External" user type (unless you have a Google Workspace account)
|
||||||
|
3. Click "Create"
|
||||||
|
4. Fill in the required fields:
|
||||||
|
- **App name**: Your application name (e.g., "Suna")
|
||||||
|
- **User support email**: Your email address
|
||||||
|
- **App logo**: Optional, but recommended
|
||||||
|
- **App domain**: Your domain (for local dev, skip this)
|
||||||
|
- **Authorized domains**: Add your domain(s)
|
||||||
|
- **Developer contact information**: Your email address
|
||||||
|
5. Click "Save and Continue"
|
||||||
|
6. **Scopes**: Click "Add or Remove Scopes"
|
||||||
|
- Select `.../auth/userinfo.email`
|
||||||
|
- Select `.../auth/userinfo.profile`
|
||||||
|
- Select `openid`
|
||||||
|
- Click "Update"
|
||||||
|
7. Click "Save and Continue"
|
||||||
|
8. **Test users**: Add test email addresses if in testing mode
|
||||||
|
9. Click "Save and Continue"
|
||||||
|
10. Review and click "Back to Dashboard"
|
||||||
|
|
||||||
|
## Step 3: Create OAuth 2.0 Credentials
|
||||||
|
|
||||||
|
1. Go to "APIs & Services" → "Credentials"
|
||||||
|
2. Click "Create Credentials" → "OAuth client ID"
|
||||||
|
3. Select "Web application" as the application type
|
||||||
|
4. Configure the client:
|
||||||
|
- **Name**: "Suna Web Client" (or any name you prefer)
|
||||||
|
- **Authorized JavaScript origins**:
|
||||||
|
- Add `http://localhost:3000` (for local development)
|
||||||
|
- Add `https://yourdomain.com` (for production)
|
||||||
|
- Add your Supabase URL (e.g., `https://yourproject.supabase.co`)
|
||||||
|
- **Authorized redirect URIs**:
|
||||||
|
- Add `http://localhost:3000/auth/callback` (for local development)
|
||||||
|
- Add `https://yourdomain.com/auth/callback` (for production)
|
||||||
|
- Add your Supabase auth callback URL: `https://yourproject.supabase.co/auth/v1/callback`
|
||||||
|
5. Click "Create"
|
||||||
|
6. **Important**: Copy the "Client ID" - you'll need this
|
||||||
|
|
||||||
|
## Step 4: Configure Supabase
|
||||||
|
|
||||||
|
1. Go to your [Supabase Dashboard](https://app.supabase.com)
|
||||||
|
2. Select your project
|
||||||
|
3. Go to "Authentication" → "Providers"
|
||||||
|
4. Find "Google" and enable it
|
||||||
|
5. Add your Google OAuth credentials:
|
||||||
|
- **Client ID**: Paste the Client ID from Step 3
|
||||||
|
- **Client Secret**: Leave empty (not needed for web applications)
|
||||||
|
6. **Authorized Client IDs**: Add your Client ID here as well
|
||||||
|
7. Click "Save"
|
||||||
|
|
||||||
|
## Step 5: Configure Your Application
|
||||||
|
|
||||||
|
1. Add the Google Client ID to your environment variables:
|
||||||
|
|
||||||
|
**Frontend** (`frontend/.env.local`):
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-client-id-here
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Restart your development server
|
||||||
|
|
||||||
|
## Step 6: Test Your Setup
|
||||||
|
|
||||||
|
1. Open your application in a browser
|
||||||
|
2. Click the "Continue with Google" button
|
||||||
|
3. You should see the Google sign-in popup
|
||||||
|
4. Select an account and authorize the application
|
||||||
|
5. You should be redirected back to your application and logged in
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### "Access blocked: This app's request is invalid"
|
||||||
|
|
||||||
|
This error usually means:
|
||||||
|
- **Missing redirect URI**: Make sure all your redirect URIs are added in Google Cloud Console
|
||||||
|
- **Wrong Client ID**: Verify you're using the correct Client ID
|
||||||
|
- **OAuth consent screen not configured**: Complete all required fields in the consent screen
|
||||||
|
|
||||||
|
### "redirect_uri_mismatch"
|
||||||
|
|
||||||
|
- Check that your redirect URIs in Google Cloud Console exactly match your application URLs
|
||||||
|
- Include the protocol (`http://` or `https://`)
|
||||||
|
- Don't include trailing slashes
|
||||||
|
- For local development, use `http://localhost:3000`, not `http://127.0.0.1:3000`
|
||||||
|
|
||||||
|
### "invalid_client"
|
||||||
|
|
||||||
|
- Verify your Client ID is correct in the environment variables
|
||||||
|
- Make sure you're using the Web application client ID, not a different type
|
||||||
|
- Check that the OAuth client hasn't been deleted in Google Cloud Console
|
||||||
|
|
||||||
|
### Google button doesn't appear
|
||||||
|
|
||||||
|
- Check browser console for errors
|
||||||
|
- Verify `NEXT_PUBLIC_GOOGLE_CLIENT_ID` is set in your environment
|
||||||
|
- Make sure the Google Identity Services script is loading
|
||||||
|
|
||||||
|
## Production Deployment
|
||||||
|
|
||||||
|
When deploying to production:
|
||||||
|
|
||||||
|
1. Update Google Cloud Console:
|
||||||
|
- Add your production domain to "Authorized JavaScript origins"
|
||||||
|
- Add your production callback URL to "Authorized redirect URIs"
|
||||||
|
- Update the OAuth consent screen with production information
|
||||||
|
|
||||||
|
2. Update your production environment variables:
|
||||||
|
- Set `NEXT_PUBLIC_GOOGLE_CLIENT_ID` in your deployment platform
|
||||||
|
|
||||||
|
3. Verify Supabase settings:
|
||||||
|
- Ensure Google provider is enabled
|
||||||
|
- Confirm the Client ID is set correctly
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
1. **Never commit your Client ID to version control** - always use environment variables
|
||||||
|
2. **Use HTTPS in production** - Google requires secure connections for OAuth
|
||||||
|
3. **Restrict your OAuth client** - Only add the domains you actually use
|
||||||
|
4. **Review permissions regularly** - Remove unused test users and unnecessary scopes
|
||||||
|
5. **Monitor usage** - Check Google Cloud Console for unusual activity
|
||||||
|
|
||||||
|
## Publishing Your App
|
||||||
|
|
||||||
|
If you want to remove the "unverified app" warning:
|
||||||
|
|
||||||
|
1. Go to "OAuth consent screen" in Google Cloud Console
|
||||||
|
2. Click "Publish App"
|
||||||
|
3. Google may require verification for certain scopes
|
||||||
|
4. Follow the verification process if required
|
||||||
|
|
||||||
|
## Need Help?
|
||||||
|
|
||||||
|
If you're still experiencing issues:
|
||||||
|
1. Check the browser console for detailed error messages
|
||||||
|
2. Verify all URLs and IDs are correctly copied
|
||||||
|
3. Ensure your Supabase project is properly configured
|
||||||
|
4. Try using an incognito/private browser window to rule out cache issues
|
|
@ -1,231 +1,220 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useCallback, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Script from 'next/script';
|
import Script from 'next/script';
|
||||||
import { createClient } from '@/lib/supabase/client';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
import { useAuthMethodTracking } from '@/lib/stores/auth-tracking';
|
import { useAuthMethodTracking } from '@/lib/stores/auth-tracking';
|
||||||
|
import { toast } from 'sonner';
|
||||||
import { FcGoogle } from "react-icons/fc";
|
import { FcGoogle } from "react-icons/fc";
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
handleGoogleSignIn?: (response: GoogleSignInResponse) => void;
|
google?: {
|
||||||
google: {
|
|
||||||
accounts: {
|
accounts: {
|
||||||
id: {
|
id: {
|
||||||
initialize: (config: GoogleInitializeConfig) => void;
|
initialize: (config: any) => void;
|
||||||
prompt: (
|
renderButton: (element: HTMLElement, config: any) => void;
|
||||||
callback?: (notification: GoogleNotification) => void,
|
prompt: (notification?: (notification: any) => void) => void;
|
||||||
) => void;
|
|
||||||
cancel: () => void;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GoogleSignInResponse {
|
|
||||||
credential: string;
|
|
||||||
clientId?: string;
|
|
||||||
select_by?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GoogleInitializeConfig {
|
|
||||||
client_id: string | undefined;
|
|
||||||
callback: ((response: GoogleSignInResponse) => void) | undefined;
|
|
||||||
nonce?: string;
|
|
||||||
use_fedcm?: boolean;
|
|
||||||
context?: string;
|
|
||||||
itp_support?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GoogleNotification {
|
|
||||||
isNotDisplayed: () => boolean;
|
|
||||||
getNotDisplayedReason: () => string;
|
|
||||||
isSkippedMoment: () => boolean;
|
|
||||||
getSkippedReason: () => string;
|
|
||||||
isDismissedMoment: () => boolean;
|
|
||||||
getDismissedReason: () => string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GoogleSignInProps {
|
interface GoogleSignInProps {
|
||||||
returnUrl?: string;
|
returnUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GoogleSignIn({ returnUrl }: GoogleSignInProps) {
|
export default function GoogleSignIn({ returnUrl }: GoogleSignInProps) {
|
||||||
const googleClientId = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID;
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false);
|
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false);
|
||||||
|
|
||||||
const { wasLastMethod, markAsUsed } = useAuthMethodTracking('google');
|
const { wasLastMethod, markAsUsed } = useAuthMethodTracking('google');
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
|
|
||||||
const handleGoogleSignIn = useCallback(
|
const handleGoogleResponse = async (response: any) => {
|
||||||
async (response: GoogleSignInResponse) => {
|
try {
|
||||||
try {
|
setIsLoading(true);
|
||||||
setIsLoading(true);
|
markAsUsed();
|
||||||
|
|
||||||
console.log('Starting Google sign in process');
|
const { data, error } = await supabase.auth.signInWithIdToken({
|
||||||
markAsUsed();
|
provider: 'google',
|
||||||
|
token: response.credential,
|
||||||
|
});
|
||||||
|
|
||||||
const { error } = await supabase.auth.signInWithIdToken({
|
if (error) {
|
||||||
|
const redirectTo = `${window.location.origin}/auth/callback${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ''}`;
|
||||||
|
console.log('OAuth redirect URI:', redirectTo);
|
||||||
|
|
||||||
|
const { error: oauthError } = await supabase.auth.signInWithOAuth({
|
||||||
provider: 'google',
|
provider: 'google',
|
||||||
token: response.credential,
|
options: {
|
||||||
|
redirectTo,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) throw error;
|
if (oauthError) {
|
||||||
|
throw oauthError;
|
||||||
console.log(
|
}
|
||||||
'Google sign in successful, preparing redirect to:',
|
} else {
|
||||||
returnUrl || '/dashboard',
|
window.location.href = returnUrl || '/dashboard';
|
||||||
);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('Executing redirect now to:', returnUrl || '/dashboard');
|
|
||||||
window.location.href = returnUrl || '/dashboard';
|
|
||||||
}, 500);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error signing in with Google:', error);
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error('Google sign-in failed. Please try again.');
|
|
||||||
}
|
}
|
||||||
},
|
} catch (error: any) {
|
||||||
[returnUrl, markAsUsed],
|
console.error('Google sign-in error:', error);
|
||||||
);
|
|
||||||
|
if (error.message?.includes('redirect_uri_mismatch')) {
|
||||||
|
const redirectUri = `${window.location.origin}/auth/callback`;
|
||||||
|
toast.error(
|
||||||
|
`Google OAuth configuration error. Add this exact URL to your Google Cloud Console: ${redirectUri}`,
|
||||||
|
{ duration: 10000 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(error.message || 'Failed to sign in with Google');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleCustomGoogleSignIn = useCallback(() => {
|
useEffect(() => {
|
||||||
if (isLoading) return;
|
const initializeGoogleSignIn = () => {
|
||||||
|
if (!window.google || !process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) return;
|
||||||
|
|
||||||
if (!window.google || !googleClientId || !isGoogleLoaded) {
|
window.google.accounts.id.initialize({
|
||||||
console.error('Google sign-in not properly initialized');
|
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
|
||||||
toast.error('Google sign-in not ready. Please try again.');
|
callback: handleGoogleResponse,
|
||||||
|
auto_select: false,
|
||||||
|
cancel_on_tap_outside: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsGoogleLoaded(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (window.google) {
|
||||||
|
initializeGoogleSignIn();
|
||||||
|
}
|
||||||
|
}, [returnUrl, markAsUsed, supabase]);
|
||||||
|
|
||||||
|
const handleScriptLoad = () => {
|
||||||
|
if (window.google && process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) {
|
||||||
|
window.google.accounts.id.initialize({
|
||||||
|
client_id: process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID,
|
||||||
|
callback: handleGoogleResponse,
|
||||||
|
auto_select: false,
|
||||||
|
cancel_on_tap_outside: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsGoogleLoaded(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoogleSignIn = () => {
|
||||||
|
if (!window.google || !isGoogleLoaded) {
|
||||||
|
toast.error('Google Sign-In is still loading. Please try again.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
window.google.accounts.id.prompt((notification: any) => {
|
||||||
|
|
||||||
const timeoutId = setTimeout(() => {
|
|
||||||
console.log('Google sign-in timeout - resetting loading state');
|
|
||||||
setIsLoading(false);
|
|
||||||
toast.error('Google sign-in popup failed to load. Please try again.');
|
|
||||||
}, 5000);
|
|
||||||
window.google.accounts.id.prompt(async (notification) => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
if (notification.isNotDisplayed() || notification.isSkippedMoment()) {
|
||||||
console.log('Google sign-in was not displayed or skipped:',
|
console.log('One Tap not displayed, using OAuth flow');
|
||||||
notification.isNotDisplayed() ? notification.getNotDisplayedReason() : notification.getSkippedReason()
|
setIsLoading(true);
|
||||||
);
|
|
||||||
await supabase.auth.signInWithOAuth({
|
const redirectTo = `${window.location.origin}/auth/callback${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ''}`;
|
||||||
|
console.log('OAuth redirect URI:', redirectTo);
|
||||||
|
|
||||||
|
supabase.auth.signInWithOAuth({
|
||||||
provider: 'google',
|
provider: 'google',
|
||||||
options: {
|
options: {
|
||||||
queryParams: {
|
redirectTo,
|
||||||
prompt: 'select_account',
|
|
||||||
},
|
|
||||||
redirectTo: `${window.location.origin}/dashboard`,
|
|
||||||
},
|
},
|
||||||
|
}).then(({ error }) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('OAuth error:', error);
|
||||||
|
|
||||||
|
if (error.message?.includes('redirect_uri_mismatch')) {
|
||||||
|
const redirectUri = `${window.location.origin}/auth/callback`;
|
||||||
|
toast.error(
|
||||||
|
`Google OAuth configuration error. Add this exact URL to your Google Cloud Console: ${redirectUri}`,
|
||||||
|
{ duration: 10000 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(error.message || 'Failed to sign in with Google');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setIsLoading(false);
|
|
||||||
} else if (notification.isDismissedMoment()) {
|
|
||||||
console.log('Google sign-in was dismissed:', notification.getDismissedReason());
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error showing Google sign-in prompt:', error);
|
console.error('Error triggering Google sign-in:', error);
|
||||||
setIsLoading(false);
|
setIsLoading(true);
|
||||||
toast.error('Failed to start Google sign-in. Please try again.');
|
|
||||||
}
|
const redirectTo = `${window.location.origin}/auth/callback${returnUrl ? `?returnUrl=${encodeURIComponent(returnUrl)}` : ''}`;
|
||||||
}, [googleClientId, isGoogleLoaded, isLoading]);
|
supabase.auth.signInWithOAuth({
|
||||||
|
provider: 'google',
|
||||||
useEffect(() => {
|
options: {
|
||||||
window.handleGoogleSignIn = handleGoogleSignIn;
|
redirectTo,
|
||||||
|
},
|
||||||
if (window.google && googleClientId && !isGoogleLoaded) {
|
}).then(({ error }) => {
|
||||||
window.google.accounts.id.initialize({
|
if (error) {
|
||||||
client_id: googleClientId,
|
console.error('OAuth error:', error);
|
||||||
callback: handleGoogleSignIn,
|
|
||||||
use_fedcm: true,
|
if (error.message?.includes('redirect_uri_mismatch')) {
|
||||||
context: 'signin',
|
const redirectUri = `${window.location.origin}/auth/callback`;
|
||||||
itp_support: true,
|
toast.error(
|
||||||
|
`Google OAuth configuration error. Add this exact URL to your Google Cloud Console: ${redirectUri}`,
|
||||||
|
{ duration: 10000 }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(error.message || 'Failed to sign in with Google');
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
setIsGoogleLoaded(true);
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return () => {
|
if (!process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID) {
|
||||||
delete window.handleGoogleSignIn;
|
|
||||||
};
|
|
||||||
}, [googleClientId, handleGoogleSignIn, isGoogleLoaded]);
|
|
||||||
|
|
||||||
if (!googleClientId) {
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div className="w-full text-center text-sm text-gray-500 py-3">
|
||||||
disabled
|
Google Sign-In not configured
|
||||||
className="w-full h-12 flex items-center justify-center gap-2 text-sm font-medium tracking-wide rounded-full bg-background border border-border opacity-60 cursor-not-allowed"
|
</div>
|
||||||
>
|
|
||||||
<FcGoogle className="w-4 h-4 mr-2" />
|
|
||||||
Google Sign-In Not Configured
|
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="relative">
|
||||||
<div
|
<button
|
||||||
id="g_id_onload"
|
onClick={handleGoogleSignIn}
|
||||||
data-client_id={googleClientId}
|
disabled={isLoading || !isGoogleLoaded}
|
||||||
data-context="signin"
|
className="w-full h-12 flex items-center justify-center text-sm font-medium tracking-wide rounded-full bg-background text-foreground border border-border hover:bg-accent/30 transition-all duration-200 disabled:opacity-60 disabled:cursor-not-allowed font-sans"
|
||||||
data-ux_mode="popup"
|
aria-label={isLoading ? 'Signing in with Google...' : 'Sign in with Google'}
|
||||||
data-auto_prompt="false"
|
type="button"
|
||||||
data-itp_support="true"
|
>
|
||||||
data-callback="handleGoogleSignIn"
|
{isLoading ? (
|
||||||
style={{ display: 'none' }}
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
/>
|
) : (
|
||||||
<div className="relative">
|
<FcGoogle className="w-4 h-4 mr-2" />
|
||||||
<button
|
|
||||||
onClick={handleCustomGoogleSignIn}
|
|
||||||
disabled={isLoading || !isGoogleLoaded}
|
|
||||||
className="w-full h-12 flex items-center justify-center text-sm font-medium tracking-wide rounded-full bg-background text-foreground border border-border hover:bg-accent/30 transition-all duration-200 disabled:opacity-60 disabled:cursor-not-allowed font-sans"
|
|
||||||
aria-label={
|
|
||||||
isLoading ? 'Signing in with Google...' : 'Sign in with Google'
|
|
||||||
}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<FcGoogle className="w-4 h-4 mr-2" />
|
|
||||||
)}
|
|
||||||
<span className="font-medium">
|
|
||||||
{isLoading ? 'Signing in...' : 'Continue with Google'}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{wasLastMethod && (
|
|
||||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-background shadow-sm">
|
|
||||||
<div className="w-full h-full bg-green-500 rounded-full animate-pulse" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<span className="font-medium">
|
||||||
|
{isLoading ? 'Signing in...' : 'Continue with Google'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{wasLastMethod && (
|
||||||
|
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-background shadow-sm">
|
||||||
|
<div className="w-full h-full bg-green-500 rounded-full animate-pulse" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Script
|
<Script
|
||||||
src="https://accounts.google.com/gsi/client"
|
src="https://accounts.google.com/gsi/client"
|
||||||
strategy="afterInteractive"
|
strategy="afterInteractive"
|
||||||
onLoad={() => {
|
onLoad={handleScriptLoad}
|
||||||
if (window.google && googleClientId && !isGoogleLoaded) {
|
|
||||||
window.google.accounts.id.initialize({
|
|
||||||
client_id: googleClientId,
|
|
||||||
callback: handleGoogleSignIn,
|
|
||||||
use_fedcm: true,
|
|
||||||
context: 'signin',
|
|
||||||
itp_support: true,
|
|
||||||
});
|
|
||||||
setIsGoogleLoaded(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
Loading…
Reference in New Issue