Skip to content

Instantly share code, notes, and snippets.

@larrybotha
Last active December 26, 2019 23:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save larrybotha/4914ff3af8c90cf844af74ed03ff599e to your computer and use it in GitHub Desktop.
Save larrybotha/4914ff3af8c90cf844af74ed03ff599e to your computer and use it in GitHub Desktop.
XState + AWS Amplify example

XState + AWS Amplify example

A breakdown of a project that uses XState to manage state for authenticating a user with Cognito, and then finding the authorized application user with an invoked machine.

  1. create auth machine
  2. create auth context
  3. handle AWS authentication
  4. once user authenticates with Cognito, redirect to user route to get user from db
  5. at user route use userMachine service from authMachine to get application user
  6. once associated application user is found, send user to organization route to allow user to associate session with specific organization
import React, {createContext, useContext} from 'react';
import {useMachine} from '@xstate/react';
import {useHistory} from 'react-router-dom';
import {Auth} from 'aws-amplify';
import {orgMachine} from './organization-machine';
import {userMachine} from './user-machine';
import {xeroOrgMachine} from './xero-organization-machine';
import {routes} from './routes';
/**
* machine services
*/
const services = {
logOut: () => Auth.signOut(),
orgMachine,
userMachine,
xeroOrgMachine,
};
/**
* machine guards
*/
const guards = {
isAlreadyAuthd = () => {
const authState = // get cognito auth state from localstorage
return authState === 'signedIn';
},
};
/**
* machine actions
*/
const actions = {
redirectToGetUserRoute = ({history}) => {
history.push(routes.authIndex);
}.
redirectToLoginRoute = ({history}) => {
history.push(routes.login);
},
};
const AuthContext = createContext(undefined);
const AuthProvider = ({children}) => {
const history = useHistory();
const [current, send] = useMachine(authMachine.withContext({history}), {
actions: authMachineActions,
guards: authMachineGuards,
services: authMachineServices,
});
const value = {
current,
orgMachine,
send,
userMachine,
xeroOrgMachine,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
const useAuthMachine = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuthMachine must be used within an AuthProvider component');
}
return context;
};
export {AuthContext, AuthProvider, useAuthMachine};
import {Machine} from 'xstate';
const authMachine: AuthMachine = Machine({
id: 'auth',
initial: 'unknown',
states: {
unknown: {
on: {
'': [
{
cond: 'isAlreadyAuthd',
target: 'cognitoAuthd',
},
{
target: 'notAuthd',
},
],
},
},
notAuthd: {
on: {
COGNITO_AUTH_SUCCESS: 'cognitoAuthd',
},
},
loggingOut: {
invoke: {
src: 'logOut',
onDone: {
actions: 'redirectToLoginRoute',
target: 'notAuthd',
},
onError: 'cognitoAuthd',
},
},
cognitoAuthd: {
type: 'parallel',
entry: 'redirectToGetUserRoute',
on: {
COGNITO_LOGOUT: 'loggingOut',
},
states: {
user: {
initial: 'idle',
states: {
idle: {
invoke: {
src: 'userMachine',
onDone: 'found',
},
},
found: {
entry: 'redirectToOrgRoute',
type: 'final',
},
},
},
org: {
initial: 'notSelected',
states: {
notSelected: {
on: {
ORG_SELECT_FROM_DB: 'selectFromDb',
ORG_SELECT_FROM_XERO: 'selectFromXero',
},
},
selectFromDb: {
entry: 'unpersistOrg',
invoke: {
src: 'orgMachine',
onDone: {
actions: 'persistOrg',
target: 'selected',
},
},
},
selectFromXero: {
invoke: {
src: 'xeroOrgMachine',
onDone: {
actions: 'persistOrg',
target: 'selectedWithXero',
},
},
},
selected: {
on: {
ORG_CHANGE: 'selectFromDb',
ORG_SELECT_FROM_XERO: 'selectFromXero',
},
},
selectedWithXero: {
exit: 'removeXeroAccessToken',
on: {
ORG_CHANGE: 'selectFromDb',
XERO_LOGOUT: 'selected',
},
},
},
},
},
},
},
});
export {authMachine};
import React, {useEffect} from 'react';
import {useMachine} from '@xstate/react';
import {useAuthMachine} from './auth-machine-context';
/**
* Once we are authenticated with AWS, we need to find the associated user in our
* own app's db
*/
const UserAuthIndex = () => {
const {send: sendToParent, userMachine} = useAuthMachine();
const [current, send] = useMachine(userMachine);
const {value: userState} = current;
const fetchUser = () => send('FETCH_USER_FROM_DB');
const createUser = () => send('CREATE_USER');
useEffect(() => {
send('FETCH_USER_FROM_DB');
}, [])
useEffect(() => {
if (userState === 'found') {
sendToParent('user.found');
}
}, [userState]);
return (
<div>
{current.matches('fetching') ? 'loading user...' : null}
{current.matches('creating') ? 'creating user...' : null}
{current.matches('success') ? 'user found, redirecting you to select an organization...' : null}
{current.matches('failed') ?
<div>
<button onClick={fetchUser}>retry</button>
<button onClick={createUser}>create user</button>
</div>
: null}
</div>
);
};
export default UserAuthIndex;
import React from 'react';
import {Authenticator as CognitoAuthenticator} from 'aws-amplify-react';
import {amplifyConfig} from './amplify-config';
import {useAuthMachine} from './auth-machine-context';
const LoginIndex = () => {
const {send} = useAuthMachine();
const handleStateChange = state => {
if (state === 'signedIn') {
send('COGNITO_AUTH_SUCCESS');
}
};
return (
<CognitoAuthenticator onStateChange={handleStateChange} amplifyConfig={amplifyConfig} />
);
};
export default LoginIndex;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment