├── .github ├── FUNDING.yaml └── workflows │ ├── main.yml │ └── size.yml ├── .gitignore ├── .storybook ├── main.ts └── preview.ts ├── LICENSE ├── README.md ├── example ├── .gitignore ├── .npmignore ├── index.html ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── pages │ │ ├── dashboard.tsx │ │ ├── login.tsx │ │ └── resetPassword.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── package-lock.json ├── package.json ├── src ├── components │ ├── AuthorizerBasicAuthLogin.tsx │ ├── AuthorizerForgotPassword.tsx │ ├── AuthorizerMagicLinkLogin.tsx │ ├── AuthorizerResetPassword.tsx │ ├── AuthorizerRoot.tsx │ ├── AuthorizerSignup.tsx │ ├── AuthorizerSocialLogin.tsx │ ├── AuthorizerTOTPScanner.tsx │ ├── AuthorizerVerifyOtp.tsx │ ├── Message.tsx │ └── PasswordStrengthIndicator.tsx ├── constants │ └── index.ts ├── contexts │ └── AuthorizerContext.tsx ├── icons │ ├── apple.tsx │ ├── close.tsx │ ├── facebook.tsx │ ├── github.tsx │ ├── google.tsx │ ├── linkedin.tsx │ ├── microsoft.tsx │ ├── roblox.tsx │ ├── twitch.tsx │ └── twitter.tsx ├── index.tsx ├── stories │ ├── StyledButton.stories.tsx │ ├── StyledFlex.stories.tsx │ └── assets │ │ ├── accessibility.png │ │ ├── accessibility.svg │ │ ├── addon-library.png │ │ ├── assets.png │ │ ├── avif-test-image.avif │ │ ├── context.png │ │ ├── discord.svg │ │ ├── docs.png │ │ ├── figma-plugin.png │ │ ├── github.svg │ │ ├── share.png │ │ ├── styling.png │ │ ├── testing.png │ │ ├── theming.png │ │ ├── tutorials.svg │ │ └── youtube.svg ├── styledComponents │ ├── StyledButton.tsx │ ├── StyledFlex.tsx │ ├── StyledFooter.tsx │ ├── StyledLink.tsx │ ├── StyledMessageWrapper.tsx │ ├── StyledPasswordStrength.tsx │ ├── StyledPasswordStrengthWrapper.tsx │ ├── StyledSeparator.tsx │ ├── StyledWrapper.tsx │ └── index.ts ├── styles │ └── default.css ├── types │ └── index.ts ├── typings.d.ts └── utils │ ├── common.ts │ ├── format.ts │ ├── labels.ts │ ├── url.ts │ ├── validations.ts │ └── window.ts ├── tsconfig.json └── tsdx.config.js /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: authorizerdev 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | with: 25 | useLockfile: false 26 | 27 | # - name: Lint 28 | # run: yarn lint 29 | 30 | # - name: Test 31 | # run: yarn test --ci --coverage --maxWorkers=2 32 | 33 | - name: Build 34 | run: yarn build 35 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | .cache 5 | dist 6 | .parcel-cache 7 | .yalc 8 | *storybook.log -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-webpack5'; 2 | import type { WebpackConfiguration } from '@storybook/core-webpack'; 3 | 4 | const config: StorybookConfig = { 5 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], 6 | addons: [ 7 | '@storybook/addon-webpack5-compiler-swc', 8 | '@storybook/addon-onboarding', 9 | '@storybook/addon-links', 10 | '@storybook/addon-essentials', 11 | '@storybook/addon-interactions', 12 | '@storybook/addon-styling-webpack', 13 | '@storybook/preset-scss' 14 | ], 15 | framework: { 16 | name: '@storybook/react-webpack5', 17 | options: { 18 | strictMode: true, 19 | }, 20 | }, 21 | webpackFinal: async (currentConfig: WebpackConfiguration, { configType }) => { 22 | // get index of css rule 23 | const ruleCssIndex = currentConfig.module.rules.findIndex( 24 | (rule) => rule.test?.toString() === "/\\.css$/" 25 | ); 26 | 27 | // map over the 'use' array of the css rule and set the 'module' option to true 28 | currentConfig.module.rules[ruleCssIndex].use.map((item) => { 29 | if (item.loader && item.loader.includes("/css-loader/")) { 30 | item.options.modules = { 31 | mode: "local", 32 | localIdentName: 33 | configType === "PRODUCTION" 34 | ? "[local]__[hash:base64:5]" 35 | : "[name]__[local]__[hash:base64:5]", 36 | }; 37 | } 38 | 39 | return item; 40 | }); 41 | 42 | return currentConfig; 43 | }, 44 | }; 45 | export default config; 46 | -------------------------------------------------------------------------------- /.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react'; 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/i, 9 | }, 10 | }, 11 | }, 12 | tags: ['autodocs'] 13 | }; 14 | 15 | export default preview; 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Lakhan Samani 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # authorizer-react 2 | 3 | Authorizer React SDK allows you to implement authentication in your [React](https://reactjs.org/) application quickly. It also allows you to access the user profile. 4 | 5 | Here is a quick guide on getting started with `@authorizerdev/authorizer-react` package. 6 | 7 | ## Code Sandbox Demo: https://codesandbox.io/s/authorizer-demo-qgjpw 8 | 9 | ## Step 1 - Create Instance 10 | 11 | Get Authorizer URL by instantiating [Authorizer instance](/deployment) and configuring it with necessary [environment variables](/core/env). 12 | 13 | ## Step 2 - Install package 14 | 15 | Install `@authorizerdev/authorizer-react` library 16 | 17 | ```sh 18 | npm i --save @authorizerdev/authorizer-react 19 | OR 20 | yarn add @authorizerdev/authorizer-react 21 | ``` 22 | 23 | ## Step 3 - Configure Provider and use Authorizer Components 24 | 25 | Authorizer comes with [react context](https://reactjs.org/docs/context.html) which serves as `Provider` component for the application 26 | 27 | ```jsx 28 | import { 29 | AuthorizerProvider, 30 | Authorizer, 31 | useAuthorizer, 32 | } from '@authorizerdev/authorizer-react'; 33 | 34 | const App = () => { 35 | return ( 36 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | 49 | const LoginSignup = () => { 50 | return ; 51 | }; 52 | 53 | const Profile = () => { 54 | const { user } = useAuthorizer(); 55 | 56 | if (user) { 57 | return
{user.email}
; 58 | } 59 | 60 | return null; 61 | }; 62 | ``` 63 | 64 | ## Commands 65 | 66 | ### Local Development 67 | 68 | The recommended workflow is to run authorizer in one terminal: 69 | 70 | ```bash 71 | npm start # or yarn start 72 | ``` 73 | 74 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 75 | 76 | Then run either Storybook or the example playground: 77 | 78 | ### Example 79 | 80 | Then run the example inside another: 81 | 82 | ```bash 83 | cd example 84 | npm i # or yarn to install dependencies 85 | npm start # or yarn start 86 | ``` 87 | 88 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, we use [Parcel's aliasing](https://parceljs.org/module_resolution.html#aliases). 89 | 90 | To do a one-off build, use `npm run build` or `yarn build`. 91 | 92 | To run tests, use `npm test` or `yarn test`. 93 | 94 | ## Configuration 95 | 96 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 97 | 98 | ### Storybook commands 99 | 100 | ```bash 101 | npm run storybook 102 | ``` 103 | 104 | ```bash 105 | npm run build-storybook 106 | ``` 107 | 108 | ### Jest 109 | 110 | Jest tests are set up to run with `npm test` or `yarn test`. 111 | 112 | ### Bundle analysis 113 | 114 | Calculates the real cost of your library using [size-limit](https://github.com/ai/size-limit) with `npm run size` and visulize it with `npm run analyze`. 115 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Authorizer Demo App 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "start": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^18.11.18", 17 | "@types/react": "^18.0.24", 18 | "@types/react-dom": "^18.0.8", 19 | "@vitejs/plugin-react": "^2.2.0", 20 | "react-router-dom": "^6.4.3", 21 | "typescript": "^4.6.4", 22 | "vite": "^3.2.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/example/public/favicon.ico -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Routes, Route } from 'react-router-dom'; 2 | import { useAuthorizer } from 'authorizer-react'; 3 | import Dashboard from './pages/dashboard'; 4 | import Login from './pages/login'; 5 | import ResetPassword from './pages/resetPassword'; 6 | 7 | function App() { 8 | const { token, loading } = useAuthorizer(); 9 | 10 | if (loading) { 11 | return

Loading...

; 12 | } 13 | 14 | if (token) { 15 | return ( 16 | 17 | } /> 18 | 19 | ); 20 | } 21 | 22 | return ( 23 | 24 | } /> 25 | } /> 26 | 27 | ); 28 | } 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, system-ui, sans-serif; 3 | color: #374151; 4 | font-size: 14px; 5 | } 6 | 7 | *, 8 | *:before, 9 | *:after { 10 | box-sizing: inherit; 11 | } 12 | -------------------------------------------------------------------------------- /example/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import { AuthorizerProvider } from 'authorizer-react'; 5 | import App from './App'; 6 | import './index.css'; 7 | 8 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 9 | 10 |
11 |
20 | 21 | { 27 | console.log(user, token); 28 | }} 29 | > 30 | 31 | 32 | 33 |
34 |
35 |
36 | ); 37 | -------------------------------------------------------------------------------- /example/src/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useAuthorizer } from 'authorizer-react'; 3 | 4 | const Dashboard: React.FC = () => { 5 | const { user, loading, logout } = useAuthorizer(); 6 | 7 | return ( 8 |
9 |

Hey 👋,

10 |

Thank you for joining Authorizer demo app.

11 |

12 | Your email address is{' '} 13 | 14 | {user?.email} 15 | 16 |

17 | 18 |
19 | {loading ? ( 20 |

Processing....

21 | ) : ( 22 |

29 | Logout 30 |

31 | )} 32 |
33 | ); 34 | }; 35 | 36 | export default Dashboard; 37 | -------------------------------------------------------------------------------- /example/src/pages/login.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Authorizer } from 'authorizer-react'; 3 | 4 | const Login: React.FC = () => { 5 | return ( 6 | <> 7 |

Welcome to Authorizer

8 |
9 | { 11 | console.log({ loginData }); 12 | }} 13 | onMagicLinkLogin={(mData: any) => { 14 | console.log({ mData }); 15 | }} 16 | onSignup={(sData: any) => { 17 | console.log({ sData }); 18 | }} 19 | /> 20 | 21 | ); 22 | }; 23 | 24 | export default Login; 25 | -------------------------------------------------------------------------------- /example/src/pages/resetPassword.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AuthorizerResetPassword } from 'authorizer-react'; 3 | 4 | const ResetPassword: React.FC = () => { 5 | return ( 6 | <> 7 |

Reset Password

8 |
9 | 10 | 11 | ); 12 | }; 13 | 14 | export default ResetPassword; 15 | -------------------------------------------------------------------------------- /example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module 'authorizer-react'; 3 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'node:path'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | resolve: { 9 | alias: [ 10 | { 11 | find: 'authorizer-react', 12 | replacement: path.resolve(__dirname, '../dist/authorizer-react.esm.js'), 13 | }, 14 | ], 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.3.3", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "files": [ 7 | "dist", 8 | "src" 9 | ], 10 | "engines": { 11 | "node": ">=10" 12 | }, 13 | "repository": "https://github.com/authorizerdev/authorizer-react", 14 | "scripts": { 15 | "start": "tsdx watch", 16 | "build": "tsdx build", 17 | "test": "tsdx test --passWithNoTests", 18 | "lint": "tsdx lint", 19 | "prepare": "tsdx build", 20 | "size": "size-limit", 21 | "analyze": "size-limit --why", 22 | "storybook": "storybook dev -p 6006", 23 | "build-storybook": "storybook build" 24 | }, 25 | "peerDependencies": { 26 | "react": ">=16" 27 | }, 28 | "husky": { 29 | "hooks": { 30 | "pre-commit": "tsdx lint" 31 | } 32 | }, 33 | "prettier": { 34 | "printWidth": 80, 35 | "semi": true, 36 | "singleQuote": true, 37 | "trailingComma": "es5" 38 | }, 39 | "name": "@authorizerdev/authorizer-react", 40 | "author": "Lakhan Samani", 41 | "module": "dist/authorizer-react.esm.js", 42 | "size-limit": [ 43 | { 44 | "path": "dist/authorizer-react.cjs.production.min.js", 45 | "limit": "200 KB" 46 | }, 47 | { 48 | "path": "dist/authorizer-react.esm.js", 49 | "limit": "200 KB" 50 | } 51 | ], 52 | "devDependencies": { 53 | "@babel/core": "^7.23.2", 54 | "@size-limit/preset-small-lib": "^8.1.0", 55 | "@storybook/addon-essentials": "^8.2.7", 56 | "@storybook/addon-interactions": "^8.2.7", 57 | "@storybook/addon-links": "^8.2.7", 58 | "@storybook/addon-onboarding": "^8.2.7", 59 | "@storybook/addon-styling-webpack": "^1.0.0", 60 | "@storybook/addon-webpack5-compiler-swc": "^1.0.5", 61 | "@storybook/blocks": "^8.2.7", 62 | "@storybook/react": "^8.2.7", 63 | "@storybook/react-webpack5": "^8.2.7", 64 | "@storybook/test": "^8.2.7", 65 | "@types/react": "^18.0.25", 66 | "@types/react-dom": "^18.0.9", 67 | "@types/validator": "^13.11.7", 68 | "@typescript-eslint/eslint-plugin": "^7.16.1", 69 | "@typescript-eslint/parser": "^7.16.1", 70 | "babel-loader": "^9.1.0", 71 | "husky": "^8.0.2", 72 | "postcss": "^8.4.19", 73 | "react": "^18.2.0", 74 | "react-dom": "^18.2.0", 75 | "react-is": "^18.2.0", 76 | "rollup-plugin-postcss": "^4.0.2", 77 | "size-limit": "^8.1.0", 78 | "storybook": "^8.2.7", 79 | "tsdx": "^0.14.1", 80 | "tslib": "^2.6.2", 81 | "typescript": "^5.2.2" 82 | }, 83 | "dependencies": { 84 | "@authorizerdev/authorizer-js": "^2.0.3", 85 | "@storybook/preset-scss": "^1.0.3", 86 | "validator": "^13.11.0" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/AuthorizerBasicAuthLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | import { AuthToken, LoginInput } from '@authorizerdev/authorizer-js'; 3 | import isEmail from 'validator/es/lib/isEmail'; 4 | import isMobilePhone from 'validator/es/lib/isMobilePhone'; 5 | 6 | import styles from '../styles/default.css'; 7 | import { ButtonAppearance, MessageType, Views } from '../constants'; 8 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 9 | import { StyledButton, StyledFooter, StyledLink } from '../styledComponents'; 10 | import { Message } from './Message'; 11 | import { AuthorizerVerifyOtp } from './AuthorizerVerifyOtp'; 12 | import { OtpDataType, TotpDataType } from '../types'; 13 | import { AuthorizerTOTPScanner } from './AuthorizerTOTPScanner'; 14 | import { getEmailPhoneLabels, getEmailPhonePlaceholder } from '../utils/labels'; 15 | 16 | const initOtpData: OtpDataType = { 17 | is_screen_visible: false, 18 | email: '', 19 | phone_number: '', 20 | }; 21 | 22 | const initTotpData: TotpDataType = { 23 | is_screen_visible: false, 24 | email: '', 25 | phone_number: '', 26 | authenticator_scanner_image: '', 27 | authenticator_secret: '', 28 | authenticator_recovery_codes: [], 29 | }; 30 | 31 | interface InputDataType { 32 | email_or_phone_number: string | null; 33 | password: string | null; 34 | } 35 | 36 | export const AuthorizerBasicAuthLogin: FC<{ 37 | setView?: (v: Views) => void; 38 | onLogin?: (data: AuthToken | void) => void; 39 | urlProps?: Record; 40 | roles?: string[]; 41 | }> = ({ setView, onLogin, urlProps, roles }) => { 42 | const [error, setError] = useState(``); 43 | const [loading, setLoading] = useState(false); 44 | const [otpData, setOtpData] = useState({ ...initOtpData }); 45 | const [totpData, setTotpData] = useState({ ...initTotpData }); 46 | const [formData, setFormData] = useState({ 47 | email_or_phone_number: null, 48 | password: null, 49 | }); 50 | const [errorData, setErrorData] = useState({ 51 | email_or_phone_number: null, 52 | password: null, 53 | }); 54 | const { setAuthData, config, authorizerRef } = useAuthorizer(); 55 | 56 | const onInputChange = async (field: string, value: string) => { 57 | setFormData({ ...formData, [field]: value }); 58 | }; 59 | 60 | const onSubmit = async (e: any) => { 61 | e.preventDefault(); 62 | setLoading(true); 63 | try { 64 | let email: string = ''; 65 | let phone_number: string = ''; 66 | if (formData.email_or_phone_number) { 67 | if (isEmail(formData.email_or_phone_number)) { 68 | email = formData.email_or_phone_number; 69 | } else if (isMobilePhone(formData.email_or_phone_number)) { 70 | phone_number = formData.email_or_phone_number; 71 | } 72 | } 73 | if (!email && !phone_number) { 74 | setErrorData({ 75 | ...errorData, 76 | email_or_phone_number: 'Invalid email or phone number', 77 | }); 78 | setLoading(false); 79 | return; 80 | } 81 | const data: LoginInput = { 82 | email: email, 83 | phone_number: phone_number, 84 | password: formData.password || '', 85 | }; 86 | if (urlProps?.scope) { 87 | data.scope = urlProps.scope; 88 | } 89 | if (urlProps?.state) { 90 | data.state = urlProps.state; 91 | } 92 | 93 | if (roles && roles.length) { 94 | data.roles = roles; 95 | } 96 | 97 | const { data: res, errors } = await authorizerRef.login(data); 98 | if (errors && errors.length) { 99 | setError(errors[0].message); 100 | setLoading(false); 101 | return; 102 | } 103 | // if totp is enabled for the first time show totp screen with scanner 104 | if ( 105 | res && 106 | res.should_show_totp_screen && 107 | res.authenticator_scanner_image && 108 | res.authenticator_secret && 109 | res.authenticator_recovery_codes 110 | ) { 111 | setTotpData({ 112 | is_screen_visible: true, 113 | email: data.email || ``, 114 | phone_number: data.phone_number || ``, 115 | authenticator_scanner_image: res.authenticator_scanner_image, 116 | authenticator_secret: res.authenticator_secret, 117 | authenticator_recovery_codes: res.authenticator_recovery_codes, 118 | }); 119 | return; 120 | } 121 | if ( 122 | res && 123 | (res?.should_show_email_otp_screen || 124 | res?.should_show_mobile_otp_screen || 125 | res?.should_show_totp_screen) 126 | ) { 127 | setOtpData({ 128 | is_screen_visible: true, 129 | email: data.email || ``, 130 | phone_number: data.phone_number || ``, 131 | is_totp: res?.should_show_totp_screen || false, 132 | }); 133 | return; 134 | } 135 | 136 | if (res) { 137 | setError(``); 138 | setAuthData({ 139 | user: res.user || null, 140 | token: { 141 | access_token: res.access_token, 142 | expires_in: res.expires_in, 143 | refresh_token: res.refresh_token, 144 | id_token: res.id_token, 145 | }, 146 | config, 147 | loading: false, 148 | }); 149 | } 150 | 151 | if (onLogin) { 152 | onLogin(res); 153 | } 154 | } catch (err) { 155 | setLoading(false); 156 | setError((err as Error).message); 157 | } 158 | }; 159 | 160 | const onErrorClose = () => { 161 | setError(``); 162 | }; 163 | 164 | useEffect(() => { 165 | if (formData.email_or_phone_number === '') { 166 | setErrorData({ 167 | ...errorData, 168 | email_or_phone_number: 'Email OR Phone Number is required', 169 | }); 170 | } else if ( 171 | !isEmail(formData.email_or_phone_number || '') && 172 | !isMobilePhone(formData.email_or_phone_number || '') 173 | ) { 174 | setErrorData({ 175 | ...errorData, 176 | email_or_phone_number: 'Invalid Email OR Phone Number', 177 | }); 178 | } else { 179 | setErrorData({ ...errorData, email_or_phone_number: null }); 180 | } 181 | }, [formData.email_or_phone_number]); 182 | 183 | useEffect(() => { 184 | if (formData.password === '') { 185 | setErrorData({ ...errorData, password: 'Password is required' }); 186 | } else { 187 | setErrorData({ ...errorData, password: null }); 188 | } 189 | }, [formData.password]); 190 | 191 | if (totpData.is_screen_visible) { 192 | return ( 193 | 206 | ); 207 | } 208 | 209 | if (otpData.is_screen_visible) { 210 | return ( 211 | 221 | ); 222 | } 223 | 224 | return ( 225 | <> 226 | {error && ( 227 | 228 | )} 229 | <> 230 |
231 |
232 | 239 | 251 | onInputChange('email_or_phone_number', e.target.value) 252 | } 253 | /> 254 | {errorData.email_or_phone_number && ( 255 |
256 | {errorData.email_or_phone_number} 257 |
258 | )} 259 |
260 |
261 | 267 | onInputChange('password', e.target.value)} 277 | /> 278 | {errorData.password && ( 279 |
280 | {errorData.password} 281 |
282 | )} 283 |
284 |
285 | 296 | {loading ? `Processing ...` : `Log In`} 297 | 298 |
299 | 300 | {setView && ( 301 | 302 | setView(Views.ForgotPassword)} 304 | marginBottom="10px" 305 | > 306 | Forgot Password? 307 | 308 | 309 | {config.is_sign_up_enabled && ( 310 |
311 | Don't have an account?{' '} 312 | setView(Views.Signup)}> 313 | Sign Up 314 | 315 |
316 | )} 317 |
318 | )} 319 | 320 | 321 | ); 322 | }; 323 | -------------------------------------------------------------------------------- /src/components/AuthorizerForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | import isEmail from 'validator/es/lib/isEmail'; 3 | import isMobilePhone from 'validator/es/lib/isMobilePhone'; 4 | 5 | import styles from '../styles/default.css'; 6 | import { ButtonAppearance, MessageType, Views } from '../constants'; 7 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 8 | import { StyledButton, StyledFooter, StyledLink } from '../styledComponents'; 9 | import { formatErrorMessage } from '../utils/format'; 10 | import { Message } from './Message'; 11 | import { OtpDataType } from '../types'; 12 | import { AuthorizerResetPassword } from './AuthorizerResetPassword'; 13 | import { getEmailPhoneLabels, getEmailPhonePlaceholder } from '../utils/labels'; 14 | 15 | interface InputDataType { 16 | email_or_phone_number: string | null; 17 | } 18 | 19 | const initOtpData: OtpDataType = { 20 | is_screen_visible: false, 21 | email: '', 22 | phone_number: '', 23 | }; 24 | 25 | export const AuthorizerForgotPassword: FC<{ 26 | setView?: (v: Views) => void; 27 | onForgotPassword?: (data: any) => void; 28 | onPasswordReset?: () => void; 29 | urlProps?: Record; 30 | }> = ({ setView, onForgotPassword, onPasswordReset, urlProps }) => { 31 | const [error, setError] = useState(``); 32 | const [loading, setLoading] = useState(false); 33 | const [successMessage, setSuccessMessage] = useState(``); 34 | const [otpData, setOtpData] = useState({ ...initOtpData }); 35 | const [formData, setFormData] = useState({ 36 | email_or_phone_number: null, 37 | }); 38 | const [errorData, setErrorData] = useState({ 39 | email_or_phone_number: null, 40 | }); 41 | const { authorizerRef, config } = useAuthorizer(); 42 | 43 | const onInputChange = async (field: string, value: string) => { 44 | setFormData({ ...formData, [field]: value }); 45 | }; 46 | 47 | const onSubmit = async (e: any) => { 48 | e.preventDefault(); 49 | try { 50 | setLoading(true); 51 | let email: string = ''; 52 | let phone_number: string = ''; 53 | if (formData.email_or_phone_number) { 54 | if (isEmail(formData.email_or_phone_number)) { 55 | email = formData.email_or_phone_number; 56 | } else if (isMobilePhone(formData.email_or_phone_number)) { 57 | phone_number = formData.email_or_phone_number; 58 | } 59 | } 60 | if (!email && !phone_number) { 61 | setErrorData({ 62 | ...errorData, 63 | email_or_phone_number: 'Invalid email or phone number', 64 | }); 65 | setLoading(false); 66 | return; 67 | } 68 | const { data: res, errors } = await authorizerRef.forgotPassword({ 69 | email: email, 70 | phone_number: phone_number, 71 | state: urlProps?.state || '', 72 | redirect_uri: 73 | urlProps?.redirect_uri || 74 | config.redirectURL || 75 | window.location.origin, 76 | }); 77 | setLoading(false); 78 | if (errors && errors.length) { 79 | setError(formatErrorMessage(errors[0]?.message)); 80 | return; 81 | } 82 | if (res?.message) { 83 | setError(``); 84 | setSuccessMessage(res.message); 85 | if (res?.should_show_mobile_otp_screen) { 86 | setOtpData({ 87 | ...otpData, 88 | is_screen_visible: true, 89 | email: email, 90 | phone_number: phone_number, 91 | }); 92 | return; 93 | } 94 | } 95 | if (onForgotPassword) { 96 | onForgotPassword(res); 97 | } 98 | } catch (err) { 99 | setLoading(false); 100 | setError(formatErrorMessage((err as Error)?.message)); 101 | } 102 | }; 103 | 104 | const onErrorClose = () => { 105 | setError(``); 106 | }; 107 | 108 | useEffect(() => { 109 | if (formData.email_or_phone_number === '') { 110 | setErrorData({ 111 | ...errorData, 112 | email_or_phone_number: 'Email OR Phone Number is required', 113 | }); 114 | } else if ( 115 | formData.email_or_phone_number !== null && 116 | !isEmail(formData.email_or_phone_number || '') && 117 | !isMobilePhone(formData.email_or_phone_number || '') 118 | ) { 119 | setErrorData({ 120 | ...errorData, 121 | email_or_phone_number: 'Invalid Email OR Phone Number', 122 | }); 123 | } else { 124 | setErrorData({ ...errorData, email_or_phone_number: null }); 125 | } 126 | }, [formData.email_or_phone_number]); 127 | 128 | if (successMessage) { 129 | return ( 130 | <> 131 | 132 | {otpData.is_screen_visible && ( 133 | 138 | )} 139 | 140 | ); 141 | } 142 | 143 | return ( 144 | <> 145 | {error && ( 146 | 147 | )} 148 |

149 | Please enter your {getEmailPhoneLabels(config)}. 150 |
We will send you an email / otp to reset your password. 151 |

152 |
153 |
154 |
155 | 162 | 174 | onInputChange('email_or_phone_number', e.target.value) 175 | } 176 | /> 177 | {errorData.email_or_phone_number && ( 178 |
179 | {errorData.email_or_phone_number} 180 |
181 | )} 182 |
183 |
184 | 193 | {loading ? `Processing ...` : `Request Change`} 194 | 195 |
196 | {setView && ( 197 | 198 |
199 | Remember your password?{' '} 200 | setView(Views.Login)}>Log In 201 |
202 |
203 | )} 204 | 205 | ); 206 | }; 207 | -------------------------------------------------------------------------------- /src/components/AuthorizerMagicLinkLogin.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | import isEmail from 'validator/es/lib/isEmail'; 3 | 4 | import styles from '../styles/default.css'; 5 | import { ButtonAppearance, MessageType } from '../constants'; 6 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 7 | import { StyledButton } from '../styledComponents'; 8 | import { formatErrorMessage } from '../utils/format'; 9 | import { Message } from './Message'; 10 | import { MagicLinkLoginInput } from '@authorizerdev/authorizer-js'; 11 | 12 | interface InputDataType { 13 | email: string | null; 14 | } 15 | 16 | export const AuthorizerMagicLinkLogin: FC<{ 17 | onMagicLinkLogin?: (data: any) => void; 18 | urlProps?: Record; 19 | roles?: string[]; 20 | }> = ({ onMagicLinkLogin, urlProps, roles }) => { 21 | const [error, setError] = useState(``); 22 | const [loading, setLoading] = useState(false); 23 | const [successMessage, setSuccessMessage] = useState(``); 24 | const [formData, setFormData] = useState({ 25 | email: null, 26 | }); 27 | const [errorData, setErrorData] = useState({ 28 | email: null, 29 | }); 30 | const { authorizerRef } = useAuthorizer(); 31 | 32 | const onInputChange = async (field: string, value: string) => { 33 | setFormData({ ...formData, [field]: value }); 34 | }; 35 | 36 | const onSubmit = async (e: any) => { 37 | e.preventDefault(); 38 | try { 39 | setLoading(true); 40 | 41 | const data: MagicLinkLoginInput = { 42 | email: formData.email || '', 43 | state: urlProps?.state || '', 44 | redirect_uri: urlProps?.redirect_uri || '', 45 | }; 46 | 47 | if (roles && roles.length) { 48 | data.roles = roles; 49 | } 50 | const { data: res, errors } = await authorizerRef.magicLinkLogin(data); 51 | setLoading(false); 52 | if (errors && errors.length) { 53 | setError(formatErrorMessage(errors[0]?.message)); 54 | return; 55 | } 56 | 57 | if (res) { 58 | setError(``); 59 | setSuccessMessage(res.message || ``); 60 | 61 | if (onMagicLinkLogin) { 62 | onMagicLinkLogin(res); 63 | } 64 | } 65 | 66 | if (urlProps?.redirect_uri) { 67 | setTimeout(() => { 68 | window.location.replace(urlProps.redirect_uri); 69 | }, 3000); 70 | } 71 | } catch (err) { 72 | setLoading(false); 73 | setError(formatErrorMessage((err as Error)?.message)); 74 | } 75 | }; 76 | 77 | const onErrorClose = () => { 78 | setError(``); 79 | }; 80 | 81 | useEffect(() => { 82 | if (formData.email === '') { 83 | setErrorData({ ...errorData, email: 'Email is required' }); 84 | } else if (formData.email && !isEmail(formData.email)) { 85 | setErrorData({ ...errorData, email: 'Please enter valid email' }); 86 | } else { 87 | setErrorData({ ...errorData, email: null }); 88 | } 89 | }, [formData.email]); 90 | 91 | if (successMessage) { 92 | return ; 93 | } 94 | 95 | return ( 96 | <> 97 | {error && ( 98 | 99 | )} 100 |
101 |
102 | 108 | onInputChange('email', e.target.value)} 118 | /> 119 | {errorData.email && ( 120 |
{errorData.email}
121 | )} 122 |
123 |
124 | 129 | {loading ? `Processing ...` : `Send Email`} 130 | 131 |
132 | 133 | ); 134 | }; 135 | -------------------------------------------------------------------------------- /src/components/AuthorizerResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | import styles from '../styles/default.css'; 3 | 4 | import { ButtonAppearance, MessageType } from '../constants'; 5 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 6 | import { StyledButton, StyledWrapper } from '../styledComponents'; 7 | import { formatErrorMessage } from '../utils/format'; 8 | import { Message } from './Message'; 9 | import { getSearchParams } from '../utils/url'; 10 | import PasswordStrengthIndicator from './PasswordStrengthIndicator'; 11 | 12 | type Props = { 13 | showOTPInput?: boolean; 14 | onReset?: (res: any) => void; 15 | phone_number?: string; 16 | }; 17 | 18 | interface InputDataType { 19 | otp: string | null; 20 | password: string | null; 21 | confirmPassword: string | null; 22 | } 23 | 24 | export const AuthorizerResetPassword: FC = ({ 25 | onReset, 26 | showOTPInput, 27 | phone_number, 28 | }) => { 29 | const { token, redirect_uri } = getSearchParams(); 30 | const [error, setError] = useState(''); 31 | const [loading, setLoading] = useState(false); 32 | const [formData, setFormData] = useState({ 33 | otp: null, 34 | password: null, 35 | confirmPassword: null, 36 | }); 37 | const [errorData, setErrorData] = useState({ 38 | otp: null, 39 | password: null, 40 | confirmPassword: null, 41 | }); 42 | const { authorizerRef, config } = useAuthorizer(); 43 | const [disableContinueButton, setDisableContinueButton] = useState(false); 44 | 45 | const onInputChange = async (field: string, value: string) => { 46 | setFormData({ ...formData, [field]: value }); 47 | }; 48 | 49 | const onSubmit = async (e: any) => { 50 | e.preventDefault(); 51 | setLoading(true); 52 | try { 53 | const { data: res, errors } = await authorizerRef.resetPassword({ 54 | token, 55 | otp: formData.otp || '', 56 | phone_number: phone_number || '', 57 | password: formData.password || '', 58 | confirm_password: formData.confirmPassword || '', 59 | }); 60 | setLoading(false); 61 | if (errors && errors.length) { 62 | setError(formatErrorMessage(errors[0]?.message)); 63 | return; 64 | } 65 | setError(``); 66 | if (onReset) { 67 | onReset(res); 68 | } else { 69 | window.location.href = 70 | redirect_uri || config.redirectURL || window.location.origin; 71 | } 72 | } catch (err) { 73 | setLoading(false); 74 | setError(formatErrorMessage((err as Error).message)); 75 | } 76 | }; 77 | 78 | const onErrorClose = () => { 79 | setError(``); 80 | }; 81 | 82 | useEffect(() => { 83 | if (formData.password === '') { 84 | setErrorData({ ...errorData, password: 'Password is required' }); 85 | } else { 86 | setErrorData({ ...errorData, password: null }); 87 | } 88 | }, [formData.password]); 89 | 90 | useEffect(() => { 91 | if (formData.confirmPassword === '') { 92 | setErrorData({ 93 | ...errorData, 94 | confirmPassword: 'Confirm password is required', 95 | }); 96 | } else { 97 | setErrorData({ ...errorData, confirmPassword: null }); 98 | } 99 | }, [formData.confirmPassword]); 100 | 101 | useEffect(() => { 102 | if (formData.password && formData.confirmPassword) { 103 | if (formData.confirmPassword !== formData.password) { 104 | setErrorData({ 105 | ...errorData, 106 | password: `Password and confirm passwords don't match`, 107 | confirmPassword: `Password and confirm passwords don't match`, 108 | }); 109 | } else { 110 | setErrorData({ 111 | ...errorData, 112 | password: null, 113 | confirmPassword: null, 114 | }); 115 | } 116 | } 117 | }, [formData.password, formData.confirmPassword]); 118 | 119 | return ( 120 | 121 | {error && ( 122 | 123 | )} 124 |
125 | {showOTPInput && ( 126 |
127 | 133 | onInputChange('otp', e.target.value)} 143 | /> 144 | {errorData.otp && ( 145 |
{errorData.otp}
146 | )} 147 |
148 | )} 149 |
150 | 156 | onInputChange('password', e.target.value)} 166 | /> 167 | {errorData.password && ( 168 |
169 | {errorData.password} 170 |
171 | )} 172 |
173 |
174 | 180 | onInputChange('confirmPassword', e.target.value)} 190 | /> 191 | {errorData.confirmPassword && ( 192 |
193 | {errorData.confirmPassword} 194 |
195 | )} 196 |
197 | {config.is_strong_password_enabled && ( 198 | <> 199 | 203 |
204 | 205 | )} 206 | 218 | {loading ? `Processing ...` : `Continue`} 219 | 220 | 221 |
222 | ); 223 | }; 224 | -------------------------------------------------------------------------------- /src/components/AuthorizerRoot.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { AuthToken } from '@authorizerdev/authorizer-js'; 3 | 4 | import { AuthorizerBasicAuthLogin } from './AuthorizerBasicAuthLogin'; 5 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 6 | import { StyledWrapper } from '../styledComponents'; 7 | import { Views } from '../constants'; 8 | import { AuthorizerSignup } from './AuthorizerSignup'; 9 | import type { FormFieldsOverrides } from './AuthorizerSignup'; 10 | import { AuthorizerForgotPassword } from './AuthorizerForgotPassword'; 11 | import { AuthorizerSocialLogin } from './AuthorizerSocialLogin'; 12 | import { AuthorizerMagicLinkLogin } from './AuthorizerMagicLinkLogin'; 13 | import { createRandomString } from '../utils/common'; 14 | import { hasWindow } from '../utils/window'; 15 | 16 | export const AuthorizerRoot: FC<{ 17 | onLogin?: (data: AuthToken | void) => void; 18 | onSignup?: (data: AuthToken | void) => void; 19 | onMagicLinkLogin?: (data: any) => void; 20 | onForgotPassword?: (data: any) => void; 21 | onPasswordReset?: () => void; 22 | roles?: string[]; 23 | signupFieldsOverrides?: FormFieldsOverrides 24 | }> = ({ 25 | onLogin, 26 | onSignup, 27 | onMagicLinkLogin, 28 | onForgotPassword, 29 | onPasswordReset, 30 | roles, 31 | signupFieldsOverrides 32 | }) => { 33 | const [view, setView] = useState(Views.Login); 34 | const { config } = useAuthorizer(); 35 | const searchParams = new URLSearchParams( 36 | hasWindow() ? window.location.search : `` 37 | ); 38 | const state = searchParams.get('state') || createRandomString(); 39 | const scope = searchParams.get('scope') 40 | ? searchParams 41 | .get('scope') 42 | ?.toString() 43 | .split(' ') 44 | : ['openid', 'profile', 'email']; 45 | 46 | const urlProps: Record = { 47 | state, 48 | scope, 49 | }; 50 | 51 | const redirectURL = 52 | searchParams.get('redirect_uri') || searchParams.get('redirectURL'); 53 | if (redirectURL) { 54 | urlProps.redirectURL = redirectURL; 55 | } else { 56 | urlProps.redirectURL = hasWindow() ? window.location.origin : redirectURL; 57 | } 58 | 59 | urlProps.redirect_uri = urlProps.redirectURL; 60 | return ( 61 | 62 | 63 | {view === Views.Login && 64 | (config.is_basic_authentication_enabled || 65 | config.is_mobile_basic_authentication_enabled) && 66 | !config.is_magic_link_login_enabled && ( 67 | 73 | )} 74 | 75 | {view === Views.Signup && 76 | (config.is_basic_authentication_enabled || 77 | config.is_mobile_basic_authentication_enabled) && 78 | !config.is_magic_link_login_enabled && 79 | config.is_sign_up_enabled && ( 80 | 87 | )} 88 | 89 | {view === Views.Login && config.is_magic_link_login_enabled && ( 90 | 95 | )} 96 | 97 | {view === Views.ForgotPassword && ( 98 | 104 | )} 105 | 106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /src/components/AuthorizerSignup.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | import { AuthToken, SignupInput } from '@authorizerdev/authorizer-js'; 3 | import isEmail from 'validator/es/lib/isEmail'; 4 | import isMobilePhone from 'validator/es/lib/isMobilePhone'; 5 | 6 | import styles from '../styles/default.css'; 7 | import { ButtonAppearance, MessageType, Views } from '../constants'; 8 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 9 | import { StyledButton, StyledFooter, StyledLink } from '../styledComponents'; 10 | import { formatErrorMessage } from '../utils/format'; 11 | import { Message } from './Message'; 12 | import PasswordStrengthIndicator from './PasswordStrengthIndicator'; 13 | import { OtpDataType } from '../types'; 14 | import { AuthorizerVerifyOtp } from './AuthorizerVerifyOtp'; 15 | import { getEmailPhoneLabels, getEmailPhonePlaceholder } from '../utils/labels'; 16 | 17 | type Field = 18 | | 'given_name' 19 | | 'family_name' 20 | | 'email_or_phone_number' 21 | | 'password' 22 | | 'confirmPassword'; 23 | 24 | type FieldOverride = { 25 | label: string; 26 | placeholder: string; 27 | hide?: boolean; 28 | notRequired?: boolean; 29 | }; 30 | 31 | type InputDataType = { 32 | [K in Field]: string | null; 33 | }; 34 | 35 | export type FormFieldsOverrides = { 36 | [K in Field]?: FieldOverride; 37 | }; 38 | 39 | const initOtpData: OtpDataType = { 40 | is_screen_visible: false, 41 | email: '', 42 | phone_number: '', 43 | }; 44 | 45 | export const AuthorizerSignup: FC<{ 46 | setView?: (v: Views) => void; 47 | onSignup?: (data: AuthToken) => void; 48 | urlProps?: Record; 49 | roles?: string[]; 50 | fieldOverrides?: FormFieldsOverrides; 51 | }> = ({ setView, onSignup, urlProps, roles, fieldOverrides }) => { 52 | const [error, setError] = useState(``); 53 | const [loading, setLoading] = useState(false); 54 | const [otpData, setOtpData] = useState({ ...initOtpData }); 55 | const [successMessage, setSuccessMessage] = useState(``); 56 | const [formData, setFormData] = useState({ 57 | given_name: null, 58 | family_name: null, 59 | email_or_phone_number: null, 60 | password: null, 61 | confirmPassword: null, 62 | }); 63 | const [errorData, setErrorData] = useState({ 64 | given_name: null, 65 | family_name: null, 66 | email_or_phone_number: null, 67 | password: null, 68 | confirmPassword: null, 69 | }); 70 | const { authorizerRef, config, setAuthData } = useAuthorizer(); 71 | const [disableSignupButton, setDisableSignupButton] = useState(false); 72 | 73 | const onInputChange = async (field: string, value: string) => 74 | setFormData({ ...formData, [field]: value }); 75 | 76 | const onSubmit = async (e: any) => { 77 | e.preventDefault(); 78 | try { 79 | setLoading(true); 80 | let email: string = ''; 81 | let phone_number: string = ''; 82 | if (formData.email_or_phone_number) { 83 | if (isEmail(formData.email_or_phone_number)) { 84 | email = formData.email_or_phone_number; 85 | } else if (isMobilePhone(formData.email_or_phone_number)) { 86 | phone_number = formData.email_or_phone_number; 87 | } 88 | } 89 | if (!email && !phone_number) { 90 | setErrorData({ 91 | ...errorData, 92 | email_or_phone_number: 'Invalid email or phone number', 93 | }); 94 | setLoading(false); 95 | return; 96 | } 97 | const data: SignupInput = { 98 | email, 99 | phone_number, 100 | given_name: formData.given_name || '', 101 | family_name: formData.family_name || '', 102 | password: formData.password || '', 103 | confirm_password: formData.confirmPassword || '', 104 | }; 105 | if (urlProps?.scope) { 106 | data.scope = urlProps.scope; 107 | } 108 | if (urlProps?.roles) { 109 | data.roles = urlProps.roles; 110 | } 111 | if (urlProps?.redirect_uri) { 112 | data.redirect_uri = urlProps.redirect_uri; 113 | } 114 | if (urlProps?.state) { 115 | data.state = urlProps.state; 116 | } 117 | if (roles && roles.length) { 118 | data.roles = roles; 119 | } 120 | const { data: res, errors } = await authorizerRef.signup(data); 121 | if (errors && errors.length) { 122 | setError(formatErrorMessage(errors[0]?.message)); 123 | setLoading(false); 124 | return; 125 | } 126 | if ( 127 | res && 128 | (res?.should_show_email_otp_screen || 129 | res?.should_show_mobile_otp_screen) 130 | ) { 131 | setOtpData({ 132 | is_screen_visible: true, 133 | email: data.email || ``, 134 | phone_number: data.phone_number || ``, 135 | }); 136 | return; 137 | } 138 | if (res) { 139 | setError(``); 140 | if (res.access_token) { 141 | setError(``); 142 | setAuthData({ 143 | user: res.user || null, 144 | token: { 145 | access_token: res.access_token, 146 | expires_in: res.expires_in, 147 | refresh_token: res.refresh_token, 148 | id_token: res.id_token, 149 | }, 150 | config, 151 | loading: false, 152 | }); 153 | } else { 154 | setLoading(false); 155 | setSuccessMessage(res.message || ``); 156 | } 157 | 158 | if (onSignup) { 159 | onSignup(res); 160 | } 161 | } 162 | } catch (err) { 163 | setLoading(false); 164 | setError(formatErrorMessage((err as Error).message)); 165 | } 166 | }; 167 | 168 | const onErrorClose = () => setError(``); 169 | 170 | useEffect(() => { 171 | if ( 172 | fieldOverrides?.given_name?.notRequired || 173 | fieldOverrides?.given_name?.hide 174 | ) { 175 | return; 176 | } 177 | if ((formData.given_name || '').trim() === '') { 178 | setErrorData({ ...errorData, given_name: 'First Name is required' }); 179 | } else { 180 | setErrorData({ ...errorData, given_name: null }); 181 | } 182 | }, [formData.given_name]); 183 | 184 | useEffect(() => { 185 | if ( 186 | fieldOverrides?.family_name?.notRequired || 187 | fieldOverrides?.family_name?.hide 188 | ) { 189 | return; 190 | } 191 | if ((formData.family_name || '').trim() === '') { 192 | setErrorData({ ...errorData, family_name: 'Last Name is required' }); 193 | } else { 194 | setErrorData({ ...errorData, family_name: null }); 195 | } 196 | }, [formData.family_name]); 197 | 198 | useEffect(() => { 199 | if (formData.email_or_phone_number === '') { 200 | setErrorData({ 201 | ...errorData, 202 | email_or_phone_number: 'Email OR Phone Number is required', 203 | }); 204 | } else if ( 205 | !isEmail(formData.email_or_phone_number || '') && 206 | !isMobilePhone(formData.email_or_phone_number || '') 207 | ) { 208 | setErrorData({ 209 | ...errorData, 210 | email_or_phone_number: 'Invalid Email OR Phone Number', 211 | }); 212 | } else { 213 | setErrorData({ ...errorData, email_or_phone_number: null }); 214 | } 215 | }, [formData.email_or_phone_number]); 216 | 217 | useEffect(() => { 218 | if (formData.password === '') { 219 | setErrorData({ ...errorData, password: 'Password is required' }); 220 | } else { 221 | setErrorData({ ...errorData, password: null }); 222 | } 223 | }, [formData.password]); 224 | 225 | useEffect(() => { 226 | if (formData.confirmPassword === '') { 227 | setErrorData({ 228 | ...errorData, 229 | confirmPassword: 'Confirm password is required', 230 | }); 231 | } else { 232 | setErrorData({ ...errorData, confirmPassword: null }); 233 | } 234 | }, [formData.confirmPassword]); 235 | 236 | useEffect(() => { 237 | if (formData.password && formData.confirmPassword) { 238 | if (formData.confirmPassword !== formData.password) { 239 | setErrorData({ 240 | ...errorData, 241 | password: `Password and confirm passwords don't match`, 242 | confirmPassword: `Password and confirm passwords don't match`, 243 | }); 244 | } else { 245 | setErrorData({ 246 | ...errorData, 247 | password: null, 248 | confirmPassword: null, 249 | }); 250 | } 251 | } 252 | }, [formData.password, formData.confirmPassword]); 253 | 254 | if (otpData.is_screen_visible) { 255 | return ( 256 | <> 257 | {successMessage && ( 258 | 259 | )} 260 | 270 | 271 | ); 272 | } 273 | 274 | const renderField = ( 275 | key: Field, 276 | label: string, 277 | placeholder: string, 278 | type?: 'text' | 'password' 279 | ) => { 280 | const fieldOverride = fieldOverrides?.[key]; 281 | if (fieldOverride?.hide) { 282 | return null; 283 | } 284 | return ( 285 |
286 | 293 | onInputChange(key, e.target.value)} 303 | /> 304 | {errorData[key] && ( 305 |
{errorData[key]}
306 | )} 307 |
308 | ); 309 | }; 310 | 311 | const shouldFieldBlockSubmit = (key: Field) => { 312 | if ( 313 | (formData[key] || 314 | fieldOverrides?.[key]?.notRequired || 315 | fieldOverrides?.[key]?.hide) && 316 | !errorData[key] 317 | ) { 318 | return false; 319 | } 320 | return true; 321 | }; 322 | 323 | return ( 324 | <> 325 | {error && ( 326 | 327 | )} 328 | {successMessage && ( 329 | 330 | )} 331 | {(config.is_basic_authentication_enabled || 332 | config.is_mobile_basic_authentication_enabled) && 333 | !config.is_magic_link_login_enabled && ( 334 | <> 335 |
336 | {renderField('given_name', 'First Name', 'eg. John', 'text')} 337 | {renderField('family_name', 'Last Name', 'eg. Doe', 'text')} 338 | {renderField( 339 | 'email_or_phone_number', 340 | getEmailPhoneLabels(config), 341 | getEmailPhonePlaceholder(config) 342 | )} 343 | {renderField('password', 'Password', '********', 'password')} 344 | {renderField( 345 | 'confirmPassword', 346 | 'Confirm Password', 347 | '********', 348 | 'password' 349 | )} 350 | {config.is_strong_password_enabled && ( 351 | <> 352 | 356 |
357 | 358 | )} 359 |
360 | 376 | {loading ? `Processing ...` : `Sign Up`} 377 | 378 | 379 | {setView && ( 380 | 381 |
382 | Already have an account?{' '} 383 | setView(Views.Login)}> 384 | Log In 385 | 386 |
387 |
388 | )} 389 | 390 | )} 391 | 392 | ); 393 | }; 394 | -------------------------------------------------------------------------------- /src/components/AuthorizerSocialLogin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Github } from '../icons/github'; 3 | import { Google } from '../icons/google'; 4 | import { Facebook } from '../icons/facebook'; 5 | import { StyledButton, StyledSeparator } from '../styledComponents'; 6 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 7 | import { ButtonAppearance } from '../constants'; 8 | import { createQueryParams } from '../utils/common'; 9 | import { LinkedIn } from '../icons/linkedin'; 10 | import { Apple } from '../icons/apple'; 11 | import { Twitter } from '../icons/twitter'; 12 | import { Microsoft } from '../icons/microsoft'; 13 | import { Twitch } from '../icons/twitch'; 14 | import { Roblox } from '../icons/roblox'; 15 | 16 | export const AuthorizerSocialLogin: React.FC<{ 17 | urlProps?: Record; 18 | roles?: string[]; 19 | }> = ({ urlProps, roles }) => { 20 | const { config } = useAuthorizer(); 21 | const hasSocialLogin = 22 | config.is_google_login_enabled || 23 | config.is_github_login_enabled || 24 | config.is_facebook_login_enabled || 25 | config.is_linkedin_login_enabled || 26 | config.is_apple_login_enabled || 27 | config.is_twitter_login_enabled || 28 | config.is_microsoft_login_enabled || 29 | config.is_twitch_login_enabled || 30 | config.is_roblox_login_enabled; 31 | 32 | const data: { 33 | scope?: string; 34 | roles?: string[]; 35 | redirect_uri?: string; 36 | redirectURL?: string; 37 | } = { 38 | ...(urlProps || {}), 39 | scope: urlProps?.scope.join(' '), 40 | }; 41 | 42 | if (roles && roles.length) { 43 | data.roles = roles; 44 | } 45 | 46 | if (!data.redirect_uri && !data.redirectURL) { 47 | data.redirect_uri = config.redirectURL; 48 | } 49 | 50 | const queryParams = createQueryParams(data); 51 | 52 | return ( 53 | <> 54 | {config.is_apple_login_enabled && ( 55 |
56 | { 59 | window.location.href = `${config.authorizerURL}/oauth_login/apple?${queryParams}`; 60 | }} 61 | > 62 | 63 | Continue with Apple 64 | 65 |
66 |
67 | )} 68 | {config.is_google_login_enabled && ( 69 | <> 70 | { 73 | window.location.href = `${config.authorizerURL}/oauth_login/google?${queryParams}`; 74 | }} 75 | > 76 | 77 | Continue with Google 78 | 79 |
80 | 81 | )} 82 | {config.is_github_login_enabled && ( 83 | <> 84 | { 87 | window.location.href = `${config.authorizerURL}/oauth_login/github?${queryParams}`; 88 | }} 89 | > 90 | 91 | Continue with Github 92 | 93 |
94 | 95 | )} 96 | {config.is_facebook_login_enabled && ( 97 | <> 98 | { 101 | window.location.href = `${config.authorizerURL}/oauth_login/facebook?${queryParams}`; 102 | }} 103 | > 104 | 105 | Continue with Facebook 106 | 107 |
108 | 109 | )} 110 | {config.is_linkedin_login_enabled && ( 111 | <> 112 | { 115 | window.location.href = `${config.authorizerURL}/oauth_login/linkedin?${queryParams}`; 116 | }} 117 | > 118 | 119 | Continue with LinkedIn 120 | 121 |
122 | 123 | )} 124 | {config.is_twitter_login_enabled && ( 125 | <> 126 | { 129 | window.location.href = `${config.authorizerURL}/oauth_login/twitter?${queryParams}`; 130 | }} 131 | > 132 | 133 | Continue with Twitter 134 | 135 |
136 | 137 | )} 138 | {config.is_microsoft_login_enabled && ( 139 | <> 140 | { 143 | window.location.href = `${config.authorizerURL}/oauth_login/microsoft?${queryParams}`; 144 | }} 145 | > 146 | 147 | Continue with Microsoft 148 | 149 |
150 | 151 | )} 152 | {config.is_twitch_login_enabled && ( 153 | <> 154 | { 157 | window.location.href = `${config.authorizerURL}/oauth_login/twitch?${queryParams}`; 158 | }} 159 | > 160 | 161 | Continue with Twitch 162 | 163 |
164 | 165 | )} 166 | {config.is_roblox_login_enabled && ( 167 | <> 168 | { 171 | window.location.href = `${config.authorizerURL}/oauth_login/roblox?${queryParams}`; 172 | }} 173 | > 174 | 175 | Continue with Roblox 176 | 177 |
178 | 179 | )} 180 | {hasSocialLogin && 181 | (config.is_basic_authentication_enabled || 182 | config.is_mobile_basic_authentication_enabled || 183 | config.is_magic_link_login_enabled) && ( 184 | OR 185 | )} 186 | 187 | ); 188 | }; 189 | -------------------------------------------------------------------------------- /src/components/AuthorizerTOTPScanner.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useState } from 'react'; 2 | import { StyledButton, StyledFlex, StyledSeparator } from '../styledComponents'; 3 | import { ButtonAppearance, Views } from '../constants'; 4 | import { AuthorizerVerifyOtp } from './AuthorizerVerifyOtp'; 5 | 6 | export const AuthorizerTOTPScanner: FC<{ 7 | setView?: (v: Views) => void; 8 | onLogin?: (data: any) => void; 9 | email?: string; 10 | phone_number?: string; 11 | urlProps?: Record; 12 | authenticator_scanner_image: string; 13 | authenticator_secret: string; 14 | authenticator_recovery_codes: string[]; 15 | }> = ({ 16 | setView, 17 | onLogin, 18 | email, 19 | phone_number, 20 | authenticator_scanner_image, 21 | authenticator_secret, 22 | authenticator_recovery_codes, 23 | urlProps, 24 | }) => { 25 | const [isOTPScreenVisisble, setIsOTPScreenVisisble] = 26 | useState(false); 27 | 28 | const handleContinue = () => { 29 | setIsOTPScreenVisisble(true); 30 | }; 31 | 32 | if (isOTPScreenVisisble) { 33 | return ( 34 | 44 | ); 45 | } 46 | 47 | return ( 48 | <> 49 |

50 | Scan the QR code or enter the secret key into your authenticator app. 51 |

52 | 53 | scanner 57 | 58 |

59 | If you are unable to scan the QR code, please enter the secret key 60 | manually. 61 |

62 |

63 | {authenticator_secret} 64 |

65 | 66 |

67 | If you lose access to your authenticator app, you can use the recovery 68 | codes below to regain access to your account. Please save these codes 69 | safely and do not share them with anyone. 70 |

71 |
    72 | {authenticator_recovery_codes.map((code, index) => { 73 | return
  • {code}
  • ; 74 | })} 75 |
76 | 81 | Continue 82 | 83 | 84 | ); 85 | }; 86 | -------------------------------------------------------------------------------- /src/components/AuthorizerVerifyOtp.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, useEffect, useState } from 'react'; 2 | import { VerifyOtpInput } from '@authorizerdev/authorizer-js'; 3 | import styles from '../styles/default.css'; 4 | 5 | import { ButtonAppearance, MessageType, Views } from '../constants'; 6 | import { useAuthorizer } from '../contexts/AuthorizerContext'; 7 | import { StyledButton, StyledFooter, StyledLink } from '../styledComponents'; 8 | import { Message } from './Message'; 9 | 10 | interface InputDataType { 11 | otp: string | null; 12 | } 13 | 14 | export const AuthorizerVerifyOtp: FC<{ 15 | setView?: (v: Views) => void; 16 | onLogin?: (data: any) => void; 17 | email?: string; 18 | phone_number?: string; 19 | urlProps?: Record; 20 | is_totp?: boolean; 21 | }> = ({ setView, onLogin, email, phone_number, urlProps, is_totp }) => { 22 | const [error, setError] = useState(``); 23 | const [successMessage, setSuccessMessage] = useState(``); 24 | const [loading, setLoading] = useState(false); 25 | const [sendingOtp, setSendingOtp] = useState(false); 26 | const [formData, setFormData] = useState({ 27 | otp: null, 28 | }); 29 | const [errorData, setErrorData] = useState({ 30 | otp: null, 31 | }); 32 | const { authorizerRef, config, setAuthData } = useAuthorizer(); 33 | useEffect(() => { 34 | if (!email && !phone_number) { 35 | setError(`Email or Phone Number is required`); 36 | } 37 | }, []); 38 | 39 | const onInputChange = async (field: string, value: string) => { 40 | setFormData({ ...formData, [field]: value }); 41 | }; 42 | 43 | const onSubmit = async (e: any) => { 44 | e.preventDefault(); 45 | setSuccessMessage(``); 46 | try { 47 | setLoading(true); 48 | const data: VerifyOtpInput = { 49 | email, 50 | phone_number, 51 | otp: formData.otp || '', 52 | }; 53 | if (urlProps?.state) { 54 | data.state = urlProps.state; 55 | } 56 | data.is_totp = !!is_totp; 57 | const { data: res, errors } = await authorizerRef.verifyOtp(data); 58 | setLoading(false); 59 | if (errors && errors.length) { 60 | setError(errors[0]?.message || ``); 61 | return; 62 | } 63 | if (res) { 64 | setError(``); 65 | setAuthData({ 66 | user: res.user || null, 67 | token: { 68 | access_token: res.access_token, 69 | expires_in: res.expires_in, 70 | refresh_token: res.refresh_token, 71 | id_token: res.id_token, 72 | }, 73 | config, 74 | loading: false, 75 | }); 76 | } 77 | 78 | if (onLogin) { 79 | onLogin(res); 80 | } 81 | } catch (err) { 82 | setLoading(false); 83 | setError((err as Error).message); 84 | } 85 | }; 86 | 87 | const onErrorClose = () => { 88 | setError(``); 89 | }; 90 | 91 | const onSuccessClose = () => { 92 | setSuccessMessage(``); 93 | }; 94 | 95 | const resendOtp = async () => { 96 | setSuccessMessage(``); 97 | try { 98 | setSendingOtp(true); 99 | 100 | const { data: res, errors } = await authorizerRef.resendOtp({ 101 | email, 102 | phone_number, 103 | }); 104 | setSendingOtp(false); 105 | if (errors && errors.length) { 106 | setError(errors[0]?.message || ``); 107 | return; 108 | } 109 | 110 | if (res && res?.message) { 111 | setError(``); 112 | setSuccessMessage(res.message); 113 | } 114 | 115 | if (onLogin) { 116 | onLogin(res); 117 | } 118 | } catch (err) { 119 | setLoading(false); 120 | setError((err as Error).message); 121 | } 122 | }; 123 | 124 | useEffect(() => { 125 | if (formData.otp === '') { 126 | setErrorData({ ...errorData, otp: 'OTP is required' }); 127 | } else { 128 | setErrorData({ ...errorData, otp: null }); 129 | } 130 | }, [formData.otp]); 131 | 132 | return ( 133 | <> 134 | {successMessage && ( 135 | 140 | )} 141 | {error && ( 142 | 143 | )} 144 |

145 | Please enter the OTP sent to your email or phone number or authenticator 146 |

147 |
148 |
149 |
150 | 156 | onInputChange('otp', e.target.value)} 166 | /> 167 | {errorData.otp && ( 168 |
{errorData.otp}
169 | )} 170 | {is_totp && ( 171 | 178 | )} 179 |
180 |
181 | 186 | {loading ? `Processing ...` : `Submit`} 187 | 188 |
189 | {setView && ( 190 | 191 | {sendingOtp ? ( 192 |
Sending ...
193 | ) : ( 194 | 195 | Resend OTP 196 | 197 | )} 198 | {config.is_sign_up_enabled && ( 199 |
200 | Don't have an account?{' '} 201 | setView(Views.Signup)}> 202 | Sign Up 203 | 204 |
205 | )} 206 |
207 | )} 208 | 209 | ); 210 | }; 211 | -------------------------------------------------------------------------------- /src/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { MessageType } from '../constants'; 3 | import { IconClose } from '../icons/close'; 4 | import { StyledMessageWrapper, StyledFlex } from '../styledComponents'; 5 | import { capitalizeFirstLetter } from '../utils/format'; 6 | 7 | type Props = { 8 | type: MessageType; 9 | text: string; 10 | onClose?: () => void; 11 | extraStyles?: Record; 12 | }; 13 | 14 | export const Message: FC = ({ type, text, extraStyles, onClose }) => { 15 | if (text.trim()) { 16 | return ( 17 | 18 | 19 |
{capitalizeFirstLetter(text)}
20 | {onClose && ( 21 | 22 | 23 | 24 | )} 25 |
26 |
27 | ); 28 | } 29 | 30 | return null; 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/PasswordStrengthIndicator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StyledFlex, 4 | StyledPasswordStrengthWrapper, 5 | StyledPasswordStrength, 6 | } from '../styledComponents'; 7 | import { validatePassword } from '../utils/validations'; 8 | 9 | interface PropTypes { 10 | value: string; 11 | setDisableButton: Function; 12 | } 13 | 14 | const PasswordStrengthIndicator = ({ value, setDisableButton }: PropTypes) => { 15 | const [ 16 | { 17 | strength, 18 | score, 19 | hasSixChar, 20 | hasLowerCase, 21 | hasNumericChar, 22 | hasSpecialChar, 23 | hasUpperCase, 24 | maxThirtySixChar, 25 | }, 26 | setValidations, 27 | ] = React.useState({ ...validatePassword(value || '') }); 28 | 29 | React.useEffect(() => { 30 | const validationData = validatePassword(value || ''); 31 | setValidations({ ...validationData }); 32 | if (!validationData.isValid) { 33 | setDisableButton(true); 34 | } else { 35 | setDisableButton(false); 36 | } 37 | }, [value]); 38 | 39 | return ( 40 |
41 | 42 | 43 | 2 ? `weak` : `default`} /> 44 | 3 ? `good` : `default`} /> 45 | 4 ? `strong` : `default`} /> 46 | 5 ? `veryStrong` : `default`} 48 | /> 49 | {!!score &&
{strength}
} 50 |
51 |
52 |

53 | Criteria for a strong password: 54 |

55 | 56 | 61 | 62 |
At least 6 characters
63 |
64 | 69 | 70 |
At least 1 lowercase letter
71 |
72 | 77 | 78 |
At least 1 uppercase letter
79 |
80 | 85 | 86 |
At least 1 numeric character
87 |
88 | 93 | 94 |
At least 1 special character
95 |
96 | 101 | 102 |
Maximum 36 characters
103 |
104 |
105 |
106 | ); 107 | }; 108 | 109 | export default PasswordStrengthIndicator; 110 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export enum Views { 2 | Login, 3 | Signup, 4 | ForgotPassword, 5 | } 6 | 7 | export enum ButtonAppearance { 8 | Primary, 9 | Default, 10 | } 11 | 12 | export enum MessageType { 13 | Error, 14 | Success, 15 | Info, 16 | } 17 | 18 | export enum AuthorizerProviderActionType { 19 | SET_USER = 'SET_USER', 20 | SET_TOKEN = 'SET_TOKEN', 21 | SET_LOADING = 'SET_LOADING', 22 | SET_AUTH_DATA = 'SET_AUTH_DATA', 23 | SET_CONFIG = 'SET_CONFIG', 24 | } 25 | 26 | // TODO use based on theme primary color 27 | export const passwordStrengthIndicatorOpacity: Record = { 28 | default: 0.15, 29 | weak: 0.4, 30 | good: 0.6, 31 | strong: 0.8, 32 | veryStrong: 1, 33 | }; 34 | -------------------------------------------------------------------------------- /src/contexts/AuthorizerContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FC, 3 | createContext, 4 | useReducer, 5 | useContext, 6 | useRef, 7 | useEffect, 8 | } from 'react'; 9 | import { Authorizer, User, AuthToken } from '@authorizerdev/authorizer-js'; 10 | 11 | import { 12 | AuthorizerContextPropsType, 13 | AuthorizerState, 14 | AuthorizerProviderAction, 15 | } from '../types'; 16 | import { AuthorizerProviderActionType } from '../constants'; 17 | import { hasWindow } from '../utils/window'; 18 | 19 | const AuthorizerContext = createContext({ 20 | config: { 21 | authorizerURL: '', 22 | redirectURL: '/', 23 | client_id: '', 24 | is_google_login_enabled: false, 25 | is_github_login_enabled: false, 26 | is_facebook_login_enabled: false, 27 | is_linkedin_login_enabled: false, 28 | is_apple_login_enabled: false, 29 | is_twitter_login_enabled: false, 30 | is_microsoft_login_enabled: false, 31 | is_twitch_login_enabled: false, 32 | is_roblox_login_enabled: false, 33 | is_email_verification_enabled: false, 34 | is_basic_authentication_enabled: false, 35 | is_magic_link_login_enabled: false, 36 | is_sign_up_enabled: false, 37 | is_strong_password_enabled: true, 38 | is_multi_factor_auth_enabled: false, 39 | is_mobile_basic_authentication_enabled: false, 40 | is_phone_verification_enabled: false, 41 | }, 42 | user: null, 43 | token: null, 44 | loading: false, 45 | setLoading: () => {}, 46 | setToken: () => {}, 47 | setUser: () => {}, 48 | setAuthData: () => {}, 49 | authorizerRef: new Authorizer({ 50 | authorizerURL: `http://localhost:8080`, 51 | redirectURL: hasWindow() ? window.location.origin : '/', 52 | clientID: '', 53 | }), 54 | logout: async () => {}, 55 | }); 56 | 57 | function reducer( 58 | state: AuthorizerState, 59 | action: AuthorizerProviderAction 60 | ): AuthorizerState { 61 | switch (action.type) { 62 | case AuthorizerProviderActionType.SET_USER: 63 | return { ...state, user: action.payload.user }; 64 | case AuthorizerProviderActionType.SET_TOKEN: 65 | return { 66 | ...state, 67 | token: action.payload.token, 68 | }; 69 | case AuthorizerProviderActionType.SET_LOADING: 70 | return { 71 | ...state, 72 | loading: action.payload.loading, 73 | }; 74 | case AuthorizerProviderActionType.SET_CONFIG: 75 | return { 76 | ...state, 77 | config: action.payload.config, 78 | }; 79 | case AuthorizerProviderActionType.SET_AUTH_DATA: 80 | return { 81 | ...action.payload, 82 | }; 83 | 84 | default: 85 | throw new Error(); 86 | } 87 | } 88 | 89 | let initialState: AuthorizerState = { 90 | user: null, 91 | token: null, 92 | loading: true, 93 | config: { 94 | authorizerURL: '', 95 | redirectURL: '/', 96 | client_id: '', 97 | is_google_login_enabled: false, 98 | is_github_login_enabled: false, 99 | is_facebook_login_enabled: false, 100 | is_linkedin_login_enabled: false, 101 | is_apple_login_enabled: false, 102 | is_twitter_login_enabled: false, 103 | is_microsoft_login_enabled: false, 104 | is_twitch_login_enabled: false, 105 | is_roblox_login_enabled: false, 106 | is_email_verification_enabled: false, 107 | is_basic_authentication_enabled: false, 108 | is_magic_link_login_enabled: false, 109 | is_sign_up_enabled: false, 110 | is_strong_password_enabled: true, 111 | is_multi_factor_auth_enabled: false, 112 | is_mobile_basic_authentication_enabled: false, 113 | is_phone_verification_enabled: false, 114 | }, 115 | }; 116 | 117 | export const AuthorizerProvider: FC<{ 118 | children: React.ReactNode; 119 | config: { 120 | authorizerURL: string; 121 | redirectURL: string; 122 | clientID?: string; 123 | }; 124 | onStateChangeCallback?: (stateData: AuthorizerState) => Promise; 125 | }> = ({ config: defaultConfig, onStateChangeCallback, children }) => { 126 | const [state, dispatch] = useReducer(reducer, { 127 | ...initialState, 128 | config: { 129 | ...initialState.config, 130 | ...defaultConfig, 131 | }, 132 | }); 133 | 134 | let intervalRef: any = null; 135 | 136 | const authorizerRef = useRef( 137 | new Authorizer({ 138 | authorizerURL: state.config.authorizerURL, 139 | redirectURL: hasWindow() 140 | ? state.config.redirectURL || window.location.origin 141 | : state.config.redirectURL || '/', 142 | clientID: state.config.client_id, 143 | }) 144 | ); 145 | 146 | const getToken = async () => { 147 | const { 148 | data: metaRes, 149 | errors: metaResErrors, 150 | } = await authorizerRef.current.getMetaData(); 151 | try { 152 | if (metaResErrors && metaResErrors.length) { 153 | throw new Error(metaResErrors[0].message); 154 | } 155 | const { data: res, errors } = await authorizerRef.current.getSession(); 156 | if (errors && errors.length) { 157 | throw new Error(errors[0].message); 158 | } 159 | if (res && res.access_token && res.user) { 160 | const token = { 161 | access_token: res.access_token, 162 | expires_in: res.expires_in, 163 | id_token: res.id_token, 164 | refresh_token: res.refresh_token || '', 165 | }; 166 | dispatch({ 167 | type: AuthorizerProviderActionType.SET_AUTH_DATA, 168 | payload: { 169 | ...state, 170 | token, 171 | user: res.user, 172 | config: { 173 | ...state.config, 174 | ...metaRes, 175 | }, 176 | loading: false, 177 | }, 178 | }); 179 | 180 | // const millisecond = getIntervalDiff(res.expires_at); 181 | // if (millisecond > 0) { 182 | // if (intervalRef) clearInterval(intervalRef); 183 | // intervalRef = setInterval(() => { 184 | // getToken(); 185 | // }, millisecond); 186 | // } 187 | if (intervalRef) clearInterval(intervalRef); 188 | intervalRef = setInterval(() => { 189 | getToken(); 190 | }, res.expires_in * 1000); 191 | } else { 192 | dispatch({ 193 | type: AuthorizerProviderActionType.SET_AUTH_DATA, 194 | payload: { 195 | ...state, 196 | token: null, 197 | user: null, 198 | config: { 199 | ...state.config, 200 | ...metaRes, 201 | }, 202 | loading: false, 203 | }, 204 | }); 205 | } 206 | } catch (err) { 207 | dispatch({ 208 | type: AuthorizerProviderActionType.SET_AUTH_DATA, 209 | payload: { 210 | ...state, 211 | token: null, 212 | user: null, 213 | config: { 214 | ...state.config, 215 | ...metaRes, 216 | }, 217 | loading: false, 218 | }, 219 | }); 220 | } 221 | }; 222 | 223 | useEffect(() => { 224 | getToken(); 225 | return () => { 226 | if (intervalRef) { 227 | clearInterval(intervalRef); 228 | } 229 | }; 230 | }, []); 231 | 232 | useEffect(() => { 233 | if (onStateChangeCallback) { 234 | onStateChangeCallback(state); 235 | } 236 | }, [state]); 237 | 238 | const handleTokenChange = (token: AuthToken | null) => { 239 | dispatch({ 240 | type: AuthorizerProviderActionType.SET_TOKEN, 241 | payload: { 242 | token, 243 | }, 244 | }); 245 | 246 | if (token?.access_token) { 247 | if (intervalRef) clearInterval(intervalRef); 248 | intervalRef = setInterval(() => { 249 | getToken(); 250 | }, token.expires_in * 1000); 251 | } 252 | }; 253 | 254 | const setAuthData = (data: AuthorizerState) => { 255 | dispatch({ 256 | type: AuthorizerProviderActionType.SET_AUTH_DATA, 257 | payload: data, 258 | }); 259 | 260 | if (data.token?.access_token) { 261 | if (intervalRef) clearInterval(intervalRef); 262 | intervalRef = setInterval(() => { 263 | getToken(); 264 | }, data.token.expires_in * 1000); 265 | } 266 | }; 267 | 268 | const setUser = (user: User | null) => { 269 | dispatch({ 270 | type: AuthorizerProviderActionType.SET_USER, 271 | payload: { 272 | user, 273 | }, 274 | }); 275 | }; 276 | 277 | const setLoading = (loading: boolean) => { 278 | dispatch({ 279 | type: AuthorizerProviderActionType.SET_LOADING, 280 | payload: { 281 | loading, 282 | }, 283 | }); 284 | }; 285 | 286 | const logout = async () => { 287 | dispatch({ 288 | type: AuthorizerProviderActionType.SET_LOADING, 289 | payload: { 290 | loading: true, 291 | }, 292 | }); 293 | await authorizerRef.current.logout(); 294 | const loggedOutState = { 295 | user: null, 296 | token: null, 297 | loading: false, 298 | config: state.config, 299 | }; 300 | dispatch({ 301 | type: AuthorizerProviderActionType.SET_AUTH_DATA, 302 | payload: loggedOutState, 303 | }); 304 | }; 305 | 306 | return ( 307 | 318 | {children} 319 | 320 | ); 321 | }; 322 | 323 | export const useAuthorizer = () => useContext(AuthorizerContext); 324 | -------------------------------------------------------------------------------- /src/icons/apple.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Apple = () => { 4 | return ( 5 |
13 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/icons/close.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const IconClose = () => ( 4 | 10 | 11 | 15 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /src/icons/facebook.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Facebook = () => { 4 | return ( 5 |
13 | 20 | 21 | 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /src/icons/github.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Github = () => { 4 | return ( 5 |
13 | 19 | 23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/icons/google.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Google = () => { 4 | return ( 5 |
13 | 14 | 15 | 19 | 23 | 27 | 31 | 32 | 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/icons/linkedin.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const LinkedIn = () => { 4 | return ( 5 |
13 | 19 | 23 | 27 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/icons/microsoft.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Microsoft = () => { 4 | return ( 5 |
13 | 19 | 20 | 25 | 30 | 35 | 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/icons/roblox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Roblox = () => { 4 | return ( 5 |
13 | roblox_logo 17 |
18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/icons/twitch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Twitch = () => { 4 | return ( 5 |
13 | 19 | 20 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/icons/twitter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Twitter = () => { 4 | return ( 5 |
13 | 19 | 23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AuthorizerProvider, 3 | useAuthorizer, 4 | } from './contexts/AuthorizerContext'; 5 | import { AuthorizerSignup } from './components/AuthorizerSignup'; 6 | import { AuthorizerBasicAuthLogin } from './components/AuthorizerBasicAuthLogin'; 7 | import { AuthorizerMagicLinkLogin } from './components/AuthorizerMagicLinkLogin'; 8 | import { AuthorizerForgotPassword } from './components/AuthorizerForgotPassword'; 9 | import { AuthorizerSocialLogin } from './components/AuthorizerSocialLogin'; 10 | import { AuthorizerResetPassword } from './components/AuthorizerResetPassword'; 11 | import { AuthorizerVerifyOtp } from './components/AuthorizerVerifyOtp'; 12 | import { AuthorizerRoot as Authorizer } from './components/AuthorizerRoot'; 13 | import { AuthorizerTOTPScanner } from './components/AuthorizerTOTPScanner'; 14 | 15 | export { 16 | useAuthorizer, 17 | Authorizer, 18 | AuthorizerProvider, 19 | AuthorizerSignup, 20 | AuthorizerBasicAuthLogin, 21 | AuthorizerMagicLinkLogin, 22 | AuthorizerForgotPassword, 23 | AuthorizerSocialLogin, 24 | AuthorizerResetPassword, 25 | AuthorizerVerifyOtp, 26 | AuthorizerTOTPScanner, 27 | }; 28 | -------------------------------------------------------------------------------- /src/stories/StyledButton.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react'; 2 | import { fn } from '@storybook/test'; 3 | 4 | import StyledButton from '../styledComponents/StyledButton'; 5 | import { ButtonAppearance } from '../constants'; 6 | 7 | // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 8 | const meta: Meta = { 9 | title: 'Example/StyledButton', 10 | component: StyledButton, 11 | parameters: { 12 | // Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout 13 | layout: 'centered', 14 | }, 15 | argTypes: { 16 | appearance: { 17 | control: 'select', 18 | options: [ButtonAppearance.Default, ButtonAppearance.Primary] 19 | }, 20 | type: { 21 | control: 'select', 22 | options: ['button', 'submit', 'reset'] 23 | } 24 | }, 25 | // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args 26 | args: { 27 | onClick: fn(), 28 | type: 'button' 29 | }, 30 | }; 31 | 32 | export default meta; 33 | type Story = StoryObj; 34 | 35 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args 36 | export const Default: Story = { 37 | args: { 38 | appearance: ButtonAppearance.Default, 39 | children: 'Default Button' 40 | }, 41 | }; 42 | 43 | export const Primary: Story = { 44 | args: { 45 | appearance: ButtonAppearance.Primary, 46 | children: 'Primary Button' 47 | }, 48 | }; 49 | 50 | export const DefaultDisabled: Story = { 51 | args: { 52 | appearance: ButtonAppearance.Default, 53 | children: 'Primary Button', 54 | disabled: true 55 | }, 56 | }; 57 | 58 | export const PrimaryDisabled: Story = { 59 | args: { 60 | appearance: ButtonAppearance.Primary, 61 | children: 'Primary Button', 62 | disabled: true 63 | }, 64 | }; 65 | -------------------------------------------------------------------------------- /src/stories/StyledFlex.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { Meta, StoryObj } from '@storybook/react'; 3 | 4 | import StyledFlex from '../styledComponents/StyledFlex'; 5 | 6 | type TemplateArgs = React.ComponentProps; 7 | 8 | const meta: Meta = { 9 | title: 'Example/StyledFlex', 10 | component: StyledFlex, 11 | argTypes: { 12 | flexDirection: { 13 | control: 'select', 14 | options: ['row', 'row-reverse', 'column', 'column-reverse'] 15 | }, 16 | wrap: { 17 | control: 'select', 18 | options: ['nowrap', 'wrap', 'wrap-reverse'] 19 | }, 20 | children: Element 21 | }, 22 | args: { 23 | flexDirection: 'row', 24 | wrap: 'nowrap' 25 | }, 26 | render: ({...args}) => ( 27 | 28 | 29 |
At least 6 characters
30 |
31 | ) 32 | }; 33 | 34 | export default meta; 35 | type Story = StoryObj; 36 | 37 | // More on writing stories with args: https://storybook.js.org/docs/writing-stories/args 38 | export const RowStyledFlex: Story = { 39 | args: { 40 | flexDirection: 'row', 41 | wrap: 'nowrap', 42 | }, 43 | }; 44 | 45 | export const ColumnStyledFlex: Story = { 46 | args: { 47 | flexDirection: 'column', 48 | wrap: 'nowrap' 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /src/stories/assets/accessibility.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/accessibility.png -------------------------------------------------------------------------------- /src/stories/assets/accessibility.svg: -------------------------------------------------------------------------------- 1 | Accessibility -------------------------------------------------------------------------------- /src/stories/assets/addon-library.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/addon-library.png -------------------------------------------------------------------------------- /src/stories/assets/assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/assets.png -------------------------------------------------------------------------------- /src/stories/assets/avif-test-image.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/avif-test-image.avif -------------------------------------------------------------------------------- /src/stories/assets/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/context.png -------------------------------------------------------------------------------- /src/stories/assets/discord.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stories/assets/docs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/docs.png -------------------------------------------------------------------------------- /src/stories/assets/figma-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/figma-plugin.png -------------------------------------------------------------------------------- /src/stories/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stories/assets/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/share.png -------------------------------------------------------------------------------- /src/stories/assets/styling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/styling.png -------------------------------------------------------------------------------- /src/stories/assets/testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/testing.png -------------------------------------------------------------------------------- /src/stories/assets/theming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/authorizerdev/authorizer-react/6a39f3de73f8b019bf746bdd5d6daddf58420e1a/src/stories/assets/theming.png -------------------------------------------------------------------------------- /src/stories/assets/tutorials.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/stories/assets/youtube.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styledComponents/StyledButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler, ReactNode } from 'react'; 2 | import { ButtonAppearance } from '../constants'; 3 | import styles from '../styles/default.css'; 4 | 5 | const StyledButton = ({ 6 | style = { 7 | width: '100%', 8 | }, 9 | type, 10 | appearance = ButtonAppearance.Default, 11 | disabled = false, 12 | onClick, 13 | children, 14 | }: { 15 | type?: 'button' | 'submit' | 'reset' | undefined; 16 | style?: Record; 17 | appearance?: ButtonAppearance; 18 | disabled?: boolean; 19 | onClick?: MouseEventHandler; 20 | children: ReactNode; 21 | }) => { 22 | return ( 23 | 44 | ); 45 | }; 46 | 47 | export default StyledButton; 48 | -------------------------------------------------------------------------------- /src/styledComponents/StyledFlex.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styles from '../styles/default.css'; 3 | 4 | const StyledFlex = ({ 5 | flexDirection = 'row', 6 | alignItems = 'center', 7 | justifyContent = 'center', 8 | wrap = 'wrap', 9 | width = 'inherit', 10 | children, 11 | }: { 12 | flexDirection?: 'row' | 'row-reverse' | 'column' | 'column-reverse'; 13 | alignItems?: string; 14 | justifyContent?: string; 15 | wrap?: 'nowrap' | 'wrap' | 'wrap-reverse'; 16 | width?: string; 17 | children: ReactNode; 18 | }) => { 19 | return ( 20 |
30 | {children} 31 |
32 | ); 33 | }; 34 | 35 | export default StyledFlex; 36 | -------------------------------------------------------------------------------- /src/styledComponents/StyledFooter.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styles from '../styles/default.css'; 3 | 4 | const StyledFooter = ({ children }: { children: ReactNode }) => { 5 | return
{children}
; 6 | }; 7 | 8 | export default StyledFooter; 9 | -------------------------------------------------------------------------------- /src/styledComponents/StyledLink.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler, ReactNode } from 'react'; 2 | import styles from '../styles/default.css'; 3 | 4 | const StyledLink = ({ 5 | marginBottom = '0px', 6 | children, 7 | onClick, 8 | }: { 9 | marginBottom?: string; 10 | children: ReactNode; 11 | onClick: MouseEventHandler; 12 | }) => { 13 | return ( 14 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default StyledLink; 25 | -------------------------------------------------------------------------------- /src/styledComponents/StyledMessageWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { MessageType } from '../constants'; 3 | import styles from '../styles/default.css'; 4 | 5 | const getBackgroundColor = (type: MessageType): string => { 6 | switch (type) { 7 | case MessageType.Error: 8 | return 'var(--authorizer-danger-color)'; 9 | case MessageType.Success: 10 | return 'var(--authorizer-success-color)'; 11 | case MessageType.Info: 12 | return 'var(--authorizer-slate-color)'; 13 | default: 14 | return 'var(--authorizer-success-color)'; 15 | } 16 | }; 17 | 18 | const StyledMessageWrapper = ({ 19 | type = MessageType.Success, 20 | styles: extraStyles = {}, 21 | children, 22 | }: { 23 | type: MessageType; 24 | children: ReactNode; 25 | styles?: Record; 26 | }) => { 27 | return ( 28 |
35 | {children} 36 |
37 | ); 38 | }; 39 | 40 | export default StyledMessageWrapper; 41 | -------------------------------------------------------------------------------- /src/styledComponents/StyledPasswordStrength.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { passwordStrengthIndicatorOpacity } from '../constants'; 3 | import styles from '../styles/default.css'; 4 | 5 | const StyledPasswordStrength = ({ 6 | strength = 'default', 7 | children, 8 | }: { 9 | strength: string; 10 | children?: ReactNode; 11 | }) => { 12 | return ( 13 |
17 | {children} 18 |
19 | ); 20 | }; 21 | 22 | export default StyledPasswordStrength; 23 | -------------------------------------------------------------------------------- /src/styledComponents/StyledPasswordStrengthWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styles from '../styles/default.css'; 3 | 4 | const StyledPasswordStrengthWrapper = ({ 5 | children, 6 | }: { 7 | children: ReactNode; 8 | }) => { 9 | return ( 10 |
{children}
11 | ); 12 | }; 13 | 14 | export default StyledPasswordStrengthWrapper; 15 | -------------------------------------------------------------------------------- /src/styledComponents/StyledSeparator.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styles from '../styles/default.css'; 3 | 4 | const StyledSeparator = ({ children }: { children?: ReactNode }) => { 5 | return
{children}
; 6 | }; 7 | 8 | export default StyledSeparator; 9 | -------------------------------------------------------------------------------- /src/styledComponents/StyledWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import styles from '../styles/default.css'; 3 | 4 | const StyledWrapper = ({ children }: { children: ReactNode }) => { 5 | return
{children}
; 6 | }; 7 | 8 | export default StyledWrapper; 9 | -------------------------------------------------------------------------------- /src/styledComponents/index.ts: -------------------------------------------------------------------------------- 1 | import StyledWrapper from './StyledWrapper'; 2 | import StyledButton from './StyledButton'; 3 | import StyledLink from './StyledLink'; 4 | import StyledSeparator from './StyledSeparator'; 5 | import StyledFooter from './StyledFooter'; 6 | import StyledMessageWrapper from './StyledMessageWrapper'; 7 | import StyledFlex from './StyledFlex'; 8 | import StyledPasswordStrength from './StyledPasswordStrength'; 9 | import StyledPasswordStrengthWrapper from './StyledPasswordStrengthWrapper'; 10 | 11 | export { 12 | StyledWrapper, 13 | StyledButton, 14 | StyledLink, 15 | StyledSeparator, 16 | StyledFooter, 17 | StyledMessageWrapper, 18 | StyledFlex, 19 | StyledPasswordStrength, 20 | StyledPasswordStrengthWrapper, 21 | }; 22 | -------------------------------------------------------------------------------- /src/styles/default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --authorizer-primary-color: #3b82f6; 3 | --authorizer-primary-disabled-color: #60a5fa; 4 | --authorizer-gray-color: #d1d5db; 5 | --authorizer-slate-color: #e2e8f0; 6 | --authorizer-white-color: #ffffff; 7 | --authorizer-danger-color: #dc2626; 8 | --authorizer-success-color: #10b981; 9 | --authorizer-text-color: #374151; 10 | --authorizer-fonts-font-stack: -apple-system, system-ui, sans-serif; 11 | --authorizer-fonts-large-text: 18px; 12 | --authorizer-fonts-medium-text: 14px; 13 | --authorizer-fonts-small-text: 12px; 14 | --authorizer-fonts-tiny-text: 10px; 15 | --authorizer-radius-card: 5px; 16 | --authorizer-radius-button: 5px; 17 | --authorizer-radius-input: 5px; 18 | } 19 | .styled-button { 20 | padding: 15px 10px !important; 21 | display: flex; 22 | justify-content: center; 23 | align-items: center; 24 | max-height: 64px; 25 | border-radius: var(--authorizer-radius-button); 26 | border-color: var(--authorizer-text-color) !important; 27 | border-style: solid !important; 28 | cursor: pointer; 29 | position: relative; 30 | } 31 | .styled-button:disabled { 32 | cursor: not-allowed; 33 | background-color: var(--authorizer-primary-disabled-color); 34 | } 35 | .styled-flex { 36 | display: flex; 37 | } 38 | .styled-footer { 39 | display: flex; 40 | flex-direction: column; 41 | justify-content: center; 42 | align-items: center; 43 | margin-top: 15px; 44 | } 45 | .styled-form-group { 46 | width: 100%; 47 | border: 0px; 48 | background-color: var(--authorizer-white-color); 49 | padding: 0 0 15px; 50 | } 51 | .form-input-label { 52 | padding: 2.5px; 53 | } 54 | .form-input-label > span { 55 | color: var(--authorizer-danger-color); 56 | } 57 | .form-input-field { 58 | width: 100%; 59 | margin-top: 5px; 60 | padding: 10px; 61 | display: flex; 62 | flex-direction: column; 63 | align-items: center; 64 | border-radius: var(--authorizer-radius-input); 65 | border: 1px; 66 | border-style: solid; 67 | border-color: var(--authorizer-text-color); 68 | } 69 | .input-error-content { 70 | border-color: var(--authorizer-danger-color) !important; 71 | } 72 | .input-error-content:hover { 73 | outline-color: var(--authorizer-danger-color); 74 | } 75 | .input-error-content:focus { 76 | outline-color: var(--authorizer-danger-color); 77 | } 78 | .form-input-error { 79 | font-size: 12px; 80 | font-weight: 400; 81 | color: red; 82 | border-color: var(--authorizer-danger-color); 83 | } 84 | .styled-link { 85 | color: var(--authorizer-primary-color); 86 | cursor: pointer; 87 | } 88 | .styled-message-wrapper { 89 | padding: 10px; 90 | color: white; 91 | border-radius: var(--authorizer-radius-card); 92 | margin: 10px 0px; 93 | font-size: var(--authorizer-fonts-small-text); 94 | } 95 | .styled-password-strength { 96 | width: 100%; 97 | height: 10px; 98 | flex: 0.75; 99 | border-radius: 5px; 100 | margin-right: 5px; 101 | background-color: var(--authorizer-primary-color); 102 | } 103 | .styled-password-strength-wrapper { 104 | margin: 2% 0 0; 105 | } 106 | .styled-separator { 107 | display: flex; 108 | align-items: center; 109 | text-align: center; 110 | margin: 10px 0px; 111 | } 112 | .styled-separator::before { 113 | content: ''; 114 | flex: 1; 115 | border-bottom: 1px solid var(--authorizer-gray-color); 116 | } 117 | .styled-separator::after { 118 | content: ''; 119 | flex: 1; 120 | border-bottom: 1px solid var(--authorizer-gray-color); 121 | } 122 | .styled-separator:not(:empty)::before { 123 | margin-right: 0.25em; 124 | } 125 | .styled-separator:not(:empty)::after { 126 | margin-left: 0.25em; 127 | } 128 | .styled-wrapper { 129 | font-family: var(--authorizer-fonts-font-stack); 130 | color: var(--authorizer-text-color); 131 | font-size: var(--authorizer-fonts-medium-text); 132 | box-sizing: border-box; 133 | width: 100%; 134 | min-width: 300px; 135 | } 136 | .styled-wrapper *, 137 | *:before, 138 | *:after { 139 | box-sizing: inherit; 140 | } 141 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { AuthToken, User, Authorizer } from '@authorizerdev/authorizer-js'; 2 | import { AuthorizerProviderActionType } from '../constants'; 3 | 4 | export type AuthorizerConfig = { 5 | authorizerURL: string; 6 | redirectURL: string; 7 | client_id: string; 8 | is_google_login_enabled: boolean; 9 | is_github_login_enabled: boolean; 10 | is_facebook_login_enabled: boolean; 11 | is_linkedin_login_enabled: boolean; 12 | is_apple_login_enabled: boolean; 13 | is_twitter_login_enabled: boolean; 14 | is_microsoft_login_enabled: boolean; 15 | is_twitch_login_enabled: boolean; 16 | is_roblox_login_enabled: boolean; 17 | is_email_verification_enabled: boolean; 18 | is_basic_authentication_enabled: boolean; 19 | is_magic_link_login_enabled: boolean; 20 | is_sign_up_enabled: boolean; 21 | is_strong_password_enabled: boolean; 22 | is_multi_factor_auth_enabled: boolean; 23 | is_mobile_basic_authentication_enabled: boolean; 24 | is_phone_verification_enabled: boolean; 25 | }; 26 | 27 | export type AuthorizerState = { 28 | user: User | null; 29 | token: AuthToken | null; 30 | loading: boolean; 31 | config: AuthorizerConfig; 32 | }; 33 | 34 | export type AuthorizerProviderAction = { 35 | type: AuthorizerProviderActionType; 36 | payload: any; 37 | }; 38 | 39 | export type AuthorizerContextPropsType = { 40 | config: { 41 | authorizerURL: string; 42 | redirectURL: string; 43 | client_id: string; 44 | is_google_login_enabled: boolean; 45 | is_facebook_login_enabled: boolean; 46 | is_github_login_enabled: boolean; 47 | is_linkedin_login_enabled: boolean; 48 | is_apple_login_enabled: boolean; 49 | is_twitter_login_enabled: boolean; 50 | is_microsoft_login_enabled: boolean; 51 | is_twitch_login_enabled: boolean; 52 | is_roblox_login_enabled: boolean; 53 | is_email_verification_enabled: boolean; 54 | is_basic_authentication_enabled: boolean; 55 | is_magic_link_login_enabled: boolean; 56 | is_sign_up_enabled: boolean; 57 | is_strong_password_enabled: boolean; 58 | is_multi_factor_auth_enabled: boolean; 59 | is_mobile_basic_authentication_enabled: boolean; 60 | is_phone_verification_enabled: boolean; 61 | }; 62 | user: null | User; 63 | token: null | AuthToken; 64 | loading: boolean; 65 | logout: () => Promise; 66 | setLoading: (data: boolean) => void; 67 | setUser: (data: null | User) => void; 68 | setToken: (data: null | AuthToken) => void; 69 | setAuthData: (data: AuthorizerState) => void; 70 | authorizerRef: Authorizer; 71 | }; 72 | 73 | export type OtpDataType = { 74 | is_screen_visible: boolean; 75 | email?: string; 76 | phone_number?: string; 77 | is_totp?: boolean; 78 | }; 79 | 80 | export type TotpDataType = { 81 | is_screen_visible: boolean; 82 | email?: string; 83 | phone_number?: string; 84 | authenticator_scanner_image: string; 85 | authenticator_secret: string; 86 | authenticator_recovery_codes: string[]; 87 | }; 88 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' { 2 | const content: { [className: string]: string }; 3 | export default content; 4 | } 5 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { hasWindow } from './window'; 2 | 3 | export const getIntervalDiff = (accessTokenExpiresAt: number): number => { 4 | const expiresAt = accessTokenExpiresAt * 1000 - 300000; 5 | const currentDate = new Date(); 6 | 7 | const millisecond = new Date(expiresAt).getTime() - currentDate.getTime(); 8 | return millisecond; 9 | }; 10 | 11 | export const getCrypto = () => { 12 | //ie 11.x uses msCrypto 13 | return hasWindow() 14 | ? ((window.crypto || (window as any).msCrypto) as Crypto) 15 | : null; 16 | }; 17 | 18 | export const createRandomString = () => { 19 | const charset = 20 | '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_~.'; 21 | let random = ''; 22 | const crypto = getCrypto(); 23 | if (crypto) { 24 | const randomValues = Array.from(crypto.getRandomValues(new Uint8Array(43))); 25 | randomValues.forEach((v) => (random += charset[v % charset.length])); 26 | } 27 | return random; 28 | }; 29 | 30 | export const createQueryParams = (params: any) => { 31 | return Object.keys(params) 32 | .filter((k) => typeof params[k] !== 'undefined') 33 | .map((k) => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) 34 | .join('&'); 35 | }; 36 | -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | export const formatErrorMessage = (message: string): string => { 2 | return message.replace(`[GraphQL] `, ''); 3 | }; 4 | 5 | export const capitalizeFirstLetter = (data: string): string => { 6 | return data.charAt(0).toUpperCase() + data.slice(1); 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/labels.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizerConfig } from '../types'; 2 | 3 | export const getEmailPhoneLabels = (config: AuthorizerConfig): string => { 4 | const emailLabel = 'Email'; 5 | const phoneLabel = 'Phone Number'; 6 | if ( 7 | config.is_basic_authentication_enabled && 8 | config.is_mobile_basic_authentication_enabled 9 | ) { 10 | return `${emailLabel} / ${phoneLabel}`; 11 | } else if (config.is_basic_authentication_enabled) { 12 | return emailLabel; 13 | } else if (config.is_mobile_basic_authentication_enabled) { 14 | return phoneLabel; 15 | } 16 | return emailLabel; 17 | }; 18 | 19 | export const getEmailPhonePlaceholder = (config: AuthorizerConfig): string => { 20 | const emailPlaceholder = 'hello@world.com'; 21 | const phonePlaceholder = '+919999999999'; 22 | const prefix = 'eg.'; 23 | if ( 24 | config.is_basic_authentication_enabled && 25 | config.is_mobile_basic_authentication_enabled 26 | ) { 27 | return `${prefix} ${emailPlaceholder} / ${phonePlaceholder}`; 28 | } else if (config.is_basic_authentication_enabled) { 29 | return `${prefix} ${emailPlaceholder}`; 30 | } else if (config.is_mobile_basic_authentication_enabled) { 31 | return `${prefix} ${phonePlaceholder}`; 32 | } 33 | return emailPlaceholder; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/url.ts: -------------------------------------------------------------------------------- 1 | import { hasWindow } from './window'; 2 | 3 | export const getSearchParams = (search = ''): Record => { 4 | let searchPrams = search; 5 | if (!searchPrams && hasWindow()) { 6 | searchPrams = window.location.search; 7 | } 8 | const urlSearchParams = new URLSearchParams(`${searchPrams}`); 9 | // @ts-ignore 10 | const params = Object.fromEntries(urlSearchParams.entries()); 11 | return params; 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/validations.ts: -------------------------------------------------------------------------------- 1 | export const isValidOtp = (otp: string): boolean => { 2 | const re = /^([A-Z0-9]{6})$/; 3 | return re.test(String(otp.trim())); 4 | }; 5 | 6 | export const hasSpecialChar = (char: string): boolean => { 7 | const re = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/; 8 | return re.test(char); 9 | }; 10 | 11 | export const validatePassword = ( 12 | value: string 13 | ): { 14 | score: number; 15 | strength: string; 16 | hasSixChar: boolean; 17 | hasLowerCase: boolean; 18 | hasUpperCase: boolean; 19 | hasNumericChar: boolean; 20 | hasSpecialChar: boolean; 21 | maxThirtySixChar: boolean; 22 | isValid: boolean; 23 | } => { 24 | const res = { 25 | score: 0, 26 | strength: '', 27 | hasSixChar: false, 28 | hasLowerCase: false, 29 | hasUpperCase: false, 30 | hasNumericChar: false, 31 | hasSpecialChar: false, 32 | maxThirtySixChar: false, 33 | }; 34 | 35 | if (value.length >= 6) { 36 | res.score = res.score + 1; 37 | res.hasSixChar = true; 38 | } 39 | 40 | if (value.length > 0 && value.length <= 36) { 41 | res.score = res.score + 1; 42 | res.maxThirtySixChar = true; 43 | } 44 | 45 | Array.from(value).forEach((char: any) => { 46 | if (char >= 'A' && char <= 'Z' && !res.hasUpperCase) { 47 | res.score = res.score + 1; 48 | res.hasUpperCase = true; 49 | } else if (char >= 'a' && char <= 'z' && !res.hasLowerCase) { 50 | res.score = res.score + 1; 51 | res.hasLowerCase = true; 52 | } else if (char >= '0' && char <= '9' && !res.hasNumericChar) { 53 | res.score = res.score + 1; 54 | res.hasNumericChar = true; 55 | } else if (hasSpecialChar(char) && !res.hasSpecialChar) { 56 | res.score = res.score + 1; 57 | res.hasSpecialChar = true; 58 | } 59 | }); 60 | 61 | if (res.score <= 2) { 62 | res.strength = 'Weak'; 63 | } else if (res.score <= 4) { 64 | res.strength = 'Good'; 65 | } else if (res.score <= 5) { 66 | res.strength = 'Strong'; 67 | } else { 68 | res.strength = 'Very Strong'; 69 | } 70 | 71 | const isValid = Object.values(res).every((i) => Boolean(i)); 72 | return { ...res, isValid }; 73 | }; 74 | -------------------------------------------------------------------------------- /src/utils/window.ts: -------------------------------------------------------------------------------- 1 | export const hasWindow = (): boolean => typeof window !== 'undefined'; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types", "typings.d.ts", "typings.d.ts"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsdx.config.js: -------------------------------------------------------------------------------- 1 | const replace = require('@rollup/plugin-replace'); 2 | const postcss = require('rollup-plugin-postcss'); 3 | 4 | module.exports = { 5 | // This function will run for each entry/format/env combination 6 | rollup(config, opts) { 7 | config.plugins = config.plugins.map((p) => 8 | p.name === 'replace' 9 | ? replace({ 10 | 'process.env.NODE_ENV': JSON.stringify(opts.env), 11 | preventAssignment: true, 12 | }) 13 | : p 14 | ); 15 | config.plugins.push( 16 | postcss({ 17 | modules: true, 18 | }) 19 | ); 20 | return config; 21 | }, 22 | }; 23 | --------------------------------------------------------------------------------