@trainon-inc/capacitor-clerk-native 1.22.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.
package/README.md CHANGED
@@ -339,19 +339,21 @@ function Profile() {
339
339
  └─────────────────────────────────────────────────┘
340
340
  ```
341
341
 
342
- ### Android Architecture
342
+ ### Android Architecture (Web Provider)
343
343
 
344
344
  ```
345
345
  ┌─────────────────────────────────────────────────┐
346
346
  │ JavaScript/React (Capacitor WebView) │
347
- │ - Uses capacitor-clerk-native hooks
348
- └─────────────────┬───────────────────────────────┘
349
- │ Capacitor Bridge
350
- ┌─────────────────▼───────────────────────────────┐
351
- │ ClerkNativePlugin (Gradle Module) │
352
- │ - Android library module │
353
- - Receives calls from JavaScript
354
- │ - Can integrate with Clerk Android SDK
347
+ │ - Uses @clerk/clerk-react (web provider)
348
+ │ - Full Clerk functionality via web SDK │
349
+ └─────────────────────────────────────────────────┘
350
+ (No native plugin needed for auth)
351
+
352
+ ┌─────────────────────────────────────────────────┐
353
+ ClerkNativePlugin (Gradle Module) - Stub
354
+ │ - Exists for Capacitor plugin registration
355
+ │ - Returns "use web provider" for all methods │
356
+ │ - No native Clerk SDK dependency │
355
357
  └─────────────────────────────────────────────────┘
356
358
  ```
357
359
 
@@ -414,80 +416,51 @@ function Profile() {
414
416
 
415
417
  ## Android Setup
416
418
 
417
- Android is fully supported with the native Clerk Android SDK integration. Unlike iOS, Android doesn't require a bridge pattern - the plugin directly integrates with the Clerk Android SDK.
418
-
419
- ### Prerequisites
420
-
421
- - ✅ A [Clerk account](https://dashboard.clerk.com/sign-up)
422
- - ✅ A Clerk application set up in the dashboard
423
- - ✅ **Native API enabled** in Clerk Dashboard → Settings → Native Applications
424
- - ✅ Android Studio with Gradle 8.7+ and AGP 8.5+
425
- - ✅ JDK 17 or higher
426
-
427
- ### 1. Register Your Android App with Clerk
428
-
429
- 1. Go to the [**Native Applications**](https://dashboard.clerk.com/last-active?path=native-applications) page in Clerk Dashboard
430
- 2. Click **"Add Application"**
431
- 3. Select **Android** tab
432
- 4. Enter your Android app details:
433
- - **Package Name**: Your app's package name (e.g., `com.trainon.member`)
434
- - **SHA256 Fingerprint**: Your app's signing certificate fingerprint (get it with `./gradlew signingReport`)
435
-
436
- ### 2. Sync Capacitor
437
-
438
- After installing the plugin, sync your Android project:
419
+ **Important**: On Android, this plugin provides a stub implementation. Android WebViews work well with web-based authentication (unlike iOS which has cookie issues), so **Android should use the web Clerk provider (`@clerk/clerk-react`)** instead of the native plugin.
439
420
 
440
- ```bash
441
- npx cap sync android
442
- ```
421
+ ### Why Web Provider for Android?
443
422
 
444
- This will:
445
- - Add the plugin to `capacitor.settings.gradle`
446
- - Add the plugin dependency to `capacitor.build.gradle`
447
- - Include the Clerk Android SDK automatically
423
+ - ✅ Android WebViews handle cookies correctly - no authentication issues
424
+ - The Clerk Android SDK is still in early stages (v0.1.x) with evolving APIs
425
+ - Using `@clerk/clerk-react` provides a stable, well-tested experience
426
+ - Simpler setup - no native configuration required
448
427
 
449
- ### 3. Configure Clerk in Your App
428
+ ### Recommended App Setup
450
429
 
451
- The plugin automatically handles Clerk initialization when you call `configure()` from JavaScript. No additional Android-specific setup is required!
430
+ Configure your app to use the native plugin only on iOS:
452
431
 
453
432
  ```typescript
454
- import { ClerkNative } from '@trainon-inc/capacitor-clerk-native';
455
-
456
- // Configure Clerk (call once at app startup)
457
- await ClerkNative.configure({
458
- publishableKey: 'pk_test_your_clerk_key_here'
459
- });
433
+ import { Capacitor } from "@capacitor/core";
434
+ import { ClerkProvider as WebClerkProvider } from "@clerk/clerk-react";
435
+ import { ClerkProvider as NativeClerkProvider } from "@trainon-inc/capacitor-clerk-native";
436
+
437
+ // Use native Clerk only on iOS (due to WebView cookie issues)
438
+ // Android WebViews work fine with web Clerk
439
+ const isIOS = Capacitor.getPlatform() === "ios";
440
+ const ClerkProvider = isIOS ? NativeClerkProvider : WebClerkProvider;
441
+
442
+ export function App() {
443
+ const clerkProps = isIOS
444
+ ? { publishableKey: "pk_test_..." }
445
+ : {
446
+ publishableKey: "pk_test_...",
447
+ signInFallbackRedirectUrl: "/home",
448
+ signUpFallbackRedirectUrl: "/home",
449
+ };
460
450
 
461
- // Load and check for existing session
462
- const { user } = await ClerkNative.load();
451
+ return (
452
+ <ClerkProvider {...clerkProps}>
453
+ <YourApp />
454
+ </ClerkProvider>
455
+ );
456
+ }
463
457
  ```
464
458
 
465
- ### 4. Plugin Features
466
-
467
- The Android implementation supports all authentication methods:
468
-
469
- | Method | Description |
470
- |--------|-------------|
471
- | `configure()` | Initialize Clerk with publishable key |
472
- | `load()` | Load Clerk and check for existing session |
473
- | `signInWithPassword()` | Sign in with email and password |
474
- | `signInWithEmail()` | Start email code sign in flow |
475
- | `verifyEmailCode()` | Verify email code |
476
- | `signUp()` | Create a new account |
477
- | `verifySignUpEmail()` | Verify sign up email |
478
- | `getUser()` | Get current user |
479
- | `getToken()` | Get authentication token |
480
- | `signOut()` | Sign out current user |
481
- | `updateUser()` | Update user profile |
482
- | `requestPasswordReset()` | Request password reset |
483
- | `resetPassword()` | Reset password with code |
484
- | `refreshSession()` | Refresh session token |
485
-
486
- ### 5. Build Requirements
459
+ ### Build Requirements
487
460
 
488
461
  - **Gradle**: 8.11.1+
489
- - **Android Gradle Plugin**: 8.7.2+
490
- - **Java/Kotlin**: 17+
462
+ - **Android Gradle Plugin**: 8.5.0+
463
+ - **Java**: 17+
491
464
  - **Min SDK**: 23 (Android 6.0)
492
465
  - **Target SDK**: 35 (Android 15)
493
466
 
@@ -499,14 +472,14 @@ Could not resolve project :trainon-inc-capacitor-clerk-native
499
472
  No matching variant of project was found. No variants exist.
500
473
  ```
501
474
 
502
- **Solution**: Ensure you have the latest version of the plugin which includes the required build files:
475
+ **Solution**: Update to the latest plugin version:
503
476
  ```bash
504
477
  npm update @trainon-inc/capacitor-clerk-native
505
478
  npx cap sync android
506
479
  ```
507
480
 
508
481
  #### "invalid source release: 21" error
509
- The plugin uses Java 17 by default. Ensure your Android Studio uses JDK 17+:
482
+ The plugin uses Java 17. Ensure your Android Studio uses JDK 17+:
510
483
  - **File → Project Structure → SDK Location → Gradle JDK** → Select JDK 17+
511
484
 
512
485
  #### Gradle sync fails
@@ -514,12 +487,6 @@ The plugin uses Java 17 by default. Ensure your Android Studio uses JDK 17+:
514
487
  - Invalidate caches: **File → Invalidate Caches / Restart**
515
488
  - Delete `.gradle` folder and re-sync
516
489
 
517
- #### Network errors
518
- Ensure INTERNET permission is in your `AndroidManifest.xml`:
519
- ```xml
520
- <uses-permission android:name="android.permission.INTERNET" />
521
- ```
522
-
523
490
  ## Contributing
524
491
 
525
492
  Contributions are welcome! This plugin was created to solve a real problem we encountered, and we'd love to make it better.
@@ -3,7 +3,6 @@ ext {
3
3
  androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
4
  androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
5
  androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
6
- kotlinVersion = project.hasProperty('kotlinVersion') ? rootProject.ext.kotlinVersion : '1.9.25'
7
6
  }
8
7
 
9
8
  buildscript {
@@ -13,12 +12,10 @@ buildscript {
13
12
  }
14
13
  dependencies {
15
14
  classpath 'com.android.tools.build:gradle:8.7.2'
16
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25"
17
15
  }
18
16
  }
19
17
 
20
18
  apply plugin: 'com.android.library'
21
- apply plugin: 'kotlin-android'
22
19
 
23
20
  android {
24
21
  namespace "com.trainon.capacitor.clerk"
@@ -43,9 +40,6 @@ android {
43
40
  sourceCompatibility JavaVersion.VERSION_17
44
41
  targetCompatibility JavaVersion.VERSION_17
45
42
  }
46
- kotlinOptions {
47
- jvmTarget = '17'
48
- }
49
43
  }
50
44
 
51
45
  repositories {
@@ -58,14 +52,9 @@ dependencies {
58
52
  implementation project(':capacitor-android')
59
53
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
60
54
 
61
- // Kotlin
62
- implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion"
63
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
64
-
65
- // Clerk Android SDK
66
- implementation "com.clerk:clerk-android:0.8.0"
55
+ // OkHttp for making HTTP requests to Clerk API
56
+ implementation 'com.squareup.okhttp3:okhttp:4.12.0'
67
57
 
68
- // Testing
69
58
  testImplementation "junit:junit:$junitVersion"
70
59
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
71
60
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -0,0 +1,558 @@
1
+ package com.trainon.capacitor.clerk;
2
+
3
+ import android.content.Context;
4
+ import android.content.SharedPreferences;
5
+ import android.util.Log;
6
+
7
+ import com.getcapacitor.JSObject;
8
+ import com.getcapacitor.Plugin;
9
+ import com.getcapacitor.PluginCall;
10
+ import com.getcapacitor.PluginMethod;
11
+ import com.getcapacitor.annotation.CapacitorPlugin;
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
+
27
+ /**
28
+ * Android implementation of Clerk Native plugin.
29
+ *
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.
32
+ */
33
+ @CapacitorPlugin(name = "ClerkNative")
34
+ public class ClerkNativePlugin extends Plugin {
35
+
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
+ }
96
+
97
+ @PluginMethod
98
+ public void configure(PluginCall call) {
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
+
121
+ call.resolve();
122
+ }
123
+
124
+ @PluginMethod
125
+ public void load(PluginCall call) {
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
+ }
229
+ }
230
+
231
+ @PluginMethod
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
+ });
334
+ }
335
+
336
+ @PluginMethod
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);
349
+ }
350
+
351
+ @PluginMethod
352
+ public void getToken(PluginCall call) {
353
+ JSObject result = new JSObject();
354
+ result.put("token", sessionToken != null ? sessionToken : JSObject.NULL);
355
+ call.resolve(result);
356
+ }
357
+
358
+ @PluginMethod
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
+ });
386
+ }
387
+
388
+ @PluginMethod
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
+ });
428
+ }
429
+
430
+ @PluginMethod
431
+ public void verifyEmailCode(PluginCall call) {
432
+ // TODO: Implement email code verification
433
+ call.reject("Email code verification not yet implemented on Android");
434
+ }
435
+
436
+ @PluginMethod
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
+ });
504
+ }
505
+
506
+ @PluginMethod
507
+ public void verifySignUpEmail(PluginCall call) {
508
+ call.reject("Sign up email verification not yet implemented on Android");
509
+ }
510
+
511
+ @PluginMethod
512
+ public void updateUser(PluginCall call) {
513
+ call.reject("Update user not yet implemented on Android");
514
+ }
515
+
516
+ @PluginMethod
517
+ public void requestPasswordReset(PluginCall call) {
518
+ call.reject("Password reset not yet implemented on Android");
519
+ }
520
+
521
+ @PluginMethod
522
+ public void resetPassword(PluginCall call) {
523
+ call.reject("Password reset not yet implemented on Android");
524
+ }
525
+
526
+ @PluginMethod
527
+ public void refreshSession(PluginCall call) {
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;
557
+ }
558
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trainon-inc/capacitor-clerk-native",
3
- "version": "1.22.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",
@@ -1,509 +0,0 @@
1
- package com.trainon.capacitor.clerk
2
-
3
- import android.util.Log
4
- import com.clerk.android.Clerk
5
- import com.clerk.android.models.Session
6
- import com.clerk.android.models.SignIn
7
- import com.clerk.android.models.SignUp
8
- import com.clerk.android.models.User
9
- import com.getcapacitor.JSObject
10
- import com.getcapacitor.Plugin
11
- import com.getcapacitor.PluginCall
12
- import com.getcapacitor.PluginMethod
13
- import com.getcapacitor.annotation.CapacitorPlugin
14
- import kotlinx.coroutines.CoroutineScope
15
- import kotlinx.coroutines.Dispatchers
16
- import kotlinx.coroutines.SupervisorJob
17
- import kotlinx.coroutines.launch
18
-
19
- @CapacitorPlugin(name = "ClerkNative")
20
- class ClerkNativePlugin : Plugin() {
21
-
22
- private val TAG = "ClerkNativePlugin"
23
- private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
24
- private var isConfigured = false
25
-
26
- @PluginMethod
27
- fun configure(call: PluginCall) {
28
- val publishableKey = call.getString("publishableKey")
29
-
30
- if (publishableKey.isNullOrEmpty()) {
31
- call.reject("Must provide publishableKey")
32
- return
33
- }
34
-
35
- try {
36
- // Initialize Clerk with the publishable key
37
- Clerk.configure(context, publishableKey)
38
- isConfigured = true
39
- Log.d(TAG, "Clerk configured successfully")
40
- call.resolve()
41
- } catch (e: Exception) {
42
- Log.e(TAG, "Failed to configure Clerk", e)
43
- call.reject("Failed to configure Clerk: ${e.message}")
44
- }
45
- }
46
-
47
- @PluginMethod
48
- fun load(call: PluginCall) {
49
- if (!isConfigured) {
50
- call.reject("Clerk not configured. Call configure() first.")
51
- return
52
- }
53
-
54
- scope.launch {
55
- try {
56
- val user = Clerk.user
57
- val result = JSObject()
58
- if (user != null) {
59
- result.put("user", userToJSObject(user))
60
- } else {
61
- result.put("user", null)
62
- }
63
- call.resolve(result)
64
- } catch (e: Exception) {
65
- Log.e(TAG, "Failed to load Clerk", e)
66
- call.reject("Failed to load Clerk: ${e.message}")
67
- }
68
- }
69
- }
70
-
71
- @PluginMethod
72
- fun signInWithEmail(call: PluginCall) {
73
- val email = call.getString("email")
74
-
75
- if (email.isNullOrEmpty()) {
76
- call.reject("Must provide email")
77
- return
78
- }
79
-
80
- scope.launch {
81
- try {
82
- val signIn = SignIn.create(identifier = email)
83
- signIn.onSuccess { si ->
84
- // Prepare email code verification
85
- si.prepareFirstFactor(
86
- strategy = SignIn.PrepareFirstFactorStrategy.EmailCode(
87
- emailAddressId = si.supportedFirstFactors
88
- ?.filterIsInstance<SignIn.Factor.EmailCode>()
89
- ?.firstOrNull()
90
- ?.emailAddressId ?: ""
91
- )
92
- ).onSuccess {
93
- val result = JSObject()
94
- result.put("requiresCode", true)
95
- call.resolve(result)
96
- }.onFailure { error ->
97
- call.reject("Failed to prepare email code: ${error.message}")
98
- }
99
- }.onFailure { error ->
100
- call.reject("Sign in with email failed: ${error.message}")
101
- }
102
- } catch (e: Exception) {
103
- Log.e(TAG, "Sign in with email failed", e)
104
- call.reject("Sign in with email failed: ${e.message}")
105
- }
106
- }
107
- }
108
-
109
- @PluginMethod
110
- fun verifyEmailCode(call: PluginCall) {
111
- val code = call.getString("code")
112
-
113
- if (code.isNullOrEmpty()) {
114
- call.reject("Must provide code")
115
- return
116
- }
117
-
118
- scope.launch {
119
- try {
120
- val signIn = Clerk.client?.signIn
121
- if (signIn == null) {
122
- call.reject("No active sign in session")
123
- return@launch
124
- }
125
-
126
- signIn.attemptFirstFactor(
127
- strategy = SignIn.AttemptFirstFactorStrategy.EmailCode(code = code)
128
- ).onSuccess { si ->
129
- if (si.status == SignIn.Status.COMPLETE) {
130
- // Set active session
131
- Clerk.client?.activeSessions?.firstOrNull()?.let { session ->
132
- Clerk.setActive(sessionId = session.id)
133
- }
134
-
135
- val user = Clerk.user
136
- val result = JSObject()
137
- result.put("user", user?.let { userToJSObject(it) })
138
- call.resolve(result)
139
- } else {
140
- call.reject("Verification incomplete")
141
- }
142
- }.onFailure { error ->
143
- call.reject("Email code verification failed: ${error.message}")
144
- }
145
- } catch (e: Exception) {
146
- Log.e(TAG, "Email code verification failed", e)
147
- call.reject("Email code verification failed: ${e.message}")
148
- }
149
- }
150
- }
151
-
152
- @PluginMethod
153
- fun signInWithPassword(call: PluginCall) {
154
- val email = call.getString("email")
155
- val password = call.getString("password")
156
-
157
- if (email.isNullOrEmpty() || password.isNullOrEmpty()) {
158
- call.reject("Must provide email and password")
159
- return
160
- }
161
-
162
- scope.launch {
163
- try {
164
- val signIn = SignIn.create(identifier = email)
165
- signIn.onSuccess { si ->
166
- si.attemptFirstFactor(
167
- strategy = SignIn.AttemptFirstFactorStrategy.Password(password = password)
168
- ).onSuccess { completedSignIn ->
169
- if (completedSignIn.status == SignIn.Status.COMPLETE) {
170
- // Set active session
171
- completedSignIn.createdSessionId?.let { sessionId ->
172
- Clerk.setActive(sessionId = sessionId)
173
- }
174
-
175
- // Get user after sign in
176
- val user = Clerk.user
177
- val result = JSObject()
178
- result.put("user", user?.let { userToJSObject(it) })
179
- call.resolve(result)
180
- } else {
181
- call.reject("Sign in incomplete")
182
- }
183
- }.onFailure { error ->
184
- call.reject("Sign in failed: ${error.message}")
185
- }
186
- }.onFailure { error ->
187
- call.reject("Sign in failed: ${error.message}")
188
- }
189
- } catch (e: Exception) {
190
- Log.e(TAG, "Sign in with password failed", e)
191
- call.reject("Sign in with password failed: ${e.message}")
192
- }
193
- }
194
- }
195
-
196
- @PluginMethod
197
- fun signUp(call: PluginCall) {
198
- val emailAddress = call.getString("emailAddress")
199
- val password = call.getString("password")
200
- val firstName = call.getString("firstName")
201
- val lastName = call.getString("lastName")
202
-
203
- if (emailAddress.isNullOrEmpty() || password.isNullOrEmpty()) {
204
- call.reject("Must provide emailAddress and password")
205
- return
206
- }
207
-
208
- scope.launch {
209
- try {
210
- val signUp = SignUp.create(
211
- emailAddress = emailAddress,
212
- password = password,
213
- firstName = firstName,
214
- lastName = lastName
215
- )
216
-
217
- signUp.onSuccess { su ->
218
- when (su.status) {
219
- SignUp.Status.COMPLETE -> {
220
- // Sign up complete, set active session
221
- su.createdSessionId?.let { sessionId ->
222
- Clerk.setActive(sessionId = sessionId)
223
- }
224
-
225
- val user = Clerk.user
226
- val result = JSObject()
227
- result.put("user", user?.let { userToJSObject(it) })
228
- result.put("requiresVerification", false)
229
- call.resolve(result)
230
- }
231
- SignUp.Status.MISSING_REQUIREMENTS -> {
232
- // Needs email verification
233
- su.prepareEmailAddressVerification().onSuccess {
234
- val result = JSObject()
235
- result.put("user", null)
236
- result.put("requiresVerification", true)
237
- call.resolve(result)
238
- }.onFailure { error ->
239
- call.reject("Failed to prepare email verification: ${error.message}")
240
- }
241
- }
242
- else -> {
243
- call.reject("Sign up incomplete: ${su.status}")
244
- }
245
- }
246
- }.onFailure { error ->
247
- call.reject("Sign up failed: ${error.message}")
248
- }
249
- } catch (e: Exception) {
250
- Log.e(TAG, "Sign up failed", e)
251
- call.reject("Sign up failed: ${e.message}")
252
- }
253
- }
254
- }
255
-
256
- @PluginMethod
257
- fun verifySignUpEmail(call: PluginCall) {
258
- val code = call.getString("code")
259
-
260
- if (code.isNullOrEmpty()) {
261
- call.reject("Must provide code")
262
- return
263
- }
264
-
265
- scope.launch {
266
- try {
267
- val signUp = Clerk.client?.signUp
268
- if (signUp == null) {
269
- call.reject("No active sign up session")
270
- return@launch
271
- }
272
-
273
- signUp.attemptEmailAddressVerification(code = code)
274
- .onSuccess { su ->
275
- if (su.status == SignUp.Status.COMPLETE) {
276
- su.createdSessionId?.let { sessionId ->
277
- Clerk.setActive(sessionId = sessionId)
278
- }
279
-
280
- val user = Clerk.user
281
- val result = JSObject()
282
- result.put("user", user?.let { userToJSObject(it) })
283
- call.resolve(result)
284
- } else {
285
- call.reject("Verification incomplete")
286
- }
287
- }.onFailure { error ->
288
- call.reject("Email verification failed: ${error.message}")
289
- }
290
- } catch (e: Exception) {
291
- Log.e(TAG, "Email verification failed", e)
292
- call.reject("Email verification failed: ${e.message}")
293
- }
294
- }
295
- }
296
-
297
- @PluginMethod
298
- fun getUser(call: PluginCall) {
299
- scope.launch {
300
- try {
301
- val user = Clerk.user
302
- val result = JSObject()
303
- if (user != null) {
304
- result.put("user", userToJSObject(user))
305
- } else {
306
- result.put("user", null)
307
- }
308
- call.resolve(result)
309
- } catch (e: Exception) {
310
- Log.e(TAG, "Get user failed", e)
311
- call.reject("Get user failed: ${e.message}")
312
- }
313
- }
314
- }
315
-
316
- @PluginMethod
317
- fun getToken(call: PluginCall) {
318
- scope.launch {
319
- try {
320
- val session = Clerk.session
321
- if (session == null) {
322
- val result = JSObject()
323
- result.put("token", null)
324
- call.resolve(result)
325
- return@launch
326
- }
327
-
328
- session.getToken().onSuccess { tokenResult ->
329
- val result = JSObject()
330
- result.put("token", tokenResult?.jwt)
331
- call.resolve(result)
332
- }.onFailure { error ->
333
- call.reject("Get token failed: ${error.message}")
334
- }
335
- } catch (e: Exception) {
336
- Log.e(TAG, "Get token failed", e)
337
- call.reject("Get token failed: ${e.message}")
338
- }
339
- }
340
- }
341
-
342
- @PluginMethod
343
- fun signOut(call: PluginCall) {
344
- scope.launch {
345
- try {
346
- val session = Clerk.session
347
- if (session != null) {
348
- session.revoke().onSuccess {
349
- call.resolve()
350
- }.onFailure { error ->
351
- call.reject("Sign out failed: ${error.message}")
352
- }
353
- } else {
354
- // No active session, consider it signed out
355
- call.resolve()
356
- }
357
- } catch (e: Exception) {
358
- Log.e(TAG, "Sign out failed", e)
359
- call.reject("Sign out failed: ${e.message}")
360
- }
361
- }
362
- }
363
-
364
- @PluginMethod
365
- fun updateUser(call: PluginCall) {
366
- val firstName = call.getString("firstName")
367
- val lastName = call.getString("lastName")
368
-
369
- scope.launch {
370
- try {
371
- val user = Clerk.user
372
- if (user == null) {
373
- call.reject("No user signed in")
374
- return@launch
375
- }
376
-
377
- user.update(
378
- firstName = firstName,
379
- lastName = lastName
380
- ).onSuccess { updatedUser ->
381
- val result = JSObject()
382
- result.put("user", userToJSObject(updatedUser))
383
- call.resolve(result)
384
- }.onFailure { error ->
385
- call.reject("Update user failed: ${error.message}")
386
- }
387
- } catch (e: Exception) {
388
- Log.e(TAG, "Update user failed", e)
389
- call.reject("Update user failed: ${e.message}")
390
- }
391
- }
392
- }
393
-
394
- @PluginMethod
395
- fun requestPasswordReset(call: PluginCall) {
396
- val email = call.getString("email")
397
-
398
- if (email.isNullOrEmpty()) {
399
- call.reject("Must provide email")
400
- return
401
- }
402
-
403
- scope.launch {
404
- try {
405
- val signIn = SignIn.create(identifier = email)
406
- signIn.onSuccess { si ->
407
- si.prepareFirstFactor(
408
- strategy = SignIn.PrepareFirstFactorStrategy.ResetPasswordEmailCode(
409
- emailAddressId = si.supportedFirstFactors
410
- ?.filterIsInstance<SignIn.Factor.ResetPasswordEmailCode>()
411
- ?.firstOrNull()
412
- ?.emailAddressId ?: ""
413
- )
414
- ).onSuccess {
415
- call.resolve()
416
- }.onFailure { error ->
417
- call.reject("Failed to request password reset: ${error.message}")
418
- }
419
- }.onFailure { error ->
420
- call.reject("Failed to request password reset: ${error.message}")
421
- }
422
- } catch (e: Exception) {
423
- Log.e(TAG, "Request password reset failed", e)
424
- call.reject("Request password reset failed: ${e.message}")
425
- }
426
- }
427
- }
428
-
429
- @PluginMethod
430
- fun resetPassword(call: PluginCall) {
431
- val code = call.getString("code")
432
- val newPassword = call.getString("newPassword")
433
-
434
- if (code.isNullOrEmpty() || newPassword.isNullOrEmpty()) {
435
- call.reject("Must provide code and newPassword")
436
- return
437
- }
438
-
439
- scope.launch {
440
- try {
441
- val signIn = Clerk.client?.signIn
442
- if (signIn == null) {
443
- call.reject("No active sign in session")
444
- return@launch
445
- }
446
-
447
- signIn.attemptFirstFactor(
448
- strategy = SignIn.AttemptFirstFactorStrategy.ResetPasswordEmailCode(code = code)
449
- ).onSuccess { si ->
450
- si.resetPassword(newPassword = newPassword).onSuccess { completedSignIn ->
451
- if (completedSignIn.status == SignIn.Status.COMPLETE) {
452
- completedSignIn.createdSessionId?.let { sessionId ->
453
- Clerk.setActive(sessionId = sessionId)
454
- }
455
- call.resolve()
456
- } else {
457
- call.reject("Password reset incomplete")
458
- }
459
- }.onFailure { error ->
460
- call.reject("Failed to reset password: ${error.message}")
461
- }
462
- }.onFailure { error ->
463
- call.reject("Failed to verify reset code: ${error.message}")
464
- }
465
- } catch (e: Exception) {
466
- Log.e(TAG, "Reset password failed", e)
467
- call.reject("Reset password failed: ${e.message}")
468
- }
469
- }
470
- }
471
-
472
- @PluginMethod
473
- fun refreshSession(call: PluginCall) {
474
- scope.launch {
475
- try {
476
- val session = Clerk.session
477
- if (session == null) {
478
- val result = JSObject()
479
- result.put("token", null)
480
- call.resolve(result)
481
- return@launch
482
- }
483
-
484
- // Force refresh the token
485
- session.getToken(forceRefresh = true).onSuccess { tokenResult ->
486
- val result = JSObject()
487
- result.put("token", tokenResult?.jwt)
488
- call.resolve(result)
489
- }.onFailure { error ->
490
- call.reject("Refresh session failed: ${error.message}")
491
- }
492
- } catch (e: Exception) {
493
- Log.e(TAG, "Refresh session failed", e)
494
- call.reject("Refresh session failed: ${e.message}")
495
- }
496
- }
497
- }
498
-
499
- private fun userToJSObject(user: User): JSObject {
500
- val jsObject = JSObject()
501
- jsObject.put("id", user.id)
502
- jsObject.put("firstName", user.firstName)
503
- jsObject.put("lastName", user.lastName)
504
- jsObject.put("emailAddress", user.primaryEmailAddress?.emailAddress)
505
- jsObject.put("imageUrl", user.imageUrl)
506
- jsObject.put("username", user.username)
507
- return jsObject
508
- }
509
- }