@proveanything/smartlinks-auth-ui 0.5.21 → 0.6.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.
@@ -0,0 +1,775 @@
1
+ # Native Bridge for Google Sign-In (AuthKit)
2
+
3
+ This guide explains how to implement the native bridge in your app to enable Google Sign-In within a WebView. The bridge allows the web-based auth UI to delegate Google authentication to the native SDK, which provides a better user experience and avoids WebView restrictions.
4
+
5
+ ## Overview
6
+
7
+ The native bridge is exposed via `window.AuthKit` and provides three methods:
8
+
9
+ | Method | Purpose |
10
+ |--------|---------|
11
+ | `signInWithGoogle` | Initiates Google Sign-In flow |
12
+ | `signOutGoogle` | Clears cached Google session (enables account picker on next login) |
13
+ | `checkGoogleSignIn` | Silently checks for existing Google session (auto-login) |
14
+
15
+ > **Note:** The bridge uses `AuthKit` as the interface name (registered via `addJavascriptInterface(bridge, "AuthKit")`). This allows multiple app features to share a single bridge object, avoiding the WebView limitation where registering the same name twice overwrites the previous object.
16
+
17
+ ```
18
+ +------------------+ +------------------+
19
+ | | AuthKit.signInWithGoogle(...) | |
20
+ | | ----------------------------------> | |
21
+ | WebView | AuthKit.signOutGoogle(...) | Native App |
22
+ | (Auth UI) | ----------------------------------> | (Android/iOS) |
23
+ | | AuthKit.checkGoogleSignIn(...) | |
24
+ | | ----------------------------------> | |
25
+ | | | |
26
+ | | <---------------------------------- | |
27
+ | | smartlinksNativeCallback(result) | |
28
+ +------------------+ +------------------+
29
+ ```
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Prerequisites
35
+
36
+ ### 1. Google Cloud Console Setup
37
+
38
+ You need **two** OAuth Client IDs:
39
+
40
+ 1. **Android Client ID** - For your Android app
41
+ - Application type: Android
42
+ - Package name: Your app's `applicationId` from `build.gradle`
43
+ - SHA-1 fingerprint: From your signing key (debug or release)
44
+
45
+ 2. **Web Client ID** - For requesting ID tokens
46
+ - Application type: Web application
47
+ - This is the `serverClientId` passed from the web app
48
+
49
+ ### 2. Get SHA-1 Fingerprint
50
+
51
+ ```bash
52
+ # In your Android project directory
53
+ ./gradlew signingReport
54
+ ```
55
+
56
+ Or in Android Studio: **Gradle Panel → app → Tasks → android → signingReport**
57
+
58
+ ### 3. Add Dependencies
59
+
60
+ ```groovy
61
+ // app/build.gradle
62
+ dependencies {
63
+ implementation 'com.google.android.gms:play-services-auth:20.7.0'
64
+ }
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Implementation
70
+
71
+ ### SmartlinksNativeBridge.kt (Kotlin)
72
+
73
+ ```kotlin
74
+ package com.yourapp.bridge
75
+
76
+ import android.content.Context
77
+ import android.content.Intent
78
+ import android.util.Log
79
+ import android.webkit.JavascriptInterface
80
+ import android.webkit.WebView
81
+ import androidx.activity.result.ActivityResultLauncher
82
+ import com.google.android.gms.auth.api.signin.GoogleSignIn
83
+ import com.google.android.gms.auth.api.signin.GoogleSignInAccount
84
+ import com.google.android.gms.auth.api.signin.GoogleSignInClient
85
+ import com.google.android.gms.auth.api.signin.GoogleSignInOptions
86
+ import com.google.android.gms.common.api.ApiException
87
+ import org.json.JSONObject
88
+
89
+ class AndroidBridge(
90
+ private val context: Context,
91
+ private val webView: WebView,
92
+ private val signInLauncher: ActivityResultLauncher<Intent>
93
+ ) {
94
+ companion object {
95
+ private const val TAG = "AndroidBridge"
96
+ }
97
+
98
+ // Store pending callback ID for sign-in flow
99
+ private var pendingSignInCallbackId: String? = null
100
+ private var pendingServerClientId: String? = null
101
+
102
+ // ==================== SIGN IN ====================
103
+
104
+ @JavascriptInterface
105
+ fun signInWithGoogle(payload: String) {
106
+ Log.d(TAG, "signInWithGoogle called with payload: $payload")
107
+
108
+ try {
109
+ val json = JSONObject(payload)
110
+ val callbackId = json.getString("callbackId")
111
+ val serverClientId = json.getString("serverClientId")
112
+
113
+ // Store for use in onActivityResult
114
+ pendingSignInCallbackId = callbackId
115
+ pendingServerClientId = serverClientId
116
+
117
+ Log.d(TAG, "Initiating Google Sign-In with serverClientId: $serverClientId")
118
+
119
+ // Configure Google Sign-In
120
+ val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
121
+ .requestIdToken(serverClientId) // This is the Web Client ID!
122
+ .requestEmail()
123
+ .requestProfile()
124
+ .build()
125
+
126
+ val googleSignInClient = GoogleSignIn.getClient(context, gso)
127
+
128
+ // Sign out first to ensure account picker is shown
129
+ googleSignInClient.signOut().addOnCompleteListener {
130
+ // Launch sign-in intent
131
+ val signInIntent = googleSignInClient.signInIntent
132
+ signInLauncher.launch(signInIntent)
133
+ }
134
+
135
+ } catch (e: Exception) {
136
+ Log.e(TAG, "Error parsing sign-in payload", e)
137
+ sendCallback(JSONObject().apply {
138
+ put("callbackId", "unknown")
139
+ put("success", false)
140
+ put("error", e.message ?: "Failed to parse payload")
141
+ put("errorCode", "PARSE_ERROR")
142
+ })
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Call this from your Activity's ActivityResultCallback
148
+ */
149
+ fun handleSignInResult(data: Intent?) {
150
+ val callbackId = pendingSignInCallbackId ?: run {
151
+ Log.w(TAG, "No pending callback ID for sign-in result")
152
+ return
153
+ }
154
+
155
+ try {
156
+ val task = GoogleSignIn.getSignedInAccountFromIntent(data)
157
+ val account = task.getResult(ApiException::class.java)
158
+
159
+ Log.d(TAG, "Sign-in successful: ${account.email}")
160
+
161
+ sendCallback(JSONObject().apply {
162
+ put("callbackId", callbackId)
163
+ put("success", true)
164
+ put("idToken", account.idToken)
165
+ put("email", account.email)
166
+ put("name", account.displayName)
167
+ put("picture", account.photoUrl?.toString())
168
+ })
169
+
170
+ } catch (e: ApiException) {
171
+ Log.e(TAG, "Sign-in failed with status code: ${e.statusCode}", e)
172
+
173
+ val errorMessage = when (e.statusCode) {
174
+ 12501 -> "Sign-in cancelled by user"
175
+ 12502 -> "Sign-in currently in progress"
176
+ 10 -> "Developer error: Check your SHA-1 and package name configuration"
177
+ 7 -> "Network error: Please check your internet connection"
178
+ else -> "Sign-in failed (code: ${e.statusCode})"
179
+ }
180
+
181
+ sendCallback(JSONObject().apply {
182
+ put("callbackId", callbackId)
183
+ put("success", false)
184
+ put("error", errorMessage)
185
+ put("errorCode", "STATUS_${e.statusCode}")
186
+ })
187
+ } finally {
188
+ pendingSignInCallbackId = null
189
+ pendingServerClientId = null
190
+ }
191
+ }
192
+
193
+ // ==================== SIGN OUT ====================
194
+
195
+ @JavascriptInterface
196
+ fun signOutGoogle(payload: String) {
197
+ Log.d(TAG, "signOutGoogle called with payload: $payload")
198
+
199
+ try {
200
+ val json = JSONObject(payload)
201
+ val callbackId = json.getString("callbackId")
202
+
203
+ // Get Google Sign-In client with minimal config (no serverClientId needed)
204
+ val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
205
+ .requestEmail()
206
+ .build()
207
+
208
+ val googleSignInClient = GoogleSignIn.getClient(context, gso)
209
+
210
+ // Sign out and revoke access to ensure account picker shows next time
211
+ googleSignInClient.signOut().addOnCompleteListener { signOutTask ->
212
+ googleSignInClient.revokeAccess().addOnCompleteListener { revokeTask ->
213
+ Log.d(TAG, "Sign-out complete: signOut=${signOutTask.isSuccessful}, revoke=${revokeTask.isSuccessful}")
214
+
215
+ sendCallback(JSONObject().apply {
216
+ put("callbackId", callbackId)
217
+ put("success", true)
218
+ })
219
+ }
220
+ }
221
+
222
+ } catch (e: Exception) {
223
+ Log.e(TAG, "Error in signOutGoogle", e)
224
+
225
+ try {
226
+ val json = JSONObject(payload)
227
+ val callbackId = json.optString("callbackId", "unknown")
228
+ sendCallback(JSONObject().apply {
229
+ put("callbackId", callbackId)
230
+ put("success", false)
231
+ put("error", e.message ?: "Sign-out failed")
232
+ })
233
+ } catch (parseError: Exception) {
234
+ Log.e(TAG, "Failed to send error callback", parseError)
235
+ }
236
+ }
237
+ }
238
+
239
+ // ==================== SILENT SIGN-IN CHECK ====================
240
+
241
+ @JavascriptInterface
242
+ fun checkGoogleSignIn(payload: String) {
243
+ Log.d(TAG, "checkGoogleSignIn called with payload: $payload")
244
+
245
+ try {
246
+ val json = JSONObject(payload)
247
+ val callbackId = json.getString("callbackId")
248
+ val serverClientId = json.getString("serverClientId")
249
+
250
+ // Check for existing signed-in account
251
+ val lastAccount = GoogleSignIn.getLastSignedInAccount(context)
252
+
253
+ if (lastAccount != null && !lastAccount.isExpired) {
254
+ Log.d(TAG, "Found existing account: ${lastAccount.email}, attempting silent sign-in")
255
+
256
+ // Try silent sign-in to get fresh ID token
257
+ val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
258
+ .requestIdToken(serverClientId)
259
+ .requestEmail()
260
+ .build()
261
+
262
+ val googleSignInClient = GoogleSignIn.getClient(context, gso)
263
+
264
+ googleSignInClient.silentSignIn().addOnCompleteListener { task ->
265
+ if (task.isSuccessful) {
266
+ val account = task.result
267
+ Log.d(TAG, "Silent sign-in successful: ${account?.email}")
268
+
269
+ sendCallback(JSONObject().apply {
270
+ put("callbackId", callbackId)
271
+ put("success", true)
272
+ put("isSignedIn", true)
273
+ put("idToken", account?.idToken)
274
+ put("email", account?.email)
275
+ put("name", account?.displayName)
276
+ put("picture", account?.photoUrl?.toString())
277
+ })
278
+ } else {
279
+ Log.d(TAG, "Silent sign-in failed, user needs interactive sign-in")
280
+
281
+ sendCallback(JSONObject().apply {
282
+ put("callbackId", callbackId)
283
+ put("success", true)
284
+ put("isSignedIn", false)
285
+ })
286
+ }
287
+ }
288
+ } else {
289
+ Log.d(TAG, "No existing account or account expired")
290
+
291
+ sendCallback(JSONObject().apply {
292
+ put("callbackId", callbackId)
293
+ put("success", true)
294
+ put("isSignedIn", false)
295
+ })
296
+ }
297
+
298
+ } catch (e: Exception) {
299
+ Log.e(TAG, "Error in checkGoogleSignIn", e)
300
+
301
+ try {
302
+ val json = JSONObject(payload)
303
+ val callbackId = json.optString("callbackId", "unknown")
304
+ sendCallback(JSONObject().apply {
305
+ put("callbackId", callbackId)
306
+ put("success", false)
307
+ put("error", e.message ?: "Check sign-in failed")
308
+ })
309
+ } catch (parseError: Exception) {
310
+ Log.e(TAG, "Failed to send error callback", parseError)
311
+ }
312
+ }
313
+ }
314
+
315
+ // ==================== HELPER ====================
316
+
317
+ private fun sendCallback(result: JSONObject) {
318
+ val script = "window.smartlinksNativeCallback(${result})"
319
+ Log.d(TAG, "Sending callback: $script")
320
+
321
+ webView.post {
322
+ webView.evaluateJavascript(script, null)
323
+ }
324
+ }
325
+ }
326
+ ```
327
+
328
+ ### Activity Integration (Kotlin)
329
+
330
+ ```kotlin
331
+ package com.yourapp
332
+
333
+ import android.os.Bundle
334
+ import android.webkit.WebView
335
+ import android.webkit.WebViewClient
336
+ import androidx.activity.result.contract.ActivityResultContracts
337
+ import androidx.appcompat.app.AppCompatActivity
338
+ import com.yourapp.bridge.AndroidBridge
339
+
340
+ class WebViewActivity : AppCompatActivity() {
341
+
342
+ private lateinit var webView: WebView
343
+ private lateinit var androidBridge: AndroidBridge
344
+
345
+ // Register for sign-in result
346
+ private val signInLauncher = registerForActivityResult(
347
+ ActivityResultContracts.StartActivityForResult()
348
+ ) { result ->
349
+ androidBridge.handleSignInResult(result.data)
350
+ }
351
+
352
+ override fun onCreate(savedInstanceState: Bundle?) {
353
+ super.onCreate(savedInstanceState)
354
+
355
+ webView = WebView(this).apply {
356
+ settings.javaScriptEnabled = true
357
+ settings.domStorageEnabled = true
358
+ webViewClient = WebViewClient()
359
+ }
360
+ setContentView(webView)
361
+
362
+ // Create and attach the native bridge
363
+ // IMPORTANT: Use "AuthKit" as the interface name - this allows combining
364
+ // auth features with other app features in a single bridge object
365
+ androidBridge = AndroidBridge(this, webView, signInLauncher)
366
+ webView.addJavascriptInterface(androidBridge, "AuthKit")
367
+
368
+ // Load your auth URL
369
+ webView.loadUrl("https://your-app.lovable.app")
370
+ }
371
+ }
372
+ ```
373
+
374
+ ---
375
+
376
+ ## Java Implementation
377
+
378
+ <details>
379
+ <summary>Click to expand Java version</summary>
380
+
381
+ ### AndroidBridge.java
382
+
383
+ ```java
384
+ package com.yourapp.bridge;
385
+
386
+ import android.content.Context;
387
+ import android.content.Intent;
388
+ import android.util.Log;
389
+ import android.webkit.JavascriptInterface;
390
+ import android.webkit.WebView;
391
+
392
+ import androidx.activity.result.ActivityResultLauncher;
393
+
394
+ import com.google.android.gms.auth.api.signin.GoogleSignIn;
395
+ import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
396
+ import com.google.android.gms.auth.api.signin.GoogleSignInClient;
397
+ import com.google.android.gms.auth.api.signin.GoogleSignInOptions;
398
+ import com.google.android.gms.common.api.ApiException;
399
+ import com.google.android.gms.tasks.Task;
400
+
401
+ import org.json.JSONException;
402
+ import org.json.JSONObject;
403
+
404
+ public class AndroidBridge {
405
+ private static final String TAG = "AndroidBridge";
406
+
407
+ private final Context context;
408
+ private final WebView webView;
409
+ private final ActivityResultLauncher<Intent> signInLauncher;
410
+
411
+ private String pendingSignInCallbackId;
412
+ private String pendingServerClientId;
413
+
414
+ public AndroidBridge(
415
+ Context context,
416
+ WebView webView,
417
+ ActivityResultLauncher<Intent> signInLauncher
418
+ ) {
419
+ this.context = context;
420
+ this.webView = webView;
421
+ this.signInLauncher = signInLauncher;
422
+ }
423
+
424
+ // ==================== SIGN IN ====================
425
+
426
+ @JavascriptInterface
427
+ public void signInWithGoogle(String payload) {
428
+ Log.d(TAG, "signInWithGoogle called with payload: " + payload);
429
+
430
+ try {
431
+ JSONObject json = new JSONObject(payload);
432
+ String callbackId = json.getString("callbackId");
433
+ String serverClientId = json.getString("serverClientId");
434
+
435
+ pendingSignInCallbackId = callbackId;
436
+ pendingServerClientId = serverClientId;
437
+
438
+ GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
439
+ .requestIdToken(serverClientId)
440
+ .requestEmail()
441
+ .requestProfile()
442
+ .build();
443
+
444
+ GoogleSignInClient googleSignInClient = GoogleSignIn.getClient(context, gso);
445
+
446
+ googleSignInClient.signOut().addOnCompleteListener(task -> {
447
+ Intent signInIntent = googleSignInClient.getSignInIntent();
448
+ signInLauncher.launch(signInIntent);
449
+ });
450
+
451
+ } catch (JSONException e) {
452
+ Log.e(TAG, "Error parsing sign-in payload", e);
453
+ sendErrorCallback("unknown", "Failed to parse payload", "PARSE_ERROR");
454
+ }
455
+ }
456
+
457
+ public void handleSignInResult(Intent data) {
458
+ if (pendingSignInCallbackId == null) {
459
+ Log.w(TAG, "No pending callback ID for sign-in result");
460
+ return;
461
+ }
462
+
463
+ String callbackId = pendingSignInCallbackId;
464
+
465
+ try {
466
+ Task<GoogleSignInAccount> task = GoogleSignIn.getSignedInAccountFromIntent(data);
467
+ GoogleSignInAccount account = task.getResult(ApiException.class);
468
+
469
+ Log.d(TAG, "Sign-in successful: " + account.getEmail());
470
+
471
+ JSONObject result = new JSONObject();
472
+ result.put("callbackId", callbackId);
473
+ result.put("success", true);
474
+ result.put("idToken", account.getIdToken());
475
+ result.put("email", account.getEmail());
476
+ result.put("name", account.getDisplayName());
477
+ result.put("picture", account.getPhotoUrl() != null ? account.getPhotoUrl().toString() : null);
478
+
479
+ sendCallback(result);
480
+
481
+ } catch (ApiException e) {
482
+ Log.e(TAG, "Sign-in failed with status code: " + e.getStatusCode(), e);
483
+
484
+ String errorMessage;
485
+ switch (e.getStatusCode()) {
486
+ case 12501:
487
+ errorMessage = "Sign-in cancelled by user";
488
+ break;
489
+ case 12502:
490
+ errorMessage = "Sign-in currently in progress";
491
+ break;
492
+ case 10:
493
+ errorMessage = "Developer error: Check your SHA-1 and package name configuration";
494
+ break;
495
+ case 7:
496
+ errorMessage = "Network error: Please check your internet connection";
497
+ break;
498
+ default:
499
+ errorMessage = "Sign-in failed (code: " + e.getStatusCode() + ")";
500
+ }
501
+
502
+ sendErrorCallback(callbackId, errorMessage, "STATUS_" + e.getStatusCode());
503
+
504
+ } catch (JSONException e) {
505
+ Log.e(TAG, "Error building result JSON", e);
506
+ sendErrorCallback(callbackId, "Internal error", "JSON_ERROR");
507
+ } finally {
508
+ pendingSignInCallbackId = null;
509
+ pendingServerClientId = null;
510
+ }
511
+ }
512
+
513
+ // ==================== SIGN OUT ====================
514
+
515
+ @JavascriptInterface
516
+ public void signOutGoogle(String payload) {
517
+ Log.d(TAG, "signOutGoogle called with payload: " + payload);
518
+
519
+ try {
520
+ JSONObject json = new JSONObject(payload);
521
+ String callbackId = json.getString("callbackId");
522
+
523
+ GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
524
+ .requestEmail()
525
+ .build();
526
+
527
+ GoogleSignInClient googleSignInClient = GoogleSignIn.getClient(context, gso);
528
+
529
+ googleSignInClient.signOut().addOnCompleteListener(signOutTask -> {
530
+ googleSignInClient.revokeAccess().addOnCompleteListener(revokeTask -> {
531
+ try {
532
+ JSONObject result = new JSONObject();
533
+ result.put("callbackId", callbackId);
534
+ result.put("success", true);
535
+ sendCallback(result);
536
+ } catch (JSONException e) {
537
+ Log.e(TAG, "Error building result", e);
538
+ }
539
+ });
540
+ });
541
+
542
+ } catch (JSONException e) {
543
+ Log.e(TAG, "Error in signOutGoogle", e);
544
+ }
545
+ }
546
+
547
+ // ==================== SILENT SIGN-IN CHECK ====================
548
+
549
+ @JavascriptInterface
550
+ public void checkGoogleSignIn(String payload) {
551
+ Log.d(TAG, "checkGoogleSignIn called with payload: " + payload);
552
+
553
+ try {
554
+ JSONObject json = new JSONObject(payload);
555
+ String callbackId = json.getString("callbackId");
556
+ String serverClientId = json.getString("serverClientId");
557
+
558
+ GoogleSignInAccount lastAccount = GoogleSignIn.getLastSignedInAccount(context);
559
+
560
+ if (lastAccount != null && !lastAccount.isExpired()) {
561
+ GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
562
+ .requestIdToken(serverClientId)
563
+ .requestEmail()
564
+ .build();
565
+
566
+ GoogleSignInClient googleSignInClient = GoogleSignIn.getClient(context, gso);
567
+
568
+ googleSignInClient.silentSignIn().addOnCompleteListener(task -> {
569
+ try {
570
+ JSONObject result = new JSONObject();
571
+ result.put("callbackId", callbackId);
572
+ result.put("success", true);
573
+
574
+ if (task.isSuccessful() && task.getResult() != null) {
575
+ GoogleSignInAccount account = task.getResult();
576
+ result.put("isSignedIn", true);
577
+ result.put("idToken", account.getIdToken());
578
+ result.put("email", account.getEmail());
579
+ result.put("name", account.getDisplayName());
580
+ result.put("picture", account.getPhotoUrl() != null ?
581
+ account.getPhotoUrl().toString() : null);
582
+ } else {
583
+ result.put("isSignedIn", false);
584
+ }
585
+
586
+ sendCallback(result);
587
+ } catch (JSONException e) {
588
+ Log.e(TAG, "Error building result", e);
589
+ }
590
+ });
591
+ } else {
592
+ JSONObject result = new JSONObject();
593
+ result.put("callbackId", callbackId);
594
+ result.put("success", true);
595
+ result.put("isSignedIn", false);
596
+ sendCallback(result);
597
+ }
598
+
599
+ } catch (JSONException e) {
600
+ Log.e(TAG, "Error in checkGoogleSignIn", e);
601
+ }
602
+ }
603
+
604
+ // ==================== HELPERS ====================
605
+
606
+ private void sendCallback(JSONObject result) {
607
+ String script = "window.smartlinksNativeCallback(" + result.toString() + ")";
608
+ Log.d(TAG, "Sending callback: " + script);
609
+
610
+ webView.post(() -> webView.evaluateJavascript(script, null));
611
+ }
612
+
613
+ private void sendErrorCallback(String callbackId, String error, String errorCode) {
614
+ try {
615
+ JSONObject result = new JSONObject();
616
+ result.put("callbackId", callbackId);
617
+ result.put("success", false);
618
+ result.put("error", error);
619
+ result.put("errorCode", errorCode);
620
+ sendCallback(result);
621
+ } catch (JSONException e) {
622
+ Log.e(TAG, "Failed to send error callback", e);
623
+ }
624
+ }
625
+ }
626
+ ```
627
+
628
+ </details>
629
+
630
+ ---
631
+
632
+ ## Payload Reference
633
+
634
+ ### signInWithGoogle
635
+
636
+ **Request:**
637
+ ```json
638
+ {
639
+ "type": "GOOGLE_SIGN_IN",
640
+ "clientId": "smartlinks-client-id",
641
+ "googleClientId": "696509063554-xxx.apps.googleusercontent.com",
642
+ "serverClientId": "696509063554-xxx.apps.googleusercontent.com",
643
+ "callbackId": "google_auth_1706104800000",
644
+ "scopes": ["email", "profile"],
645
+ "requestIdToken": true,
646
+ "requestServerAuthCode": false
647
+ }
648
+ ```
649
+
650
+ **Success Response:**
651
+ ```json
652
+ {
653
+ "callbackId": "google_auth_1706104800000",
654
+ "success": true,
655
+ "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6...",
656
+ "email": "user@example.com",
657
+ "name": "John Doe",
658
+ "picture": "https://lh3.googleusercontent.com/..."
659
+ }
660
+ ```
661
+
662
+ **Error Response:**
663
+ ```json
664
+ {
665
+ "callbackId": "google_auth_1706104800000",
666
+ "success": false,
667
+ "error": "Sign-in cancelled by user",
668
+ "errorCode": "STATUS_12501"
669
+ }
670
+ ```
671
+
672
+ ### signOutGoogle
673
+
674
+ **Request:**
675
+ ```json
676
+ {
677
+ "type": "GOOGLE_SIGN_OUT",
678
+ "callbackId": "google_signout_1706104800000"
679
+ }
680
+ ```
681
+
682
+ **Response:**
683
+ ```json
684
+ {
685
+ "callbackId": "google_signout_1706104800000",
686
+ "success": true
687
+ }
688
+ ```
689
+
690
+ ### checkGoogleSignIn
691
+
692
+ **Request:**
693
+ ```json
694
+ {
695
+ "type": "GOOGLE_CHECK_SIGN_IN",
696
+ "clientId": "smartlinks-client-id",
697
+ "googleClientId": "696509063554-xxx.apps.googleusercontent.com",
698
+ "serverClientId": "696509063554-xxx.apps.googleusercontent.com",
699
+ "callbackId": "google_check_1706104800000"
700
+ }
701
+ ```
702
+
703
+ **Response (Signed In):**
704
+ ```json
705
+ {
706
+ "callbackId": "google_check_1706104800000",
707
+ "success": true,
708
+ "isSignedIn": true,
709
+ "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6...",
710
+ "email": "user@example.com",
711
+ "name": "John Doe",
712
+ "picture": "https://lh3.googleusercontent.com/..."
713
+ }
714
+ ```
715
+
716
+ **Response (Not Signed In):**
717
+ ```json
718
+ {
719
+ "callbackId": "google_check_1706104800000",
720
+ "success": true,
721
+ "isSignedIn": false
722
+ }
723
+ ```
724
+
725
+ ---
726
+
727
+ ## Common Error Codes
728
+
729
+ | Code | Meaning | Solution |
730
+ |------|---------|----------|
731
+ | 10 | Developer error | Check SHA-1 fingerprint and package name match Google Cloud Console |
732
+ | 12501 | User cancelled | Normal - user closed the account picker |
733
+ | 12502 | Sign-in in progress | Wait for current sign-in to complete |
734
+ | 7 | Network error | Check internet connectivity |
735
+
736
+ ---
737
+
738
+ ## Troubleshooting
739
+
740
+ ### "Developer error" (code 10)
741
+
742
+ This means your Android credentials don't match. Verify:
743
+
744
+ 1. **Package name** in Google Cloud Console matches `applicationId` in `build.gradle`
745
+ 2. **SHA-1 fingerprint** matches your signing key:
746
+ - Debug key: `~/.android/debug.keystore`
747
+ - Release key: Your production keystore
748
+ 3. You're using the **Web Client ID** (not Android Client ID) for `requestIdToken()`
749
+
750
+ ### ID Token is null
751
+
752
+ Make sure you're calling `requestIdToken(serverClientId)` with the **Web Client ID**, not the Android Client ID.
753
+
754
+ ### Account picker not showing
755
+
756
+ The `signInWithGoogle` implementation calls `signOut()` before launching the sign-in intent. If you want to skip the picker for returning users, remove that call.
757
+
758
+ ---
759
+
760
+ ## Web Library Props
761
+
762
+ Enable silent sign-in in your SmartlinksAuthUI component:
763
+
764
+ ```tsx
765
+ <SmartlinksAuthUI
766
+ apiEndpoint="https://api.smartlinks.app"
767
+ clientId="your-client-id"
768
+ enableSilentGoogleSignIn={true} // Check for existing Google session on mount
769
+ onAuthSuccess={(token, user) => {
770
+ console.log('Authenticated:', user.email);
771
+ }}
772
+ />
773
+ ```
774
+
775
+ This will automatically authenticate users who already have a Google session in the native app.