@trainon-inc/capacitor-clerk-native 1.23.0 → 1.24.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.
@@ -51,6 +51,10 @@ dependencies {
51
51
  implementation fileTree(dir: 'libs', include: ['*.jar'])
52
52
  implementation project(':capacitor-android')
53
53
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
54
+
55
+ // OkHttp for making HTTP requests to Clerk API
56
+ implementation 'com.squareup.okhttp3:okhttp:4.12.0'
57
+
54
58
  testImplementation "junit:junit:$junitVersion"
55
59
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
56
60
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -1,92 +1,558 @@
1
1
  package com.trainon.capacitor.clerk;
2
2
 
3
+ import android.content.Context;
4
+ import android.content.SharedPreferences;
5
+ import android.util.Log;
6
+
7
+ import com.getcapacitor.JSObject;
3
8
  import com.getcapacitor.Plugin;
4
9
  import com.getcapacitor.PluginCall;
5
10
  import com.getcapacitor.PluginMethod;
6
11
  import com.getcapacitor.annotation.CapacitorPlugin;
7
12
 
13
+ import org.json.JSONArray;
14
+ import org.json.JSONException;
15
+ import org.json.JSONObject;
16
+
17
+ import java.io.IOException;
18
+ import java.util.concurrent.ExecutorService;
19
+ import java.util.concurrent.Executors;
20
+
21
+ import okhttp3.MediaType;
22
+ import okhttp3.OkHttpClient;
23
+ import okhttp3.Request;
24
+ import okhttp3.RequestBody;
25
+ import okhttp3.Response;
26
+
8
27
  /**
9
- * Android stub for Clerk Native plugin.
10
- *
11
- * On Android, authentication is handled by the web Clerk provider (@clerk/clerk-react)
12
- * because Android WebViews work well with web-based auth (unlike iOS which has cookie issues).
28
+ * Android implementation of Clerk Native plugin.
13
29
  *
14
- * This plugin exists to satisfy Capacitor's plugin registration but all methods
15
- * will return errors indicating to use the web provider instead.
30
+ * Makes HTTP requests directly to Clerk's Frontend API, bypassing WebView origin restrictions.
31
+ * This allows the custom Clerk proxy domain to work on Android.
16
32
  */
17
33
  @CapacitorPlugin(name = "ClerkNative")
18
34
  public class ClerkNativePlugin extends Plugin {
19
35
 
20
- private static final String USE_WEB_MESSAGE = "Android uses web Clerk provider. Configure your app to use @clerk/clerk-react on Android.";
36
+ private static final String TAG = "ClerkNativePlugin";
37
+ private static final String PREFS_NAME = "ClerkNativePrefs";
38
+ private static final String PREF_SESSION_TOKEN = "session_token";
39
+ private static final String PREF_CLIENT_TOKEN = "client_token";
40
+ private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
41
+
42
+ private OkHttpClient client;
43
+ private ExecutorService executor;
44
+ private String publishableKey;
45
+ private String clerkDomain;
46
+ private String clientToken;
47
+ private String sessionToken;
48
+ private JSONObject currentUser;
49
+
50
+ @Override
51
+ public void load() {
52
+ super.load();
53
+ client = new OkHttpClient();
54
+ executor = Executors.newSingleThreadExecutor();
55
+
56
+ // Load saved tokens
57
+ SharedPreferences prefs = getContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
58
+ clientToken = prefs.getString(PREF_CLIENT_TOKEN, null);
59
+ sessionToken = prefs.getString(PREF_SESSION_TOKEN, null);
60
+ }
61
+
62
+ private void saveTokens() {
63
+ SharedPreferences prefs = getContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
64
+ SharedPreferences.Editor editor = prefs.edit();
65
+ editor.putString(PREF_CLIENT_TOKEN, clientToken);
66
+ editor.putString(PREF_SESSION_TOKEN, sessionToken);
67
+ editor.apply();
68
+ }
69
+
70
+ private void clearTokens() {
71
+ clientToken = null;
72
+ sessionToken = null;
73
+ currentUser = null;
74
+ SharedPreferences prefs = getContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
75
+ prefs.edit().clear().apply();
76
+ }
77
+
78
+ private String getClerkApiUrl(String path) {
79
+ return "https://" + clerkDomain + path;
80
+ }
81
+
82
+ private Request.Builder createRequestBuilder(String url) {
83
+ Request.Builder builder = new Request.Builder()
84
+ .url(url)
85
+ .header("Content-Type", "application/json")
86
+ .header("Authorization", publishableKey)
87
+ .header("Origin", "https://app.trainonapp.com") // Spoof origin to match web
88
+ .header("User-Agent", "Mozilla/5.0 (Linux; Android) ClerkNative/1.0");
89
+
90
+ if (clientToken != null) {
91
+ builder.header("Cookie", "__client=" + clientToken);
92
+ }
93
+
94
+ return builder;
95
+ }
21
96
 
22
97
  @PluginMethod
23
98
  public void configure(PluginCall call) {
24
- // Allow configure to succeed silently - the web provider will handle auth
99
+ publishableKey = call.getString("publishableKey");
100
+
101
+ if (publishableKey == null || publishableKey.isEmpty()) {
102
+ call.reject("Publishable key is required");
103
+ return;
104
+ }
105
+
106
+ // Extract domain from publishable key (base64 decode)
107
+ try {
108
+ String decoded = new String(android.util.Base64.decode(
109
+ publishableKey.replace("pk_live_", "").replace("pk_test_", ""),
110
+ android.util.Base64.DEFAULT
111
+ ));
112
+ // Remove trailing $ if present
113
+ clerkDomain = decoded.replace("$", "").trim();
114
+ Log.d(TAG, "Configured with domain: " + clerkDomain);
115
+ } catch (Exception e) {
116
+ // Fallback to default domain
117
+ clerkDomain = "clerk.trainonapp.com";
118
+ Log.w(TAG, "Could not decode domain from key, using default: " + clerkDomain);
119
+ }
120
+
25
121
  call.resolve();
26
122
  }
27
123
 
28
124
  @PluginMethod
29
125
  public void load(PluginCall call) {
30
- call.reject(USE_WEB_MESSAGE);
126
+ executor.execute(() -> {
127
+ try {
128
+ // First, get or create a client
129
+ if (clientToken == null) {
130
+ createClient(call);
131
+ } else {
132
+ // Verify existing session
133
+ fetchClientAndUser(call);
134
+ }
135
+ } catch (Exception e) {
136
+ Log.e(TAG, "Load error", e);
137
+ call.reject("Failed to load: " + e.getMessage());
138
+ }
139
+ });
140
+ }
141
+
142
+ private void createClient(PluginCall call) throws IOException, JSONException {
143
+ String url = getClerkApiUrl("/v1/client?_clerk_js_version=5.117.0");
144
+
145
+ Request request = createRequestBuilder(url)
146
+ .post(RequestBody.create("{}", JSON))
147
+ .build();
148
+
149
+ try (Response response = client.newCall(request).execute()) {
150
+ String body = response.body().string();
151
+ Log.d(TAG, "Create client response: " + response.code());
152
+
153
+ if (!response.isSuccessful()) {
154
+ call.reject("Failed to create client: " + response.code() + " - " + body);
155
+ return;
156
+ }
157
+
158
+ JSONObject json = new JSONObject(body);
159
+ JSONObject responseObj = json.optJSONObject("response");
160
+
161
+ if (responseObj != null) {
162
+ clientToken = responseObj.optString("id", null);
163
+
164
+ // Check for active session
165
+ JSONArray sessions = responseObj.optJSONArray("sessions");
166
+ if (sessions != null && sessions.length() > 0) {
167
+ JSONObject session = sessions.getJSONObject(0);
168
+ sessionToken = session.optString("last_active_token", null);
169
+ currentUser = session.optJSONObject("user");
170
+ }
171
+
172
+ saveTokens();
173
+ }
174
+
175
+ JSObject result = new JSObject();
176
+ if (currentUser != null) {
177
+ result.put("user", convertUser(currentUser));
178
+ } else {
179
+ result.put("user", JSObject.NULL);
180
+ }
181
+ call.resolve(result);
182
+ }
183
+ }
184
+
185
+ private void fetchClientAndUser(PluginCall call) throws IOException, JSONException {
186
+ String url = getClerkApiUrl("/v1/client?_clerk_js_version=5.117.0");
187
+
188
+ Request request = createRequestBuilder(url)
189
+ .get()
190
+ .build();
191
+
192
+ try (Response response = client.newCall(request).execute()) {
193
+ String body = response.body().string();
194
+ Log.d(TAG, "Fetch client response: " + response.code());
195
+
196
+ if (!response.isSuccessful()) {
197
+ // Token might be invalid, clear and retry
198
+ clearTokens();
199
+ createClient(call);
200
+ return;
201
+ }
202
+
203
+ JSONObject json = new JSONObject(body);
204
+ JSONObject responseObj = json.optJSONObject("response");
205
+
206
+ if (responseObj != null) {
207
+ // Check for active session
208
+ JSONArray sessions = responseObj.optJSONArray("sessions");
209
+ if (sessions != null && sessions.length() > 0) {
210
+ JSONObject session = sessions.getJSONObject(0);
211
+ JSONObject lastToken = session.optJSONObject("last_active_token");
212
+ if (lastToken != null) {
213
+ sessionToken = lastToken.optString("jwt", null);
214
+ }
215
+ currentUser = session.optJSONObject("user");
216
+ }
217
+
218
+ saveTokens();
219
+ }
220
+
221
+ JSObject result = new JSObject();
222
+ if (currentUser != null) {
223
+ result.put("user", convertUser(currentUser));
224
+ } else {
225
+ result.put("user", JSObject.NULL);
226
+ }
227
+ call.resolve(result);
228
+ }
31
229
  }
32
230
 
33
231
  @PluginMethod
34
- public void signInWithEmail(PluginCall call) {
35
- call.reject(USE_WEB_MESSAGE);
232
+ public void signInWithPassword(PluginCall call) {
233
+ String email = call.getString("email");
234
+ String password = call.getString("password");
235
+
236
+ if (email == null || password == null) {
237
+ call.reject("Email and password are required");
238
+ return;
239
+ }
240
+
241
+ executor.execute(() -> {
242
+ try {
243
+ // Step 1: Create sign-in attempt
244
+ String createUrl = getClerkApiUrl("/v1/client/sign_ins?_clerk_js_version=5.117.0");
245
+
246
+ JSONObject createBody = new JSONObject();
247
+ createBody.put("identifier", email);
248
+
249
+ Request createRequest = createRequestBuilder(createUrl)
250
+ .post(RequestBody.create(createBody.toString(), JSON))
251
+ .build();
252
+
253
+ String signInId;
254
+ try (Response response = client.newCall(createRequest).execute()) {
255
+ String body = response.body().string();
256
+ Log.d(TAG, "Create sign-in response: " + response.code());
257
+
258
+ if (!response.isSuccessful()) {
259
+ call.reject("Failed to start sign in: " + body);
260
+ return;
261
+ }
262
+
263
+ JSONObject json = new JSONObject(body);
264
+ JSONObject responseObj = json.optJSONObject("response");
265
+ signInId = responseObj.optString("id");
266
+
267
+ // Update client token from response
268
+ JSONObject clientObj = json.optJSONObject("client");
269
+ if (clientObj != null) {
270
+ clientToken = clientObj.optString("id", clientToken);
271
+ }
272
+ }
273
+
274
+ // Step 2: Attempt with password
275
+ String attemptUrl = getClerkApiUrl("/v1/client/sign_ins/" + signInId + "/attempt_first_factor?_clerk_js_version=5.117.0");
276
+
277
+ JSONObject attemptBody = new JSONObject();
278
+ attemptBody.put("strategy", "password");
279
+ attemptBody.put("password", password);
280
+
281
+ Request attemptRequest = createRequestBuilder(attemptUrl)
282
+ .post(RequestBody.create(attemptBody.toString(), JSON))
283
+ .build();
284
+
285
+ try (Response response = client.newCall(attemptRequest).execute()) {
286
+ String body = response.body().string();
287
+ Log.d(TAG, "Attempt password response: " + response.code());
288
+
289
+ if (!response.isSuccessful()) {
290
+ JSONObject errorJson = new JSONObject(body);
291
+ JSONArray errors = errorJson.optJSONArray("errors");
292
+ if (errors != null && errors.length() > 0) {
293
+ String message = errors.getJSONObject(0).optString("message", "Sign in failed");
294
+ call.reject(message);
295
+ } else {
296
+ call.reject("Sign in failed: " + body);
297
+ }
298
+ return;
299
+ }
300
+
301
+ JSONObject json = new JSONObject(body);
302
+
303
+ // Extract session and user from response
304
+ JSONObject clientObj = json.optJSONObject("client");
305
+ if (clientObj != null) {
306
+ clientToken = clientObj.optString("id", clientToken);
307
+
308
+ JSONArray sessions = clientObj.optJSONArray("sessions");
309
+ if (sessions != null && sessions.length() > 0) {
310
+ JSONObject session = sessions.getJSONObject(0);
311
+ JSONObject lastToken = session.optJSONObject("last_active_token");
312
+ if (lastToken != null) {
313
+ sessionToken = lastToken.optString("jwt", null);
314
+ }
315
+ currentUser = session.optJSONObject("user");
316
+ }
317
+ }
318
+
319
+ saveTokens();
320
+
321
+ JSObject result = new JSObject();
322
+ if (currentUser != null) {
323
+ result.put("user", convertUser(currentUser));
324
+ } else {
325
+ result.put("user", JSObject.NULL);
326
+ }
327
+ call.resolve(result);
328
+ }
329
+ } catch (Exception e) {
330
+ Log.e(TAG, "Sign in error", e);
331
+ call.reject("Sign in failed: " + e.getMessage());
332
+ }
333
+ });
36
334
  }
37
335
 
38
336
  @PluginMethod
39
- public void verifyEmailCode(PluginCall call) {
40
- call.reject(USE_WEB_MESSAGE);
337
+ public void getUser(PluginCall call) {
338
+ JSObject result = new JSObject();
339
+ if (currentUser != null) {
340
+ try {
341
+ result.put("user", convertUser(currentUser));
342
+ } catch (JSONException e) {
343
+ result.put("user", JSObject.NULL);
344
+ }
345
+ } else {
346
+ result.put("user", JSObject.NULL);
347
+ }
348
+ call.resolve(result);
41
349
  }
42
350
 
43
351
  @PluginMethod
44
- public void signInWithPassword(PluginCall call) {
45
- call.reject(USE_WEB_MESSAGE);
352
+ public void getToken(PluginCall call) {
353
+ JSObject result = new JSObject();
354
+ result.put("token", sessionToken != null ? sessionToken : JSObject.NULL);
355
+ call.resolve(result);
46
356
  }
47
357
 
48
358
  @PluginMethod
49
- public void signUp(PluginCall call) {
50
- call.reject(USE_WEB_MESSAGE);
359
+ public void signOut(PluginCall call) {
360
+ if (clientToken == null) {
361
+ clearTokens();
362
+ call.resolve();
363
+ return;
364
+ }
365
+
366
+ executor.execute(() -> {
367
+ try {
368
+ String url = getClerkApiUrl("/v1/client/sessions?_clerk_js_version=5.117.0");
369
+
370
+ Request request = createRequestBuilder(url)
371
+ .delete()
372
+ .build();
373
+
374
+ try (Response response = client.newCall(request).execute()) {
375
+ Log.d(TAG, "Sign out response: " + response.code());
376
+ // Clear tokens regardless of response
377
+ clearTokens();
378
+ call.resolve();
379
+ }
380
+ } catch (Exception e) {
381
+ Log.e(TAG, "Sign out error", e);
382
+ clearTokens();
383
+ call.resolve(); // Still resolve since we cleared local state
384
+ }
385
+ });
51
386
  }
52
387
 
53
388
  @PluginMethod
54
- public void verifySignUpEmail(PluginCall call) {
55
- call.reject(USE_WEB_MESSAGE);
389
+ public void signInWithEmail(PluginCall call) {
390
+ String email = call.getString("email");
391
+
392
+ if (email == null) {
393
+ call.reject("Email is required");
394
+ return;
395
+ }
396
+
397
+ executor.execute(() -> {
398
+ try {
399
+ String createUrl = getClerkApiUrl("/v1/client/sign_ins?_clerk_js_version=5.117.0");
400
+
401
+ JSONObject createBody = new JSONObject();
402
+ createBody.put("identifier", email);
403
+
404
+ Request request = createRequestBuilder(createUrl)
405
+ .post(RequestBody.create(createBody.toString(), JSON))
406
+ .build();
407
+
408
+ try (Response response = client.newCall(request).execute()) {
409
+ String body = response.body().string();
410
+ Log.d(TAG, "Sign in with email response: " + response.code());
411
+
412
+ if (!response.isSuccessful()) {
413
+ call.reject("Failed to start sign in: " + body);
414
+ return;
415
+ }
416
+
417
+ // TODO: Prepare email code flow
418
+ // For now, return that code is required
419
+ JSObject result = new JSObject();
420
+ result.put("requiresCode", true);
421
+ call.resolve(result);
422
+ }
423
+ } catch (Exception e) {
424
+ Log.e(TAG, "Sign in with email error", e);
425
+ call.reject("Failed: " + e.getMessage());
426
+ }
427
+ });
56
428
  }
57
429
 
58
430
  @PluginMethod
59
- public void getUser(PluginCall call) {
60
- call.reject(USE_WEB_MESSAGE);
431
+ public void verifyEmailCode(PluginCall call) {
432
+ // TODO: Implement email code verification
433
+ call.reject("Email code verification not yet implemented on Android");
61
434
  }
62
435
 
63
436
  @PluginMethod
64
- public void getToken(PluginCall call) {
65
- call.reject(USE_WEB_MESSAGE);
437
+ public void signUp(PluginCall call) {
438
+ String email = call.getString("emailAddress");
439
+ String password = call.getString("password");
440
+ String firstName = call.getString("firstName");
441
+ String lastName = call.getString("lastName");
442
+
443
+ if (email == null || password == null) {
444
+ call.reject("Email and password are required");
445
+ return;
446
+ }
447
+
448
+ executor.execute(() -> {
449
+ try {
450
+ String url = getClerkApiUrl("/v1/client/sign_ups?_clerk_js_version=5.117.0");
451
+
452
+ JSONObject body = new JSONObject();
453
+ body.put("email_address", email);
454
+ body.put("password", password);
455
+ if (firstName != null) body.put("first_name", firstName);
456
+ if (lastName != null) body.put("last_name", lastName);
457
+
458
+ Request request = createRequestBuilder(url)
459
+ .post(RequestBody.create(body.toString(), JSON))
460
+ .build();
461
+
462
+ try (Response response = client.newCall(request).execute()) {
463
+ String responseBody = response.body().string();
464
+ Log.d(TAG, "Sign up response: " + response.code());
465
+
466
+ if (!response.isSuccessful()) {
467
+ JSONObject errorJson = new JSONObject(responseBody);
468
+ JSONArray errors = errorJson.optJSONArray("errors");
469
+ if (errors != null && errors.length() > 0) {
470
+ String message = errors.getJSONObject(0).optString("message", "Sign up failed");
471
+ call.reject(message);
472
+ } else {
473
+ call.reject("Sign up failed: " + responseBody);
474
+ }
475
+ return;
476
+ }
477
+
478
+ JSONObject json = new JSONObject(responseBody);
479
+
480
+ // Check if email verification is required
481
+ JSONObject responseObj = json.optJSONObject("response");
482
+ boolean requiresVerification = false;
483
+ if (responseObj != null) {
484
+ JSONArray verifications = responseObj.optJSONArray("verifications");
485
+ if (verifications != null) {
486
+ requiresVerification = true;
487
+ }
488
+ }
489
+
490
+ JSObject result = new JSObject();
491
+ result.put("requiresVerification", requiresVerification);
492
+ if (currentUser != null) {
493
+ result.put("user", convertUser(currentUser));
494
+ } else {
495
+ result.put("user", JSObject.NULL);
496
+ }
497
+ call.resolve(result);
498
+ }
499
+ } catch (Exception e) {
500
+ Log.e(TAG, "Sign up error", e);
501
+ call.reject("Sign up failed: " + e.getMessage());
502
+ }
503
+ });
66
504
  }
67
505
 
68
506
  @PluginMethod
69
- public void signOut(PluginCall call) {
70
- call.reject(USE_WEB_MESSAGE);
507
+ public void verifySignUpEmail(PluginCall call) {
508
+ call.reject("Sign up email verification not yet implemented on Android");
71
509
  }
72
510
 
73
511
  @PluginMethod
74
512
  public void updateUser(PluginCall call) {
75
- call.reject(USE_WEB_MESSAGE);
513
+ call.reject("Update user not yet implemented on Android");
76
514
  }
77
515
 
78
516
  @PluginMethod
79
517
  public void requestPasswordReset(PluginCall call) {
80
- call.reject(USE_WEB_MESSAGE);
518
+ call.reject("Password reset not yet implemented on Android");
81
519
  }
82
520
 
83
521
  @PluginMethod
84
522
  public void resetPassword(PluginCall call) {
85
- call.reject(USE_WEB_MESSAGE);
523
+ call.reject("Password reset not yet implemented on Android");
86
524
  }
87
525
 
88
526
  @PluginMethod
89
527
  public void refreshSession(PluginCall call) {
90
- call.reject(USE_WEB_MESSAGE);
528
+ call.reject("Refresh session not yet implemented on Android");
529
+ }
530
+
531
+ private JSObject convertUser(JSONObject clerkUser) throws JSONException {
532
+ JSObject user = new JSObject();
533
+ user.put("id", clerkUser.optString("id", null));
534
+ user.put("firstName", clerkUser.optString("first_name", null));
535
+ user.put("lastName", clerkUser.optString("last_name", null));
536
+ user.put("imageUrl", clerkUser.optString("image_url", null));
537
+ user.put("username", clerkUser.optString("username", null));
538
+
539
+ // Get primary email
540
+ JSONArray emailAddresses = clerkUser.optJSONArray("email_addresses");
541
+ if (emailAddresses != null && emailAddresses.length() > 0) {
542
+ String primaryEmailId = clerkUser.optString("primary_email_address_id", null);
543
+ for (int i = 0; i < emailAddresses.length(); i++) {
544
+ JSONObject emailObj = emailAddresses.getJSONObject(i);
545
+ if (primaryEmailId != null && primaryEmailId.equals(emailObj.optString("id"))) {
546
+ user.put("emailAddress", emailObj.optString("email_address", null));
547
+ break;
548
+ }
549
+ }
550
+ // Fallback to first email if no primary found
551
+ if (user.optString("emailAddress", null) == null) {
552
+ user.put("emailAddress", emailAddresses.getJSONObject(0).optString("email_address", null));
553
+ }
554
+ }
555
+
556
+ return user;
91
557
  }
92
558
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainon-inc/capacitor-clerk-native",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Capacitor plugin for Clerk native authentication using bridge pattern to integrate Clerk iOS/Android SDKs with CocoaPods/Gradle",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",