-
Notifications
You must be signed in to change notification settings - Fork 297
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
Enable useAuthenticator
usage outside <Authenticator />
#1168
Changes from 24 commits
e0c4216
5d91efe
d0b9dd3
ccf4cc2
af70d1e
1e9b3d7
c361bb9
96ca00f
2231840
4dc50b4
3b5abb6
cbc1f07
104cd62
4ba6855
37e4f2c
9e71360
33a5e68
bf6105b
392ef6e
dc4e507
712b40e
e556d47
fbe623a
12a4d35
c4d9999
480d76f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
--- | ||
"@aws-amplify/ui-react": minor | ||
--- | ||
|
||
This enables `useAuthenticator` usage outside <Authenticator /> to access commonly requested authenticator context like `user` and `route`. | ||
|
||
First wrap your App with `Authenticator.Provider`: | ||
|
||
```tsx | ||
const App = ( | ||
<Authenticator.Provider> | ||
<MyApp /> | ||
</Authenticator.Provider> | ||
) | ||
``` | ||
|
||
To avoid repeated re-renders, you can pass a function that takes in Authenticator context and returns an array of desired context values. This hook will only trigger re-render if any of the array value changes. | ||
|
||
```tsx | ||
const Home = () => { | ||
const { user, signOut } = useAuthenticator((context) => [context.user]); | ||
|
||
return ( | ||
<> | ||
<h2>Welcome, {user.username}!</h2> | ||
<button onClick={signOut}>Sign Out</button> | ||
</> | ||
); | ||
}; | ||
|
||
const Login = () => <Authenticator />; | ||
|
||
function MyApp() { | ||
const { route } = useAuthenticator((context) => [context.route]); | ||
|
||
return route === 'authenticated' ? <Home /> : <Login />; | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import awsExports from '@environments/auth-with-username-no-attributes/src/aws-exports'; | ||
export default awsExports; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { Amplify } from 'aws-amplify'; | ||
|
||
import { Authenticator, useAuthenticator } from '@aws-amplify/ui-react'; | ||
import '@aws-amplify/ui-react/styles.css'; | ||
|
||
import awsExports from './aws-exports'; | ||
|
||
Amplify.configure(awsExports); | ||
|
||
const Home = () => { | ||
const { user, signOut } = useAuthenticator((context) => [context.user]); | ||
|
||
return ( | ||
<> | ||
<h2>Welcome, {user.username}!</h2> | ||
<button onClick={signOut}>Sign Out</button> | ||
</> | ||
); | ||
}; | ||
|
||
const Login = () => <Authenticator />; | ||
|
||
function App() { | ||
const { route } = useAuthenticator((context) => [context.route]); | ||
return route === 'authenticated' ? <Home /> : <Login />; | ||
} | ||
|
||
export default function AppWithProvider() { | ||
return ( | ||
<Authenticator.Provider> | ||
<App></App> | ||
</Authenticator.Provider> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
Feature: Headless Usage | ||
|
||
Authenticator supports headless usage that provides access to current authenticator context | ||
outside the Authenticator component. React and Vue provides it through `useAuthenticator` | ||
hook and composable respectively, and Angular provides it through `AuthenticatorService` | ||
service. They can be used to access current authState (routes), authenticated user, etc. | ||
|
||
See https://ui.docs.amplify.aws/components/authenticator#headless for details. | ||
|
||
Background: | ||
Given I'm running the example "/ui/components/authenticator/useAuthenticator" | ||
|
||
@angular @react @vue @todo-angular @todo-vue | ||
Scenario: Conditionally render Login and Logout component | ||
|
||
/useAuthenticator example uses headless API to get access to conditionally render | ||
components for Login and Logout page. Both share the same authenticator context. | ||
|
||
When I type my "username" with status "CONFIRMED" | ||
And I type my password | ||
And I click the "Sign in" button | ||
Then I see "Sign out" | ||
When I reload the page | ||
Then I see "Sign out" | ||
And I click the "Sign out" button | ||
Then I see "Sign in" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,33 +1,68 @@ | ||
import { Provider, ProviderProps } from './Provider'; | ||
import { useEffect } from 'react'; | ||
import { AuthenticatorMachineOptions } from '@aws-amplify/ui'; | ||
import { Provider, useAuthenticator } from './hooks/useAuthenticator'; | ||
import { ResetPassword } from './ResetPassword'; | ||
import { Router, RouterProps } from './Router'; | ||
import { SetupTOTP } from './SetupTOTP'; | ||
import { SignIn } from './SignIn'; | ||
import { SignUp } from './SignUp'; | ||
import { | ||
CustomComponentsContext, | ||
ComponentsProviderProps, | ||
} from './hooks/useCustomComponents'; | ||
import { defaultComponents } from './hooks/useCustomComponents/defaultComponents'; | ||
|
||
export type AuthenticatorProps = ProviderProps & RouterProps; | ||
export interface ComponentsProp {} | ||
|
||
export type AuthenticatorProps = AuthenticatorMachineOptions & | ||
RouterProps & | ||
ComponentsProviderProps; | ||
|
||
export function Authenticator({ | ||
children, | ||
className, | ||
components, | ||
components: customComponents, | ||
initialState, | ||
loginMechanisms, | ||
services, | ||
signUpAttributes, | ||
socialProviders, | ||
variation, | ||
}: AuthenticatorProps) { | ||
const components = { ...defaultComponents, ...customComponents }; | ||
const machineProps = { | ||
initialState, | ||
loginMechanisms, | ||
services, | ||
signUpAttributes, | ||
socialProviders, | ||
}; | ||
|
||
// Helper component that sends init event to the parent provider | ||
function InitMachine({ children, ...machineProps }) { | ||
const { _send, route } = useAuthenticator(); | ||
useEffect(() => { | ||
if (route === 'idle') { | ||
_send({ | ||
type: 'INIT', | ||
data: machineProps, | ||
}); | ||
Comment on lines
+41
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a follow up from #1162. We're setting |
||
} | ||
}, []); | ||
return <>{children}</>; | ||
} | ||
|
||
return ( | ||
<Provider | ||
components={components} | ||
initialState={initialState} | ||
loginMechanisms={loginMechanisms} | ||
services={services} | ||
signUpAttributes={signUpAttributes} | ||
socialProviders={socialProviders} | ||
> | ||
<Router className={className} children={children} variation={variation} /> | ||
<Provider> | ||
<CustomComponentsContext.Provider value={{ components }}> | ||
<InitMachine {...machineProps}> | ||
<Router | ||
className={className} | ||
children={children} | ||
variation={variation} | ||
/> | ||
</InitMachine> | ||
</CustomComponentsContext.Provider> | ||
</Provider> | ||
); | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ import { translate } from '@aws-amplify/ui'; | |
import { useAuthenticator } from '../..'; | ||
import { Button, Flex, Heading, Text } from '../../..'; | ||
import { isInputOrSelectElement, isInputElement } from '../../../helpers/utils'; | ||
import { useCustomComponents } from '../hooks/useCustomComponents'; | ||
|
||
import { | ||
ConfirmationCodeInput, | ||
|
@@ -12,18 +13,20 @@ import { | |
|
||
export function ConfirmSignUp() { | ||
const { | ||
components: { | ||
ConfirmSignUp: { | ||
Header = ConfirmSignUp.Header, | ||
Footer = ConfirmSignUp.Footer, | ||
}, | ||
}, | ||
isPending, | ||
resendCode, | ||
submitForm, | ||
updateForm, | ||
codeDeliveryDetails: { DeliveryMedium, Destination } = {}, | ||
} = useAuthenticator(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assert this is not a breaking change because components were only used internally to emulate And There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you for this explanation! |
||
const { | ||
components: { | ||
ConfirmSignUp: { | ||
Header = ConfirmSignUp.Header, | ||
Footer = ConfirmSignUp.Footer, | ||
}, | ||
}, | ||
} = useCustomComponents(); | ||
|
||
const handleChange = (event: React.FormEvent<HTMLFormElement>) => { | ||
if (isInputOrSelectElement(event.target)) { | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,19 @@ | ||
import { CognitoUserAmplify } from '@aws-amplify/ui'; | ||
import * as React from 'react'; | ||
|
||
import { useAuthenticator } from '..'; | ||
import { View } from '../../..'; | ||
import { ConfirmSignIn } from '../ConfirmSignIn'; | ||
import { ConfirmSignUp } from '../ConfirmSignUp'; | ||
import { ForceNewPassword } from '../ForceNewPassword'; | ||
import { useCustomComponents } from '../hooks/useCustomComponents'; | ||
import { ConfirmResetPassword, ResetPassword } from '../ResetPassword'; | ||
import { SetupTOTP } from '../SetupTOTP'; | ||
import { SignInSignUpTabs } from '../shared'; | ||
import { ConfirmVerifyUser, VerifyUser } from '../VerifyUser'; | ||
|
||
export type RouterProps = { | ||
className?: string; | ||
children: ({ | ||
children?: ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Glad you made this optional. Always bothered me you had to add |
||
signOut, | ||
user, | ||
}: { | ||
|
@@ -32,15 +32,15 @@ export function Router({ | |
className, | ||
variation = 'default', | ||
}: RouterProps) { | ||
const { route, signOut, user } = useAuthenticator(); | ||
|
||
const { | ||
components: { Header, Footer }, | ||
route, | ||
signOut, | ||
user, | ||
} = useAuthenticator(); | ||
} = useCustomComponents(); | ||
|
||
// `Authenticator` might not have `children` for non SPA use cases. | ||
if (['authenticated', 'signOut'].includes(route)) { | ||
return children({ signOut, user }); | ||
return children ? children({ signOut, user }) : null; | ||
} | ||
|
||
return ( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can try to make sure Vue works in the future here, with this new useAuthenticator feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, I'll do one for Angular too. Let's do that on separate PRs though, in case there are
useAuthenticator
gotchas in other frameworks. (and tbh I'm scared I broke something this PR and have to revert it)