@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.
- package/ACCOUNT_CACHING.md +349 -0
- package/ANDROID_NATIVE_BRIDGE.md +775 -0
- package/AUTH_STATE_MANAGEMENT.md +262 -0
- package/CAPACITOR_INTEGRATION.md +244 -0
- package/CUSTOMIZATION_GUIDE.md +411 -0
- package/README.md +73 -13
- package/SDK_DEBUGGING_GUIDE.md +217 -0
- package/SMARTLINKS_FRAME.md +302 -0
- package/SMARTLINKS_INTEGRATION.md +330 -0
- package/WHATSAPP_OTP_PLAN.md +106 -0
- package/dist/api.d.ts +15 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/components/ProviderButtons.d.ts +1 -0
- package/dist/components/ProviderButtons.d.ts.map +1 -1
- package/dist/components/SmartlinksAuthUI.d.ts.map +1 -1
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +386 -34
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +386 -33
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +98 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/persistentStorage.d.ts +14 -0
- package/dist/utils/persistentStorage.d.ts.map +1 -1
- package/dist/utils/tokenStorage.d.ts +7 -0
- package/dist/utils/tokenStorage.d.ts.map +1 -1
- package/package.json +15 -6
|
@@ -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.
|