-
Couldn't load subscription status.
- Fork 402
feat(clerk-expo): Add native Apple Sign-In support for iOS #7053
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
Open
chriscanin
wants to merge
41
commits into
main
Choose a base branch
from
chris/mobile-279-expo-sign-in-with-apple
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
41 commits
Select commit
Hold shift + click to select a range
05a7607
feat: add Apple Sign-In support with useAppleSignIn hook
chriscanin f78267e
feat: add native Apple Sign-In support with useAppleSignIn hook and c…
chriscanin 4eb2f18
Merge branch 'main' into chris/mobile-279-expo-sign-in-with-apple
chriscanin b791faf
fix(pnpm-lock): clean up dependency versions and remove unused turbo-…
chriscanin f643dfe
Merge branch 'main' into chris/mobile-279-expo-sign-in-with-apple
chriscanin a3d83bd
fix(useAppleSignIn): add missing type import and improve nonce genera…
chriscanin 03235ee
feat: add expo-crypto dependency and integrate randomUUID for nonce g…
chriscanin fdb49e3
fix(useAppleSignIn): refine error handling and remove unused type import
chriscanin f314e21
Merge branch 'main' into chris/mobile-279-expo-sign-in-with-apple
chriscanin 669581a
fix(pnpm-lock): clean up dependency version formatting for consistenc…
chriscanin 3fa5f8f
feat(package): add expo-crypto as an optional dependency
chriscanin f3a7d02
fix(useAppleSignIn): lazy load expo-crypto to prevent import issues o…
chriscanin 7e8cdec
fix(tests): update expo-crypto mock to include default export for ran…
chriscanin b6d1e20
fix(metro.config): enhance handling of clerkExpoPath and improve bloc…
chriscanin b78ff24
Merge branch 'main' into chris/mobile-279-expo-sign-in-with-apple
chriscanin 3575bfa
fix(metro.config): enhance blockList for React/React-Native and add c…
chriscanin 3157caf
fix(metro.config): disable file watching to prevent infinite reload l…
chriscanin 811564c
feat(longRunningApplication): add detailed logging during initializat…
chriscanin 129064a
temp-log-fix(application): logging for detached dev server and handl…
chriscanin d7de3b9
fix(application): improve logging for detached dev server startup and…
chriscanin fc35816
Revert "fix(application): improve logging for detached dev server sta…
chriscanin 76d1014
Merge branch 'main' into chris/mobile-279-expo-sign-in-with-apple
chriscanin ff0a778
fix(application): improve server startup error handling and logging
chriscanin b4fdde7
fix(application): enhance logging for detached dev server startup
chriscanin 62f9e55
fix(application): enhance early and late logging for dev server output
chriscanin ff8d018
fix(hooks): lazy load expo-apple-authentication to prevent import iss…
chriscanin 1ea2d40
fix(hooks): implement web stub for Apple Sign-In with error handling
chriscanin 11b71b3
fix(application): streamline server startup logging and error handling
chriscanin ab2774e
fix(hooks): Resolve CodeRabbit: update useAppleSignIn return type to …
chriscanin 676621b
fix(metro.config): correct typo in warning message and enhance debug …
chriscanin 1ba03c4
fix(application): enhance server wait logic with exit condition and m…
chriscanin d1a4d70
fix(metro.config): refactor clerk path handling and remove debug logging
chriscanin bed16e5
fix(metro.config): CodeRabit fix: reorder variable declarations for i…
chriscanin ea5e36a
fix(package.json): revert version to 2.17.0 for consistency
chriscanin 6e7bb2c
Merge branch 'main' of https://github.com/clerk/javascript into chris…
chriscanin d20e523
pnpm lock file resolution.
chriscanin ecd27ad
refactor: rename useAppleAuthentication to useSignInWithApple for con…
chriscanin b4c317b
feat: rename useSignInWithApple
chriscanin 6839fde
Resolve coderabbit comment about unneccesary any
chriscanin 884ba62
refactor: update import and function names in useSignInWithApple test…
chriscanin 8a30335
Merge branch 'main' into chris/mobile-279-expo-sign-in-with-apple
chriscanin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
209 changes: 209 additions & 0 deletions
209
packages/expo/src/hooks/__tests__/useSignInWithApple.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.