Skip to content

Commit 81ad6cf

Browse files
feat(auth): add saveCredentials method to useAuth0 hook (#1285)
1 parent 484579a commit 81ad6cf

File tree

3 files changed

+171
-0
lines changed

3 files changed

+171
-0
lines changed

src/hooks/Auth0Context.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ export interface Auth0ContextInterface extends AuthState {
4848
*/
4949
clearSession(parameters?: ClearSessionParameters): Promise<void>;
5050

51+
/**
52+
* Saves the user's credentials.
53+
* @param credentials The credentials to save.
54+
* @returns A promise that resolves when the credentials have been saved.
55+
* @throws {AuthError} If the save fails.
56+
*/
57+
saveCredentials(credentials: Credentials): Promise<void>;
58+
5159
/**
5260
* Retrieves the stored credentials, refreshing them if necessary.
5361
* @param scope The scopes to request for the new access token (used during refresh).
@@ -208,6 +216,7 @@ const initialContext: Auth0ContextInterface = {
208216
isLoading: true,
209217
authorize: stub,
210218
clearSession: stub,
219+
saveCredentials: stub,
211220
getCredentials: stub,
212221
clearCredentials: stub,
213222
hasValidCredentials: stub,

src/hooks/Auth0Provider.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,19 @@ export const Auth0Provider = ({
170170
[client]
171171
);
172172

173+
const saveCredentials = useCallback(
174+
async (credentials: Credentials) => {
175+
try {
176+
await client.credentialsManager.saveCredentials(credentials);
177+
} catch (e) {
178+
const error = e as AuthError;
179+
dispatch({ type: 'ERROR', error });
180+
throw error;
181+
}
182+
},
183+
[client]
184+
);
185+
173186
const clearCredentials = useCallback(async (): Promise<void> => {
174187
try {
175188
await client.credentialsManager.clearCredentials();
@@ -290,6 +303,7 @@ export const Auth0Provider = ({
290303
...state,
291304
authorize,
292305
clearSession,
306+
saveCredentials,
293307
getCredentials,
294308
hasValidCredentials,
295309
clearCredentials,
@@ -313,6 +327,7 @@ export const Auth0Provider = ({
313327
state,
314328
authorize,
315329
clearSession,
330+
saveCredentials,
316331
getCredentials,
317332
hasValidCredentials,
318333
clearCredentials,

src/hooks/__tests__/Auth0Provider.spec.tsx

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,151 @@ describe('Auth0Provider', () => {
468468
'Not logged in'
469469
);
470470
});
471+
472+
describe('saveCredentials', () => {
473+
const TestSaveCredentialsConsumer = () => {
474+
const { saveCredentials, error } = useAuth0();
475+
476+
const handleSaveCredentials = () => {
477+
const credentials = {
478+
idToken: 'id_token_123',
479+
accessToken: 'access_token_456',
480+
tokenType: 'Bearer' as const,
481+
expiresAt: Date.now() / 1000 + 3600,
482+
scope: 'openid profile email',
483+
refreshToken: 'refresh_token_789',
484+
};
485+
saveCredentials(credentials).catch(() => {});
486+
};
487+
488+
if (error) {
489+
return <Text testID="error">Error: {error.message}</Text>;
490+
}
491+
492+
return (
493+
<View>
494+
<Button
495+
title="Save Credentials"
496+
onPress={handleSaveCredentials}
497+
testID="save-credentials-button"
498+
/>
499+
</View>
500+
);
501+
};
502+
503+
it('should save credentials successfully', async () => {
504+
mockClientInstance.credentialsManager.saveCredentials.mockResolvedValueOnce(
505+
undefined
506+
);
507+
508+
await act(async () => {
509+
render(
510+
<Auth0Provider domain="test.com" clientId="123">
511+
<TestSaveCredentialsConsumer />
512+
</Auth0Provider>
513+
);
514+
});
515+
516+
const saveButton = screen.getByTestId('save-credentials-button');
517+
await act(async () => {
518+
fireEvent.click(saveButton);
519+
});
520+
521+
expect(
522+
mockClientInstance.credentialsManager.saveCredentials
523+
).toHaveBeenCalledTimes(1);
524+
expect(
525+
mockClientInstance.credentialsManager.saveCredentials
526+
).toHaveBeenCalledWith({
527+
idToken: 'id_token_123',
528+
accessToken: 'access_token_456',
529+
tokenType: 'Bearer',
530+
expiresAt: expect.any(Number),
531+
scope: 'openid profile email',
532+
refreshToken: 'refresh_token_789',
533+
});
534+
});
535+
536+
it('should handle save credentials error and dispatch to state', async () => {
537+
const saveError = new Error('Failed to save credentials to Keychain');
538+
mockClientInstance.credentialsManager.saveCredentials.mockRejectedValueOnce(
539+
saveError
540+
);
541+
542+
await act(async () => {
543+
render(
544+
<Auth0Provider domain="test.com" clientId="123">
545+
<TestSaveCredentialsConsumer />
546+
</Auth0Provider>
547+
);
548+
});
549+
550+
const saveButton = screen.getByTestId('save-credentials-button');
551+
await act(async () => {
552+
fireEvent.click(saveButton);
553+
});
554+
555+
await waitFor(() => {
556+
expect(screen.getByTestId('error')).toHaveTextContent(
557+
'Error: Failed to save credentials to Keychain'
558+
);
559+
});
560+
561+
expect(
562+
mockClientInstance.credentialsManager.saveCredentials
563+
).toHaveBeenCalledTimes(1);
564+
});
565+
566+
it('should save minimal credentials', async () => {
567+
const TestMinimalCredentialsConsumer = () => {
568+
const { saveCredentials } = useAuth0();
569+
570+
const handleSaveMinimalCredentials = () => {
571+
const minimalCredentials = {
572+
idToken: 'id_token_minimal',
573+
accessToken: 'access_token_minimal',
574+
tokenType: 'Bearer' as const,
575+
expiresAt: Date.now() / 1000 + 1800,
576+
scope: 'openid',
577+
};
578+
saveCredentials(minimalCredentials).catch(() => {});
579+
};
580+
581+
return (
582+
<Button
583+
title="Save Minimal Credentials"
584+
onPress={handleSaveMinimalCredentials}
585+
testID="save-minimal-button"
586+
/>
587+
);
588+
};
589+
590+
mockClientInstance.credentialsManager.saveCredentials.mockResolvedValueOnce(
591+
undefined
592+
);
593+
594+
await act(async () => {
595+
render(
596+
<Auth0Provider domain="test.com" clientId="123">
597+
<TestMinimalCredentialsConsumer />
598+
</Auth0Provider>
599+
);
600+
});
601+
602+
const saveButton = screen.getByTestId('save-minimal-button');
603+
await act(async () => {
604+
fireEvent.click(saveButton);
605+
});
606+
607+
expect(
608+
mockClientInstance.credentialsManager.saveCredentials
609+
).toHaveBeenCalledWith({
610+
idToken: 'id_token_minimal',
611+
accessToken: 'access_token_minimal',
612+
tokenType: 'Bearer',
613+
expiresAt: expect.any(Number),
614+
scope: 'openid',
615+
});
616+
});
617+
});
471618
});

0 commit comments

Comments
 (0)