diff --git a/.changeset/brave-apples-sign.md b/.changeset/brave-apples-sign.md
new file mode 100644
index 00000000000..19a347e7d9b
--- /dev/null
+++ b/.changeset/brave-apples-sign.md
@@ -0,0 +1,6 @@
+---
+'@clerk/clerk-expo': minor
+'@clerk/types': patch
+---
+
+Add native Apple Sign-In support for iOS via `useAppleSignIn()` hook. Requires `expo-apple-authentication` and native build (EAS Build or local prebuild).
diff --git a/.changeset/khaki-zoos-take.md b/.changeset/khaki-zoos-take.md
deleted file mode 100644
index a845151cc84..00000000000
--- a/.changeset/khaki-zoos-take.md
+++ /dev/null
@@ -1,2 +0,0 @@
----
----
diff --git a/integration/templates/expo-web/metro.config.js b/integration/templates/expo-web/metro.config.js
index f1f3a8a25aa..1874df5a11a 100644
--- a/integration/templates/expo-web/metro.config.js
+++ b/integration/templates/expo-web/metro.config.js
@@ -1,5 +1,5 @@
/**
- * DO NOT EDIT THIS FILE UNLESS YOU DEFINITELY KNWO WHAT YOU ARE DOING.
+ * DO NOT EDIT THIS FILE UNLESS YOU DEFINITELY KNOW WHAT YOU ARE DOING.
* THIS ENSURES THAT INTEGRATION TESTS ARE LOADING THE CORRECT DEPENDENCIES.
*/
const { getDefaultConfig } = require('expo/metro-config');
@@ -30,25 +30,93 @@ const clerkExpoPath = getClerkExpoPath();
const clerkMonorepoPath = clerkExpoPath?.replace(/\/packages\/expo$/, '');
/** @type {import('expo/metro-config').MetroConfig} */
-const config = {
- ...getDefaultConfig(__dirname),
- watchFolders: [clerkMonorepoPath],
- resolver: {
- sourceExts: ['js', 'json', 'ts', 'tsx', 'cjs', 'mjs'],
- nodeModulesPaths: [
- path.resolve(__dirname, 'node_modules'),
- clerkExpoPath && `${clerkMonorepoPath}/node_modules`,
- clerkExpoPath && `${clerkExpoPath}/node_modules`,
- ],
- // This is a workaround for a to prevent multiple versions of react and react-native from being loaded.
- // https://github.com/expo/expo/pull/26209
- blockList: [
- clerkExpoPath && new RegExp(`${clerkMonorepoPath}/node_modules/react`),
- clerkExpoPath && new RegExp(`${clerkMonorepoPath}/node_modules/react-native`),
- ],
- },
-};
+const config = getDefaultConfig(__dirname);
-module.exports = {
- ...config,
-};
+// Only customize Metro config when running from monorepo
+if (clerkMonorepoPath) {
+ console.log('[Metro Config] Applying monorepo customizations');
+ config.watchFolders = [clerkMonorepoPath];
+
+ // Disable file watching to prevent infinite reload loops in integration tests
+ config.watchFolders = [clerkMonorepoPath];
+ config.watcher = {
+ healthCheck: {
+ enabled: false,
+ },
+ };
+
+ // Prioritize local node_modules over monorepo node_modules
+ config.resolver.nodeModulesPaths = [path.resolve(__dirname, 'node_modules'), `${clerkMonorepoPath}/node_modules`];
+
+ // Explicitly map @clerk packages to their source locations
+ // Point to the root of the package so Metro can properly resolve subpath exports
+ config.resolver.extraNodeModules = {
+ '@clerk/clerk-react': path.resolve(clerkMonorepoPath, 'packages/react'),
+ '@clerk/clerk-expo': path.resolve(clerkMonorepoPath, 'packages/expo'),
+ '@clerk/shared': path.resolve(clerkMonorepoPath, 'packages/shared'),
+ '@clerk/types': path.resolve(clerkMonorepoPath, 'packages/types'),
+ };
+
+ // This is a workaround to prevent multiple versions of react and react-native from being loaded.
+ // Block React/React-Native in both monorepo root and all package node_modules
+ // Use word boundaries to avoid blocking clerk-react
+ // https://github.com/expo/expo/pull/26209
+ const escapedPath = clerkMonorepoPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ config.resolver.blockList = [
+ // Block monorepo root node_modules for react/react-native/react-dom
+ new RegExp(`${escapedPath}/node_modules/react/`),
+ new RegExp(`${escapedPath}/node_modules/react$`),
+ new RegExp(`${escapedPath}/node_modules/react-dom/`),
+ new RegExp(`${escapedPath}/node_modules/react-dom$`),
+ new RegExp(`${escapedPath}/node_modules/react-native/`),
+ new RegExp(`${escapedPath}/node_modules/react-native$`),
+ // Block react in monorepo's pnpm store
+ new RegExp(`${escapedPath}/node_modules/\\.pnpm/.*/node_modules/react/`),
+ new RegExp(`${escapedPath}/node_modules/\\.pnpm/.*/node_modules/react$`),
+ new RegExp(`${escapedPath}/node_modules/\\.pnpm/.*/node_modules/react-dom/`),
+ new RegExp(`${escapedPath}/node_modules/\\.pnpm/.*/node_modules/react-dom$`),
+ new RegExp(`${escapedPath}/node_modules/\\.pnpm/.*/node_modules/react-native/`),
+ new RegExp(`${escapedPath}/node_modules/\\.pnpm/.*/node_modules/react-native$`),
+ // Block react/react-native/react-dom in all package node_modules
+ new RegExp(`${escapedPath}/packages/.*/node_modules/react/`),
+ new RegExp(`${escapedPath}/packages/.*/node_modules/react$`),
+ new RegExp(`${escapedPath}/packages/.*/node_modules/react-dom/`),
+ new RegExp(`${escapedPath}/packages/.*/node_modules/react-dom$`),
+ new RegExp(`${escapedPath}/packages/.*/node_modules/react-native/`),
+ new RegExp(`${escapedPath}/packages/.*/node_modules/react-native$`),
+ ];
+
+ // Custom resolver to handle package.json subpath exports for @clerk packages
+ // This enables Metro to resolve imports like '@clerk/clerk-react/internal'
+ const originalResolveRequest = config.resolver.resolveRequest;
+ config.resolver.resolveRequest = (context, moduleName, platform) => {
+ // Check if this is a @clerk package with a subpath
+ const clerkPackageMatch = moduleName.match(/^(@clerk\/[^/]+)\/(.+)$/);
+ if (clerkPackageMatch && config.resolver.extraNodeModules) {
+ const [, packageName, subpath] = clerkPackageMatch;
+ const packageRoot = config.resolver.extraNodeModules[packageName];
+
+ if (packageRoot) {
+ // Try to resolve via the subpath-workaround directory (e.g., internal/package.json)
+ const subpathDir = path.join(packageRoot, subpath);
+ try {
+ const subpathPkg = require(path.join(subpathDir, 'package.json'));
+ if (subpathPkg.main) {
+ const resolvedPath = path.join(subpathDir, subpathPkg.main);
+ return { type: 'sourceFile', filePath: resolvedPath };
+ }
+ } catch (e) {
+ // Subpath directory doesn't exist, continue with default resolution
+ }
+ }
+ }
+
+ // Fall back to default resolution
+ if (originalResolveRequest) {
+ return originalResolveRequest(context, moduleName, platform);
+ }
+ return context.resolveRequest(context, moduleName, platform);
+ };
+}
+
+module.exports = config;
diff --git a/packages/expo/package.json b/packages/expo/package.json
index 4897620e98f..2a0f544d156 100644
--- a/packages/expo/package.json
+++ b/packages/expo/package.json
@@ -94,7 +94,9 @@
"devDependencies": {
"@clerk/expo-passkeys": "workspace:*",
"@types/base-64": "^1.0.2",
+ "expo-apple-authentication": "^7.2.4",
"expo-auth-session": "^5.4.0",
+ "expo-crypto": "^15.0.7",
"expo-local-authentication": "^13.8.0",
"expo-secure-store": "^12.8.1",
"expo-web-browser": "^12.8.2",
@@ -102,7 +104,9 @@
},
"peerDependencies": {
"@clerk/expo-passkeys": ">=0.0.6",
+ "expo-apple-authentication": ">=7.0.0",
"expo-auth-session": ">=5",
+ "expo-crypto": ">=12",
"expo-local-authentication": ">=13.5.0",
"expo-secure-store": ">=12.4.0",
"expo-web-browser": ">=12.5.0",
@@ -114,6 +118,12 @@
"@clerk/expo-passkeys": {
"optional": true
},
+ "expo-apple-authentication": {
+ "optional": true
+ },
+ "expo-crypto": {
+ "optional": true
+ },
"expo-local-authentication": {
"optional": true
},
diff --git a/packages/expo/src/hooks/__tests__/useSignInWithApple.test.ts b/packages/expo/src/hooks/__tests__/useSignInWithApple.test.ts
new file mode 100644
index 00000000000..8813defb1ce
--- /dev/null
+++ b/packages/expo/src/hooks/__tests__/useSignInWithApple.test.ts
@@ -0,0 +1,209 @@
+import { renderHook } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
+
+import { useSignInWithApple } from '../useSignInWithApple.ios';
+
+const mocks = vi.hoisted(() => {
+ return {
+ useSignIn: vi.fn(),
+ useSignUp: vi.fn(),
+ signInAsync: vi.fn(),
+ isAvailableAsync: vi.fn(),
+ randomUUID: vi.fn(),
+ };
+});
+
+vi.mock('@clerk/clerk-react', () => {
+ return {
+ useSignIn: mocks.useSignIn,
+ useSignUp: mocks.useSignUp,
+ };
+});
+
+vi.mock('expo-apple-authentication', () => {
+ return {
+ signInAsync: mocks.signInAsync,
+ isAvailableAsync: mocks.isAvailableAsync,
+ AppleAuthenticationScope: {
+ FULL_NAME: 0,
+ EMAIL: 1,
+ },
+ };
+});
+
+vi.mock('expo-crypto', () => {
+ return {
+ default: {
+ randomUUID: mocks.randomUUID,
+ },
+ randomUUID: mocks.randomUUID,
+ };
+});
+
+vi.mock('react-native', () => {
+ return {
+ Platform: {
+ OS: 'ios',
+ },
+ };
+});
+
+describe('useSignInWithApple', () => {
+ const mockSignIn = {
+ create: vi.fn(),
+ createdSessionId: 'test-session-id',
+ firstFactorVerification: {
+ status: 'verified',
+ },
+ };
+
+ const mockSignUp = {
+ create: vi.fn(),
+ createdSessionId: null,
+ };
+
+ const mockSetActive = vi.fn();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ mocks.useSignIn.mockReturnValue({
+ signIn: mockSignIn,
+ setActive: mockSetActive,
+ isLoaded: true,
+ });
+
+ mocks.useSignUp.mockReturnValue({
+ signUp: mockSignUp,
+ isLoaded: true,
+ });
+
+ mocks.isAvailableAsync.mockResolvedValue(true);
+ mocks.randomUUID.mockReturnValue('test-nonce-uuid');
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('startAppleAuthenticationFlow', () => {
+ test('should return the hook with startAppleAuthenticationFlow function', () => {
+ const { result } = renderHook(() => useSignInWithApple());
+
+ expect(result.current).toHaveProperty('startAppleAuthenticationFlow');
+ expect(typeof result.current.startAppleAuthenticationFlow).toBe('function');
+ });
+
+ test('should successfully sign in existing user', async () => {
+ const mockIdentityToken = 'mock-identity-token';
+ mocks.signInAsync.mockResolvedValue({
+ identityToken: mockIdentityToken,
+ });
+
+ mockSignIn.create.mockResolvedValue(undefined);
+ mockSignIn.firstFactorVerification.status = 'verified';
+ mockSignIn.createdSessionId = 'test-session-id';
+
+ const { result } = renderHook(() => useSignInWithApple());
+
+ const response = await result.current.startAppleAuthenticationFlow();
+
+ expect(mocks.isAvailableAsync).toHaveBeenCalled();
+ expect(mocks.randomUUID).toHaveBeenCalled();
+ expect(mocks.signInAsync).toHaveBeenCalledWith(
+ expect.objectContaining({
+ requestedScopes: expect.any(Array),
+ nonce: 'test-nonce-uuid',
+ }),
+ );
+ expect(mockSignIn.create).toHaveBeenCalledWith({
+ strategy: 'oauth_token_apple',
+ token: mockIdentityToken,
+ });
+ expect(response.createdSessionId).toBe('test-session-id');
+ expect(response.setActive).toBe(mockSetActive);
+ });
+
+ test('should handle transfer flow for new user', async () => {
+ const mockIdentityToken = 'mock-identity-token';
+ mocks.signInAsync.mockResolvedValue({
+ identityToken: mockIdentityToken,
+ });
+
+ mockSignIn.create.mockResolvedValue(undefined);
+ mockSignIn.firstFactorVerification.status = 'transferable';
+
+ const mockSignUpWithSession = { ...mockSignUp, createdSessionId: 'new-user-session-id' };
+ mocks.useSignUp.mockReturnValue({
+ signUp: mockSignUpWithSession,
+ isLoaded: true,
+ });
+
+ const { result } = renderHook(() => useSignInWithApple());
+
+ const response = await result.current.startAppleAuthenticationFlow({
+ unsafeMetadata: { source: 'test' },
+ });
+
+ expect(mockSignIn.create).toHaveBeenCalledWith({
+ strategy: 'oauth_token_apple',
+ token: mockIdentityToken,
+ });
+ expect(mockSignUp.create).toHaveBeenCalledWith({
+ transfer: true,
+ unsafeMetadata: { source: 'test' },
+ });
+ expect(response.createdSessionId).toBe('new-user-session-id');
+ });
+
+ test('should handle user cancellation gracefully', async () => {
+ const cancelError = Object.assign(new Error('User canceled'), { code: 'ERR_REQUEST_CANCELED' });
+ mocks.signInAsync.mockRejectedValue(cancelError);
+
+ const { result } = renderHook(() => useSignInWithApple());
+
+ const response = await result.current.startAppleAuthenticationFlow();
+
+ expect(response.createdSessionId).toBe(null);
+ expect(response.setActive).toBe(mockSetActive);
+ });
+
+ test('should throw error when Apple Authentication is not available', async () => {
+ mocks.isAvailableAsync.mockResolvedValue(false);
+
+ const { result } = renderHook(() => useSignInWithApple());
+
+ await expect(result.current.startAppleAuthenticationFlow()).rejects.toThrow(
+ 'Apple Authentication is not available on this device.',
+ );
+ });
+
+ test('should throw error when no identity token received', async () => {
+ mocks.signInAsync.mockResolvedValue({
+ identityToken: null,
+ });
+
+ const { result } = renderHook(() => useSignInWithApple());
+
+ await expect(result.current.startAppleAuthenticationFlow()).rejects.toThrow(
+ 'No identity token received from Apple Sign-In.',
+ );
+ });
+
+ test('should return early when clerk is not loaded', async () => {
+ mocks.useSignIn.mockReturnValue({
+ signIn: mockSignIn,
+ setActive: mockSetActive,
+ isLoaded: false,
+ });
+
+ const { result } = renderHook(() => useSignInWithApple());
+
+ const response = await result.current.startAppleAuthenticationFlow();
+
+ expect(mocks.isAvailableAsync).not.toHaveBeenCalled();
+ expect(mocks.signInAsync).not.toHaveBeenCalled();
+ expect(response.createdSessionId).toBe(null);
+ });
+ });
+});
diff --git a/packages/expo/src/hooks/index.ts b/packages/expo/src/hooks/index.ts
index 40ceb2d56f7..2cdac716738 100644
--- a/packages/expo/src/hooks/index.ts
+++ b/packages/expo/src/hooks/index.ts
@@ -11,6 +11,7 @@ export {
useReverification,
} from '@clerk/clerk-react';
+export * from './useSignInWithApple';
export * from './useSSO';
export * from './useOAuth';
export * from './useAuth';
diff --git a/packages/expo/src/hooks/useSignInWithApple.ios.ts b/packages/expo/src/hooks/useSignInWithApple.ios.ts
new file mode 100644
index 00000000000..36be6034000
--- /dev/null
+++ b/packages/expo/src/hooks/useSignInWithApple.ios.ts
@@ -0,0 +1,149 @@
+import { useSignIn, useSignUp } from '@clerk/clerk-react';
+import type { SetActive, SignInResource, SignUpResource } from '@clerk/types';
+import * as AppleAuthentication from 'expo-apple-authentication';
+import * as Crypto from 'expo-crypto';
+
+import { errorThrower } from '../utils/errors';
+
+export type StartAppleAuthenticationFlowParams = {
+ unsafeMetadata?: SignUpUnsafeMetadata;
+};
+
+export type StartAppleAuthenticationFlowReturnType = {
+ createdSessionId: string | null;
+ setActive?: SetActive;
+ signIn?: SignInResource;
+ signUp?: SignUpResource;
+};
+
+/**
+ * Hook for native Apple Authentication on iOS using expo-apple-authentication.
+ *
+ * This hook provides a simplified way to authenticate users with their Apple ID
+ * using the native iOS Sign in with Apple UI. The authentication flow automatically
+ * handles the ID token exchange with Clerk's backend and manages the transfer flow
+ * between sign-in and sign-up.
+ *
+ * @example
+ * ```tsx
+ * import { useSignInWithApple } from '@clerk/clerk-expo';
+ * import { Button } from 'react-native';
+ *
+ * function AppleSignInButton() {
+ * const { startAppleAuthenticationFlow } = useSignInWithApple();
+ *
+ * const onPress = async () => {
+ * try {
+ * const { createdSessionId, setActive } = await startAppleAuthenticationFlow();
+ *
+ * if (createdSessionId && setActive) {
+ * await setActive({ session: createdSessionId });
+ * }
+ * } catch (err) {
+ * console.error('Apple Authentication error:', err);
+ * }
+ * };
+ *
+ * return ;
+ * }
+ * ```
+ *
+ * @requires expo-apple-authentication - Must be installed as a peer dependency
+ * @platform iOS - This is the iOS-specific implementation
+ *
+ * @returns An object containing the `startAppleAuthenticationFlow` function
+ */
+export function useSignInWithApple() {
+ const { signIn, setActive, isLoaded: isSignInLoaded } = useSignIn();
+ const { signUp, isLoaded: isSignUpLoaded } = useSignUp();
+
+ async function startAppleAuthenticationFlow(
+ startAppleAuthenticationFlowParams?: StartAppleAuthenticationFlowParams,
+ ): Promise {
+ if (!isSignInLoaded || !isSignUpLoaded) {
+ return {
+ createdSessionId: null,
+ signIn,
+ signUp,
+ setActive,
+ };
+ }
+
+ // Check if Apple Authentication is available on the device
+ const isAvailable = await AppleAuthentication.isAvailableAsync();
+ if (!isAvailable) {
+ return errorThrower.throw('Apple Authentication is not available on this device.');
+ }
+
+ try {
+ // Generate a cryptographically secure nonce for the Apple Sign-In request (required by Clerk)
+ const nonce = Crypto.randomUUID();
+
+ // Request Apple authentication with requested scopes
+ const credential = await AppleAuthentication.signInAsync({
+ requestedScopes: [
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
+ AppleAuthentication.AppleAuthenticationScope.EMAIL,
+ ],
+ nonce,
+ });
+
+ // Extract the identity token from the credential
+ const { identityToken } = credential;
+
+ if (!identityToken) {
+ return errorThrower.throw('No identity token received from Apple Sign-In.');
+ }
+
+ // Create a SignIn with the Apple ID token strategy
+ await signIn.create({
+ strategy: 'oauth_token_apple',
+ token: identityToken,
+ });
+
+ // Check if we need to transfer to SignUp (user doesn't exist yet)
+ const userNeedsToBeCreated = signIn.firstFactorVerification.status === 'transferable';
+
+ if (userNeedsToBeCreated) {
+ // User doesn't exist - create a new SignUp with transfer
+ await signUp.create({
+ transfer: true,
+ unsafeMetadata: startAppleAuthenticationFlowParams?.unsafeMetadata,
+ });
+
+ return {
+ createdSessionId: signUp.createdSessionId,
+ setActive,
+ signIn,
+ signUp,
+ };
+ }
+
+ // User exists - return the SignIn session
+ return {
+ createdSessionId: signIn.createdSessionId,
+ setActive,
+ signIn,
+ signUp,
+ };
+ } catch (error: unknown) {
+ // Handle Apple Authentication errors
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_REQUEST_CANCELED') {
+ // User canceled the sign-in flow
+ return {
+ createdSessionId: null,
+ setActive,
+ signIn,
+ signUp,
+ };
+ }
+
+ // Re-throw other errors
+ throw error;
+ }
+ }
+
+ return {
+ startAppleAuthenticationFlow,
+ };
+}
diff --git a/packages/expo/src/hooks/useSignInWithApple.ts b/packages/expo/src/hooks/useSignInWithApple.ts
new file mode 100644
index 00000000000..5e03e705420
--- /dev/null
+++ b/packages/expo/src/hooks/useSignInWithApple.ts
@@ -0,0 +1,69 @@
+import type { SetActive, SignInResource, SignUpResource } from '@clerk/types';
+
+import { errorThrower } from '../utils/errors';
+
+export type StartAppleAuthenticationFlowParams = {
+ unsafeMetadata?: SignUpUnsafeMetadata;
+};
+
+export type StartAppleAuthenticationFlowReturnType = {
+ createdSessionId: string | null;
+ setActive?: SetActive;
+ signIn?: SignInResource;
+ signUp?: SignUpResource;
+};
+
+/**
+ * Stub for Apple Authentication hook on non-iOS platforms.
+ *
+ * Native Apple Authentication using expo-apple-authentication is only available on iOS.
+ * For web platforms, use the OAuth-based Apple Sign-In flow instead via useSSO.
+ *
+ * @example
+ * ```tsx
+ * import { useSSO } from '@clerk/clerk-expo';
+ * import { Button } from 'react-native';
+ *
+ * function AppleSignInButton() {
+ * const { startSSOFlow } = useSSO();
+ *
+ * const onPress = async () => {
+ * try {
+ * const { createdSessionId, setActive } = await startSSOFlow({
+ * strategy: 'oauth_apple'
+ * });
+ *
+ * if (createdSessionId && setActive) {
+ * await setActive({ session: createdSessionId });
+ * }
+ * } catch (err) {
+ * console.error('Apple Authentication error:', err);
+ * }
+ * };
+ *
+ * return ;
+ * }
+ * ```
+ *
+ * @platform iOS - This hook only works on iOS. On other platforms, it will throw an error.
+ *
+ * @returns An object containing the `startAppleAuthenticationFlow` function that throws an error
+ */
+export function useSignInWithApple(): {
+ startAppleAuthenticationFlow: (
+ startAppleAuthenticationFlowParams?: StartAppleAuthenticationFlowParams,
+ ) => Promise;
+} {
+ function startAppleAuthenticationFlow(
+ _startAppleAuthenticationFlowParams?: StartAppleAuthenticationFlowParams,
+ ): Promise {
+ return errorThrower.throw(
+ 'Apple Authentication via expo-apple-authentication is only available on iOS. ' +
+ 'For web and other platforms, please use the OAuth-based flow with useSSO and strategy: "oauth_apple".',
+ );
+ }
+
+ return {
+ startAppleAuthenticationFlow,
+ };
+}
diff --git a/packages/expo/tsconfig.json b/packages/expo/tsconfig.json
index 53a1bad91ed..193a7812407 100644
--- a/packages/expo/tsconfig.json
+++ b/packages/expo/tsconfig.json
@@ -21,7 +21,7 @@
"target": "ES2019",
"noEmitOnError": false,
"incremental": true,
- "moduleSuffixes": [".ios", ".android", ".native", ""]
+ "moduleSuffixes": [".web", ".ios", ".android", ".native", ""]
},
"include": ["src"]
}
diff --git a/packages/shared/src/types/signInCommon.ts b/packages/shared/src/types/signInCommon.ts
index 52fbaaa946c..30cacb7e517 100644
--- a/packages/shared/src/types/signInCommon.ts
+++ b/packages/shared/src/types/signInCommon.ts
@@ -40,6 +40,7 @@ import type {
Web3WalletIdentifier,
} from './identifiers';
import type {
+ AppleIdTokenStrategy,
BackupCodeStrategy,
EmailCodeStrategy,
EmailLinkStrategy,
@@ -137,6 +138,10 @@ export type SignInCreateParams = (
strategy: GoogleOneTapStrategy;
token: string;
}
+ | {
+ strategy: AppleIdTokenStrategy;
+ token: string;
+ }
| {
strategy: PasswordStrategy;
password: string;
diff --git a/packages/shared/src/types/signUpCommon.ts b/packages/shared/src/types/signUpCommon.ts
index 62b81715a2a..a40504a0c2a 100644
--- a/packages/shared/src/types/signUpCommon.ts
+++ b/packages/shared/src/types/signUpCommon.ts
@@ -9,6 +9,7 @@ import type {
import type { PhoneCodeChannel } from './phoneCodeChannel';
import type { SignUpVerificationJSONSnapshot, SignUpVerificationsJSONSnapshot } from './snapshots';
import type {
+ AppleIdTokenStrategy,
EmailCodeStrategy,
EmailLinkStrategy,
EnterpriseSSOStrategy,
@@ -89,6 +90,7 @@ export type SignUpCreateParams = Partial<
| EnterpriseSSOStrategy
| TicketStrategy
| GoogleOneTapStrategy
+ | AppleIdTokenStrategy
| PhoneCodeStrategy;
redirectUrl: string;
actionCompleteRedirectUrl: string;
diff --git a/packages/shared/src/types/strategies.ts b/packages/shared/src/types/strategies.ts
index 607947def56..b8391619931 100644
--- a/packages/shared/src/types/strategies.ts
+++ b/packages/shared/src/types/strategies.ts
@@ -2,6 +2,7 @@ import type { OAuthProvider } from './oauth';
import type { Web3Provider } from './web3';
export type GoogleOneTapStrategy = 'google_one_tap';
+export type AppleIdTokenStrategy = 'oauth_token_apple';
export type PasskeyStrategy = 'passkey';
export type PasswordStrategy = 'password';
export type PhoneCodeStrategy = 'phone_code';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 087df7cfb9d..69e02f089f1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -649,9 +649,15 @@ importers:
'@types/base-64':
specifier: ^1.0.2
version: 1.0.2
+ expo-apple-authentication:
+ specifier: ^7.2.4
+ version: 7.2.4(expo@54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))
expo-auth-session:
specifier: ^5.4.0
version: 5.4.0(expo@54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1))
+ expo-crypto:
+ specifier: ^15.0.7
+ version: 15.0.7(expo@54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1))
expo-local-authentication:
specifier: ^13.8.0
version: 13.8.0(expo@54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1))
@@ -2592,7 +2598,7 @@ packages:
'@expo/bunyan@4.0.1':
resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==}
- engines: {node: '>=0.10.0'}
+ engines: {'0': node >=0.10.0}
'@expo/cli@0.22.26':
resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==}
@@ -8391,6 +8397,12 @@ packages:
resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+ expo-apple-authentication@7.2.4:
+ resolution: {integrity: sha512-T2agaLLPT4Ax97FeXImB7BCCEzEJ0gB+ZwlFa/FXBtbp6WFKcGRlTVKiX2YPYLZzN5QjXcmQ9HHJ17jRthNHMg==}
+ peerDependencies:
+ expo: '*'
+ react-native: '*'
+
expo-application@5.8.3:
resolution: {integrity: sha512-IISxzpPX+Xe4ynnwX8yY52T6dm1g9sME1GCj4lvUlrdc5xeTPM6U35x7Wj82V7lLWBaVGe+/Tg9EeKqfylCEwA==}
peerDependencies:
@@ -8435,6 +8447,11 @@ packages:
peerDependencies:
expo: '*'
+ expo-crypto@15.0.7:
+ resolution: {integrity: sha512-FUo41TwwGT2e5rA45PsjezI868Ch3M6wbCZsmqTWdF/hr+HyPcrp1L//dsh/hsrsyrQdpY/U96Lu71/wXePJeg==}
+ peerDependencies:
+ expo: '*'
+
expo-file-system@18.0.12:
resolution: {integrity: sha512-HAkrd/mb8r+G3lJ9MzmGeuW2B+BxQR1joKfeCyY4deLl1zoZ48FrAWjgZjHK9aHUVhJ0ehzInu/NQtikKytaeg==}
peerDependencies:
@@ -24561,6 +24578,11 @@ snapshots:
jest-message-util: 29.7.0
jest-util: 29.7.0
+ expo-apple-authentication@7.2.4(expo@54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1)):
+ dependencies:
+ expo: 54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1)
+ react-native: 0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1)
+
expo-application@5.8.3(expo@54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1)):
dependencies:
expo: 54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1)
@@ -24629,6 +24651,11 @@ snapshots:
base64-js: 1.5.1
expo: 54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1)
+ expo-crypto@15.0.7(expo@54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1)):
+ dependencies:
+ base64-js: 1.5.1
+ expo: 54.0.13(@babel/core@7.28.4)(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1)
+
expo-file-system@18.0.12(expo@52.0.47(@babel/core@7.28.4)(@babel/preset-env@7.26.0(@babel/core@7.28.4))(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1))(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1)):
dependencies:
expo: 52.0.47(@babel/core@7.28.4)(@babel/preset-env@7.26.0(@babel/core@7.28.4))(graphql@16.11.0)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@12.3.7)(@types/react@18.3.26)(react@18.3.1))(react@18.3.1)