Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
24cf072
feat(expo): add hosted auth hook
mikepitre Jun 22, 2026
e3fcdad
chore(expo): add hosted auth changeset
mikepitre Jun 22, 2026
f04d4c8
fix(expo): activate hosted auth session
mikepitre Jun 22, 2026
26e048d
fix(expo): redeem hosted auth with code verifier
mikepitre Jun 23, 2026
ad5f7d7
fix(expo): rename hosted auth option to mode
mikepitre Jun 24, 2026
0e543d3
fix(expo): satisfy hosted auth lint rules
mikepitre Jun 24, 2026
142788e
fix(expo): publish hosted auth verifier dependencies
mikepitre Jun 24, 2026
e36ebb9
fix(expo): address hosted auth review feedback
mikepitre Jun 25, 2026
82aa2d3
fix(expo): harden hosted auth session handoff
mikepitre Jun 25, 2026
0fab777
fix(clerk-js): type hosted auth verifier body
mikepitre Jun 25, 2026
b42a61a
test(js): harden hosted auth verifier handling
mikepitre Jun 25, 2026
6b6b986
fix(clerk-js): trim hosted auth reload path
mikepitre Jun 25, 2026
af3badf
fix(expo): stabilize hosted auth CI
mikepitre Jun 25, 2026
dd7e3da
fix(expo): scope hosted auth completion to expo
mikepitre Jun 25, 2026
3b66e52
fix(expo): move hosted auth to subpath export
mikepitre Jun 25, 2026
03fd4c5
fix(expo): align hosted auth callbacks with native apps
mikepitre Jul 9, 2026
3148788
fix(expo): harden hosted auth completion
mikepitre Jul 10, 2026
13d20cc
fix(expo): tighten hosted auth flow
mikepitre Jul 10, 2026
9b6b252
fix(expo): complete hosted auth platform wiring
mikepitre Jul 10, 2026
dfa6627
fix(expo): harden hosted auth flow
mikepitre Jul 13, 2026
fb0ffab
chore(expo): release hosted auth as a minor version
mikepitre Jul 13, 2026
d7a7c1c
fix(expo): retry stale hosted auth creation
mikepitre Jul 15, 2026
1892027
fix(expo): validate hosted auth intent filters
mikepitre Jul 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/hosted-auth-expo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/expo': minor
---

Add `@clerk/expo/hosted-auth` for signing in or signing up through Account Portal from native Expo apps.
41 changes: 41 additions & 0 deletions packages/expo/app.plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
* Native modules and views are registered via Expo Modules autolinking.
*/
const {
AndroidConfig,
withXcodeProject,
withDangerousMod,
withInfoPlist,
withAppBuildGradle,
withAndroidManifest,
withEntitlementsPlist,
} = require('@expo/config-plugins');
const path = require('path');
Expand All @@ -21,6 +23,35 @@ const packageJson = require('./package.json');

const CLERK_MIN_IOS_VERSION = '17.0';

const addHostedAuthIntentFilter = (mainActivity, packageName) => {
const callbackHost = `${packageName}.callback`;
const intentFilters = mainActivity['intent-filter'] || [];
const hasAndroidName = (entries, name) => entries?.some(entry => entry.$?.['android:name'] === name);
const callbackIsRegistered = intentFilters.some(
intentFilter =>
hasAndroidName(intentFilter.action, 'android.intent.action.VIEW') &&
hasAndroidName(intentFilter.category, 'android.intent.category.DEFAULT') &&
hasAndroidName(intentFilter.category, 'android.intent.category.BROWSABLE') &&
intentFilter.data?.some(
data => data.$?.['android:scheme'] === 'clerk' && data.$?.['android:host'] === callbackHost,
),
);

if (callbackIsRegistered) {
return;
}

intentFilters.push({
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
category: [
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
{ $: { 'android:name': 'android.intent.category.BROWSABLE' } },
],
data: [{ $: { 'android:scheme': 'clerk', 'android:host': callbackHost } }],
});
mainActivity['intent-filter'] = intentFilters;
};

const withClerkIOS = config => {
console.log('✅ Clerk iOS plugin loaded');

Expand Down Expand Up @@ -94,6 +125,15 @@ const withClerkIOS = config => {
const withClerkAndroid = config => {
console.log('✅ Clerk Android plugin loaded');

config = withAndroidManifest(config, modConfig => {
const packageName = config.android?.package;
if (packageName) {
const mainActivity = AndroidConfig.Manifest.getMainActivityOrThrow(modConfig.modResults);
addHostedAuthIntentFilter(mainActivity, packageName);
}
return modConfig;
});

return withAppBuildGradle(config, modConfig => {
let buildGradle = modConfig.modResults.contents;

Expand Down Expand Up @@ -354,6 +394,7 @@ const withClerkExpo = (config, props = {}) => {

module.exports = withClerkExpo;
module.exports._testing = {
addHostedAuthIntentFilter,
validateThemeJson,
isPlainObject,
VALID_COLOR_KEYS,
Expand Down
4 changes: 4 additions & 0 deletions packages/expo/hosted-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "../dist/hosted-auth/index.js",
"types": "../dist/hosted-auth/index.d.ts"
}
5 changes: 5 additions & 0 deletions packages/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@
"types": "./dist/apple/index.d.ts",
"default": "./dist/apple/index.js"
},
"./hosted-auth": {
"types": "./dist/hosted-auth/index.d.ts",
"default": "./dist/hosted-auth/index.js"
},
"./resource-cache": {
"types": "./dist/resource-cache/index.d.ts",
"default": "./dist/resource-cache/index.js"
Expand Down Expand Up @@ -92,6 +96,7 @@
"token-cache",
"google",
"apple",
"hosted-auth",
"experimental",
"legacy",
"src/specs",
Expand Down
60 changes: 60 additions & 0 deletions packages/expo/src/__tests__/appPlugin.hostedAuth.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, test } from 'vitest';

// eslint-disable-next-line @typescript-eslint/no-require-imports -- CJS plugin, no ESM export
const { addHostedAuthIntentFilter } = require('../../app.plugin.js')._testing;

const hostedAuthIntentFilter = () => ({
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
category: [
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
{ $: { 'android:name': 'android.intent.category.BROWSABLE' } },
],
data: [{ $: { 'android:scheme': 'clerk', 'android:host': 'com.example.app.callback' } }],
});

describe('addHostedAuthIntentFilter', () => {
test('registers the canonical Android hosted auth callback', () => {
const mainActivity = {};

addHostedAuthIntentFilter(mainActivity, 'com.example.app');

expect(mainActivity['intent-filter']).toContainEqual(hostedAuthIntentFilter());
});

test('does not duplicate an existing hosted auth callback', () => {
const mainActivity = {};

addHostedAuthIntentFilter(mainActivity, 'com.example.app');
addHostedAuthIntentFilter(mainActivity, 'com.example.app');

expect(mainActivity['intent-filter']).toHaveLength(1);
});

test.each([
['VIEW action', filter => ({ ...filter, action: [] })],
[
'DEFAULT category',
filter => ({
...filter,
category: filter.category.filter(category => category.$['android:name'] !== 'android.intent.category.DEFAULT'),
}),
],
[
'BROWSABLE category',
filter => ({
...filter,
category: filter.category.filter(
category => category.$['android:name'] !== 'android.intent.category.BROWSABLE',
),
}),
],
])('registers a valid callback when a matching filter lacks the %s', (_requirement, omitRequirement) => {
const mainActivity = {
'intent-filter': [omitRequirement(hostedAuthIntentFilter())],
};

addHostedAuthIntentFilter(mainActivity, 'com.example.app');

expect(mainActivity['intent-filter']).toContainEqual(hostedAuthIntentFilter());
});
});
Loading
Loading