From f0d762d11ba1a44152c10f9470276154374695b0 Mon Sep 17 00:00:00 2001 From: Caleb Pollman Date: Tue, 30 Aug 2022 19:38:01 -0700 Subject: [PATCH] chore(rwa): remove user destructure in getServiceContextFacade --- .../hooks/useAuthenticator/index.js | 8 ++ .../useAuthenticator/useAuthenticator.js | 120 ++++++++++++++++++ .../react-core/dist/__tests__/index.spec.js | 8 ++ .../hooks/useAuthenticator/index.js | 1 + .../useAuthenticator/useAuthenticator.js | 113 +++++++++++++++++ .../dist/esm/__tests__/index.spec.js | 6 + packages/react-core/dist/esm/index.js | 1 + packages/react-core/dist/index.js | 4 + .../hooks/useAuthenticator/index.d.ts | 1 + .../useAuthenticator/useAuthenticator.d.ts | 50 ++++++++ .../dist/types/__tests__/index.spec.d.ts | 1 + packages/react-core/dist/types/index.d.ts | 1 + .../ui/src/helpers/authenticator/facade.ts | 2 +- 13 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 packages/react-core/dist/Authenticator/hooks/useAuthenticator/index.js create mode 100644 packages/react-core/dist/Authenticator/hooks/useAuthenticator/useAuthenticator.js create mode 100644 packages/react-core/dist/__tests__/index.spec.js create mode 100644 packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/index.js create mode 100644 packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/useAuthenticator.js create mode 100644 packages/react-core/dist/esm/__tests__/index.spec.js create mode 100644 packages/react-core/dist/esm/index.js create mode 100644 packages/react-core/dist/index.js create mode 100644 packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/index.d.ts create mode 100644 packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/useAuthenticator.d.ts create mode 100644 packages/react-core/dist/types/__tests__/index.spec.d.ts create mode 100644 packages/react-core/dist/types/index.d.ts diff --git a/packages/react-core/dist/Authenticator/hooks/useAuthenticator/index.js b/packages/react-core/dist/Authenticator/hooks/useAuthenticator/index.js new file mode 100644 index 00000000000..586f5f9cda0 --- /dev/null +++ b/packages/react-core/dist/Authenticator/hooks/useAuthenticator/index.js @@ -0,0 +1,8 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useAuthenticator = void 0; +var useAuthenticator_1 = require("./useAuthenticator"); +Object.defineProperty(exports, "useAuthenticator", { enumerable: true, get: function () { return __importDefault(useAuthenticator_1).default; } }); diff --git a/packages/react-core/dist/Authenticator/hooks/useAuthenticator/useAuthenticator.js b/packages/react-core/dist/Authenticator/hooks/useAuthenticator/useAuthenticator.js new file mode 100644 index 00000000000..0215f745e1c --- /dev/null +++ b/packages/react-core/dist/Authenticator/hooks/useAuthenticator/useAuthenticator.js @@ -0,0 +1,120 @@ +"use strict"; +// export default function useAuthenticator(): null { +// return null; +// } +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Provider = exports.AuthenticatorContext = exports.areArrayValuesEqual = void 0; +const tslib_1 = require("tslib"); +const react_1 = tslib_1.__importDefault(require("react")); +const ui_1 = require("@aws-amplify/ui"); +const react_2 = require("@xstate/react"); +const isEmpty_1 = tslib_1.__importDefault(require("lodash/isEmpty")); +const isArray_1 = tslib_1.__importDefault(require("lodash/isArray")); +const isObject_1 = tslib_1.__importDefault(require("lodash/isObject")); +function isEmptyObj(val) { + return (0, isObject_1.default)(val) && (0, isEmpty_1.default)(val); +} +function isEmptyArr(val) { + return (0, isArray_1.default)(val) && (0, isEmpty_1.default)(val); +} +/** + * Does a comparison of each array value, plus a value equality check for empty + * objects and arrays. + */ +const areArrayValuesEqual = (arr1, arr2) => { + if (arr1.length !== arr2.length) + return false; + return arr1.every((elem1, index) => { + const elem2 = arr2[index]; + /** + * edge cases: if both values are empty object/array, we consider them equal. + * These can catch empty default values (`[]`, `{}`) that unintentionally point + * to different refernces. + * + * We can consider doing a deep comparison, but left it here for efficiency + * + practicality for authenticator state comparison purposes. + */ + if (isEmptyArr(elem1) && isEmptyArr(elem2)) + return true; + if (isEmptyObj(elem1) && isEmptyObj(elem2)) + return true; + return elem1 === elem2; + }); +}; +exports.areArrayValuesEqual = areArrayValuesEqual; +/** + * AuthenticatorContext serves static reference to the auth machine service. + * + * https://xstate.js.org/docs/recipes/react.html#context-provider + */ +exports.AuthenticatorContext = react_1.default.createContext({}); +const Provider = ({ children, }) => { + /** + * Based on use cases, developer might already have added another Provider + * outside Authenticator. In that case, we sync the two providers by just + * passing the parent value. + * + * TODO(BREAKING): enforce only one provider in App tree + */ + const parentProviderVal = react_1.default.useContext(exports.AuthenticatorContext); + /** + * Ideally, `useInterpret` shouldn't even be run if `parentProviderVal` is + * not empty. But conditionally running `useInterpret` breaks rules of hooks. + * + * Leaving this as is for now in the interest of suggested code guideline. + */ + const service = (0, react_2.useInterpret)(ui_1.createAuthenticatorMachine); + const value = react_1.default.useMemo(() => ((0, isEmpty_1.default)(parentProviderVal) ? { service } : parentProviderVal), [parentProviderVal, service]); + const { service: activeService } = value; + react_1.default.useEffect(() => { + return (0, ui_1.listenToAuthHub)(activeService); + }, [activeService]); + return (react_1.default.createElement(exports.AuthenticatorContext.Provider, { value: value }, children)); +}; +exports.Provider = Provider; +const useAuthenticatorService = () => { + const { service } = react_1.default.useContext(exports.AuthenticatorContext); + if (!service) { + throw new Error('Please ensure you wrap your App with `Authenticator.Provider`.\nSee the `useAuthenticator` section on https://ui.docs.amplify.aws/connected-components/authenticator.'); + } + return service; +}; +/** + * [📖 Docs](https://ui.docs.amplify.aws/react/connected-components/authenticator/headless#useauthenticator-hook) + */ +function useAuthenticator(selector) { + const service = useAuthenticatorService(); + const { send } = service; + const getFacade = react_1.default.useCallback((state) => (Object.assign({}, (0, ui_1.getServiceFacade)({ send, state }))), [send]); + /** + * For `useSelector`'s selector argument, we transform `state` into + * public facade values using `getFacade`. + * + * This is to hide the internal xstate implementation details to customers. + */ + const xstateSelector = (state) => getFacade(state); + /** + * comparator decides whether or not the new authState should trigger a + * re-render. Does a deep equality check. + */ + const comparator = (prevFacade, nextFacade) => { + if (!selector) + return false; + /** + * Apply the passed in `selector` to get the value of their desired + * dependency array. + */ + const prevDepsArray = selector(prevFacade); + const nextDepsArray = selector(nextFacade); + // Shallow compare the array values + // TODO: is there a reason to compare deep at the cost of expensive comparisons? + return (0, exports.areArrayValuesEqual)(prevDepsArray, nextDepsArray); + }; + const facade = (0, react_2.useSelector)(service, xstateSelector, comparator); + return Object.assign(Object.assign({}, facade), { + /** @deprecated For internal use only */ + _state: service.getSnapshot(), + /** @deprecated For internal use only */ + _send: send }); +} +exports.default = useAuthenticator; diff --git a/packages/react-core/dist/__tests__/index.spec.js b/packages/react-core/dist/__tests__/index.spec.js new file mode 100644 index 00000000000..c3aa6091e6f --- /dev/null +++ b/packages/react-core/dist/__tests__/index.spec.js @@ -0,0 +1,8 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const index_1 = require("../index"); +describe('Haha', () => { + it('should be truthy', () => { + expect(index_1.Haha).toBeTruthy(); + }); +}); diff --git a/packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/index.js b/packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/index.js new file mode 100644 index 00000000000..00c182d7782 --- /dev/null +++ b/packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/index.js @@ -0,0 +1 @@ +export { default as useAuthenticator } from './useAuthenticator'; diff --git a/packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/useAuthenticator.js b/packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/useAuthenticator.js new file mode 100644 index 00000000000..6ef86cc9755 --- /dev/null +++ b/packages/react-core/dist/esm/Authenticator/hooks/useAuthenticator/useAuthenticator.js @@ -0,0 +1,113 @@ +// export default function useAuthenticator(): null { +// return null; +// } +import React from 'react'; +import { createAuthenticatorMachine, getServiceFacade, listenToAuthHub, } from '@aws-amplify/ui'; +import { useSelector, useInterpret } from '@xstate/react'; +import isEmpty from 'lodash/isEmpty'; +import isArray from 'lodash/isArray'; +import isObject from 'lodash/isObject'; +function isEmptyObj(val) { + return isObject(val) && isEmpty(val); +} +function isEmptyArr(val) { + return isArray(val) && isEmpty(val); +} +/** + * Does a comparison of each array value, plus a value equality check for empty + * objects and arrays. + */ +export const areArrayValuesEqual = (arr1, arr2) => { + if (arr1.length !== arr2.length) + return false; + return arr1.every((elem1, index) => { + const elem2 = arr2[index]; + /** + * edge cases: if both values are empty object/array, we consider them equal. + * These can catch empty default values (`[]`, `{}`) that unintentionally point + * to different refernces. + * + * We can consider doing a deep comparison, but left it here for efficiency + * + practicality for authenticator state comparison purposes. + */ + if (isEmptyArr(elem1) && isEmptyArr(elem2)) + return true; + if (isEmptyObj(elem1) && isEmptyObj(elem2)) + return true; + return elem1 === elem2; + }); +}; +/** + * AuthenticatorContext serves static reference to the auth machine service. + * + * https://xstate.js.org/docs/recipes/react.html#context-provider + */ +export const AuthenticatorContext = React.createContext({}); +export const Provider = ({ children, }) => { + /** + * Based on use cases, developer might already have added another Provider + * outside Authenticator. In that case, we sync the two providers by just + * passing the parent value. + * + * TODO(BREAKING): enforce only one provider in App tree + */ + const parentProviderVal = React.useContext(AuthenticatorContext); + /** + * Ideally, `useInterpret` shouldn't even be run if `parentProviderVal` is + * not empty. But conditionally running `useInterpret` breaks rules of hooks. + * + * Leaving this as is for now in the interest of suggested code guideline. + */ + const service = useInterpret(createAuthenticatorMachine); + const value = React.useMemo(() => (isEmpty(parentProviderVal) ? { service } : parentProviderVal), [parentProviderVal, service]); + const { service: activeService } = value; + React.useEffect(() => { + return listenToAuthHub(activeService); + }, [activeService]); + return (React.createElement(AuthenticatorContext.Provider, { value: value }, children)); +}; +const useAuthenticatorService = () => { + const { service } = React.useContext(AuthenticatorContext); + if (!service) { + throw new Error('Please ensure you wrap your App with `Authenticator.Provider`.\nSee the `useAuthenticator` section on https://ui.docs.amplify.aws/connected-components/authenticator.'); + } + return service; +}; +/** + * [📖 Docs](https://ui.docs.amplify.aws/react/connected-components/authenticator/headless#useauthenticator-hook) + */ +export default function useAuthenticator(selector) { + const service = useAuthenticatorService(); + const { send } = service; + const getFacade = React.useCallback((state) => (Object.assign({}, getServiceFacade({ send, state }))), [send]); + /** + * For `useSelector`'s selector argument, we transform `state` into + * public facade values using `getFacade`. + * + * This is to hide the internal xstate implementation details to customers. + */ + const xstateSelector = (state) => getFacade(state); + /** + * comparator decides whether or not the new authState should trigger a + * re-render. Does a deep equality check. + */ + const comparator = (prevFacade, nextFacade) => { + if (!selector) + return false; + /** + * Apply the passed in `selector` to get the value of their desired + * dependency array. + */ + const prevDepsArray = selector(prevFacade); + const nextDepsArray = selector(nextFacade); + // Shallow compare the array values + // TODO: is there a reason to compare deep at the cost of expensive comparisons? + return areArrayValuesEqual(prevDepsArray, nextDepsArray); + }; + const facade = useSelector(service, xstateSelector, comparator); + return Object.assign(Object.assign({}, facade), { + /** @deprecated For internal use only */ + _state: service.getSnapshot(), + /** @deprecated For internal use only */ + _send: send }); +} diff --git a/packages/react-core/dist/esm/__tests__/index.spec.js b/packages/react-core/dist/esm/__tests__/index.spec.js new file mode 100644 index 00000000000..82b01695c32 --- /dev/null +++ b/packages/react-core/dist/esm/__tests__/index.spec.js @@ -0,0 +1,6 @@ +import { Haha } from '../index'; +describe('Haha', () => { + it('should be truthy', () => { + expect(Haha).toBeTruthy(); + }); +}); diff --git a/packages/react-core/dist/esm/index.js b/packages/react-core/dist/esm/index.js new file mode 100644 index 00000000000..6bd91a41c9e --- /dev/null +++ b/packages/react-core/dist/esm/index.js @@ -0,0 +1 @@ +export const Haha = 'haha'; diff --git a/packages/react-core/dist/index.js b/packages/react-core/dist/index.js new file mode 100644 index 00000000000..ce958312909 --- /dev/null +++ b/packages/react-core/dist/index.js @@ -0,0 +1,4 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Haha = void 0; +exports.Haha = 'haha'; diff --git a/packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/index.d.ts b/packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/index.d.ts new file mode 100644 index 00000000000..00c182d7782 --- /dev/null +++ b/packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/index.d.ts @@ -0,0 +1 @@ +export { default as useAuthenticator } from './useAuthenticator'; diff --git a/packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/useAuthenticator.d.ts b/packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/useAuthenticator.d.ts new file mode 100644 index 00000000000..6793f8b45ff --- /dev/null +++ b/packages/react-core/dist/types/Authenticator/hooks/useAuthenticator/useAuthenticator.d.ts @@ -0,0 +1,50 @@ +import React from 'react'; +import { AuthInterpreter, AuthMachineSend, AuthMachineState, AuthenticatorServiceFacade } from '@aws-amplify/ui'; +/** + * Does a comparison of each array value, plus a value equality check for empty + * objects and arrays. + */ +export declare const areArrayValuesEqual: (arr1: unknown[], arr2: unknown[]) => boolean; +export declare type AuthenticatorContextValue = { + service?: AuthInterpreter; +}; +/** + * These are the "facades" that we provide, which contains contexts respective + * to current authenticator state. + */ +export declare type AuthenticatorContext = AuthenticatorServiceFacade; +/** + * These are internal xstate helpers to we share with `useAuthenticator`. + * + * TODO(breaking?): remove these internal contexts + */ +export declare type InternalAuthenticatorContext = { + _state: AuthMachineState; + _send: AuthMachineSend; +}; +/** + * Inspired from https://xstate.js.org/docs/packages/xstate-react/#useselector-actor-selector-compare-getsnapshot. + * + * Selector accepts current facade values and returns an array of + * desired value(s) that should trigger re-render. + */ +export declare type Selector = (context: AuthenticatorContext) => Array; +export interface UseAuthenticator extends AuthenticatorServiceFacade { + /** @deprecated For internal use only */ + _send: InternalAuthenticatorContext['_send']; + /** @deprecated For internal use only */ + _state: InternalAuthenticatorContext['_state']; +} +/** + * AuthenticatorContext serves static reference to the auth machine service. + * + * https://xstate.js.org/docs/recipes/react.html#context-provider + */ +export declare const AuthenticatorContext: React.Context; +export declare const Provider: ({ children, }: { + children: React.ReactNode; +}) => JSX.Element; +/** + * [📖 Docs](https://ui.docs.amplify.aws/react/connected-components/authenticator/headless#useauthenticator-hook) + */ +export default function useAuthenticator(selector?: Selector): UseAuthenticator; diff --git a/packages/react-core/dist/types/__tests__/index.spec.d.ts b/packages/react-core/dist/types/__tests__/index.spec.d.ts new file mode 100644 index 00000000000..cb0ff5c3b54 --- /dev/null +++ b/packages/react-core/dist/types/__tests__/index.spec.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/packages/react-core/dist/types/index.d.ts b/packages/react-core/dist/types/index.d.ts new file mode 100644 index 00000000000..7e39e215a82 --- /dev/null +++ b/packages/react-core/dist/types/index.d.ts @@ -0,0 +1 @@ +export declare const Haha = "haha"; diff --git a/packages/ui/src/helpers/authenticator/facade.ts b/packages/ui/src/helpers/authenticator/facade.ts index e25215ec29c..e7fe2556118 100644 --- a/packages/ui/src/helpers/authenticator/facade.ts +++ b/packages/ui/src/helpers/authenticator/facade.ts @@ -122,7 +122,7 @@ export const getServiceContextFacade = ( // check for user in actorContext prior to state context. actorContext is more "up to date", // but is not available on all states - const { user } = actorContext ?? state.context; + const user = actorContext?.user ?? state.context?.user; const hasValidationErrors = validationErrors && Object.keys(validationErrors).length > 0;