Seth commited on
Commit
3e30e4e
·
1 Parent(s): bd5e962
backend/app/main.py CHANGED
@@ -2316,8 +2316,9 @@ async def unipile_webhook(request: Request, db: Session = Depends(get_db)):
2316
 
2317
 
2318
  @app.get("/api/unipile/webhook-url-hint")
2319
- async def unipile_webhook_url_hint(request: Request):
2320
  """Public URL your UniPile webhooks should POST to (same host as this API)."""
 
2321
  root = _public_origin_from_request(request)
2322
  post_url = f"{root}/api/webhooks/unipile"
2323
  return {
@@ -4779,7 +4780,7 @@ async def download_sequences(file_id: str = Query(...), t: TenantContext = Depen
4779
 
4780
 
4781
  @app.get("/api/smartlead-campaigns")
4782
- async def get_smartlead_campaigns():
4783
  """Get list of campaigns from Smartlead"""
4784
  try:
4785
  client = SmartleadClient()
 
2316
 
2317
 
2318
  @app.get("/api/unipile/webhook-url-hint")
2319
+ async def unipile_webhook_url_hint(request: Request, t: TenantContext = Depends(get_tenant_context)):
2320
  """Public URL your UniPile webhooks should POST to (same host as this API)."""
2321
+ _ = t
2322
  root = _public_origin_from_request(request)
2323
  post_url = f"{root}/api/webhooks/unipile"
2324
  return {
 
4780
 
4781
 
4782
  @app.get("/api/smartlead-campaigns")
4783
+ async def get_smartlead_campaigns(t: TenantContext = Depends(get_tenant_context)):
4784
  """Get list of campaigns from Smartlead"""
4785
  try:
4786
  client = SmartleadClient()
frontend/src/App.jsx CHANGED
@@ -1,5 +1,7 @@
1
  import React from "react";
2
  import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
 
 
3
  import { GeneratorWorkflowProvider } from "./context/GeneratorWorkflowContext";
4
  import EmailSequenceGenerator from "./pages/EmailSequenceGenerator";
5
  import Contacts from "./pages/Contacts";
@@ -12,17 +14,21 @@ import "./index.css";
12
  export default function App() {
13
  return (
14
  <BrowserRouter>
15
- <GeneratorWorkflowProvider>
16
- <Routes>
17
- <Route path="/" element={<EmailSequenceGenerator />} />
18
- <Route path="/contacts" element={<Contacts />} />
19
- <Route path="/leads" element={<Leads />} />
20
- <Route path="/deals" element={<Deals />} />
21
- <Route path="/dashboard" element={<SalesDashboard />} />
22
- <Route path="/settings" element={<Settings />} />
23
- <Route path="/history" element={<Navigate to="/leads" replace />} />
24
- </Routes>
25
- </GeneratorWorkflowProvider>
 
 
 
 
26
  </BrowserRouter>
27
  );
28
  }
 
1
  import React from "react";
2
  import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
3
+ import { AuthProvider } from "./context/AuthContext";
4
+ import RequireAuth from "./components/auth/RequireAuth";
5
  import { GeneratorWorkflowProvider } from "./context/GeneratorWorkflowContext";
6
  import EmailSequenceGenerator from "./pages/EmailSequenceGenerator";
7
  import Contacts from "./pages/Contacts";
 
14
  export default function App() {
15
  return (
16
  <BrowserRouter>
17
+ <AuthProvider>
18
+ <RequireAuth>
19
+ <GeneratorWorkflowProvider>
20
+ <Routes>
21
+ <Route path="/" element={<EmailSequenceGenerator />} />
22
+ <Route path="/contacts" element={<Contacts />} />
23
+ <Route path="/leads" element={<Leads />} />
24
+ <Route path="/deals" element={<Deals />} />
25
+ <Route path="/dashboard" element={<SalesDashboard />} />
26
+ <Route path="/settings" element={<Settings />} />
27
+ <Route path="/history" element={<Navigate to="/leads" replace />} />
28
+ </Routes>
29
+ </GeneratorWorkflowProvider>
30
+ </RequireAuth>
31
+ </AuthProvider>
32
  </BrowserRouter>
33
  );
34
  }
frontend/src/components/auth/RequireAuth.jsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { useAuth } from '@/context/AuthContext';
3
+ import SignIn from '@/pages/SignIn';
4
+
5
+ /**
6
+ * Blocks the app until the user has a valid Google session.
7
+ * API routes remain protected server-side; this prevents browsing the UI while signed out.
8
+ */
9
+ export default function RequireAuth({ children }) {
10
+ const { phase, signedIn } = useAuth();
11
+
12
+ if (phase === 'loading') {
13
+ return <SignIn />;
14
+ }
15
+
16
+ if (!signedIn) {
17
+ return <SignIn />;
18
+ }
19
+
20
+ return children;
21
+ }
frontend/src/components/layout/GoogleAuthBar.jsx CHANGED
@@ -1,74 +1,27 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
2
- import { Link, useLocation, useSearchParams } from 'react-router-dom';
3
  import { LogOut, Settings } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
5
  import { cn } from '@/lib/utils';
6
  import { apiFetch } from '@/lib/api';
 
7
 
8
  /**
9
- * Sign-in (Google). Workspace details and switching context live in Settings / session. OAuth callback sets session.
10
  */
11
  export default function GoogleAuthBar() {
12
  const location = useLocation();
13
- const [searchParams, setSearchParams] = useSearchParams();
14
- const [phase, setPhase] = useState('loading');
15
- const [googleOn, setGoogleOn] = useState(false);
16
- const [user, setUser] = useState(null);
17
- const refresh = useCallback(async () => {
18
- try {
19
- const [st, me] = await Promise.all([
20
- apiFetch('/api/auth/status').then((r) => r.json()),
21
- apiFetch('/api/auth/me').then((r) => (r.ok ? r.json() : null)),
22
- ]);
23
- setGoogleOn(!!st.googleConfigured);
24
- setUser(me);
25
- setPhase('ready');
26
- if (typeof window !== 'undefined') {
27
- window.dispatchEvent(new CustomEvent('emailout-auth-changed'));
28
- }
29
- } catch {
30
- setPhase('error');
31
- }
32
- }, []);
33
-
34
- useEffect(() => {
35
- refresh();
36
- }, [refresh]);
37
-
38
- useEffect(() => {
39
- const err = searchParams.get('auth_error');
40
- if (!err) return;
41
- if (err === 'access_denied') {
42
- console.info('Google sign-in was cancelled.');
43
- } else if (err === 'invite_email_mismatch') {
44
- console.warn(
45
- 'Invitation email does not match your Google account. Sign in with the invited address.'
46
- );
47
- } else {
48
- console.warn('Google sign-in error:', err);
49
- }
50
- const next = new URLSearchParams(searchParams);
51
- next.delete('auth_error');
52
- setSearchParams(next, { replace: true });
53
- }, [searchParams, setSearchParams]);
54
 
55
  const logout = async () => {
56
  try {
57
  await apiFetch('/api/auth/logout', { method: 'POST' });
58
- setUser(null);
59
- if (typeof window !== 'undefined') {
60
- window.dispatchEvent(new CustomEvent('emailout-auth-changed'));
61
- }
62
  } catch (e) {
63
  console.error(e);
64
  }
65
  };
66
 
67
- const inviteParam = searchParams.get('invite');
68
- const googleHref = inviteParam
69
- ? `/api/auth/google?invite=${encodeURIComponent(inviteParam)}`
70
- : '/api/auth/google';
71
-
72
  if (phase === 'loading' || (phase === 'ready' && !googleOn)) {
73
  if (phase === 'loading') {
74
  return (
@@ -81,94 +34,53 @@ export default function GoogleAuthBar() {
81
  return null;
82
  }
83
 
84
- if (phase === 'error') {
85
  return null;
86
  }
87
 
88
- if (user) {
89
- const settingsActive =
90
- location.pathname === '/settings' || location.pathname.startsWith('/settings/');
91
- return (
92
- <div className="flex max-w-[min(100vw-6rem,28rem)] flex-wrap items-center justify-end gap-2">
93
- <Button
94
- asChild
95
- variant="ghost"
96
- size="icon"
97
- className={cn(
98
- 'h-9 w-9 shrink-0 text-slate-600 hover:text-violet-700 hover:bg-violet-50',
99
- settingsActive && 'text-violet-700 bg-violet-50'
100
- )}
101
- title="Settings"
102
- aria-current={settingsActive ? 'page' : undefined}
103
- >
104
- <Link to="/settings">
105
- <Settings className="h-5 w-5" aria-hidden />
106
- <span className="sr-only">Settings</span>
107
- </Link>
108
- </Button>
109
- {user.picture ? (
110
- <img
111
- src={user.picture}
112
- alt=""
113
- className="h-8 w-8 shrink-0 rounded-full border border-slate-200 object-cover"
114
- referrerPolicy="no-referrer"
115
- />
116
- ) : null}
117
- <span className="hidden min-w-0 truncate text-sm text-slate-600 lg:inline">
118
- {user.name || user.email || 'Signed in'}
119
- </span>
120
- <Button
121
- type="button"
122
- variant="outline"
123
- size="sm"
124
- className="shrink-0 gap-1"
125
- onClick={logout}
126
- title="Sign out"
127
- >
128
- <LogOut className="h-3.5 w-3.5" />
129
- <span className="hidden sm:inline">Sign out</span>
130
- </Button>
131
- </div>
132
- );
133
- }
134
-
135
- return (
136
- <Button
137
- asChild
138
- variant="outline"
139
- size="sm"
140
- className={cn(
141
- 'shrink-0 gap-2 border-slate-200 bg-white text-slate-800 shadow-sm',
142
- 'hover:bg-slate-50'
143
- )}
144
- >
145
- <a href={googleHref} className="inline-flex items-center gap-2">
146
- <GoogleMark className="h-4 w-4 shrink-0" />
147
- <span className="whitespace-nowrap">Sign in with Google</span>
148
- </a>
149
- </Button>
150
- );
151
- }
152
 
153
- function GoogleMark({ className }) {
154
  return (
155
- <svg className={className} viewBox="0 0 24 24" aria-hidden>
156
- <path
157
- fill="#4285F4"
158
- d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
159
- />
160
- <path
161
- fill="#34A853"
162
- d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
163
- />
164
- <path
165
- fill="#FBBC05"
166
- d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
167
- />
168
- <path
169
- fill="#EA4335"
170
- d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
171
- />
172
- </svg>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  );
174
  }
 
1
+ import React from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
  import { LogOut, Settings } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
5
  import { cn } from '@/lib/utils';
6
  import { apiFetch } from '@/lib/api';
7
+ import { useAuth } from '@/context/AuthContext';
8
 
9
  /**
10
+ * Signed-in user menu (sign-out, settings). Sign-in is handled by RequireAuth + SignIn page.
11
  */
12
  export default function GoogleAuthBar() {
13
  const location = useLocation();
14
+ const { phase, googleOn, user, refresh } = useAuth();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  const logout = async () => {
17
  try {
18
  await apiFetch('/api/auth/logout', { method: 'POST' });
19
+ await refresh();
 
 
 
20
  } catch (e) {
21
  console.error(e);
22
  }
23
  };
24
 
 
 
 
 
 
25
  if (phase === 'loading' || (phase === 'ready' && !googleOn)) {
26
  if (phase === 'loading') {
27
  return (
 
34
  return null;
35
  }
36
 
37
+ if (phase === 'error' || !user) {
38
  return null;
39
  }
40
 
41
+ const settingsActive =
42
+ location.pathname === '/settings' || location.pathname.startsWith('/settings/');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
 
44
  return (
45
+ <div className="flex max-w-[min(100vw-6rem,28rem)] flex-wrap items-center justify-end gap-2">
46
+ <Button
47
+ asChild
48
+ variant="ghost"
49
+ size="icon"
50
+ className={cn(
51
+ 'h-9 w-9 shrink-0 text-slate-600 hover:text-violet-700 hover:bg-violet-50',
52
+ settingsActive && 'text-violet-700 bg-violet-50'
53
+ )}
54
+ title="Settings"
55
+ aria-current={settingsActive ? 'page' : undefined}
56
+ >
57
+ <Link to="/settings">
58
+ <Settings className="h-5 w-5" aria-hidden />
59
+ <span className="sr-only">Settings</span>
60
+ </Link>
61
+ </Button>
62
+ {user.picture ? (
63
+ <img
64
+ src={user.picture}
65
+ alt=""
66
+ className="h-8 w-8 shrink-0 rounded-full border border-slate-200 object-cover"
67
+ referrerPolicy="no-referrer"
68
+ />
69
+ ) : null}
70
+ <span className="hidden min-w-0 truncate text-sm text-slate-600 lg:inline">
71
+ {user.name || user.email || 'Signed in'}
72
+ </span>
73
+ <Button
74
+ type="button"
75
+ variant="outline"
76
+ size="sm"
77
+ className="shrink-0 gap-1"
78
+ onClick={logout}
79
+ title="Sign out"
80
+ >
81
+ <LogOut className="h-3.5 w-3.5" />
82
+ <span className="hidden sm:inline">Sign out</span>
83
+ </Button>
84
+ </div>
85
  );
86
  }
frontend/src/context/AuthContext.jsx ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useState,
8
+ } from 'react';
9
+ import { apiFetch } from '@/lib/api';
10
+
11
+ const AuthContext = createContext(null);
12
+
13
+ export function AuthProvider({ children }) {
14
+ const [phase, setPhase] = useState('loading');
15
+ const [googleOn, setGoogleOn] = useState(false);
16
+ const [user, setUser] = useState(null);
17
+
18
+ const refresh = useCallback(async () => {
19
+ try {
20
+ const [st, meRes] = await Promise.all([
21
+ apiFetch('/api/auth/status').then((r) => r.json()),
22
+ apiFetch('/api/auth/me'),
23
+ ]);
24
+ setGoogleOn(!!st.googleConfigured);
25
+ setUser(meRes.ok ? await meRes.json() : null);
26
+ setPhase('ready');
27
+ } catch {
28
+ setPhase('error');
29
+ }
30
+ }, []);
31
+
32
+ useEffect(() => {
33
+ refresh();
34
+ }, [refresh]);
35
+
36
+ useEffect(() => {
37
+ const onAuth = () => refresh();
38
+ window.addEventListener('emailout-auth-changed', onAuth);
39
+ return () => window.removeEventListener('emailout-auth-changed', onAuth);
40
+ }, [refresh]);
41
+
42
+ const value = useMemo(
43
+ () => ({
44
+ phase,
45
+ googleOn,
46
+ user,
47
+ signedIn: !!user,
48
+ refresh,
49
+ }),
50
+ [phase, googleOn, user, refresh]
51
+ );
52
+
53
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
54
+ }
55
+
56
+ export function useAuth() {
57
+ const ctx = useContext(AuthContext);
58
+ if (!ctx) {
59
+ throw new Error('useAuth must be used within AuthProvider');
60
+ }
61
+ return ctx;
62
+ }
frontend/src/pages/Settings.jsx CHANGED
@@ -203,10 +203,7 @@ export default function Settings() {
203
  return (
204
  <AppShell title="Settings">
205
  <div className="rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
206
- <p className="text-slate-600 mb-4">Sign in to manage workspace settings.</p>
207
- <Button asChild variant="default">
208
- <a href="/api/auth/google">Sign in with Google</a>
209
- </Button>
210
  </div>
211
  </AppShell>
212
  );
 
203
  return (
204
  <AppShell title="Settings">
205
  <div className="rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
206
+ <p className="text-slate-600">Could not load your profile. Try signing out and back in.</p>
 
 
 
207
  </div>
208
  </AppShell>
209
  );
frontend/src/pages/SignIn.jsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+ import { Loader2, Zap } from 'lucide-react';
4
+ import { Button } from '@/components/ui/button';
5
+ import { cn } from '@/lib/utils';
6
+ import { useAuth } from '@/context/AuthContext';
7
+
8
+ export default function SignIn() {
9
+ const { phase, googleOn } = useAuth();
10
+ const [searchParams, setSearchParams] = useSearchParams();
11
+
12
+ useEffect(() => {
13
+ const err = searchParams.get('auth_error');
14
+ if (!err) return;
15
+ if (err === 'access_denied') {
16
+ console.info('Google sign-in was cancelled.');
17
+ } else if (err === 'invite_email_mismatch') {
18
+ console.warn(
19
+ 'Invitation email does not match your Google account. Sign in with the invited address.'
20
+ );
21
+ } else {
22
+ console.warn('Google sign-in error:', err);
23
+ }
24
+ const next = new URLSearchParams(searchParams);
25
+ next.delete('auth_error');
26
+ setSearchParams(next, { replace: true });
27
+ }, [searchParams, setSearchParams]);
28
+
29
+ const inviteParam = searchParams.get('invite');
30
+ const googleHref = inviteParam
31
+ ? `/api/auth/google?invite=${encodeURIComponent(inviteParam)}`
32
+ : '/api/auth/google';
33
+
34
+ if (phase === 'loading') {
35
+ return (
36
+ <div className="flex min-h-[100dvh] items-center justify-center bg-gradient-to-br from-slate-50 via-white to-violet-50">
37
+ <Loader2 className="h-10 w-10 animate-spin text-violet-600" aria-label="Loading" />
38
+ </div>
39
+ );
40
+ }
41
+
42
+ if (phase === 'error') {
43
+ return (
44
+ <div className="flex min-h-[100dvh] items-center justify-center bg-gradient-to-br from-slate-50 via-white to-violet-50 px-4">
45
+ <div className="max-w-md rounded-2xl border border-slate-200 bg-white p-8 text-center shadow-sm">
46
+ <p className="text-slate-700">Could not reach the server. Refresh and try again.</p>
47
+ </div>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ if (!googleOn) {
53
+ return (
54
+ <div className="flex min-h-[100dvh] items-center justify-center bg-gradient-to-br from-slate-50 via-white to-violet-50 px-4">
55
+ <div className="max-w-md rounded-2xl border border-amber-200 bg-white p-8 text-center shadow-sm">
56
+ <p className="font-medium text-slate-800">Google sign-in is not configured</p>
57
+ <p className="mt-2 text-sm text-slate-600">
58
+ Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET on the server to enable access.
59
+ </p>
60
+ </div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ return (
66
+ <div className="flex min-h-[100dvh] flex-col items-center justify-center bg-gradient-to-br from-slate-50 via-white to-violet-50 px-4 py-12">
67
+ <div className="w-full max-w-md rounded-2xl border border-slate-200 bg-white p-8 shadow-lg shadow-violet-100/50 sm:p-10">
68
+ <div className="mb-8 flex flex-col items-center text-center">
69
+ <div className="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-violet-600 to-purple-600 shadow-lg shadow-violet-200">
70
+ <Zap className="h-7 w-7 text-white" aria-hidden />
71
+ </div>
72
+ <h1 className="text-2xl font-bold text-slate-800">SequenceAI</h1>
73
+ <p className="mt-1 text-sm text-slate-500">CRM Workspace</p>
74
+ </div>
75
+
76
+ <p className="mb-6 text-center text-slate-600">
77
+ Sign in with your Google account to access campaigns, contacts, leads, and deals.
78
+ </p>
79
+
80
+ {inviteParam ? (
81
+ <p className="mb-6 rounded-lg bg-violet-50 px-4 py-3 text-center text-sm text-violet-800">
82
+ You have a workspace invitation. Sign in with the email address that received the
83
+ invite.
84
+ </p>
85
+ ) : null}
86
+
87
+ <Button
88
+ asChild
89
+ size="lg"
90
+ className={cn(
91
+ 'w-full gap-3 bg-white text-slate-800 border border-slate-200 shadow-sm',
92
+ 'hover:bg-slate-50'
93
+ )}
94
+ variant="outline"
95
+ >
96
+ <a href={googleHref} className="inline-flex items-center justify-center gap-3">
97
+ <GoogleMark className="h-5 w-5 shrink-0" />
98
+ <span>Sign in with Google</span>
99
+ </a>
100
+ </Button>
101
+ </div>
102
+ </div>
103
+ );
104
+ }
105
+
106
+ function GoogleMark({ className }) {
107
+ return (
108
+ <svg className={className} viewBox="0 0 24 24" aria-hidden>
109
+ <path
110
+ fill="#4285F4"
111
+ d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
112
+ />
113
+ <path
114
+ fill="#34A853"
115
+ d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
116
+ />
117
+ <path
118
+ fill="#FBBC05"
119
+ d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
120
+ />
121
+ <path
122
+ fill="#EA4335"
123
+ d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
124
+ />
125
+ </svg>
126
+ );
127
+ }