Skip to content

Commit

Permalink
Enable useAuthenticator usage outside <Authenticator /> (#1168)
Browse files Browse the repository at this point in the history
* Initial draft

* First attempt at useAuthenticator example

* Allow children to be null

* Remove initial state

* Provider now only stores static ref to service

* match new exports

* add useAuthenticator test

* remove unused variable

* Create weak-stingrays-raise.md

* Create aero-stingrays-raise.md

* Fix typo

* add more comments

* Add comment on selector choice

* useAuthenticator only returns selected values

* Update changesets

* update exports

* Fix types

* update exports list

* Update .changeset/aero-stingrays-raise.md

* Update .changeset/weak-stingrays-raise.md

* selector returns an array of dependencies

* Update test name

* Remove unused variable

* Update packages/ui/src/helpers/auth.ts

* Update comments
  • Loading branch information
wlee221 authored Jan 26, 2022
1 parent 6c1c0c4 commit b32dd86
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 106 deletions.
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
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,
});
}
}, []);
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();
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?: ({
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
7 changes: 3 additions & 4 deletions packages/react/src/components/Authenticator/SignIn/SignIn.tsx
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

0 comments on commit b32dd86

Please sign in to comment.