├── .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 |
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 |
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 |
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 |
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 |
290 | {!fieldOverride?.notRequired && * }
291 | {fieldOverride?.label ?? label}
292 |
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 |
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 |
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 |
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 |
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 |
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 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/icons/github.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Github = () => {
4 | return (
5 |
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 |
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 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/src/icons/twitch.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Twitch = () => {
4 | return (
5 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/src/icons/twitter.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Twitter = () => {
4 | return (
5 |
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 |
42 | {children}
43 |
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 |
--------------------------------------------------------------------------------