Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add React Native support #5209

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,25 @@ const useUserOwnsCredentials = ({ storeKey }: { storeKey: string }) => {
const { user } = useUser();
const [userOwnsCredentials, setUserOwnsCredentials] = useState(false);

const getUserCredentials = (storedIdentifier: string | null): boolean => {
if (!user || !storedIdentifier) {
return false;
}
useEffect(() => {
let ignore = false;

const identifiers = [
user.emailAddresses.map(e => e.emailAddress),
user.phoneNumbers.map(p => p.phoneNumber),
].flat();
const getUserCredentials = (storedIdentifier: string | null): boolean => {
if (!user || !storedIdentifier) {
return false;
}

if (user.username) {
identifiers.push(user.username);
}
return identifiers.includes(storedIdentifier);
};
const identifiers = [
user.emailAddresses.map(e => e.emailAddress),
user.phoneNumbers.map(p => p.phoneNumber),
].flat();

if (user.username) {
identifiers.push(user.username);
}
return identifiers.includes(storedIdentifier);
};

useEffect(() => {
let ignore = false;
void getItemAsync(storeKey)
.catch(() => null)
.then(res => {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native/local-credentials/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "../dist/local-credentials/index.js",
"types": "../dist/local-credentials/index.d.ts"
}
95 changes: 95 additions & 0 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
{
"name": "@clerk/clerk-react-native",
"version": "0.0.1",
"description": "Clerk React Native library",
"keywords": [
"react",
"react-native",
"auth",
"authentication",
"passwordless",
"session",
"jwt"
],
"homepage": "https://clerk.com/",
"bugs": {
"url": "https://github.com/clerk/javascript/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/clerk/javascript.git",
"directory": "packages/react-native"
},
"license": "MIT",
"author": "Clerk",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./web": {
"types": "./dist/web/index.d.ts",
"default": "./dist/web/index.js"
},
"./passkeys": {
"types": "./dist/passkeys/index.d.ts",
"default": "./dist/passkeys/index.js"
},
"./local-credentials": {
"types": "./dist/local-credentials/index.d.ts",
"default": "./dist/local-credentials/index.js"
},
"./secure-store": {
"types": "./dist/secure-store/index.d.ts",
"default": "./dist/secure-store/index.js"
}
},
"main": "./dist/index.js",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
"files": [
"dist",
"web",
"local-credentials",
"passkeys",
"secure-store"
],
"scripts": {
"build": "tsup",
"build:declarations": "tsc -p tsconfig.declarations.json",
"clean": "rimraf ./dist",
"dev": "tsup --watch",
"dev:publish": "pnpm dev -- --env.publish",
"lint": "eslint src",
"publish:local": "pnpm yalc push --replace --sig",
"test": "vitest run",
"test:watch": "vitest watch"
},
"dependencies": {
"@clerk/clerk-js": "workspace:^",
"@clerk/clerk-react": "workspace:^",
"@clerk/shared": "workspace:^",
"@clerk/types": "workspace:^",
"base-64": "^1.0.0",
"react-native-biometrics": "^3.0.1",
"react-native-keychain": "^9.2.3",
"react-native-url-polyfill": "2.0.0",
"react-native-webview": "^13.13.2",
"tslib": "catalog:repo"
},
"devDependencies": {
"@types/base-64": "^1.0.2",
"react-native": "^0.73.9"
},
"peerDependencies": {
"react": "catalog:peer-react",
"react-dom": "catalog:peer-react",
"react-native": ">=0.73"
},
"engines": {
"node": ">=18.17.0"
},
"publishConfig": {
"access": "public"
}
}
4 changes: 4 additions & 0 deletions packages/react-native/passkeys/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "../dist/passkeys/index.js",
"types": "../dist/passkeys/index.d.ts"
}
4 changes: 4 additions & 0 deletions packages/react-native/secure-store/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"main": "../dist/secure-store/index.js",
"types": "../dist/secure-store/index.d.ts"
}
20 changes: 20 additions & 0 deletions packages/react-native/src/cache/MemoryTokenCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { TokenCache } from './types';

const createMemoryTokenCache = (): TokenCache => {
const cache: Record<string, string> = {};
return {
saveToken: (key, token) => {
cache[key] = token;
return Promise.resolve();
},
getToken: key => {
return Promise.resolve(cache[key]);
},
clearToken: key => {
delete cache[key];
return Promise.resolve();
},
};
};

export const MemoryTokenCache = createMemoryTokenCache();
65 changes: 65 additions & 0 deletions packages/react-native/src/cache/ResourceCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { ClientJSONSnapshot, EnvironmentJSONSnapshot } from '@clerk/types';

import type { IStorage } from '../provider/singleton/types';
import type { ResourceCache, ResourceCacheInitOptions } from './types';

function createResourceCache<T>(key: string): ResourceCache<T> {
if (!key) {
throw new Error('Clerk: ResourceCache key is required!');
}
let storage: IStorage | null = null;
let itemKey: string | null = null;

const init = (opts: ResourceCacheInitOptions) => {
if (!opts.storage || !opts.publishableKey) {
throw new Error(`Clerk: ResourceCache for ${key} requires storage and publishableKey!`);
}
itemKey = `${key}_${opts.publishableKey.slice(-5)}`;
storage = opts.storage();
};

const checkInit = (): boolean => {
return !!storage && !!itemKey;
};

const load = async (): Promise<T | null> => {
if (!storage || !itemKey) {
throw new Error(`Clerk: ResourceCache for ${key} not initialized!`);
}
try {
const value = await storage.get(itemKey);
return value ? JSON.parse(value) : null;
} catch (error) {
console.log(`Clerk: Error loading value on ${key} from storage:`, error);
return null;
}
};

const save = async (value: T): Promise<void> => {
if (!storage || !itemKey) {
throw new Error(`Clerk: ResourceCache for ${key} not initialized!`);
}
try {
return await storage.set(itemKey, JSON.stringify(value));
} catch (error) {
console.log(`Clerk: Error saving value on ${key} in storage:`, error);
}
};

const remove = async (): Promise<void> => {
if (!storage || !itemKey) {
throw new Error(`Clerk: ResourceCache for ${key} not initialized!`);
}
try {
return await storage.set(itemKey, '');
} catch (error) {
console.log(`Clerk: Error deleting value on ${key} from storage:`, error);
}
};

return { checkInit, init, load, save, remove };
}

export const EnvironmentResourceCache = createResourceCache<EnvironmentJSONSnapshot>('__clerk_cache_environment');
export const ClientResourceCache = createResourceCache<ClientJSONSnapshot>('__clerk_cache_client');
export const SessionJWTCache = createResourceCache<string>('__clerk_cache_session_jwt');
34 changes: 34 additions & 0 deletions packages/react-native/src/cache/SecureTokenCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getGenericPassword, resetGenericPassword, setGenericPassword } from 'react-native-keychain';

import type { TokenCache } from './types';

const createSecureTokenCache = (): TokenCache => {
return {
getToken: async (key: string) => {
try {
const item = await getGenericPassword(key);
if (item) {
console.log(`${key} was used 🔐 \n`);
return item.password;
} else {
console.log('No values stored under key: ' + key);
}
return null;
} catch (error) {
console.error('keychain get item error: ', error);
await resetGenericPassword(key);
return null;
}
},
saveToken: async (key: string, token: string) => {
await setGenericPassword(key, token);
return Promise.resolve();
},
clearToken: async (key: string) => {
await resetGenericPassword(key);
return Promise.resolve();
},
};
};

export const SecureTokenCache = createSecureTokenCache();
Loading