11import { useEffect , useRef , useState } from "react" ;
22import { useNavigate } from "react-router" ;
3+ import { Capacitor } from "@capacitor/core" ;
4+ import { SocialLogin } from "@capgo/capacitor-social-login" ;
35import { useAuth } from "../../context/AuthContext" ;
46
5- // Google OAuth client ID — safe to expose (public identifier, not a secret)
7+ // Google OAuth Web Client ID — safe to expose (public identifier, not a secret).
8+ // Used in both flows:
9+ // - Web: passed to Google Identity Services `initialize({ client_id })`.
10+ // - Native: passed as `webClientId` to the Capacitor plugin. The plugin
11+ // asks Android's native Sign-In for an ID token whose `aud` equals this
12+ // value, so our backend (which validates `aud` against Google:ClientId)
13+ // accepts tokens from web AND native with zero backend changes.
614const GOOGLE_CLIENT_ID =
715 "82985006549-i2dn8m92d760lsdcsvj4sao19a4e46et.apps.googleusercontent.com" ;
816
@@ -79,15 +87,30 @@ function loadGoogleScript(): Promise<void> {
7987 document . head . appendChild ( script ) ;
8088 } ) ;
8189
82- // If it rejects, allow a later mount to try again rather than being
83- // permanently stuck on the cached rejection.
8490 gsiLoadPromise . catch ( ( ) => {
8591 gsiLoadPromise = null ;
8692 } ) ;
8793
8894 return gsiLoadPromise ;
8995}
9096
97+ // Module-level guard so we only call SocialLogin.initialize() once per app
98+ // launch. The plugin throws if initialize is called twice.
99+ let nativeInitPromise : Promise < void > | null = null ;
100+
101+ function initNativeSocialLogin ( ) : Promise < void > {
102+ if ( nativeInitPromise ) return nativeInitPromise ;
103+ nativeInitPromise = SocialLogin . initialize ( {
104+ google : {
105+ webClientId : GOOGLE_CLIENT_ID ,
106+ } ,
107+ } ) ;
108+ nativeInitPromise . catch ( ( ) => {
109+ nativeInitPromise = null ;
110+ } ) ;
111+ return nativeInitPromise ;
112+ }
113+
91114interface SocialLoginButtonsProps {
92115 label ?: string ;
93116 onSuccess ?: ( ) => void ;
@@ -101,7 +124,44 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
101124 const [ ready , setReady ] = useState ( false ) ;
102125 const buttonHostRef = useRef < HTMLDivElement | null > ( null ) ;
103126
127+ const isNative = Capacitor . isNativePlatform ( ) ;
128+
129+ async function handleGoogleSuccess ( idToken : string ) {
130+ setLoading ( true ) ;
131+ setError ( null ) ;
132+ try {
133+ await loginWithGoogle ( idToken ) ;
134+ if ( onSuccess ) onSuccess ( ) ;
135+ else navigate ( "/main" ) ;
136+ } catch ( err : unknown ) {
137+ const msg = ( err as { message ?: string } ) ?. message ?? "שגיאה בהתחברות עם Google" ;
138+ setError ( msg ) ;
139+ } finally {
140+ setLoading ( false ) ;
141+ }
142+ }
143+
144+ // Native platforms: initialize the plugin once on mount, then show our
145+ // branded button whose onClick triggers the native account picker.
104146 useEffect ( ( ) => {
147+ if ( ! isNative ) return ;
148+ let cancelled = false ;
149+ initNativeSocialLogin ( )
150+ . then ( ( ) => {
151+ if ( ! cancelled ) setReady ( true ) ;
152+ } )
153+ . catch ( ( ) => {
154+ if ( ! cancelled ) setError ( "לא ניתן לטעון את שירות Google" ) ;
155+ } ) ;
156+ return ( ) => {
157+ cancelled = true ;
158+ } ;
159+ } , [ isNative ] ) ;
160+
161+ // Web platform: load Google Identity Services and overlay the transparent
162+ // Google-rendered button on top of our branded visual.
163+ useEffect ( ( ) => {
164+ if ( isNative ) return ;
105165 let cancelled = false ;
106166
107167 loadGoogleScript ( )
@@ -116,18 +176,7 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
116176 client_id : GOOGLE_CLIENT_ID ,
117177 callback : async ( response : { credential : string } ) => {
118178 if ( ! response ?. credential ) return ;
119- setLoading ( true ) ;
120- setError ( null ) ;
121- try {
122- await loginWithGoogle ( response . credential ) ;
123- if ( onSuccess ) onSuccess ( ) ;
124- else navigate ( "/main" ) ;
125- } catch ( err : unknown ) {
126- const msg = ( err as { message ?: string } ) ?. message ?? "שגיאה בהתחברות עם Google" ;
127- setError ( msg ) ;
128- } finally {
129- setLoading ( false ) ;
130- }
179+ await handleGoogleSuccess ( response . credential ) ;
131180 } ,
132181 use_fedcm_for_prompt : true ,
133182 auto_select : false ,
@@ -153,7 +202,37 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
153202 return ( ) => {
154203 cancelled = true ;
155204 } ;
156- } , [ loginWithGoogle , navigate , onSuccess ] ) ;
205+ // handleGoogleSuccess captures loginWithGoogle/navigate/onSuccess via closure;
206+ // we intentionally only re-run when the platform type changes.
207+ // eslint-disable-next-line react-hooks/exhaustive-deps
208+ } , [ isNative ] ) ;
209+
210+ async function handleNativeClick ( ) {
211+ if ( ! ready || loading ) return ;
212+ setError ( null ) ;
213+ try {
214+ const res = await SocialLogin . login ( {
215+ provider : "google" ,
216+ options : { } ,
217+ } ) ;
218+ // Plugin response shape: { provider, result: { idToken, accessToken, profile, ... } }
219+ const result = res as { result ?: { idToken ?: string } } ;
220+ const idToken = result . result ?. idToken ;
221+ if ( ! idToken ) {
222+ setError ( "לא התקבל טוקן מ-Google" ) ;
223+ return ;
224+ }
225+ await handleGoogleSuccess ( idToken ) ;
226+ } catch ( err : unknown ) {
227+ const msg = ( err as { message ?: string } ) ?. message ?? "שגיאה בהתחברות עם Google" ;
228+ // User-cancelled flows on Android throw with codes like "12501" or
229+ // "canceled" — don't show a scary error for those.
230+ if ( / c a n c e l / i. test ( msg ) || / 1 2 5 0 1 / . test ( msg ) ) {
231+ return ;
232+ }
233+ setError ( msg ) ;
234+ }
235+ }
157236
158237 return (
159238 < div className = "flex flex-col items-center gap-4" >
@@ -162,41 +241,36 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
162241 </ p >
163242 < div className = "flex gap-3 justify-center" >
164243 { /* Google — the only provider enabled right now.
165- Facebook and Apple are intentionally commented out until we add server-side verification for them.
166-
167- Implementation note: Google Identity Services' `prompt()` call is unreliable
168- on modern browsers (FedCM, third-party cookie blockers, incognito). We use
169- `renderButton()` instead, then visually overlay our branded button on top of
170- the Google-rendered element so clicks fall through to Google's handler.
171- This is the pattern Google recommends and it works across browsers. */ }
172- < div className = "relative w-[60px] h-[44px]" >
173- { /* Branded visual — pointer-events:none so clicks hit the Google button below */ }
174- < div
175- aria-hidden = "true"
176- className = { `absolute inset-0 pointer-events-none bg-white border border-[#dadce0] rounded-[10px] flex items-center justify-center shadow-[0px_1px_3px_rgba(0,0,0,0.08)] ${
177- ! ready || loading ? "opacity-60" : ""
178- } `}
244+ Native: a regular <button> that calls the Capacitor plugin.
245+ Web: branded visual + transparent Google-rendered button overlay. */ }
246+ { isNative ? (
247+ < button
248+ type = "button"
249+ onClick = { handleNativeClick }
250+ disabled = { ! ready || loading }
251+ aria-label = "התחברות עם Google"
252+ className = "bg-white border border-[#dadce0] hover:bg-[#f8f9fa] transition-colors w-[60px] h-[44px] rounded-[10px] flex items-center justify-center cursor-pointer shadow-[0px_1px_3px_rgba(0,0,0,0.08)] disabled:opacity-60"
179253 >
180- < svg width = "22" height = "22" viewBox = "0 0 48 48" aria-hidden = "true" >
181- < path fill = "#EA4335" d = "M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" />
182- < path fill = "#4285F4" d = "M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" />
183- < path fill = "#FBBC05" d = "M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z" />
184- < path fill = "#34A853" d = "M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" />
185- </ svg >
254+ < GoogleLogo />
255+ </ button >
256+ ) : (
257+ < div className = "relative w-[60px] h-[44px]" >
258+ < div
259+ aria-hidden = "true"
260+ className = { `absolute inset-0 pointer-events-none bg-white border border-[#dadce0] rounded-[10px] flex items-center justify-center shadow-[0px_1px_3px_rgba(0,0,0,0.08)] ${
261+ ! ready || loading ? "opacity-60" : ""
262+ } `}
263+ >
264+ < GoogleLogo />
265+ </ div >
266+ < div
267+ ref = { buttonHostRef }
268+ aria-label = "התחברות עם Google"
269+ className = "absolute inset-0 opacity-0"
270+ style = { { colorScheme : "light" } }
271+ />
186272 </ div >
187- { /* Google-rendered clickable surface — transparent, sits on top, handles auth */ }
188- < div
189- ref = { buttonHostRef }
190- aria-label = "התחברות עם Google"
191- className = "absolute inset-0 opacity-0"
192- style = { { colorScheme : "light" } }
193- />
194- </ div >
195-
196- { /*
197- // Facebook — disabled until server-side Facebook token verification is added.
198- // Apple — disabled until server-side Apple token verification (JWKS + private key) is added.
199- */ }
273+ ) }
200274 </ div >
201275 { loading && (
202276 < p className = "font-['Heebo',sans-serif] text-[#626262] text-[12px]" > מתחבר עם Google...</ p >
@@ -209,3 +283,14 @@ export function SocialLoginButtons({ label = "התחברות באמצעות", on
209283 </ div >
210284 ) ;
211285}
286+
287+ function GoogleLogo ( ) {
288+ return (
289+ < svg width = "22" height = "22" viewBox = "0 0 48 48" aria-hidden = "true" >
290+ < path fill = "#EA4335" d = "M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z" />
291+ < path fill = "#4285F4" d = "M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z" />
292+ < path fill = "#FBBC05" d = "M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z" />
293+ < path fill = "#34A853" d = "M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z" />
294+ </ svg >
295+ ) ;
296+ }
0 commit comments