├── .eslintrc.js ├── .github └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .npmignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── babel.config.json ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── AuthContext.tsx ├── AuthContextForTesting.tsx ├── RequiredAuthProvider.tsx ├── hooks │ ├── additionalHooks.ts │ ├── useActiveOrg.tsx │ ├── useAuthInfo.ts │ ├── useAuthUrl.tsx │ ├── useHostedPageUrls.tsx │ ├── useLogoutFunction.ts │ └── useRedirectFunctions.tsx ├── index.test.js ├── index.tsx ├── useClientRef.tsx ├── withAuthInfo.tsx └── withRequiredAuthInfo.tsx └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended"], 7 | overrides: [ 8 | { 9 | env: { 10 | node: true, 11 | }, 12 | files: [".eslintrc.{js,cjs}"], 13 | parserOptions: { 14 | sourceType: "script", 15 | }, 16 | }, 17 | ], 18 | parser: "@typescript-eslint/parser", 19 | parserOptions: { 20 | ecmaVersion: "latest", 21 | sourceType: "module", 22 | }, 23 | plugins: ["@typescript-eslint", "react"], 24 | ignorePatterns: ["node_modules/", "dist/"], 25 | rules: {}, 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version: '16' 13 | registry-url: 'https://registry.npmjs.org' 14 | - run: npm install 15 | - run: npm test 16 | - run: npm publish 17 | env: 18 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: npm test 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-node@v2 9 | with: 10 | node-version: '16' 11 | registry-url: 'https://registry.npmjs.org' 12 | - run: npm install 13 | - run: npm test 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/intellij+all,vim,emacs,windows,macos,node 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,vim,emacs,windows,macos,node 4 | 5 | ### Emacs ### 6 | # -*- mode: gitignore; -*- 7 | *~ 8 | \#*\# 9 | /.emacs.desktop 10 | /.emacs.desktop.lock 11 | *.elc 12 | auto-save-list 13 | tramp 14 | .\#* 15 | 16 | # Org-mode 17 | .org-id-locations 18 | *_archive 19 | 20 | # flymake-mode 21 | *_flymake.* 22 | 23 | # eshell files 24 | /eshell/history 25 | /eshell/lastdir 26 | 27 | # elpa packages 28 | /elpa/ 29 | 30 | # reftex files 31 | *.rel 32 | 33 | # AUCTeX auto folder 34 | /auto/ 35 | 36 | # cask packages 37 | .cask/ 38 | dist/ 39 | 40 | # Flycheck 41 | flycheck_*.el 42 | 43 | # server auth directory 44 | /server/ 45 | 46 | # projectiles files 47 | .projectile 48 | 49 | # directory configuration 50 | .dir-locals.el 51 | 52 | # network security 53 | /network-security.data 54 | 55 | 56 | ### Intellij+all ### 57 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 58 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 59 | 60 | # User-specific stuff 61 | .idea/**/workspace.xml 62 | .idea/**/tasks.xml 63 | .idea/**/usage.statistics.xml 64 | .idea/**/dictionaries 65 | .idea/**/shelf 66 | 67 | # AWS User-specific 68 | .idea/**/aws.xml 69 | 70 | # Generated files 71 | .idea/**/contentModel.xml 72 | 73 | # Sensitive or high-churn files 74 | .idea/**/dataSources/ 75 | .idea/**/dataSources.ids 76 | .idea/**/dataSources.local.xml 77 | .idea/**/sqlDataSources.xml 78 | .idea/**/dynamic.xml 79 | .idea/**/uiDesigner.xml 80 | .idea/**/dbnavigator.xml 81 | 82 | # Gradle 83 | .idea/**/gradle.xml 84 | .idea/**/libraries 85 | 86 | # Gradle and Maven with auto-import 87 | # When using Gradle or Maven with auto-import, you should exclude module files, 88 | # since they will be recreated, and may cause churn. Uncomment if using 89 | # auto-import. 90 | # .idea/artifacts 91 | # .idea/compiler.xml 92 | # .idea/jarRepositories.xml 93 | # .idea/modules.xml 94 | # .idea/*.iml 95 | # .idea/modules 96 | # *.iml 97 | # *.ipr 98 | 99 | # CMake 100 | cmake-build-*/ 101 | 102 | # Mongo Explorer plugin 103 | .idea/**/mongoSettings.xml 104 | 105 | # File-based project format 106 | *.iws 107 | 108 | # IntelliJ 109 | out/ 110 | 111 | # VS Code 112 | .vscode 113 | 114 | # mpeltonen/sbt-idea plugin 115 | .idea_modules/ 116 | 117 | # JIRA plugin 118 | atlassian-ide-plugin.xml 119 | 120 | # Cursive Clojure plugin 121 | .idea/replstate.xml 122 | 123 | # Crashlytics plugin (for Android Studio and IntelliJ) 124 | com_crashlytics_export_strings.xml 125 | crashlytics.properties 126 | crashlytics-build.properties 127 | fabric.properties 128 | 129 | # Editor-based Rest Client 130 | .idea/httpRequests 131 | 132 | # Android studio 3.1+ serialized cache file 133 | .idea/caches/build_file_checksums.ser 134 | 135 | ### Intellij+all Patch ### 136 | # Ignores the whole .idea folder and all .iml files 137 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 138 | 139 | .idea/ 140 | 141 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 142 | 143 | *.iml 144 | modules.xml 145 | .idea/misc.xml 146 | *.ipr 147 | 148 | # Sonarlint plugin 149 | .idea/sonarlint 150 | 151 | ### macOS ### 152 | # General 153 | .DS_Store 154 | .AppleDouble 155 | .LSOverride 156 | 157 | # Icon must end with two \r 158 | Icon 159 | 160 | 161 | # Thumbnails 162 | ._* 163 | 164 | # Files that might appear in the root of a volume 165 | .DocumentRevisions-V100 166 | .fseventsd 167 | .Spotlight-V100 168 | .TemporaryItems 169 | .Trashes 170 | .VolumeIcon.icns 171 | .com.apple.timemachine.donotpresent 172 | 173 | # Directories potentially created on remote AFP share 174 | .AppleDB 175 | .AppleDesktop 176 | Network Trash Folder 177 | Temporary Items 178 | .apdisk 179 | 180 | ### Node ### 181 | # Logs 182 | logs 183 | *.log 184 | npm-debug.log* 185 | yarn-debug.log* 186 | yarn-error.log* 187 | lerna-debug.log* 188 | .pnpm-debug.log* 189 | 190 | # Diagnostic reports (https://nodejs.org/api/report.html) 191 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 192 | 193 | # Runtime data 194 | pids 195 | *.pid 196 | *.seed 197 | *.pid.lock 198 | 199 | # Directory for instrumented libs generated by jscoverage/JSCover 200 | lib-cov 201 | 202 | # Coverage directory used by tools like istanbul 203 | coverage 204 | *.lcov 205 | 206 | # nyc test coverage 207 | .nyc_output 208 | 209 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 210 | .grunt 211 | 212 | # Bower dependency directory (https://bower.io/) 213 | bower_components 214 | 215 | # node-waf configuration 216 | .lock-wscript 217 | 218 | # Compiled binary addons (https://nodejs.org/api/addons.html) 219 | build/Release 220 | 221 | # Dependency directories 222 | node_modules/ 223 | jspm_packages/ 224 | 225 | # Snowpack dependency directory (https://snowpack.dev/) 226 | web_modules/ 227 | 228 | # TypeScript cache 229 | *.tsbuildinfo 230 | 231 | # Optional npm cache directory 232 | .npm 233 | 234 | # Optional eslint cache 235 | .eslintcache 236 | 237 | # Microbundle cache 238 | .rpt2_cache/ 239 | .rts2_cache_cjs/ 240 | .rts2_cache_es/ 241 | .rts2_cache_umd/ 242 | 243 | # Optional REPL history 244 | .node_repl_history 245 | 246 | # Output of 'npm pack' 247 | *.tgz 248 | 249 | # Yarn Integrity file 250 | .yarn-integrity 251 | 252 | # dotenv environment variables file 253 | .env 254 | .env.test 255 | .env.production 256 | 257 | # parcel-bundler cache (https://parceljs.org/) 258 | .cache 259 | .parcel-cache 260 | 261 | # Next.js build output 262 | .next 263 | out 264 | 265 | # Nuxt.js build / generate output 266 | .nuxt 267 | dist 268 | 269 | # Gatsby files 270 | .cache/ 271 | # Comment in the public line in if your project uses Gatsby and not Next.js 272 | # https://nextjs.org/blog/next-9-1#public-directory-support 273 | # public 274 | 275 | # vuepress build output 276 | .vuepress/dist 277 | 278 | # Serverless directories 279 | .serverless/ 280 | 281 | # FuseBox cache 282 | .fusebox/ 283 | 284 | # DynamoDB Local files 285 | .dynamodb/ 286 | 287 | # TernJS port file 288 | .tern-port 289 | 290 | # Stores VSCode versions used for testing VSCode extensions 291 | .vscode-test 292 | 293 | # yarn v2 294 | .yarn/cache 295 | .yarn/unplugged 296 | .yarn/build-state.yml 297 | .yarn/install-state.gz 298 | .pnp.* 299 | 300 | ### Vim ### 301 | # Swap 302 | [._]*.s[a-v][a-z] 303 | !*.svg # comment out if you don't need vector files 304 | [._]*.sw[a-p] 305 | [._]s[a-rt-v][a-z] 306 | [._]ss[a-gi-z] 307 | [._]sw[a-p] 308 | 309 | # Session 310 | Session.vim 311 | Sessionx.vim 312 | 313 | # Temporary 314 | .netrwhist 315 | # Auto-generated tag files 316 | tags 317 | # Persistent undo 318 | [._]*.un~ 319 | 320 | ### Windows ### 321 | # Windows thumbnail cache files 322 | Thumbs.db 323 | Thumbs.db:encryptable 324 | ehthumbs.db 325 | ehthumbs_vista.db 326 | 327 | # Dump file 328 | *.stackdump 329 | 330 | # Folder config file 331 | [Dd]esktop.ini 332 | 333 | # Recycle Bin used on file shares 334 | $RECYCLE.BIN/ 335 | 336 | # Windows Installer files 337 | *.cab 338 | *.msi 339 | *.msix 340 | *.msm 341 | *.msp 342 | 343 | # Windows shortcuts 344 | *.lnk 345 | 346 | # Yalc 347 | .yalc 348 | yalc.lock 349 | 350 | # End of https://www.toptal.com/developers/gitignore/api/intellij+all,vim,emacs,windows,macos,node 351 | 352 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PropelAuth/react/e577dfc77e8f33555684f76540dabed3aa6b6ab1/.npmignore -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | trailingComma: "es5", 4 | tabWidth: 4, 5 | semi: false, 6 | singleQuote: false, 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 PropelAuth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 | 7 | # PropelAuth React Library 8 | 9 | A React library for managing authentication in the browser, backed by [PropelAuth](https://www.propelauth.com?ref=github). 10 | 11 | [PropelAuth](https://www.propelauth.com?ref=github) makes it easy to add authentication and authorization to your B2B/multi-tenant application. 12 | 13 | Your frontend gets a beautiful, safe, and customizable login screen. Your backend gets easy authorization with just a few lines of code. You get an easy-to-use dashboard to config and manage everything. 14 | 15 | ## Documentation 16 | 17 | - Full reference this library is [here](https://docs.propelauth.com/reference/frontend-apis/react) 18 | - Getting started guides for PropelAuth are [here](https://docs.propelauth.com/) 19 | 20 | ## Questions? 21 | 22 | Feel free to reach out at support@propelauth.com 23 | 24 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "useBuiltIns": "entry", 7 | "corejs": "3.11", 8 | "targets": { 9 | "node": "current" 10 | } 11 | } 12 | ], 13 | "@babel/preset-typescript", 14 | "@babel/react" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@propelauth/react", 3 | "description": "A React library for managing authentication, backed by PropelAuth", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/PropelAuth/react" 7 | }, 8 | "version": "2.0.29", 9 | "license": "MIT", 10 | "keywords": [ 11 | "auth", 12 | "react", 13 | "user" 14 | ], 15 | "dependencies": { 16 | "@propelauth/javascript": "^2.0.20", 17 | "hoist-non-react-statics": "^3.3.2", 18 | "utility-types": "^3.10.0" 19 | }, 20 | "devDependencies": { 21 | "@babel/cli": "^7.13.16", 22 | "@babel/core": "^7.14.0", 23 | "@babel/preset-env": "^7.14.0", 24 | "@babel/preset-react": "^7.13.13", 25 | "@babel/preset-typescript": "^7.13.0", 26 | "@rollup/plugin-babel": "^5.3.0", 27 | "@rollup/plugin-commonjs": "^18.00", 28 | "@rollup/plugin-node-resolve": "^11.2.1", 29 | "@testing-library/react": "^12.0.0", 30 | "@types/hoist-non-react-statics": "^3.3.1", 31 | "@types/jest": "^26.0.23", 32 | "@types/react": "^17.0.4", 33 | "@types/react-dom": "^17.0.3", 34 | "@typescript-eslint/eslint-plugin": "^6.12.0", 35 | "@typescript-eslint/parser": "^6.12.0", 36 | "babel-loader": "^8.2.2", 37 | "core-js": "^3.11.1", 38 | "eslint": "^8.54.0", 39 | "eslint-plugin-react": "^7.33.2", 40 | "jest": "^27.0.6", 41 | "prettier": "^2.4.1", 42 | "prettier-plugin-organize-imports": "^2.3.3", 43 | "react": "^17.0.2", 44 | "react-dom": "^17.0.2", 45 | "rollup": "^2.46.0", 46 | "rollup-plugin-peer-deps-external": "^2.2.4", 47 | "rollup-plugin-terser": "^7.0.2", 48 | "typescript": "^4.2.4", 49 | "uuid": "^8.3.2" 50 | }, 51 | "peerDependencies": { 52 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", 53 | "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 54 | }, 55 | "browserslist": [ 56 | "> 0.2%", 57 | "not dead" 58 | ], 59 | "scripts": { 60 | "type-check": "tsc --noEmit", 61 | "type-check:watch": "npm run type-check -- --watch", 62 | "build:types": "tsc --emitDeclarationOnly", 63 | "build:js": "rollup -c", 64 | "lint": "eslint --ext .ts,.tsx .", 65 | "build": "npm run test && npm run lint && npm run build:types && npm run build:js", 66 | "test": "npm run lint && jest --silent", 67 | "prepublishOnly": "npm run build" 68 | }, 69 | "main": "dist/index.cjs.js", 70 | "module": "dist/index.esm.js", 71 | "types": "dist/types/index.d.ts" 72 | } 73 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import peerDepsExternal from 'rollup-plugin-peer-deps-external'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import babel from '@rollup/plugin-babel'; 5 | import pkg from './package.json'; 6 | 7 | const extensions = [ 8 | '.js', '.jsx', '.ts', '.tsx', 9 | ]; 10 | 11 | export default { 12 | input: './src/index.tsx', 13 | 14 | plugins: [ 15 | peerDepsExternal(), 16 | 17 | // Allows node_modules resolution 18 | resolve({extensions}), 19 | 20 | // Allow bundling cjs modules. Rollup doesn't understand cjs 21 | commonjs(), 22 | 23 | // Compile TypeScript/JavaScript files 24 | babel({ 25 | extensions, 26 | babelHelpers: 'bundled', 27 | include: ['src/**/*'], 28 | exclude: ['src/**/*.test.*'] 29 | }), 30 | ], 31 | 32 | output: [{ 33 | file: pkg.main, 34 | format: 'cjs', 35 | sourcemap: true 36 | }, { 37 | file: pkg.module, 38 | format: 'esm', 39 | sourcemap: true 40 | }], 41 | }; 42 | -------------------------------------------------------------------------------- /src/AuthContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AccessTokenForActiveOrg, 3 | AuthenticationInfo, 4 | RedirectToAccountOptions, 5 | RedirectToCreateOrgOptions, 6 | RedirectToLoginOptions, 7 | RedirectToOrgPageOptions, 8 | RedirectToSetupSAMLPageOptions, 9 | RedirectToSignupOptions, 10 | } from "@propelauth/javascript" 11 | import React, { useCallback, useEffect, useReducer } from "react" 12 | import { loadOrgSelectionFromLocalStorage } from "./hooks/useActiveOrg" 13 | import { useClientRef, useClientRefCallback } from "./useClientRef" 14 | 15 | export interface Tokens { 16 | getAccessTokenForOrg: (orgId: string) => Promise 17 | getAccessToken: () => Promise 18 | } 19 | 20 | export interface InternalAuthState { 21 | loading: boolean 22 | authInfo: AuthenticationInfo | null 23 | 24 | logout: (redirectOnLogout: boolean) => Promise 25 | activeOrgFn: () => string | null 26 | 27 | redirectToLoginPage: (options?: RedirectToLoginOptions) => void 28 | redirectToSignupPage: (options?: RedirectToSignupOptions) => void 29 | redirectToAccountPage: (options?: RedirectToAccountOptions) => void 30 | redirectToOrgPage: (orgId?: string, options?: RedirectToOrgPageOptions) => void 31 | redirectToCreateOrgPage: (options?: RedirectToCreateOrgOptions) => void 32 | redirectToSetupSAMLPage: (orgId: string, options?: RedirectToSetupSAMLPageOptions) => void 33 | 34 | getSignupPageUrl(options?: RedirectToSignupOptions): string 35 | getLoginPageUrl(options?: RedirectToLoginOptions): string 36 | getAccountPageUrl(options?: RedirectToAccountOptions): string 37 | getOrgPageUrl(orgId?: string, options?: RedirectToOrgPageOptions): string 38 | getCreateOrgPageUrl(options?: RedirectToCreateOrgOptions): string 39 | getSetupSAMLPageUrl(orgId: string, options?: RedirectToSetupSAMLPageOptions): string 40 | 41 | authUrl: string 42 | 43 | tokens: Tokens 44 | refreshAuthInfo: () => Promise 45 | defaultDisplayWhileLoading?: React.ReactElement 46 | defaultDisplayIfLoggedOut?: React.ReactElement 47 | } 48 | 49 | export type AuthProviderProps = { 50 | authUrl: string 51 | defaultDisplayWhileLoading?: React.ReactElement 52 | defaultDisplayIfLoggedOut?: React.ReactElement 53 | /** 54 | * getActiveOrgFn is deprecated. Use `useActiveOrgV2` instead. 55 | */ 56 | getActiveOrgFn?: () => string | null 57 | children?: React.ReactNode 58 | minSecondsBeforeRefresh?: number 59 | } 60 | 61 | export interface RequiredAuthProviderProps 62 | extends Omit { 63 | displayWhileLoading?: React.ReactElement 64 | displayIfLoggedOut?: React.ReactElement 65 | } 66 | 67 | export const AuthContext = React.createContext(undefined) 68 | 69 | type AuthInfoState = { 70 | loading: boolean 71 | authInfo: AuthenticationInfo | null 72 | } 73 | 74 | const initialAuthInfoState: AuthInfoState = { 75 | loading: true, 76 | authInfo: null, 77 | } 78 | 79 | type AuthInfoStateAction = { 80 | authInfo: AuthenticationInfo | null 81 | } 82 | 83 | function authInfoStateReducer(_state: AuthInfoState, action: AuthInfoStateAction): AuthInfoState { 84 | if (!action.authInfo) { 85 | return { 86 | loading: false, 87 | authInfo: action.authInfo, 88 | } 89 | } else if (_state.loading) { 90 | return { 91 | loading: false, 92 | authInfo: action.authInfo, 93 | } 94 | } else if (_state?.authInfo?.accessToken !== action.authInfo?.accessToken) { 95 | return { 96 | loading: false, 97 | authInfo: action.authInfo, 98 | } 99 | } else { 100 | return _state 101 | } 102 | } 103 | 104 | export const AuthProvider = (props: AuthProviderProps) => { 105 | const { 106 | authUrl, 107 | minSecondsBeforeRefresh, 108 | getActiveOrgFn: deprecatedGetActiveOrgFn, 109 | children, 110 | defaultDisplayWhileLoading, 111 | defaultDisplayIfLoggedOut, 112 | } = props 113 | const [authInfoState, dispatch] = useReducer(authInfoStateReducer, initialAuthInfoState) 114 | const { clientRef, accessTokenChangeCounter } = useClientRef({ 115 | authUrl, 116 | minSecondsBeforeRefresh, 117 | }) 118 | 119 | // Refresh the token when the user has logged in or out 120 | useEffect(() => { 121 | let didCancel = false 122 | 123 | async function refreshToken() { 124 | const client = clientRef.current?.client 125 | if (!client) { 126 | return 127 | } 128 | 129 | try { 130 | const authInfo = await client.getAuthenticationInfoOrNull() 131 | if (!didCancel) { 132 | dispatch({ authInfo }) 133 | } 134 | } catch (_) { 135 | // Important errors are logged in the client library 136 | } 137 | } 138 | 139 | refreshToken() 140 | return () => { 141 | didCancel = true 142 | } 143 | }, [accessTokenChangeCounter]) 144 | 145 | // Deprecation warning 146 | useEffect(() => { 147 | if (deprecatedGetActiveOrgFn) { 148 | console.warn("The `getActiveOrgFn` prop is deprecated.") 149 | } 150 | }, []) 151 | 152 | const logout = useClientRefCallback(clientRef, (client) => client.logout) 153 | const redirectToLoginPage = useClientRefCallback(clientRef, (client) => client.redirectToLoginPage) 154 | const redirectToSignupPage = useClientRefCallback(clientRef, (client) => client.redirectToSignupPage) 155 | const redirectToAccountPage = useClientRefCallback(clientRef, (client) => client.redirectToAccountPage) 156 | const redirectToOrgPage = useClientRefCallback(clientRef, (client) => client.redirectToOrgPage) 157 | const redirectToCreateOrgPage = useClientRefCallback(clientRef, (client) => client.redirectToCreateOrgPage) 158 | const redirectToSetupSAMLPage = useClientRefCallback(clientRef, (client) => client.redirectToSetupSAMLPage) 159 | 160 | const getLoginPageUrl = useClientRefCallback(clientRef, (client) => client.getLoginPageUrl) 161 | const getSignupPageUrl = useClientRefCallback(clientRef, (client) => client.getSignupPageUrl) 162 | const getAccountPageUrl = useClientRefCallback(clientRef, (client) => client.getAccountPageUrl) 163 | const getOrgPageUrl = useClientRefCallback(clientRef, (client) => client.getOrgPageUrl) 164 | const getCreateOrgPageUrl = useClientRefCallback(clientRef, (client) => client.getCreateOrgPageUrl) 165 | const getSetupSAMLPageUrl = useClientRefCallback(clientRef, (client) => client.getSetupSAMLPageUrl) 166 | 167 | const getAccessTokenForOrg = useClientRefCallback(clientRef, (client) => client.getAccessTokenForOrg) 168 | const getAccessToken = useClientRefCallback(clientRef, (client) => { 169 | return async () => { 170 | const authInfo = await client.getAuthenticationInfoOrNull() 171 | return authInfo?.accessToken 172 | } 173 | }) 174 | 175 | const refreshAuthInfo = useCallback(async () => { 176 | if (clientRef.current === null) { 177 | return 178 | } 179 | 180 | const client = clientRef.current.client 181 | const authInfo = await client.getAuthenticationInfoOrNull(true) 182 | dispatch({ authInfo }) 183 | }, [dispatch]) 184 | 185 | // TODO: Remove this, as both `getActiveOrgFn` and `loadOrgSelectionFromLocalStorage` are deprecated. 186 | const deprecatedActiveOrgFn = deprecatedGetActiveOrgFn || loadOrgSelectionFromLocalStorage 187 | 188 | const value: InternalAuthState = { 189 | loading: authInfoState.loading, 190 | authInfo: authInfoState.authInfo, 191 | logout, 192 | defaultDisplayWhileLoading, 193 | defaultDisplayIfLoggedOut, 194 | redirectToLoginPage, 195 | redirectToSignupPage, 196 | redirectToAccountPage, 197 | redirectToOrgPage, 198 | redirectToCreateOrgPage, 199 | redirectToSetupSAMLPage, 200 | getLoginPageUrl, 201 | activeOrgFn: deprecatedActiveOrgFn, 202 | getSignupPageUrl, 203 | getAccountPageUrl, 204 | getOrgPageUrl, 205 | getCreateOrgPageUrl, 206 | getSetupSAMLPageUrl, 207 | authUrl, 208 | refreshAuthInfo, 209 | tokens: { 210 | getAccessTokenForOrg, 211 | getAccessToken, 212 | }, 213 | } 214 | return {children} 215 | } 216 | -------------------------------------------------------------------------------- /src/AuthContextForTesting.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AccessHelper, 3 | AccessHelperWithOrg, 4 | AccessTokenForActiveOrg, 5 | AuthenticationInfo, 6 | OrgHelper, 7 | OrgIdToOrgMemberInfo, 8 | OrgIdToOrgMemberInfoClass, 9 | OrgMemberInfo, 10 | OrgMemberInfoClass, 11 | User, 12 | UserClass, 13 | } from "@propelauth/javascript" 14 | import React, { useCallback } from "react" 15 | import { AuthContext, InternalAuthState } from "./AuthContext" 16 | 17 | // User information that we will hard code within the AuthProvider 18 | export type UserInformationForTesting = { 19 | user: User 20 | orgMemberInfos: OrgMemberInfo[] 21 | accessToken?: string 22 | getAccessTokenForOrg?: (orgId: string) => Promise 23 | } 24 | 25 | export type AuthProviderForTestingProps = { 26 | loading?: boolean 27 | userInformation?: UserInformationForTesting 28 | activeOrgFn?: () => string | null 29 | authUrl?: string 30 | children?: React.ReactNode 31 | } 32 | 33 | /** 34 | * A version of the AuthProvider specifically used for testing. It won't make any external requests, but will 35 | * instead set up the AuthProvider to act as if the information provided was returned from the API. 36 | */ 37 | export const AuthProviderForTesting = ({ 38 | loading, 39 | userInformation, 40 | activeOrgFn, 41 | authUrl, 42 | children, 43 | }: AuthProviderForTestingProps) => { 44 | const authInfo = getAuthInfoForTesting(userInformation) 45 | const activeOrgFnWithDefault = activeOrgFn ? activeOrgFn : () => null 46 | const getAccessTokenForOrg = useCallback( 47 | (orgId: string) => { 48 | if (userInformation?.getAccessTokenForOrg) { 49 | return userInformation.getAccessTokenForOrg(orgId) 50 | } 51 | return Promise.resolve({ 52 | error: undefined, 53 | accessToken: "ACCESS_TOKEN", 54 | }) 55 | }, 56 | [userInformation?.getAccessTokenForOrg] 57 | ) 58 | 59 | const contextValue: InternalAuthState = { 60 | loading: !!loading, 61 | authInfo, 62 | logout: () => Promise.resolve(), 63 | redirectToLoginPage: () => {}, 64 | redirectToSignupPage: () => {}, 65 | redirectToAccountPage: () => {}, 66 | redirectToOrgPage: () => {}, 67 | redirectToCreateOrgPage: () => {}, 68 | redirectToSetupSAMLPage: () => {}, 69 | getLoginPageUrl: () => "", 70 | getSignupPageUrl: () => "", 71 | getAccountPageUrl: () => "", 72 | getOrgPageUrl: () => "", 73 | getCreateOrgPageUrl: () => "", 74 | getSetupSAMLPageUrl: () => "", 75 | authUrl: authUrl ?? "https://auth.example.com", 76 | activeOrgFn: activeOrgFnWithDefault, 77 | refreshAuthInfo: () => Promise.resolve(), 78 | tokens: { 79 | getAccessTokenForOrg: getAccessTokenForOrg, 80 | getAccessToken: () => Promise.resolve(userInformation?.accessToken ?? "ACCESS_TOKEN"), 81 | }, 82 | } 83 | 84 | return {children} 85 | } 86 | 87 | function getAuthInfoForTesting(userInformation?: UserInformationForTesting): AuthenticationInfo | null { 88 | if (!userInformation) { 89 | return null 90 | } 91 | 92 | const orgIdToOrgMemberInfo: { [orgId: string]: OrgMemberInfo } = {} 93 | for (const orgMemberInfo of userInformation.orgMemberInfos) { 94 | orgIdToOrgMemberInfo[orgMemberInfo.orgId] = orgMemberInfo 95 | } 96 | 97 | const accessTokenWithDefault = 98 | userInformation.accessToken === undefined ? "PLACEHOLDER_ACCESS_TOKEN" : userInformation.accessToken 99 | 100 | return { 101 | accessToken: accessTokenWithDefault, 102 | expiresAtSeconds: 1701596820, 103 | orgHelper: getOrgHelper(orgIdToOrgMemberInfo), 104 | accessHelper: getAccessHelper(orgIdToOrgMemberInfo), 105 | orgIdToOrgMemberInfo: orgIdToOrgMemberInfo, 106 | user: userInformation.user, 107 | userClass: new UserClass(userInformation.user, toOrgIdToUserOrgInfo(orgIdToOrgMemberInfo)), 108 | } 109 | } 110 | 111 | function toOrgIdToUserOrgInfo(orgIdToOrgMemberInfo: OrgIdToOrgMemberInfo): OrgIdToOrgMemberInfoClass { 112 | const orgIdToUserOrgInfo: OrgIdToOrgMemberInfoClass = {} 113 | for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) { 114 | orgIdToUserOrgInfo[orgMemberInfo.orgId] = new OrgMemberInfoClass( 115 | orgMemberInfo.orgId, 116 | orgMemberInfo.orgName, 117 | {}, 118 | orgMemberInfo.urlSafeOrgName, 119 | orgMemberInfo.userAssignedRole, 120 | orgMemberInfo.userInheritedRolesPlusCurrentRole, 121 | orgMemberInfo.userPermissions 122 | ) 123 | } 124 | return orgIdToUserOrgInfo 125 | } 126 | 127 | // These helpers come from @propelauth/javascript, down the road we may want to export them from that library 128 | // instead of copying 129 | function getOrgHelper(orgIdToOrgMemberInfo: OrgIdToOrgMemberInfo): OrgHelper { 130 | return { 131 | getOrg(orgId: string): OrgMemberInfo | undefined { 132 | if (Object.prototype.hasOwnProperty.call(orgIdToOrgMemberInfo, orgId)) { 133 | return orgIdToOrgMemberInfo[orgId] 134 | } else { 135 | return undefined 136 | } 137 | }, 138 | getOrgIds(): string[] { 139 | return Object.keys(orgIdToOrgMemberInfo) 140 | }, 141 | getOrgs(): OrgMemberInfo[] { 142 | return Object.values(orgIdToOrgMemberInfo) 143 | }, 144 | getOrgByName(orgName: string): OrgMemberInfo | undefined { 145 | for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) { 146 | if (orgMemberInfo.orgName === orgName || orgMemberInfo.urlSafeOrgName === orgName) { 147 | return orgMemberInfo 148 | } 149 | } 150 | return undefined 151 | }, 152 | } 153 | } 154 | 155 | function getAccessHelper(orgIdToOrgMemberInfo: OrgIdToOrgMemberInfo): AccessHelper { 156 | function isRole(orgId: string, role: string): boolean { 157 | const orgMemberInfo = orgIdToOrgMemberInfo[orgId] 158 | if (orgMemberInfo === undefined) { 159 | return false 160 | } 161 | return orgMemberInfo.userAssignedRole === role 162 | } 163 | 164 | function isAtLeastRole(orgId: string, role: string): boolean { 165 | const orgMemberInfo = orgIdToOrgMemberInfo[orgId] 166 | if (orgMemberInfo === undefined) { 167 | return false 168 | } 169 | return orgMemberInfo.userInheritedRolesPlusCurrentRole.includes(role) 170 | } 171 | 172 | function hasPermission(orgId: string, permission: string): boolean { 173 | const orgMemberInfo = orgIdToOrgMemberInfo[orgId] 174 | if (orgMemberInfo === undefined) { 175 | return false 176 | } 177 | return orgMemberInfo.userPermissions.includes(permission) 178 | } 179 | 180 | function hasAllPermissions(orgId: string, permissions: string[]): boolean { 181 | const orgMemberInfo = orgIdToOrgMemberInfo[orgId] 182 | if (orgMemberInfo === undefined) { 183 | return false 184 | } 185 | return permissions.every((permission) => orgMemberInfo.userPermissions.includes(permission)) 186 | } 187 | 188 | function getAccessHelperWithOrgId(orgId: string): AccessHelperWithOrg { 189 | return { 190 | isRole(role: string): boolean { 191 | return isRole(orgId, role) 192 | }, 193 | isAtLeastRole(role: string): boolean { 194 | return isAtLeastRole(orgId, role) 195 | }, 196 | hasPermission(permission: string): boolean { 197 | return hasPermission(orgId, permission) 198 | }, 199 | hasAllPermissions(permissions: string[]): boolean { 200 | return hasAllPermissions(orgId, permissions) 201 | }, 202 | } 203 | } 204 | 205 | return { 206 | isRole, 207 | isAtLeastRole, 208 | hasPermission, 209 | hasAllPermissions, 210 | getAccessHelperWithOrgId, 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/RequiredAuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { AuthProvider, RequiredAuthProviderProps } from "./AuthContext" 3 | import { WithLoggedInAuthInfoProps } from "./withAuthInfo" 4 | import { withRequiredAuthInfo } from "./withRequiredAuthInfo" 5 | 6 | const RequiredAuthWrappedComponent = withRequiredAuthInfo( 7 | ({ children }: { children: React.ReactNode } & WithLoggedInAuthInfoProps) => <>{children} 8 | ) 9 | 10 | export const RequiredAuthProvider = (props: RequiredAuthProviderProps) => { 11 | const { children, displayIfLoggedOut, displayWhileLoading, ...sharedProps } = props 12 | 13 | return ( 14 | 19 | {children} 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/hooks/additionalHooks.ts: -------------------------------------------------------------------------------- 1 | import { AccessHelper, OrgHelper } from "@propelauth/javascript" 2 | import { useAuthInfo } from "./useAuthInfo" 3 | 4 | export type UseOrgHelperLoading = { 5 | loading: true 6 | orgHelper: null 7 | } 8 | 9 | export type UseOrgHelperLoaded = { 10 | loading: false 11 | orgHelper: OrgHelper | null 12 | } 13 | 14 | export type UseOrgHelper = UseOrgHelperLoading | UseOrgHelperLoaded 15 | 16 | export function useOrgHelper(): UseOrgHelper { 17 | const authInfo = useAuthInfo() 18 | 19 | if (authInfo.loading) { 20 | return { 21 | loading: true, 22 | orgHelper: null, 23 | } 24 | } else if (authInfo.isLoggedIn) { 25 | return { 26 | loading: false, 27 | orgHelper: authInfo.orgHelper, 28 | } 29 | } else { 30 | return { 31 | loading: false, 32 | orgHelper: null, 33 | } 34 | } 35 | } 36 | 37 | export type UseAccessHelperLoading = { 38 | loading: true 39 | accessHelper: null 40 | } 41 | 42 | export type UseAccessHelperLoaded = { 43 | loading: false 44 | accessHelper: AccessHelper | null 45 | } 46 | 47 | export type UseAccessHelper = UseAccessHelperLoading | UseAccessHelperLoaded 48 | 49 | export function useAccessHelper(): UseAccessHelper { 50 | const authInfo = useAuthInfo() 51 | 52 | if (authInfo.loading) { 53 | return { 54 | loading: true, 55 | accessHelper: null, 56 | } 57 | } else if (authInfo.isLoggedIn) { 58 | return { 59 | loading: false, 60 | accessHelper: authInfo.accessHelper, 61 | } 62 | } else { 63 | return { 64 | loading: false, 65 | accessHelper: null, 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/hooks/useActiveOrg.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { AuthContext } from "../AuthContext" 3 | 4 | const DEPRECATED_ORG_SELECTION_LOCAL_STORAGE_KEY = "__last_selected_org" 5 | 6 | /** 7 | * @deprecated This hook is deprecated and no longer supported. 8 | */ 9 | export function useActiveOrg() { 10 | const context = useContext(AuthContext) 11 | if (context === undefined) { 12 | throw new Error("useActiveOrg must be used within an AuthProvider or RequiredAuthProvider") 13 | } 14 | 15 | if (context.loading || !context.authInfo || !context.authInfo.orgHelper) { 16 | return null 17 | } 18 | 19 | const proposedActiveOrgIdOrName = context.activeOrgFn() 20 | if (!proposedActiveOrgIdOrName) { 21 | return null 22 | } 23 | 24 | const orgHelper = context.authInfo.orgHelper 25 | return orgHelper.getOrg(proposedActiveOrgIdOrName) || orgHelper.getOrgByName(proposedActiveOrgIdOrName) 26 | } 27 | 28 | export function saveOrgSelectionToLocalStorage(orgIdOrName: string) { 29 | if (localStorage) { 30 | localStorage.setItem(DEPRECATED_ORG_SELECTION_LOCAL_STORAGE_KEY, orgIdOrName) 31 | } 32 | } 33 | 34 | export function loadOrgSelectionFromLocalStorage(): string | null { 35 | if (localStorage) { 36 | return localStorage.getItem(DEPRECATED_ORG_SELECTION_LOCAL_STORAGE_KEY) 37 | } 38 | return null 39 | } 40 | -------------------------------------------------------------------------------- /src/hooks/useAuthInfo.ts: -------------------------------------------------------------------------------- 1 | import { AccessHelper, OrgHelper, User, UserClass } from "@propelauth/javascript" 2 | import { useContext } from "react" 3 | import { AuthContext, Tokens } from "../AuthContext" 4 | 5 | export type UseAuthInfoLoading = { 6 | loading: true 7 | isLoggedIn: undefined 8 | accessToken: undefined 9 | user: undefined 10 | userClass: undefined 11 | orgHelper: undefined 12 | accessHelper: undefined 13 | isImpersonating: undefined 14 | impersonatorUserId: undefined 15 | refreshAuthInfo: () => Promise 16 | tokens: Tokens 17 | accessTokenExpiresAtSeconds: undefined 18 | } 19 | 20 | export type UseAuthInfoLoggedInProps = { 21 | loading: false 22 | isLoggedIn: true 23 | accessToken: string 24 | user: User 25 | userClass: UserClass 26 | orgHelper: OrgHelper 27 | accessHelper: AccessHelper 28 | isImpersonating: boolean 29 | impersonatorUserId?: string 30 | refreshAuthInfo: () => Promise 31 | tokens: Tokens 32 | accessTokenExpiresAtSeconds: number 33 | } 34 | 35 | export type UseAuthInfoNotLoggedInProps = { 36 | loading: false 37 | isLoggedIn: false 38 | accessToken: null 39 | user: null 40 | userClass: null 41 | orgHelper: null 42 | accessHelper: null 43 | isImpersonating: false 44 | impersonatorUserId: undefined 45 | refreshAuthInfo: () => Promise 46 | tokens: Tokens 47 | accessTokenExpiresAtSeconds: undefined 48 | } 49 | 50 | export type UseAuthInfoProps = UseAuthInfoLoading | UseAuthInfoLoggedInProps | UseAuthInfoNotLoggedInProps 51 | 52 | export function useAuthInfo(): UseAuthInfoProps { 53 | const context = useContext(AuthContext) 54 | if (context === undefined) { 55 | throw new Error("useAuthInfo must be used within an AuthProvider or RequiredAuthProvider") 56 | } 57 | 58 | const { loading, authInfo, refreshAuthInfo, tokens } = context 59 | if (loading) { 60 | return { 61 | loading: true, 62 | isLoggedIn: undefined, 63 | accessToken: undefined, 64 | orgHelper: undefined, 65 | accessHelper: undefined, 66 | user: undefined, 67 | userClass: undefined, 68 | isImpersonating: undefined, 69 | impersonatorUserId: undefined, 70 | refreshAuthInfo, 71 | tokens, 72 | accessTokenExpiresAtSeconds: undefined, 73 | } 74 | } else if (authInfo && authInfo.accessToken) { 75 | return { 76 | loading: false, 77 | isLoggedIn: true, 78 | accessToken: authInfo.accessToken, 79 | orgHelper: authInfo.orgHelper, 80 | accessHelper: authInfo.accessHelper, 81 | user: authInfo.user, 82 | userClass: authInfo.userClass, 83 | isImpersonating: !!authInfo.impersonatorUserId, 84 | impersonatorUserId: authInfo.impersonatorUserId, 85 | refreshAuthInfo, 86 | tokens, 87 | accessTokenExpiresAtSeconds: authInfo.expiresAtSeconds, 88 | } 89 | } 90 | return { 91 | loading: false, 92 | isLoggedIn: false, 93 | accessToken: null, 94 | user: null, 95 | userClass: null, 96 | orgHelper: null, 97 | accessHelper: null, 98 | isImpersonating: false, 99 | impersonatorUserId: undefined, 100 | refreshAuthInfo, 101 | tokens, 102 | accessTokenExpiresAtSeconds: undefined, 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/hooks/useAuthUrl.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { AuthContext } from "../AuthContext" 3 | 4 | export function useAuthUrl() { 5 | const context = useContext(AuthContext) 6 | if (context === undefined) { 7 | throw new Error("useAuthUrl must be used within an AuthProvider or RequiredAuthProvider") 8 | } 9 | const { authUrl } = context 10 | return authUrl 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useHostedPageUrls.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { AuthContext } from "../AuthContext" 3 | 4 | export function useHostedPageUrls() { 5 | const context = useContext(AuthContext) 6 | if (context === undefined) { 7 | throw new Error("useHostedPageUrls must be used within an AuthProvider or RequiredAuthProvider") 8 | } 9 | const { 10 | getLoginPageUrl, 11 | getSignupPageUrl, 12 | getAccountPageUrl, 13 | getOrgPageUrl, 14 | getCreateOrgPageUrl, 15 | getSetupSAMLPageUrl, 16 | } = context 17 | return { 18 | getLoginPageUrl, 19 | getSignupPageUrl, 20 | getAccountPageUrl, 21 | getOrgPageUrl, 22 | getCreateOrgPageUrl, 23 | getSetupSAMLPageUrl, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useLogoutFunction.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react" 2 | import { AuthContext } from "../AuthContext" 3 | 4 | export function useLogoutFunction() { 5 | const context = useContext(AuthContext) 6 | if (context === undefined) { 7 | throw new Error("useLogoutFunction must be used within an AuthProvider or RequiredAuthProvider") 8 | } 9 | const { logout } = context 10 | return logout 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/useRedirectFunctions.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect } from "react" 2 | import { AuthContext } from "../AuthContext" 3 | 4 | export function useRedirectFunctions() { 5 | const context = useContext(AuthContext) 6 | if (context === undefined) { 7 | throw new Error("useRedirectFunctions must be used within an AuthProvider or RequiredAuthProvider") 8 | } 9 | const { 10 | redirectToAccountPage, 11 | redirectToSignupPage, 12 | redirectToLoginPage, 13 | redirectToOrgPage, 14 | redirectToCreateOrgPage, 15 | redirectToSetupSAMLPage, 16 | } = context 17 | return { 18 | redirectToSignupPage, 19 | redirectToLoginPage, 20 | redirectToAccountPage, 21 | redirectToOrgPage, 22 | redirectToCreateOrgPage, 23 | redirectToSetupSAMLPage, 24 | } 25 | } 26 | 27 | export interface RedirectProps { 28 | children?: React.ReactNode 29 | } 30 | 31 | export interface RedirectToSignupProps extends RedirectProps { 32 | postSignupRedirectUrl?: string 33 | userSignupQueryParameters?: Record 34 | } 35 | 36 | export function RedirectToSignup({ 37 | children, 38 | postSignupRedirectUrl, 39 | userSignupQueryParameters, 40 | }: RedirectToSignupProps) { 41 | const { redirectToSignupPage } = useRedirectFunctions() 42 | 43 | useEffect(() => redirectToSignupPage({ postSignupRedirectUrl, userSignupQueryParameters }), []) 44 | 45 | return <>{children} 46 | } 47 | 48 | export interface RedirectToLoginProps extends RedirectProps { 49 | postLoginRedirectUrl?: string 50 | } 51 | 52 | export function RedirectToLogin({ children, postLoginRedirectUrl }: RedirectToLoginProps) { 53 | const { redirectToLoginPage } = useRedirectFunctions() 54 | useEffect(() => { 55 | if (postLoginRedirectUrl) { 56 | redirectToLoginPage({ postLoginRedirectUrl }) 57 | } else { 58 | redirectToLoginPage() 59 | } 60 | }, []) 61 | return <>{children} 62 | } 63 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { createClient } from "@propelauth/javascript" 5 | import { render, screen, waitFor } from "@testing-library/react" 6 | import React from "react" 7 | import { v4 as uuidv4 } from "uuid" 8 | import { AuthProvider } from "./AuthContext" 9 | import { AuthProviderForTesting } from "./AuthContextForTesting" 10 | import { useAuthInfo } from "./hooks/useAuthInfo" 11 | import { useLogoutFunction } from "./hooks/useLogoutFunction" 12 | import { useRedirectFunctions } from "./hooks/useRedirectFunctions" 13 | import { RequiredAuthProvider } from "./RequiredAuthProvider" 14 | import { withAuthInfo } from "./withAuthInfo" 15 | import { withRequiredAuthInfo } from "./withRequiredAuthInfo" 16 | 17 | /* eslint-disable */ 18 | // Fake timer setup 19 | beforeAll(() => { 20 | jest.useFakeTimers() 21 | }) 22 | 23 | let mockClient 24 | const INITIAL_TIME_MILLIS = 1619743452595 25 | const INITIAL_TIME_SECONDS = INITIAL_TIME_MILLIS / 1000 26 | beforeEach(() => { 27 | jest.setSystemTime(INITIAL_TIME_MILLIS) 28 | mockClient = createMockClient() 29 | }) 30 | 31 | afterAll(() => { 32 | jest.useRealTimers() 33 | }) 34 | 35 | // Mocking utilities for createClient 36 | jest.mock("@propelauth/javascript", () => ({ 37 | ...jest.requireActual("@propelauth/javascript"), 38 | createClient: jest.fn(), 39 | })) 40 | createClient.mockImplementation(() => mockClient) 41 | 42 | // Tests 43 | it("withAuthInfo fails to render if not in an AuthProvider", async () => { 44 | const Component = (props) =>
Finished
45 | const WrappedComponent = withAuthInfo(Component) 46 | expect(() => { 47 | render() 48 | }).toThrowError() 49 | }) 50 | 51 | it("successfully renders withAuthInfo if in an AuthProvider", async () => { 52 | const Component = (props) =>
Finished
53 | const WrappedComponent = withAuthInfo(Component) 54 | render( 55 | 56 | 57 | 58 | ) 59 | 60 | await waitFor(() => screen.getByText("Finished")) 61 | expectCreateClientWasCalledCorrectly() 62 | }) 63 | 64 | it("withAuthInfo passes values from client as props", async () => { 65 | const accessToken = randomString() 66 | const orgA = createOrg() 67 | const orgB = createOrg() 68 | const user = createUser() 69 | const orgIdToOrgMemberInfo = { [orgA.orgId]: orgA, [orgB.orgId]: orgB } 70 | const authenticationInfo = { 71 | accessToken, 72 | expiresAtSeconds: INITIAL_TIME_SECONDS + 30 * 60, 73 | orgIdToOrgMemberInfo, 74 | orgHelper: wrapOrgIdToOrgMemberInfo(orgIdToOrgMemberInfo), 75 | user, 76 | } 77 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(authenticationInfo) 78 | 79 | const Component = (props) => { 80 | expect(props.accessToken).toBe(accessToken) 81 | expect(props.user).toStrictEqual(user) 82 | expect(props.isLoggedIn).toBe(true) 83 | expect(props.orgHelper.getOrgs().sort()).toEqual([orgA, orgB].sort()) 84 | return
Finished
85 | } 86 | 87 | const WrappedComponent = withAuthInfo(Component) 88 | render( 89 | 90 | 91 | 92 | ) 93 | 94 | await waitFor(() => screen.getByText("Finished")) 95 | expectCreateClientWasCalledCorrectly() 96 | }) 97 | 98 | it("useAuthInfo passes values correctly", async () => { 99 | const accessToken = randomString() 100 | const orgA = createOrg() 101 | const orgB = createOrg() 102 | const user = createUser() 103 | const orgIdToOrgMemberInfo = { [orgA.orgId]: orgA, [orgB.orgId]: orgB } 104 | const authenticationInfo = { 105 | accessToken, 106 | expiresAtSeconds: INITIAL_TIME_SECONDS + 30 * 60, 107 | orgIdToOrgMemberInfo, 108 | orgHelper: wrapOrgIdToOrgMemberInfo(orgIdToOrgMemberInfo), 109 | user, 110 | } 111 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(authenticationInfo) 112 | 113 | const Component = () => { 114 | const authInfo = useAuthInfo() 115 | if (authInfo.loading) { 116 | return
Loading...
117 | } else { 118 | expect(authInfo.accessToken).toBe(accessToken) 119 | expect(authInfo.user).toStrictEqual(user) 120 | expect(authInfo.isLoggedIn).toBe(true) 121 | expect(authInfo.orgHelper.getOrgs().sort()).toEqual([orgA, orgB].sort()) 122 | return
Finished
123 | } 124 | } 125 | 126 | render( 127 | 128 | 129 | 130 | ) 131 | 132 | await waitFor(() => screen.getByText("Finished")) 133 | expectCreateClientWasCalledCorrectly() 134 | }) 135 | 136 | it("withAuthInfo passes values from client as props, with RequiredAuthProvider", async () => { 137 | const authenticationInfo = createAuthenticationInfo() 138 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(authenticationInfo) 139 | 140 | const Component = (props) => { 141 | expect(props.accessToken).toBe(authenticationInfo.accessToken) 142 | expect(props.user).toStrictEqual(authenticationInfo.user) 143 | expect(props.isLoggedIn).toBe(true) 144 | expect(props.orgHelper.getOrgs().sort()).toEqual(Object.values(authenticationInfo.orgIdToOrgMemberInfo).sort()) 145 | return
Finished
146 | } 147 | 148 | const WrappedComponent = withAuthInfo(Component) 149 | render( 150 | 151 | 152 | 153 | ) 154 | 155 | await waitFor(() => screen.getByText("Finished")) 156 | expectCreateClientWasCalledCorrectly() 157 | }) 158 | 159 | it("withRequiredAuthInfo passes values from client as props, with RequiredAuthProvider", async () => { 160 | const authenticationInfo = createAuthenticationInfo() 161 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(authenticationInfo) 162 | 163 | const Component = (props) => { 164 | expect(props.accessToken).toBe(authenticationInfo.accessToken) 165 | expect(props.user).toStrictEqual(authenticationInfo.user) 166 | expect(props.isLoggedIn).toBe(true) 167 | expect(props.orgHelper.getOrgs().sort()).toEqual(Object.values(authenticationInfo.orgIdToOrgMemberInfo).sort()) 168 | return
Finished
169 | } 170 | 171 | const WrappedComponent = withRequiredAuthInfo(Component) 172 | render( 173 | 174 | 175 | 176 | ) 177 | 178 | await waitFor(() => screen.getByText("Finished")) 179 | expectCreateClientWasCalledCorrectly() 180 | }) 181 | 182 | it("RequiredAuthProvider displays logged out value if logged out", async () => { 183 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(null) 184 | 185 | const ErrorComponent = () => { 186 | return
Error
187 | } 188 | const SuccessComponent = () => { 189 | return
Finished
190 | } 191 | 192 | const WrappedComponent = withAuthInfo(ErrorComponent) 193 | render( 194 | }> 195 | 196 | 197 | ) 198 | 199 | await waitFor(() => screen.getByText("Finished")) 200 | expectCreateClientWasCalledCorrectly() 201 | }) 202 | 203 | it("withRequiredAuthInfo displays logged out value if logged out from args", async () => { 204 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(null) 205 | 206 | const ErrorComponent = () => { 207 | return
Error
208 | } 209 | const SuccessComponent = () => { 210 | return
Finished
211 | } 212 | 213 | const WrappedComponent = withRequiredAuthInfo(ErrorComponent, { 214 | displayIfLoggedOut: , 215 | }) 216 | render( 217 | 218 | 219 | 220 | ) 221 | 222 | await waitFor(() => screen.getByText("Finished")) 223 | expectCreateClientWasCalledCorrectly() 224 | }) 225 | 226 | it("withRequiredAuthInfo displays logged out value if logged out from context", async () => { 227 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(null) 228 | 229 | const ErrorComponent = () => { 230 | return
Error
231 | } 232 | const SuccessComponent = () => { 233 | return
Finished
234 | } 235 | 236 | const WrappedComponent = withRequiredAuthInfo(ErrorComponent) 237 | render( 238 | }> 239 | 240 | 241 | ) 242 | 243 | await waitFor(() => screen.getByText("Finished")) 244 | expectCreateClientWasCalledCorrectly() 245 | }) 246 | 247 | it("withRequiredAuthInfo displays logged out value from args if logged out from both args and context", async () => { 248 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(null) 249 | 250 | const ErrorComponent = () => { 251 | return
Error
252 | } 253 | const SuccessArgComponent = () => { 254 | return
Finished from Args
255 | } 256 | 257 | const SuccessContextComponent = () => { 258 | return
Finished from Context
259 | } 260 | 261 | const WrappedComponent = withRequiredAuthInfo(ErrorComponent, { 262 | displayIfLoggedOut: , 263 | }) 264 | render( 265 | }> 266 | 267 | 268 | ) 269 | 270 | await waitFor(() => screen.getByText("Finished from Args")) 271 | expectCreateClientWasCalledCorrectly() 272 | }) 273 | 274 | it("withAuthInfo passes logged out values from client as props", async () => { 275 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(null) 276 | 277 | const Component = (props) => { 278 | expect(props.accessToken).toBe(null) 279 | expect(props.user).toBe(null) 280 | expect(props.isLoggedIn).toBe(false) 281 | expect(props.orgHelper).toBe(null) 282 | return
Finished
283 | } 284 | 285 | const WrappedComponent = withAuthInfo(Component) 286 | render( 287 | 288 | 289 | 290 | ) 291 | 292 | await waitFor(() => screen.getByText("Finished")) 293 | expectCreateClientWasCalledCorrectly() 294 | }) 295 | 296 | it("useAuthInfo passes logged out values from client as props", async () => { 297 | mockClient.getAuthenticationInfoOrNull.mockReturnValue(null) 298 | 299 | const Component = () => { 300 | const authInfo = useAuthInfo() 301 | if (authInfo.loading) { 302 | return
Loading...
303 | } else { 304 | expect(authInfo.accessToken).toBe(null) 305 | expect(authInfo.user).toBe(null) 306 | expect(authInfo.isLoggedIn).toBe(false) 307 | expect(authInfo.orgHelper).toBe(null) 308 | return
Finished
309 | } 310 | } 311 | 312 | render( 313 | 314 | 315 | 316 | ) 317 | 318 | await waitFor(() => screen.getByText("Finished")) 319 | expectCreateClientWasCalledCorrectly() 320 | }) 321 | 322 | it("redirectToLoginPage calls into the client", async () => { 323 | const Component = () => { 324 | const { redirectToLoginPage } = useRedirectFunctions() 325 | redirectToLoginPage() 326 | return
Finished
327 | } 328 | render( 329 | 330 | 331 | 332 | ) 333 | await waitFor(() => screen.getByText("Finished")) 334 | expect(mockClient.redirectToLoginPage).toBeCalled() 335 | }) 336 | 337 | it("redirectToSignupPage calls into the client", async () => { 338 | const Component = () => { 339 | const { redirectToSignupPage } = useRedirectFunctions() 340 | redirectToSignupPage() 341 | return
Finished
342 | } 343 | render( 344 | 345 | 346 | 347 | ) 348 | await waitFor(() => screen.getByText("Finished")) 349 | expect(mockClient.redirectToSignupPage).toBeCalled() 350 | }) 351 | 352 | it("redirectToCreateOrgPage calls into the client", async () => { 353 | const Component = () => { 354 | const { redirectToCreateOrgPage } = useRedirectFunctions() 355 | redirectToCreateOrgPage() 356 | return
Finished
357 | } 358 | render( 359 | 360 | 361 | 362 | ) 363 | await waitFor(() => screen.getByText("Finished")) 364 | expect(mockClient.redirectToCreateOrgPage).toBeCalled() 365 | }) 366 | 367 | it("redirectToAccountPage calls into the client", async () => { 368 | const Component = () => { 369 | const { redirectToAccountPage } = useRedirectFunctions() 370 | redirectToAccountPage() 371 | return
Finished
372 | } 373 | render( 374 | 375 | 376 | 377 | ) 378 | await waitFor(() => screen.getByText("Finished")) 379 | expect(mockClient.redirectToAccountPage).toBeCalled() 380 | }) 381 | 382 | it("redirectToOrgPage calls into the client", async () => { 383 | const Component = () => { 384 | const { redirectToOrgPage } = useRedirectFunctions() 385 | redirectToOrgPage("orgId") 386 | return
Finished
387 | } 388 | render( 389 | 390 | 391 | 392 | ) 393 | await waitFor(() => screen.getByText("Finished")) 394 | expect(mockClient.redirectToOrgPage).toBeCalledWith("orgId") 395 | }) 396 | 397 | it("logout calls into the client", async () => { 398 | const Component = () => { 399 | const logout = useLogoutFunction() 400 | logout(false) 401 | return
Finished
402 | } 403 | render( 404 | 405 | 406 | 407 | ) 408 | await waitFor(() => screen.getByText("Finished")) 409 | expect(mockClient.logout).toBeCalled() 410 | }) 411 | 412 | it("when client logs out, authInfo is refreshed", async () => { 413 | const initialAuthInfo = createAuthenticationInfo() 414 | mockClient.getAuthenticationInfoOrNull.mockReturnValueOnce(initialAuthInfo).mockReturnValueOnce(null) 415 | 416 | const Component = jest.fn() 417 | Component.mockReturnValue(
Finished 1
) 418 | 419 | const WrappedComponent = withAuthInfo(Component) 420 | render( 421 | 422 | 423 | 424 | ) 425 | 426 | await waitFor(() => screen.getByText("Finished 1")) 427 | 428 | // Then simulate a logout event by calling the observer 429 | Component.mockReturnValue(
Finished 2
) 430 | expect(mockClient.addAccessTokenChangeObserver.mock.calls.length).toBe(1) 431 | const observer = mockClient.addAccessTokenChangeObserver.mock.calls[0][0] 432 | observer(false) 433 | 434 | await waitFor(() => screen.getByText("Finished 2")) 435 | 436 | const initialProps = Component.mock.calls[0][0] 437 | expect(initialProps.accessToken).toBe(initialAuthInfo.accessToken) 438 | expect(initialProps.user).toStrictEqual(initialAuthInfo.user) 439 | expect(initialProps.isLoggedIn).toBe(true) 440 | 441 | const finalProps = Component.mock.calls[Component.mock.calls.length - 1][0] 442 | expect(finalProps.accessToken).toBe(null) 443 | expect(finalProps.user).toStrictEqual(null) 444 | expect(finalProps.isLoggedIn).toBe(false) 445 | }) 446 | 447 | it("withAuthInfo renders loading correctly from args", async () => { 448 | const authInfo = createAuthenticationInfo() 449 | const Loading = () =>
Loading
450 | const Component = (props) =>
Finished
451 | const WrappedComponent = withAuthInfo(Component, { displayWhileLoading: }) 452 | 453 | // Wait 100ms to return authInfo to force loading to be displayed 454 | mockClient.getAuthenticationInfoOrNull.mockImplementation( 455 | () => new Promise((resolve) => setTimeout(() => resolve(authInfo), 100)) 456 | ) 457 | 458 | render( 459 | 460 | 461 | 462 | ) 463 | 464 | // Loading is displayed until 100ms passes 465 | await waitFor(() => screen.getByText("Loading")) 466 | jest.advanceTimersByTime(50) 467 | await waitFor(() => screen.getByText("Loading")) 468 | jest.advanceTimersByTime(50) 469 | await waitFor(() => screen.getByText("Finished")) 470 | }) 471 | 472 | it("withAuthInfo renders loading correctly from context", async () => { 473 | const authInfo = createAuthenticationInfo() 474 | const Loading = () =>
Loading
475 | const Component = (props) =>
Finished
476 | const WrappedComponent = withAuthInfo(Component) 477 | 478 | // Wait 100ms to return authInfo to force loading to be displayed 479 | mockClient.getAuthenticationInfoOrNull.mockImplementation( 480 | () => new Promise((resolve) => setTimeout(() => resolve(authInfo), 100)) 481 | ) 482 | 483 | render( 484 | }> 485 | 486 | 487 | ) 488 | 489 | // Loading is displayed until 100ms passes 490 | await waitFor(() => screen.getByText("Loading")) 491 | jest.advanceTimersByTime(50) 492 | await waitFor(() => screen.getByText("Loading")) 493 | jest.advanceTimersByTime(50) 494 | await waitFor(() => screen.getByText("Finished")) 495 | }) 496 | 497 | it("withAuthInfo renders loading correctly from args, overriding context", async () => { 498 | const authInfo = createAuthenticationInfo() 499 | const LoadingFromArg = () =>
Loading From Arg
500 | const LoadingFromContext = () =>
Loading From Context
501 | const Component = (props) =>
Finished
502 | const WrappedComponent = withAuthInfo(Component, { displayWhileLoading: }) 503 | 504 | // Wait 100ms to return authInfo to force loading to be displayed 505 | mockClient.getAuthenticationInfoOrNull.mockImplementation( 506 | () => new Promise((resolve) => setTimeout(() => resolve(authInfo), 100)) 507 | ) 508 | 509 | render( 510 | }> 511 | 512 | 513 | ) 514 | 515 | // Loading is displayed until 100ms passes 516 | await waitFor(() => screen.getByText("Loading From Arg")) 517 | jest.advanceTimersByTime(50) 518 | await waitFor(() => screen.getByText("Loading From Arg")) 519 | jest.advanceTimersByTime(50) 520 | await waitFor(() => screen.getByText("Finished")) 521 | }) 522 | 523 | it("AuthProviderForTesting can be used with useAuthInfo", async () => { 524 | const user = { 525 | email: "john.doe@example.com", 526 | emailConfirmed: true, 527 | enabled: true, 528 | locked: false, 529 | mfaEnabled: false, 530 | userId: "john.doe", 531 | username: "John Doe", 532 | firstName: "John", 533 | lastName: "Doe", 534 | } 535 | const orgMemberInfos = [ 536 | { 537 | orgId: "orgAid", 538 | orgName: "orgA", 539 | urlSafeOrgName: "orga", 540 | userAssignedRole: "Admin", 541 | userInheritedRolesPlusCurrentRole: ["Admin", "Member"], 542 | userPermissions: [], 543 | }, 544 | { 545 | orgId: "orgBid", 546 | orgName: "orgB", 547 | urlSafeOrgName: "orgB", 548 | userAssignedRole: "Owner", 549 | userInheritedRolesPlusCurrentRole: ["Owner", "Admin", "Member"], 550 | userPermissions: ["somePermission"], 551 | }, 552 | ] 553 | const userInformation = { 554 | user, 555 | orgMemberInfos, 556 | accessToken: "could be anything", 557 | } 558 | 559 | const Component = () => { 560 | const authInfo = useAuthInfo() 561 | expect(authInfo.loading).toBeFalsy() 562 | expect(authInfo.accessToken).toBe(userInformation.accessToken) 563 | expect(authInfo.user).toBe(userInformation.user) 564 | expect(authInfo.isLoggedIn).toBe(true) 565 | 566 | let orgIds = authInfo.orgHelper.getOrgIds() 567 | expect(orgIds).toContain("orgAid") 568 | expect(orgIds).toContain("orgBid") 569 | expect(orgIds).not.toContain("orgCid") 570 | 571 | expect(authInfo.accessHelper.hasPermission("orgAid", "somePermission")).toBeFalsy() 572 | expect(authInfo.accessHelper.hasPermission("orgBid", "somePermission")).toBeTruthy() 573 | 574 | return
Finished
575 | } 576 | 577 | render( 578 | 579 | 580 | 581 | ) 582 | 583 | await waitFor(() => screen.getByText("Finished")) 584 | }) 585 | 586 | function createMockClient() { 587 | return { 588 | getAuthenticationInfoOrNull: jest.fn(), 589 | logout: jest.fn(), 590 | redirectToSignupPage: jest.fn(), 591 | redirectToLoginPage: jest.fn(), 592 | redirectToAccountPage: jest.fn(), 593 | redirectToOrgPage: jest.fn(), 594 | redirectToCreateOrgPage: jest.fn(), 595 | addLoggedInChangeObserver: jest.fn(), 596 | removeLoggedInChangeObserver: jest.fn(), 597 | addAccessTokenChangeObserver: jest.fn(), 598 | removeAccessTokenChangeObserver: jest.fn(), 599 | destroy: jest.fn(), 600 | } 601 | } 602 | 603 | const AUTH_URL = "authUrl" 604 | 605 | function expectCreateClientWasCalledCorrectly() { 606 | expect(createClient).toHaveBeenCalledWith({ authUrl: AUTH_URL, enableBackgroundTokenRefresh: true }) 607 | } 608 | 609 | function createOrg() { 610 | const orgName = randomString() 611 | const urlSafeOrgName = orgName.toLowerCase() 612 | return { 613 | orgId: uuidv4(), 614 | orgName, 615 | urlSafeOrgName, 616 | userRole: choose(["Owner", "Admin", "Member"]), 617 | } 618 | } 619 | 620 | function createUser() { 621 | return { 622 | userId: uuidv4(), 623 | email: randomString(), 624 | username: randomString(), 625 | } 626 | } 627 | 628 | function randomString() { 629 | return (Math.random() + 1).toString(36).substring(3) 630 | } 631 | 632 | function choose(choices) { 633 | const index = Math.floor(Math.random() * choices.length) 634 | return choices[index] 635 | } 636 | 637 | function createAuthenticationInfo() { 638 | const accessToken = randomString() 639 | const orgA = createOrg() 640 | const orgB = createOrg() 641 | const user = createUser() 642 | const orgIdToOrgMemberInfo = { 643 | [orgA.orgId]: orgA, 644 | [orgB.orgId]: orgB, 645 | } 646 | return { 647 | accessToken, 648 | expiresAtSeconds: INITIAL_TIME_SECONDS + 30 * 60, 649 | orgIdToOrgMemberInfo, 650 | orgHelper: wrapOrgIdToOrgMemberInfo(orgIdToOrgMemberInfo), 651 | user, 652 | } 653 | } 654 | 655 | function wrapOrgIdToOrgMemberInfo(orgIdToOrgMemberInfo) { 656 | return { 657 | getOrg(orgId) { 658 | if (orgIdToOrgMemberInfo.hasOwnProperty(orgId)) { 659 | return orgIdToOrgMemberInfo[orgId] 660 | } else { 661 | return undefined 662 | } 663 | }, 664 | getOrgIds() { 665 | return Object.keys(orgIdToOrgMemberInfo) 666 | }, 667 | getOrgs() { 668 | return Object.values(orgIdToOrgMemberInfo) 669 | }, 670 | getOrgByName(orgName) { 671 | for (const orgMemberInfo of Object.values(orgIdToOrgMemberInfo)) { 672 | if (orgMemberInfo.orgName === orgName || orgMemberInfo.urlSafeOrgName === orgName) { 673 | return orgMemberInfo 674 | } 675 | } 676 | return undefined 677 | }, 678 | } 679 | } 680 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | export { OrgMemberInfoClass, UserClass } from "@propelauth/javascript" 2 | export type { 3 | AccessHelper, 4 | AccessHelperWithOrg, 5 | OrgHelper, 6 | OrgIdToOrgMemberInfo, 7 | OrgIdToOrgMemberInfoClass, 8 | OrgMemberInfo, 9 | RedirectToAccountOptions, 10 | RedirectToCreateOrgOptions, 11 | RedirectToLoginOptions, 12 | RedirectToOrgPageOptions, 13 | RedirectToSetupSAMLPageOptions, 14 | RedirectToSignupOptions, 15 | User, 16 | UserFields, 17 | UserProperties, 18 | } from "@propelauth/javascript" 19 | export { AuthProvider } from "./AuthContext" 20 | export type { AuthProviderProps, RequiredAuthProviderProps } from "./AuthContext" 21 | export { AuthProviderForTesting } from "./AuthContextForTesting" 22 | export type { AuthProviderForTestingProps, UserInformationForTesting } from "./AuthContextForTesting" 23 | export { useAccessHelper, useOrgHelper } from "./hooks/additionalHooks" 24 | export type { 25 | UseAccessHelper, 26 | UseAccessHelperLoaded, 27 | UseAccessHelperLoading, 28 | UseOrgHelper, 29 | UseOrgHelperLoaded, 30 | UseOrgHelperLoading, 31 | } from "./hooks/additionalHooks" 32 | export { loadOrgSelectionFromLocalStorage, saveOrgSelectionToLocalStorage, useActiveOrg } from "./hooks/useActiveOrg" 33 | export { useAuthInfo } from "./hooks/useAuthInfo" 34 | export { useAuthUrl } from "./hooks/useAuthUrl" 35 | export { useHostedPageUrls } from "./hooks/useHostedPageUrls" 36 | export { useLogoutFunction } from "./hooks/useLogoutFunction" 37 | export { RedirectToLogin, RedirectToSignup, useRedirectFunctions } from "./hooks/useRedirectFunctions" 38 | export type { RedirectProps } from "./hooks/useRedirectFunctions" 39 | export { RequiredAuthProvider } from "./RequiredAuthProvider" 40 | export { withAuthInfo } from "./withAuthInfo" 41 | export type { 42 | WithAuthInfoArgs, 43 | WithAuthInfoProps, 44 | WithLoggedInAuthInfoProps, 45 | WithNotLoggedInAuthInfoProps, 46 | } from "./withAuthInfo" 47 | export { withRequiredAuthInfo } from "./withRequiredAuthInfo" 48 | export type { WithRequiredAuthInfoArgs } from "./withRequiredAuthInfo" 49 | -------------------------------------------------------------------------------- /src/useClientRef.tsx: -------------------------------------------------------------------------------- 1 | import { createClient, IAuthClient } from "@propelauth/javascript" 2 | import { MutableRefObject, useCallback, useEffect, useRef, useState } from "react" 3 | 4 | type ClientRef = { 5 | authUrl: string 6 | client: IAuthClient 7 | } 8 | 9 | interface UseClientRefProps { 10 | authUrl: string 11 | minSecondsBeforeRefresh?: number 12 | } 13 | 14 | export const useClientRef = (props: UseClientRefProps) => { 15 | const [accessTokenChangeCounter, setAccessTokenChangeCounter] = useState(0) 16 | const { authUrl, minSecondsBeforeRefresh } = props 17 | 18 | // Use a ref to store the client so that it doesn't get recreated on every render 19 | const clientRef = useRef(null) 20 | if (clientRef.current === null) { 21 | const client = createClient({ authUrl, enableBackgroundTokenRefresh: true, minSecondsBeforeRefresh }) 22 | client.addAccessTokenChangeObserver(() => setAccessTokenChangeCounter((x) => x + 1)) 23 | clientRef.current = { authUrl, client } 24 | } 25 | 26 | // If the authUrl changes, destroy the old client and create a new one 27 | useEffect(() => { 28 | if (clientRef.current === null) { 29 | return 30 | } else if (clientRef.current.authUrl === authUrl) { 31 | return 32 | } else { 33 | clientRef.current.client.destroy() 34 | 35 | const newClient = createClient({ authUrl, enableBackgroundTokenRefresh: true, minSecondsBeforeRefresh }) 36 | newClient.addAccessTokenChangeObserver(() => setAccessTokenChangeCounter((x) => x + 1)) 37 | clientRef.current = { authUrl, client: newClient } 38 | } 39 | }, [authUrl]) 40 | 41 | return { clientRef, accessTokenChangeCounter } 42 | } 43 | 44 | export const useClientRefCallback = ( 45 | clientRef: MutableRefObject, 46 | callback: (client: IAuthClient) => (...inputs: I) => O 47 | ): ((...inputs: I) => O) => { 48 | return useCallback( 49 | (...inputs: I) => { 50 | const client = clientRef.current?.client 51 | if (!client) { 52 | throw new Error("Client is not initialized") 53 | } 54 | return callback(client)(...inputs) 55 | }, 56 | [callback] 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/withAuthInfo.tsx: -------------------------------------------------------------------------------- 1 | import { AccessHelper, OrgHelper, User, UserClass } from "@propelauth/javascript" 2 | import hoistNonReactStatics from "hoist-non-react-statics" 3 | import React, { useContext } from "react" 4 | import { Subtract } from "utility-types" 5 | import { AuthContext, Tokens } from "./AuthContext" 6 | 7 | export type WithLoggedInAuthInfoProps = { 8 | isLoggedIn: true 9 | accessToken: string 10 | getAccessToken: () => Promise 11 | user: User 12 | userClass: UserClass 13 | orgHelper: OrgHelper 14 | accessHelper: AccessHelper 15 | isImpersonating: boolean 16 | impersonatorUserId?: string 17 | refreshAuthInfo: () => Promise 18 | tokens: Tokens 19 | accessTokenExpiresAtSeconds: number 20 | } 21 | 22 | export type WithNotLoggedInAuthInfoProps = { 23 | isLoggedIn: false 24 | accessToken: null 25 | getAccessToken: () => Promise 26 | user: null 27 | userClass: null 28 | orgHelper: null 29 | accessHelper: null 30 | isImpersonating: false 31 | impersonatorUserId: null 32 | refreshAuthInfo: () => Promise 33 | tokens: Tokens 34 | accessTokenExpiresAtSeconds: null 35 | } 36 | 37 | export type WithAuthInfoProps = WithLoggedInAuthInfoProps | WithNotLoggedInAuthInfoProps 38 | 39 | export interface WithAuthInfoArgs { 40 | displayWhileLoading?: React.ReactElement 41 | } 42 | 43 | export function withAuthInfo

( 44 | Component: React.ComponentType

, 45 | args?: WithAuthInfoArgs 46 | ): React.ComponentType> { 47 | const displayName = `withAuthInfo(${Component.displayName || Component.name || "Component"})` 48 | 49 | const WithAuthInfoWrapper = (props: Subtract) => { 50 | const context = useContext(AuthContext) 51 | if (context === undefined) { 52 | throw new Error("withAuthInfo must be used within an AuthProvider or RequiredAuthProvider") 53 | } 54 | 55 | const { loading, authInfo, defaultDisplayWhileLoading, refreshAuthInfo, tokens } = context 56 | 57 | function displayLoading() { 58 | if (args?.displayWhileLoading) { 59 | return args.displayWhileLoading 60 | } else if (defaultDisplayWhileLoading) { 61 | return defaultDisplayWhileLoading 62 | } 63 | return 64 | } 65 | 66 | if (loading) { 67 | return displayLoading() 68 | } else if (authInfo) { 69 | const loggedInProps: P = { 70 | ...(props as P), 71 | accessToken: authInfo.accessToken, 72 | getAccessToken: tokens.getAccessToken, 73 | isLoggedIn: !!authInfo.accessToken, 74 | orgHelper: authInfo.orgHelper, 75 | accessHelper: authInfo.accessHelper, 76 | user: authInfo.user, 77 | userClass: authInfo.userClass, 78 | isImpersonating: !!authInfo.impersonatorUserId, 79 | impersonatorUserId: authInfo.impersonatorUserId, 80 | refreshAuthInfo, 81 | tokens, 82 | accessTokenExpiresAtSeconds: authInfo.expiresAtSeconds, 83 | } 84 | return 85 | } else { 86 | const notLoggedInProps: P = { 87 | ...(props as P), 88 | accessToken: null, 89 | getAccessToken: () => undefined, 90 | isLoggedIn: false, 91 | user: null, 92 | userClass: null, 93 | orgHelper: null, 94 | accessHelper: null, 95 | isImpersonating: false, 96 | impersonatorUserId: null, 97 | refreshAuthInfo, 98 | tokens, 99 | accessTokenExpiresAtSeconds: null, 100 | } 101 | return 102 | } 103 | } 104 | WithAuthInfoWrapper.displayName = displayName 105 | WithAuthInfoWrapper.WrappedComponent = Component 106 | 107 | return hoistNonReactStatics(WithAuthInfoWrapper, Component) 108 | } 109 | -------------------------------------------------------------------------------- /src/withRequiredAuthInfo.tsx: -------------------------------------------------------------------------------- 1 | import hoistNonReactStatics from "hoist-non-react-statics" 2 | import React, { useContext } from "react" 3 | import { Subtract } from "utility-types" 4 | import { AuthContext } from "./AuthContext" 5 | import { RedirectToLogin } from "./hooks/useRedirectFunctions" 6 | import { WithLoggedInAuthInfoProps } from "./withAuthInfo" 7 | 8 | export interface WithRequiredAuthInfoArgs { 9 | displayWhileLoading?: React.ReactElement 10 | displayIfLoggedOut?: React.ReactElement 11 | } 12 | 13 | export function withRequiredAuthInfo

( 14 | Component: React.ComponentType

, 15 | args?: WithRequiredAuthInfoArgs 16 | ): React.ComponentType> { 17 | const displayName = `withRequiredAuthInfo(${Component.displayName || Component.name || "Component"})` 18 | 19 | const WithRequiredAuthInfoWrapper = (props: Subtract) => { 20 | const context = useContext(AuthContext) 21 | if (context === undefined) { 22 | throw new Error("withRequiredAuthInfo must be used within an AuthProvider or RequiredAuthProvider") 23 | } 24 | 25 | const { loading, authInfo, defaultDisplayIfLoggedOut, defaultDisplayWhileLoading, refreshAuthInfo, tokens } = 26 | context 27 | 28 | function displayLoading() { 29 | if (args?.displayWhileLoading) { 30 | return args.displayWhileLoading 31 | } else if (defaultDisplayWhileLoading) { 32 | return defaultDisplayWhileLoading 33 | } 34 | return 35 | } 36 | 37 | function displayLoggedOut() { 38 | if (args?.displayIfLoggedOut) { 39 | return args.displayIfLoggedOut 40 | } else if (defaultDisplayIfLoggedOut) { 41 | return defaultDisplayIfLoggedOut 42 | } 43 | return 44 | } 45 | 46 | if (loading) { 47 | return displayLoading() 48 | } else if (authInfo) { 49 | const loggedInProps: P = { 50 | ...(props as P), 51 | accessToken: authInfo.accessToken, 52 | getAccessToken: tokens.getAccessToken, 53 | isLoggedIn: !!authInfo.accessToken, 54 | orgHelper: authInfo.orgHelper, 55 | accessHelper: authInfo.accessHelper, 56 | user: authInfo.user, 57 | userClass: authInfo.userClass, 58 | isImpersonating: !!authInfo.impersonatorUserId, 59 | impersonatorUserId: authInfo.impersonatorUserId, 60 | refreshAuthInfo, 61 | tokens, 62 | accessTokenExpiresAtSeconds: authInfo.expiresAtSeconds, 63 | } 64 | return 65 | } else { 66 | return displayLoggedOut() 67 | } 68 | } 69 | WithRequiredAuthInfoWrapper.displayName = displayName 70 | WithRequiredAuthInfoWrapper.WrappedComponent = Component 71 | 72 | return hoistNonReactStatics(WithRequiredAuthInfoWrapper, Component) 73 | } 74 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 4 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 5 | "moduleResolution": "node", 6 | "jsx": "react", 7 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 8 | "outDir": "dist/types", /* Redirect output structure to the directory. */ 9 | "removeComments": true, /* Do not emit comments to output. */ 10 | "strict": true, /* Enable all strict type-checking options. */ 11 | "noUnusedLocals": true, /* Report errors on unused locals. */ 12 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 13 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 16 | "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 17 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 18 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 19 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 20 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 21 | }, 22 | "include": [ 23 | "src/**/*" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist", 28 | "src/**/*.test.*" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------