├── .circleci
└── config.yml
├── .gitignore
├── LICENSE
├── README.md
├── example
├── .gitignore
├── index.html
├── index.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
├── package.json
├── src
├── AuthContext.tsx
├── AuthProvider.tsx
├── index.tsx
└── usePrevious.ts
├── test
└── AuthProvider.test.tsx
├── tsconfig.json
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 |
3 | jobs:
4 | install:
5 | docker:
6 | - image: circleci/node:lts
7 | steps:
8 | - checkout
9 | - restore_cache:
10 | keys:
11 | - yarn-v1-dependencies-{{ checksum "yarn.lock" }}
12 | - run:
13 | name: Install Deps
14 | command: yarn install --frozen-lockfile
15 | - save_cache:
16 | key: yarn-v1-dependencies-{{ checksum "yarn.lock" }}
17 | paths:
18 | - node_modules
19 |
20 | test:
21 | docker:
22 | - image: circleci/node:lts
23 | steps:
24 | - checkout
25 | - restore_cache:
26 | keys:
27 | - yarn-v1-dependencies-{{ checksum "yarn.lock" }}
28 | - run:
29 | name: Test
30 | command: CI=true yarn test --coverage
31 | - run: bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN
32 |
33 | lint:
34 | docker:
35 | - image: circleci/node:lts
36 | steps:
37 | - checkout
38 | - restore_cache:
39 | keys:
40 | - yarn-v1-dependencies-{{ checksum "yarn.lock" }}
41 | - run:
42 | name: Lint
43 | command: yarn lint
44 |
45 | workflows:
46 | version: 2
47 | build_and_test:
48 | jobs:
49 | - install
50 |
51 | - test:
52 | requires:
53 | - install
54 |
55 | - lint:
56 | requires:
57 | - install
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | .rts2_cache_cjs
6 | .rts2_cache_esm
7 | .rts2_cache_umd
8 | .rts2_cache_system
9 | dist
10 | coverage
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Ryan Castner
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Auth Provider
2 |
3 | > Easy to integrate react authentication management through context.
4 |
5 | [](https://codecov.io/gh/audiolion/react-auth-provider) [](https://circleci.com/gh/audiolion/react-auth-provider/tree/master)
6 |
7 | ## Features
8 |
9 | - Dead simple to integrate
10 | - Properly memoized authenticated attribute that can be consumed by your app
11 | - Hooks into `onLogin` and `onLogout`
12 |
13 | ## Install
14 |
15 | ```shell
16 | $ yarn add @ryanar/react-auth-provider
17 | ```
18 |
19 | ## Usage
20 |
21 | Add `AuthProvider` somewhere towards the top of your tree. You likely want to do some routing on login or logout so it should be below your routing context (e.g. react router's `Router` component).
22 |
23 | ```tsx
24 | // App.tsx
25 | import React from 'react';
26 | import { useHistory } from 'react-router-dom';
27 | import { AuthProvider } from '@ryanar/react-auth-provider';
28 |
29 | function App() {
30 | const history = useHistory();
31 |
32 | const handleLogin = () => {
33 | history.push('/');
34 | };
35 |
36 | const handleLogout = () => {
37 | history.push('/login');
38 | };
39 |
40 | return (
41 |
42 |
43 |
44 |
45 | );
46 | }
47 | ```
48 |
49 | Integrate `setAuthenticated` into your login and logout flow, here is an example of a login flow:
50 |
51 | ```tsx
52 | // Login.tsx
53 | import React from 'react';
54 | import { AuthContext } from '@ryanar/react-auth-provider';
55 |
56 | function Login(props: RouteComponentProps) {
57 | const { setAuthenticated } = React.useContext(AuthContext);
58 | return (
59 |
62 | );
63 | }
64 | ```
65 |
66 | Optionally configure a route that checks for authenticated, here is an example using react router:
67 |
68 | ```tsx
69 | // AuthRoute.tsx
70 | import React from 'react';
71 | import { AuthContext } from '@ryanar/react-auth-provider';
72 |
73 | function AuthRoute(props: RouteComponentProps) {
74 | const { authenticated } = React.useContext(AuthContext);
75 |
76 | if (!authenticated) {
77 | return ;
78 | }
79 |
80 | const { component: Component, ...rest } = props;
81 |
82 | return ;
83 | }
84 | ```
85 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import * as React from 'react';
3 | import * as ReactDOM from 'react-dom';
4 | import { AuthContext, AuthProvider, AuthProviderProps } from '../.';
5 |
6 | function DisplayAuthenticated() {
7 | const { authenticated } = React.useContext(AuthContext);
8 | return {authenticated ? 'true' : 'false'}
;
9 | }
10 |
11 | function Button() {
12 | const { authenticated, setAuthenticated } = React.useContext(AuthContext);
13 | return (
14 |
21 | );
22 | }
23 |
24 | function App({ onLogin, onLogout, defaultAuthenticated }: AuthProviderProps) {
25 | return (
26 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | ReactDOM.render(, document.getElementById('root'));
38 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "react-app-polyfill": "^1.0.0"
12 | },
13 | "alias": {
14 | "react": "../node_modules/react",
15 | "react-dom": "../node_modules/react-dom/profiling",
16 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
17 | },
18 | "devDependencies": {
19 | "@types/react": "^16.8.15",
20 | "@types/react-dom": "^16.8.4",
21 | "parcel": "^1.12.3",
22 | "typescript": "^3.4.5"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "baseUrl": ".",
17 | "types": ["node"]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ryanar/react-auth-provider",
3 | "version": "0.3.3",
4 | "repository": "https://github.com/audiolion/react-auth-provider",
5 | "author": "Ryan Castner ",
6 | "license": "MIT",
7 | "main": "dist/index.js",
8 | "module": "dist/react-auth-provider.esm.js",
9 | "typings": "dist/index.d.ts",
10 | "files": [
11 | "dist"
12 | ],
13 | "keywords": [
14 | "react",
15 | "auth",
16 | "authentication",
17 | "context",
18 | "provider",
19 | "typescript"
20 | ],
21 | "scripts": {
22 | "start": "tsdx watch",
23 | "build": "tsdx build",
24 | "test": "tsdx test --env=jsdom",
25 | "lint": "tsdx lint"
26 | },
27 | "peerDependencies": {
28 | "react": ">=16"
29 | },
30 | "husky": {
31 | "hooks": {
32 | "pre-commit": "tsdx lint"
33 | }
34 | },
35 | "prettier": {
36 | "printWidth": 80,
37 | "semi": true,
38 | "singleQuote": true,
39 | "trailingComma": "es5"
40 | },
41 | "devDependencies": {
42 | "@testing-library/react": "^9.3.0",
43 | "@types/jest": "^24.0.18",
44 | "@types/react": "^16.9.5",
45 | "@types/react-dom": "^16.9.1",
46 | "@types/testing-library__react": "^9.1.2",
47 | "eslint-config-react-app": "5.0.1",
48 | "husky": "^3.0.9",
49 | "react": "^16.10.2",
50 | "react-dom": "^16.10.2",
51 | "tsdx": "^0.9.3",
52 | "tslib": "^1.10.0",
53 | "typescript": "^3.6.4"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/AuthContext.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export type IAuthContext = {
4 | authenticated: boolean;
5 | setAuthenticated: (isAuthenticated: boolean) => void;
6 | };
7 |
8 | /* istanbul ignore next */
9 | const noop = () => {};
10 |
11 | export const AuthContext = React.createContext({
12 | authenticated: false,
13 | setAuthenticated: noop,
14 | });
15 |
--------------------------------------------------------------------------------
/src/AuthProvider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { usePrevious } from './usePrevious';
3 | import { AuthContext } from './AuthContext';
4 |
5 | export type AuthProviderProps = {
6 | defaultAuthenticated?: boolean;
7 | onLogin?: () => void;
8 | onLogout?: () => void;
9 | };
10 |
11 | export const AuthProvider: React.FC = ({
12 | defaultAuthenticated = false,
13 | onLogin,
14 | onLogout,
15 | children,
16 | }) => {
17 | const [authenticated, setAuthenticated] = React.useState(
18 | defaultAuthenticated
19 | );
20 |
21 | const previousAuthenticated = usePrevious(authenticated);
22 |
23 | React.useEffect(() => {
24 | if (!previousAuthenticated && authenticated) {
25 | onLogin && onLogin();
26 | }
27 | }, [previousAuthenticated, authenticated, onLogin]);
28 |
29 | React.useEffect(() => {
30 | if (previousAuthenticated && !authenticated) {
31 | onLogout && onLogout();
32 | }
33 | }, [previousAuthenticated, authenticated, onLogout]);
34 |
35 | const contextValue = React.useMemo(
36 | () => ({
37 | authenticated,
38 | setAuthenticated,
39 | }),
40 | [authenticated]
41 | );
42 |
43 | return (
44 | {children}
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | export * from './AuthProvider';
2 | export * from './AuthContext';
3 |
--------------------------------------------------------------------------------
/src/usePrevious.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function usePrevious(value: T) {
4 | // The ref object is a generic container whose current property is mutable ...
5 | // ... and can hold any value, similar to an instance property on a class
6 | const ref = React.useRef();
7 |
8 | // Store current value in ref
9 | React.useEffect(() => {
10 | ref.current = value;
11 | }, [value]); // Only re-run if value changes
12 |
13 | // Return previous value (happens before update in useEffect above)
14 | return ref.current;
15 | }
16 |
--------------------------------------------------------------------------------
/test/AuthProvider.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, fireEvent } from '@testing-library/react';
3 | import { AuthProvider, AuthContext, AuthProviderProps } from '../src';
4 |
5 | function DisplayAuthenticated() {
6 | const { authenticated } = React.useContext(AuthContext);
7 | return {authenticated ? 'true' : 'false'}
;
8 | }
9 |
10 | function Button() {
11 | const { authenticated, setAuthenticated } = React.useContext(AuthContext);
12 | return (
13 |
20 | );
21 | }
22 |
23 | function App({ onLogin, onLogout, defaultAuthenticated }: AuthProviderProps) {
24 | return (
25 |
30 |
31 |
32 |
33 | );
34 | }
35 |
36 | describe('AuthProvider', () => {
37 | it('defaults to unauthenticated', () => {
38 | const { getByTestId } = render();
39 | expect(getByTestId(/authenticated/).textContent).toEqual('false');
40 | });
41 |
42 | it('accepts defaultAuthenticated', () => {
43 | const { getByTestId } = render();
44 | expect(getByTestId(/authenticated/).textContent).toEqual('true');
45 | });
46 |
47 | it('updates authenticated through setAuthenticated', async () => {
48 | const { getByTestId } = render();
49 |
50 | fireEvent.click(getByTestId(/button/));
51 |
52 | expect(getByTestId(/authenticated/).textContent).toEqual('true');
53 | });
54 |
55 | it('calls onLogin', () => {
56 | const onLoginMock = jest.fn();
57 |
58 | const { getByTestId } = render();
59 |
60 | fireEvent.click(getByTestId(/button/));
61 |
62 | expect(onLoginMock).toHaveBeenCalledTimes(1);
63 | });
64 |
65 | it('calls onLogout', () => {
66 | const onLogoutMock = jest.fn();
67 |
68 | const { getByTestId } = render(
69 |
70 | );
71 |
72 | fireEvent.click(getByTestId(/button/));
73 |
74 | expect(onLogoutMock).toHaveBeenCalledTimes(1);
75 | });
76 | });
77 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types", "test"],
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "./",
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "strictFunctionTypes": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitThis": true,
17 | "alwaysStrict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noImplicitReturns": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "moduleResolution": "node",
23 | "baseUrl": "./",
24 | "paths": {
25 | "*": ["src/*", "node_modules/*"]
26 | },
27 | "jsx": "react",
28 | "esModuleInterop": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------