@trainon-inc/capacitor-clerk-native
v1.24.0
Published
Capacitor plugin for Clerk native authentication using bridge pattern to integrate Clerk iOS/Android SDKs with CocoaPods/Gradle
Downloads
185
Maintainers
Readme
capacitor-clerk-native
A Capacitor plugin for native Clerk authentication on iOS and Android using the bridge pattern to seamlessly integrate Clerk's native SDKs with CocoaPods/Gradle-based Capacitor plugins.
The Problem This Solves
When using Clerk authentication in Capacitor iOS apps, WebView cookie limitations cause authentication failures with the error:
Browser unauthenticated (dev_browser_unauthenticated)Additionally, Clerk's iOS SDK is only available via Swift Package Manager (SPM), but Capacitor plugins use CocoaPods, creating a dependency conflict.
The Solution
This plugin uses a bridge pattern to resolve the CocoaPods ↔ SPM conflict:
- Plugin (CocoaPods): Defines a protocol interface without depending on Clerk
- App Target (SPM): Implements the bridge using Clerk's native SDK
- AppDelegate: Connects them at runtime
This allows your Capacitor app to use Clerk's native iOS/Android SDKs (following the official Clerk iOS Quickstart), avoiding WebView cookie issues entirely.
Why This Approach?
- ✅ Uses official Clerk iOS SDK - same as native Swift apps
- ✅ Follows Clerk's best practices for iOS integration
- ✅ No WebView limitations - native authentication flows
- ✅ CocoaPods compatible - works with Capacitor's build system
- ✅ Reusable - bridge pattern can be adapted for other native SDKs
Installation
From npm (Recommended)
npm install @trainon-inc/capacitor-clerk-native
# or
pnpm add @trainon-inc/capacitor-clerk-native
# or
yarn add @trainon-inc/capacitor-clerk-nativeFrom GitHub Packages
Alternatively, install from GitHub Packages:
- Create a
.npmrcfile in your project root:
@trainon-inc:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN- Install the package:
npm install @trainon-inc/capacitor-clerk-nativeNote: You'll need a GitHub Personal Access Token with
read:packagespermission. Create one here
iOS Setup
Prerequisites
Before starting, ensure you have:
- ✅ A Clerk account
- ✅ A Clerk application set up in the dashboard
- ✅ Native API enabled in Clerk Dashboard → Settings → Native Applications
Important: You must enable the Native API in your Clerk Dashboard before proceeding. This is required for native iOS integration.
1. Register Your iOS App with Clerk
- Go to the Native Applications page in Clerk Dashboard
- Click "Add Application"
- Enter your iOS app details:
- App ID Prefix: Found in your Apple Developer account or Xcode (Team ID)
- Bundle ID: Your app's bundle identifier (e.g.,
com.trainon.member)
- Note down your Frontend API URL (you'll need this for associated domains)
2. Add Associated Domain Capability
This is required for seamless authentication flows:
- In Xcode, select your project → Select your App target
- Go to "Signing & Capabilities" tab
- Click "+ Capability" → Add "Associated Domains"
- Under Associated Domains, add:
webcredentials:{YOUR_FRONTEND_API_URL}- Example:
webcredentials:guiding-serval-42.clerk.accounts.dev - Get your Frontend API URL from the Native Applications page in Clerk Dashboard
- Example:
3. Add Clerk iOS SDK to Your App Target
- Open your iOS project in Xcode
- Select your App target
- Go to "Package Dependencies" tab
- Click "+" → "Add Package Dependency"
- Enter:
https://github.com/clerk/clerk-ios - Select version
0.69.0or later - Link to your App target
4. Create the Bridge Implementation
Create a file ClerkBridgeImpl.swift in your app's directory:
import Foundation
import Clerk
import capacitor_clerk_native
@MainActor
class ClerkBridgeImpl: NSObject, ClerkBridge {
private let clerk = Clerk.shared
func signIn(withEmail email: String, password: String, completion: @escaping (String?, Error?) -> Void) {
Task { @MainActor in
do {
let signIn = try await SignIn.create(strategy: .identifier(email))
let result = try await signIn.attemptFirstFactor(strategy: .password(password: password))
if result.status == .complete {
if let user = clerk.user {
completion(user.id, nil)
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "Sign in completed but no user found"]))
}
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "Sign in not complete"]))
}
} catch {
completion(nil, error)
}
}
}
func signUp(withEmail email: String, password: String, completion: @escaping (String?, Error?) -> Void) {
Task { @MainActor in
do {
let signUp = try await SignUp.create(
strategy: .standard(emailAddress: email, password: password)
)
if let user = clerk.user {
completion(user.id, nil)
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "Sign up completed but no user found"]))
}
} catch {
completion(nil, error)
}
}
}
func signOut(completion: @escaping (Error?) -> Void) {
Task { @MainActor in
do {
try await clerk.signOut()
completion(nil)
} catch {
completion(error)
}
}
}
func getToken(completion: @escaping (String?, Error?) -> Void) {
Task { @MainActor in
do {
if let session = clerk.session {
if let token = try await session.getToken() {
completion(token.jwt, nil)
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "No token available"]))
}
} else {
completion(nil, NSError(domain: "ClerkBridge", code: -1, userInfo: [NSLocalizedDescriptionKey: "No active session"]))
}
} catch {
completion(nil, error)
}
}
}
func getUser(completion: @escaping ([String: Any]?, Error?) -> Void) {
Task { @MainActor in
if let user = clerk.user {
let userDict: [String: Any] = [
"id": user.id,
"firstName": user.firstName ?? NSNull(),
"lastName": user.lastName ?? NSNull(),
"emailAddress": user.primaryEmailAddress?.emailAddress ?? NSNull(),
"imageUrl": user.imageUrl ?? NSNull(),
"username": user.username ?? NSNull()
]
completion(userDict, nil)
} else {
completion(nil, nil)
}
}
}
func isSignedIn(completion: @escaping (Bool, Error?) -> Void) {
Task { @MainActor in
completion(clerk.user != nil, nil)
}
}
}5. Update AppDelegate
Note: Your AppDelegate should configure Clerk when the app launches, just like in the Clerk iOS Quickstart.
import UIKit
import Capacitor
import Clerk
import capacitor_clerk_native
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private let clerkBridge = ClerkBridgeImpl()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Configure Clerk
if let publishableKey = ProcessInfo.processInfo.environment["CLERK_PUBLISHABLE_KEY"] ?? Bundle.main.infoDictionary?["ClerkPublishableKey"] as? String {
Clerk.shared.configure(publishableKey: publishableKey)
}
// Set up the Clerk bridge for the Capacitor plugin
ClerkNativePlugin.setClerkBridge(clerkBridge)
return true
}
// ... rest of AppDelegate methods
}6. Add ClerkBridgeImpl.swift to Xcode Project
- In Xcode, right-click your "App" folder
- Select "Add Files to 'App'..."
- Select
ClerkBridgeImpl.swift - Uncheck "Copy items if needed"
- Check the "App" target
- Click "Add"
7. Configure Clerk Publishable Key
Important: Get your Publishable Key from the API Keys page in Clerk Dashboard.
Option A: Build Settings
- Select the App target → Build Settings
- Click "+" → "Add User-Defined Setting"
- Name:
CLERK_PUBLISHABLE_KEY - Value: Your Clerk publishable key
Option B: xcconfig File (Recommended)
- Create
Config.xcconfigin your App folder:
CLERK_PUBLISHABLE_KEY = pk_test_your_clerk_key_here- Add it to Xcode project
- Set it for both Debug and Release configurations in Project Info
8. Update Info.plist
Add your Clerk publishable key:
<key>ClerkPublishableKey</key>
<string>$(CLERK_PUBLISHABLE_KEY)</string>React/JavaScript Usage
import { ClerkProvider, useAuth, useUser, useSignIn, useSignUp } from '@trainon-inc/capacitor-clerk-native';
// Wrap your app
function App() {
return (
<ClerkProvider publishableKey="pk_test_...">
<YourApp />
</ClerkProvider>
);
}
// Use in components
function LoginPage() {
const { signIn } = useSignIn();
const handleLogin = async () => {
const result = await signIn.create({
identifier: email,
password: password
});
if (result.status === 'complete') {
// Navigate to app
}
};
}
// Get auth state
function Profile() {
const { user } = useUser();
const { getToken } = useAuth();
const token = await getToken();
}Architecture
iOS Architecture (Bridge Pattern)
┌─────────────────────────────────────────────────┐
│ JavaScript/React (Capacitor WebView) │
│ - Uses capacitor-clerk-native hooks │
└─────────────────┬───────────────────────────────┘
│ Capacitor Bridge
┌─────────────────▼───────────────────────────────┐
│ ClerkNativePlugin (CocoaPods Pod) │
│ - Defines ClerkBridge protocol │
│ - Receives calls from JavaScript │
│ - Delegates to bridge implementation │
└─────────────────┬───────────────────────────────┘
│ Protocol/Delegate
┌─────────────────▼───────────────────────────────┐
│ ClerkBridgeImpl (App Target, SPM) │
│ - Implements ClerkBridge protocol │
│ - Uses Clerk iOS SDK directly │
│ - Handles all Clerk authentication │
└─────────────────────────────────────────────────┘Android Architecture (Web Provider)
┌─────────────────────────────────────────────────┐
│ JavaScript/React (Capacitor WebView) │
│ - Uses @clerk/clerk-react (web provider) │
│ - Full Clerk functionality via web SDK │
└─────────────────────────────────────────────────┘
(No native plugin needed for auth)
┌─────────────────────────────────────────────────┐
│ ClerkNativePlugin (Gradle Module) - Stub │
│ - Exists for Capacitor plugin registration │
│ - Returns "use web provider" for all methods │
│ - No native Clerk SDK dependency │
└─────────────────────────────────────────────────┘API
Methods
signInWithPassword(email: string, password: string)- Sign in with email/passwordsignUp(email: string, password: string)- Create a new accountsignOut()- Sign out current usergetToken()- Get the authentication tokengetUser()- Get current user dataisSignedIn()- Check if user is signed in
React Hooks
useAuth()- Authentication state and methodsuseUser()- Current user datauseSignIn()- Sign in methodsuseSignUp()- Sign up methodsuseClerk()- Full Clerk context
Troubleshooting
Common Issues
"Browser unauthenticated" error in WebView
✅ Solved! This plugin uses native SDKs instead of WebView, eliminating this issue entirely.
"Native API not enabled"
- Go to Clerk Dashboard → Settings → Native Applications
- Enable the Native API toggle
- Refresh your app
Associated Domain not working
- Verify you added
webcredentials:{YOUR_FRONTEND_API_URL}correctly - Get the exact URL from Clerk Dashboard → Native Applications
- Clean build folder and rebuild (Shift+Cmd+K in Xcode)
"No such module 'Clerk'" in ClerkBridgeImpl
- Ensure Clerk iOS SDK is added to your App target (not just the project)
- Check Package Dependencies tab shows Clerk linked to your target
- Clean and rebuild
Plugin not found or not responding
- Run
npx cap syncto sync the plugin - Verify plugin is in node_modules:
@trainon-inc/capacitor-clerk-native - Check Podfile includes the plugin after running cap sync
Authentication not persisting
- Clerk handles session persistence automatically
- Verify
clerk.load()is called in AppDelegate - Check Clerk SDK is properly configured with your publishable key
Getting Help
Android Setup
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.
Why Web Provider for Android?
- ✅ Android WebViews handle cookies correctly - no authentication issues
- ✅ The Clerk Android SDK is still in early stages (v0.1.x) with evolving APIs
- ✅ Using
@clerk/clerk-reactprovides a stable, well-tested experience - ✅ Simpler setup - no native configuration required
Recommended App Setup
Configure your app to use the native plugin only on iOS:
import { Capacitor } from "@capacitor/core";
import { ClerkProvider as WebClerkProvider } from "@clerk/clerk-react";
import { ClerkProvider as NativeClerkProvider } from "@trainon-inc/capacitor-clerk-native";
// Use native Clerk only on iOS (due to WebView cookie issues)
// Android WebViews work fine with web Clerk
const isIOS = Capacitor.getPlatform() === "ios";
const ClerkProvider = isIOS ? NativeClerkProvider : WebClerkProvider;
export function App() {
const clerkProps = isIOS
? { publishableKey: "pk_test_..." }
: {
publishableKey: "pk_test_...",
signInFallbackRedirectUrl: "/home",
signUpFallbackRedirectUrl: "/home",
};
return (
<ClerkProvider {...clerkProps}>
<YourApp />
</ClerkProvider>
);
}Build Requirements
- Gradle: 8.11.1+
- Android Gradle Plugin: 8.5.0+
- Java: 17+
- Min SDK: 23 (Android 6.0)
- Target SDK: 35 (Android 15)
Troubleshooting Android
"No matching variant" error
Could not resolve project :trainon-inc-capacitor-clerk-native
No matching variant of project was found. No variants exist.Solution: Update to the latest plugin version:
npm update @trainon-inc/capacitor-clerk-native
npx cap sync android"invalid source release: 21" error
The plugin uses Java 17. Ensure your Android Studio uses JDK 17+:
- File → Project Structure → SDK Location → Gradle JDK → Select JDK 17+
Gradle sync fails
- Clean the project: Build → Clean Project
- Invalidate caches: File → Invalidate Caches / Restart
- Delete
.gradlefolder and re-sync
Contributing
Contributions are welcome! This plugin was created to solve a real problem we encountered, and we'd love to make it better.
License
MIT
Credits
Created by the TrainOn Team to solve CocoaPods ↔ SPM conflicts when integrating Clerk authentication in Capacitor iOS apps.
