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

Enable useAuthenticator usage outside <Authenticator /> #1168

Merged
merged 26 commits into from
Jan 26, 2022
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
38 changes: 38 additions & 0 deletions .changeset/weak-stingrays-raise.md
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
Copy link
Contributor

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.

Copy link
Contributor Author

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)

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"

59 changes: 47 additions & 12 deletions packages/react/src/components/Authenticator/Authenticator.tsx
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a follow up from #1162. We're setting INIT event after we get the necessary machineProps in Authenticator.

}
}, []);
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>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useAuthenticator now returns only contexts related to auth!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 Slot functionality. There were no motives for customers to re-access custom components after they already passed in.

And useAuthenticator was never documented in the first place.

Copy link
Contributor

Choose a reason for hiding this comment

The 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)) {
Expand Down
67 changes: 0 additions & 67 deletions packages/react/src/components/Authenticator/Provider/index.tsx

This file was deleted.

14 changes: 7 additions & 7 deletions packages/react/src/components/Authenticator/Router/index.tsx
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?: ({
Copy link
Contributor

@ErikCH ErikCH Jan 19, 2022

Choose a reason for hiding this comment

The 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 and user when using authenticator

signOut,
user,
}: {
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import { Button, Flex, PasswordField, View } from '../../..';
import { FederatedSignIn } from '../FederatedSignIn';
import { RemoteErrorMessage, UserNameAlias } from '../shared';
import { isInputElement, isInputOrSelectElement } from '../../../helpers/utils';
import { useCustomComponents } from '../hooks/useCustomComponents';

export function SignIn() {
const { isPending, submitForm, updateForm } = useAuthenticator();
const {
components: {
SignIn: { Header = SignIn.Header, Footer = SignIn.Footer },
},
isPending,
submitForm,
updateForm,
} = useAuthenticator();
} = useCustomComponents();

const handleChange = (event: React.FormEvent<HTMLFormElement>) => {
if (isInputOrSelectElement(event.target)) {
Expand Down
15 changes: 9 additions & 6 deletions packages/react/src/components/Authenticator/SignUp/SignUp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ import { FederatedSignIn } from '../FederatedSignIn';
import { RemoteErrorMessage } from '../shared';
import { FormFields } from './FormFields';
import { isInputOrSelectElement, isInputElement } from '../../../helpers/utils';
import { useCustomComponents } from '../hooks/useCustomComponents';

export function SignUp() {
const { components, hasValidationErrors, isPending, submitForm, updateForm } =
const { hasValidationErrors, isPending, submitForm, updateForm } =
useAuthenticator();

const {
SignUp: {
Header = SignUp.Header,
FormFields = SignUp.FormFields,
Footer = SignUp.Footer,
components: {
SignUp: {
Header = SignUp.Header,
FormFields = SignUp.FormFields,
Footer = SignUp.Footer,
},
},
} = components;
} = useCustomComponents();

const handleChange = (event: React.FormEvent<HTMLFormElement>) => {
if (isInputOrSelectElement(event.target)) {
Expand Down
Loading