├── .editorconfig ├── .gitignore ├── .netlify └── state.json ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── examples ├── example-reach-router │ ├── .gitignore │ ├── README.md │ ├── functions │ │ ├── async-dadjoke │ │ │ ├── async-dadjoke.js │ │ │ └── package.json │ │ └── authEndPoint │ │ │ ├── authEndPoint.js │ │ │ └── package.json │ ├── index.html │ ├── package.json │ ├── public │ │ ├── _redirects │ │ ├── favicon.ico │ │ ├── index.html │ │ └── manifest.json │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ ├── logo.svg │ │ ├── useLoading.tsx │ │ └── useLocalState.tsx │ ├── tsconfig.json │ └── yarn.lock └── example-react-router │ ├── README.md │ ├── global.d.ts │ ├── package.json │ ├── src │ ├── App.tsx │ ├── assets │ │ ├── googleLogo.svg │ │ └── index.ts │ ├── components │ │ ├── AuthOption │ │ │ ├── AuthOption.styles.ts │ │ │ └── index.ts │ │ ├── AuthText │ │ │ ├── AuthText.styles.ts │ │ │ └── index.ts │ │ ├── Button │ │ │ ├── Button.styles.ts │ │ │ └── index.ts │ │ ├── ButtonGoogle │ │ │ ├── ButtonGoogle.styles.ts │ │ │ ├── ButtonGoogle.tsx │ │ │ └── index.ts │ │ ├── Container │ │ │ ├── Container.styles.ts │ │ │ └── index.ts │ │ ├── Form │ │ │ ├── Form.styles.ts │ │ │ └── index.ts │ │ ├── GlobalStyles │ │ │ ├── GlobalStyles.styles.ts │ │ │ └── index.ts │ │ ├── Header │ │ │ ├── Header.styles.ts │ │ │ ├── Header.tsx │ │ │ └── index.ts │ │ ├── Input │ │ │ ├── Input.styles.ts │ │ │ └── index.ts │ │ ├── Label │ │ │ ├── Label.styles.ts │ │ │ └── index.ts │ │ ├── TextError │ │ │ ├── TextError.styles.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── constants │ │ └── constants.ts │ ├── index.tsx │ ├── public │ │ ├── index.html │ │ └── robots.txt │ └── views │ │ ├── CreateAccount │ │ ├── CreateAccount.styles.ts │ │ ├── CreateAccount.tsx │ │ └── index.ts │ │ ├── Home │ │ ├── Home.styles.ts │ │ ├── Home.tsx │ │ └── index.ts │ │ ├── LogIn │ │ ├── LogIn.tsx │ │ └── index.ts │ │ ├── Welcome │ │ ├── Welcome.styles.ts │ │ ├── Welcome.tsx │ │ └── index.ts │ │ └── index.tsx │ └── tsconfig.json ├── netlify.toml ├── package.json ├── src ├── index.tsx ├── runRoutes.tsx └── token.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # See https://help.github.com/ignore-files/ for more about ignoring files. 3 | 4 | # dependencies 5 | node_modules 6 | 7 | # builds 8 | build 9 | dist 10 | .rpt2_cache 11 | .rts2_cache_cjs 12 | .rts2_cache_es 13 | .rts2_cache_umd 14 | 15 | # misc 16 | .DS_Store 17 | .env 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | .netlify -------------------------------------------------------------------------------- /.netlify/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "siteId": "62390d51-d3e5-4901-a676-8ae1a95ea9dc" 3 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 9 4 | - 8 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). 9 | 10 | ## [v0.2.7](https://github.com/netlify-labs/react-netlify-identity/compare/v0.2.6...v0.2.7) - 2022-06-15 11 | 12 | ### Merged 13 | 14 | - fix: package.module again [`#50`](https://github.com/netlify-labs/react-netlify-identity/pull/50) 15 | 16 | ## [v0.2.6](https://github.com/netlify-labs/react-netlify-identity/compare/v0.2.5...v0.2.6) - 2022-06-14 17 | 18 | ### Merged 19 | 20 | - fix: typo in `module` field [`#49`](https://github.com/netlify-labs/react-netlify-identity/pull/49) 21 | 22 | ### Fixed 23 | 24 | - fix: typo in `module` field [`#48`](https://github.com/netlify-labs/react-netlify-identity/issues/48) 25 | 26 | ### Commits 27 | 28 | - Update README.md [`8799796`](https://github.com/netlify-labs/react-netlify-identity/commit/879979601df6dba0ca95badb2a0988613565a5a8) 29 | 30 | ## [v0.2.5](https://github.com/netlify-labs/react-netlify-identity/compare/v0.2.4...v0.2.5) - 2020-04-20 31 | 32 | ### Merged 33 | 34 | - make tsdx a dev dependency [`#36`](https://github.com/netlify-labs/react-netlify-identity/pull/36) 35 | 36 | ## [v0.2.4](https://github.com/netlify-labs/react-netlify-identity/compare/v0.2.3...v0.2.4) - 2020-04-10 37 | 38 | ### Merged 39 | 40 | - Store user after email confirmation [`#35`](https://github.com/netlify-labs/react-netlify-identity/pull/35) 41 | 42 | ### Commits 43 | 44 | - feat(runRoutes): store user after email confirmation [`929c477`](https://github.com/netlify-labs/react-netlify-identity/commit/929c4771c2caa4b64d4232be0e37bf4221d2f27f) 45 | 46 | ## [v0.2.3](https://github.com/netlify-labs/react-netlify-identity/compare/v0.2.2...v0.2.3) - 2020-02-04 47 | 48 | ### Commits 49 | 50 | - fix accidental break in ReactNetlifyIdentityAPI.signupUser type signature [`182faf9`](https://github.com/netlify-labs/react-netlify-identity/commit/182faf90bde92c260134965d511690dceeed3265) 51 | 52 | ## [v0.2.2](https://github.com/netlify-labs/react-netlify-identity/compare/v0.2.1...v0.2.2) - 2020-01-18 53 | 54 | ### Merged 55 | 56 | - V0.2.2 [`#32`](https://github.com/netlify-labs/react-netlify-identity/pull/32) 57 | 58 | ### Commits 59 | 60 | - feat(verifyToken): add & expose verifyToken reject instead of error [`b4caa6f`](https://github.com/netlify-labs/react-netlify-identity/commit/b4caa6fe87f4f57efd1d85868786b321d51887b3) 61 | - add @see to all gotrue-js methods [`94d1698`](https://github.com/netlify-labs/react-netlify-identity/commit/94d1698488e6ea9975750191cb41386661fcc36f) 62 | - fix(hash): remove entirely [`0033b60`](https://github.com/netlify-labs/react-netlify-identity/commit/0033b604f441e54b65037dfac8c733c9918eee42) 63 | 64 | ## [v0.2.1](https://github.com/netlify-labs/react-netlify-identity/compare/v0.2.0...v0.2.1) - 2020-01-16 65 | 66 | ### Merged 67 | 68 | - v0.2.1 [`#31`](https://github.com/netlify-labs/react-netlify-identity/pull/31) 69 | 70 | ### Commits 71 | 72 | - fix(readme): update to latest changes [`abdcdad`](https://github.com/netlify-labs/react-netlify-identity/commit/abdcdada960849d7059affcfeaee35c9b8036d0f) 73 | - fix(recoverAccount): also clean up potentially rejected token [`226aea3`](https://github.com/netlify-labs/react-netlify-identity/commit/226aea39809ba08ff282acf74ece24c518e8b2a4) 74 | - fix(readme): finish docs [`8e30593`](https://github.com/netlify-labs/react-netlify-identity/commit/8e305934d6a3801cd556ea1edd6b0fc1f7316e13) 75 | 76 | ## [v0.2.0](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.9...v0.2.0) - 2020-01-15 77 | 78 | ### Merged 79 | 80 | - recoverAccount workflow... and a bit more [`#30`](https://github.com/netlify-labs/react-netlify-identity/pull/30) 81 | - Bump safer-eval from 1.3.3 to 1.3.5 in /examples/example-reach-ro… [`#23`](https://github.com/netlify-labs/react-netlify-identity/pull/23) 82 | - Bump mixin-deep from 1.3.1 to 1.3.2 [`#19`](https://github.com/netlify-labs/react-netlify-identity/pull/19) 83 | - Bump mixin-deep from 1.3.1 to 1.3.2 in /examples/example-reach-ro… [`#20`](https://github.com/netlify-labs/react-netlify-identity/pull/20) 84 | - Bump lodash from 4.17.11 to 4.17.15 [`#21`](https://github.com/netlify-labs/react-netlify-identity/pull/21) 85 | - Bump lodash from 4.17.11 to 4.17.15 in /examples/example-reach-ro… [`#22`](https://github.com/netlify-labs/react-netlify-identity/pull/22) 86 | - Added example [`#18`](https://github.com/netlify-labs/react-netlify-identity/pull/18) 87 | 88 | ### Commits 89 | 90 | - chore(deps): bumped versions of all dependencies [`2e5cdba`](https://github.com/netlify-labs/react-netlify-identity/commit/2e5cdbaf7258a4405c61fe96e532670df1a09008) 91 | - Add example [`18bf4ba`](https://github.com/netlify-labs/react-netlify-identity/commit/18bf4ba32097464ac10c16f5096a696bf965193a) 92 | - fix(index): group state & effects, memoize functions [`3cd647c`](https://github.com/netlify-labs/react-netlify-identity/commit/3cd647c2d6a25a695f696a166c29034bd40e03e1) 93 | 94 | ## [v0.1.9](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.8...v0.1.9) - 2019-07-02 95 | 96 | ### Merged 97 | 98 | - Change parameter type in updateUser method [`#15`](https://github.com/netlify-labs/react-netlify-identity/pull/15) 99 | 100 | ### Commits 101 | 102 | - gs [`edcdeb9`](https://github.com/netlify-labs/react-netlify-identity/commit/edcdeb905d4f17398e90effdecbbe8a9f90715ef) 103 | - redeploy [`6756cf1`](https://github.com/netlify-labs/react-netlify-identity/commit/6756cf1daeafdfd963e8f927ea1db6df2831c14b) 104 | 105 | ## [v0.1.8](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.7...v0.1.8) - 2019-06-23 106 | 107 | ### Merged 108 | 109 | - Add flag enableRunRoutes [`#14`](https://github.com/netlify-labs/react-netlify-identity/pull/14) 110 | 111 | ### Commits 112 | 113 | - move down the usememo [`563b9de`](https://github.com/netlify-labs/react-netlify-identity/commit/563b9de634e65eff1635b657cc96d28a41456d94) 114 | - make sure we use RNI 0.1.7 [`11b78d1`](https://github.com/netlify-labs/react-netlify-identity/commit/11b78d11257503f69ac10b36ac96e1661f36b2ae) 115 | - tweak readme [`e0cf22c`](https://github.com/netlify-labs/react-netlify-identity/commit/e0cf22c2d76de0b935716b1ec16151c96b05e45e) 116 | 117 | ## [v0.1.7](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.6...v0.1.7) - 2019-06-10 118 | 119 | ### Commits 120 | 121 | - memoize the used hook [`e92f779`](https://github.com/netlify-labs/react-netlify-identity/commit/e92f779c363e561f6c29b6bfdc8a9ed2e871859e) 122 | - use profiling build of react dom [`e05047c`](https://github.com/netlify-labs/react-netlify-identity/commit/e05047ccb959ade2b54fa4880779e7bd2a2dea6d) 123 | 124 | ## [v0.1.6](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.5...v0.1.6) - 2019-06-10 125 | 126 | ### Commits 127 | 128 | - memoize gotrueinstance creation [`aea606b`](https://github.com/netlify-labs/react-netlify-identity/commit/aea606b6e6dbea34d4c0350ad5dff2ce07db1c45) 129 | 130 | ## [v0.1.5](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.4...v0.1.5) - 2019-06-10 131 | 132 | ### Commits 133 | 134 | - switch example to always use latest from npm [`a1cee30`](https://github.com/netlify-labs/react-netlify-identity/commit/a1cee30ae0fee27437af89a6e5179c81eca61a90) 135 | - alright drop all dependencies [`8c3c6fa`](https://github.com/netlify-labs/react-netlify-identity/commit/8c3c6fa5ccfbf84fbc04b2dadaef540860b00c6a) 136 | - skip prepare script [`a8e8a79`](https://github.com/netlify-labs/react-netlify-identity/commit/a8e8a79c9ce09891f2f6f46bbaa0988f88e31b6a) 137 | 138 | ## [v0.1.4](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.3...v0.1.4) - 2019-06-10 139 | 140 | ### Commits 141 | 142 | - rename api [`2abd831`](https://github.com/netlify-labs/react-netlify-identity/commit/2abd831b5139dacde926152839ee966db76586bc) 143 | - fix useeffect infinite loop [`4af4061`](https://github.com/netlify-labs/react-netlify-identity/commit/4af40618db02631437d9618b7c17e466cc8c732f) 144 | 145 | ## [v0.1.3](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.2...v0.1.3) - 2019-05-30 146 | 147 | ### Commits 148 | 149 | - fix readme [`16f3a04`](https://github.com/netlify-labs/react-netlify-identity/commit/16f3a044ceea85a91c7e22f31f3ada631e30fb2e) 150 | - rename to useIdentityContext [`ff50d61`](https://github.com/netlify-labs/react-netlify-identity/commit/ff50d61014a8a5ff70fd74477b61d4a11701d3ea) 151 | 152 | ## [v0.1.2](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.1...v0.1.2) - 2019-05-30 153 | 154 | ### Commits 155 | 156 | - fix exampple [`6c4d722`](https://github.com/netlify-labs/react-netlify-identity/commit/6c4d7229a1672557a12147adc8dc59f027e544ac) 157 | 158 | ## [v0.1.1](https://github.com/netlify-labs/react-netlify-identity/compare/v0.1.0...v0.1.1) - 2019-05-30 159 | 160 | ### Merged 161 | 162 | - Use context api [`#9`](https://github.com/netlify-labs/react-netlify-identity/pull/9) 163 | 164 | ### Commits 165 | 166 | - change settings api and remove useMemo call [`77099a3`](https://github.com/netlify-labs/react-netlify-identity/commit/77099a307aba8525da8689d5f4a7175f27e619db) 167 | - fix build [`964b3ae`](https://github.com/netlify-labs/react-netlify-identity/commit/964b3ae401a7cffa3a2b5eeb837a4ab6aec6343a) 168 | 169 | ## [v0.1.0](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.16...v0.1.0) - 2019-05-30 170 | 171 | ### Commits 172 | 173 | - working contextapi [`149fccf`](https://github.com/netlify-labs/react-netlify-identity/commit/149fccf91317ffade6b5468c19cdfcc333cf245d) 174 | - update version [`013e1fd`](https://github.com/netlify-labs/react-netlify-identity/commit/013e1fdf8fe2a74c5844af5b87221d224e51f3be) 175 | 176 | ## [v0.0.16](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.15...v0.0.16) - 2019-05-22 177 | 178 | ### Commits 179 | 180 | - example [`55dbe28`](https://github.com/netlify-labs/react-netlify-identity/commit/55dbe287bdd10f288489fac9820abf3cbdd06c52) 181 | - move into function folders [`43dfa8b`](https://github.com/netlify-labs/react-netlify-identity/commit/43dfa8b81c80db0f292c71fb65f685b9a0d7594e) 182 | - readme [`1521c5b`](https://github.com/netlify-labs/react-netlify-identity/commit/1521c5b1b9843ce1cc1c82aec9d9c2e818599f38) 183 | 184 | ## [v0.0.15](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.14...v0.0.15) - 2019-04-28 185 | 186 | ### Commits 187 | 188 | - extract to runRoutes [`3b88029`](https://github.com/netlify-labs/react-netlify-identity/commit/3b88029ac98ea6874135569f21f4605a2263564d) 189 | - app buttons [`ef6b9c1`](https://github.com/netlify-labs/react-netlify-identity/commit/ef6b9c16ad029498935d9d4e24428d2212b5a1b6) 190 | - pkgjson [`50ae246`](https://github.com/netlify-labs/react-netlify-identity/commit/50ae2462e80195234b2813a24e946deda996d20d) 191 | 192 | ## [v0.0.14](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.13...v0.0.14) - 2019-04-28 193 | 194 | ### Commits 195 | 196 | - tryloginprovider [`b6523ff`](https://github.com/netlify-labs/react-netlify-identity/commit/b6523ff6cac9d1d09e549bc21055d32a1e6a1f29) 197 | - bump version [`bee1f88`](https://github.com/netlify-labs/react-netlify-identity/commit/bee1f888037ab22265874b7398e2dc051b9eb3b1) 198 | 199 | ## [v0.0.13](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.12...v0.0.13) - 2019-04-28 200 | 201 | ### Commits 202 | 203 | - try to deploy [`4dedb1d`](https://github.com/netlify-labs/react-netlify-identity/commit/4dedb1d74115413946cdeda613f29b9c7c76bbb6) 204 | - pkgjson [`2d15208`](https://github.com/netlify-labs/react-netlify-identity/commit/2d1520885fdc7c17eeef9587ce01f5964678dbe9) 205 | - readme [`743a354`](https://github.com/netlify-labs/react-netlify-identity/commit/743a3548b2a3b0f927b60bb76928b96b4db2c022) 206 | 207 | ## [v0.0.12](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.11...v0.0.12) - 2019-04-24 208 | 209 | ### Commits 210 | 211 | - actually make it remember [`a8a8106`](https://github.com/netlify-labs/react-netlify-identity/commit/a8a8106c9317228848c349487b8626040bc5ebf0) 212 | 213 | ## [v0.0.11](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.10...v0.0.11) - 2019-04-23 214 | 215 | ### Commits 216 | 217 | - bump dependencies [`cfb29a6`](https://github.com/netlify-labs/react-netlify-identity/commit/cfb29a6af608a3bdae412ce43d73fa5a54db7e86) 218 | - remove example, misleading [`2761f6a`](https://github.com/netlify-labs/react-netlify-identity/commit/2761f6a931889d1b23fe0ad10e157e8b08c6d2e0) 219 | 220 | ## [v0.0.10](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.9...v0.0.10) - 2019-04-23 221 | 222 | ### Commits 223 | 224 | - bump gotruejs [`0a88980`](https://github.com/netlify-labs/react-netlify-identity/commit/0a88980df1b347565b0f9e772288e66a03e6eea1) 225 | 226 | ## [v0.0.9](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.8...v0.0.9) - 2019-04-22 227 | 228 | ### Commits 229 | 230 | - proper dependencies [`20fb43a`](https://github.com/netlify-labs/react-netlify-identity/commit/20fb43a75dde9e81afef7bf6c008c5ce5fa5c8c3) 231 | 232 | ## [v0.0.8](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.7...v0.0.8) - 2019-04-22 233 | 234 | ### Merged 235 | 236 | - Add remember option to loginUser [`#2`](https://github.com/netlify-labs/react-netlify-identity/pull/2) 237 | 238 | ### Commits 239 | 240 | - Add @svgr/rollup devDependency to package.json [`0ecb730`](https://github.com/netlify-labs/react-netlify-identity/commit/0ecb730a6601d477ed6018f99aa8a2b840a6fa1c) 241 | - user [`0a99857`](https://github.com/netlify-labs/react-netlify-identity/commit/0a998577cca36567c0d5bf8a280b600b6894d9b8) 242 | - 3rd arg loginuser [`6856fad`](https://github.com/netlify-labs/react-netlify-identity/commit/6856fad19c87bb989771a218ac6d061052a335bb) 243 | 244 | ## [v0.0.7](https://github.com/netlify-labs/react-netlify-identity/compare/0.0.7...v0.0.7) - 2019-03-15 245 | 246 | ### Commits 247 | 248 | - pull currentuser on init [`3b593dd`](https://github.com/netlify-labs/react-netlify-identity/commit/3b593dded394462ca2679fcfc57baa31d450b2e2) 249 | 250 | ## [0.0.7](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.5...0.0.7) - 2019-01-10 251 | 252 | ### Commits 253 | 254 | - dont use ref [`dc1c0f0`](https://github.com/netlify-labs/react-netlify-identity/commit/dc1c0f093cf07df182444e1e4962bebcfbf2e0e2) 255 | - noproptypes [`72e75ba`](https://github.com/netlify-labs/react-netlify-identity/commit/72e75bacdd055fee1ef7b1dcf2440650be13c2e5) 256 | 257 | ## [v0.0.5](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.3...v0.0.5) - 2019-01-10 258 | 259 | ### Commits 260 | 261 | - update certain things [`561db87`](https://github.com/netlify-labs/react-netlify-identity/commit/561db8766f75d130dc64ca3f52e0a313b7dd3119) 262 | - Update README.md [`bc39e80`](https://github.com/netlify-labs/react-netlify-identity/commit/bc39e80f587bd6fbeddd9d6e2c52262ef5b2992b) 263 | 264 | ## [v0.0.3](https://github.com/netlify-labs/react-netlify-identity/compare/v0.0.2...v0.0.3) - 2018-12-10 265 | 266 | ### Commits 267 | 268 | - readme [`a20d173`](https://github.com/netlify-labs/react-netlify-identity/commit/a20d173ab4a7f00d905ee9a68091913924da1e91) 269 | - update readme [`61124da`](https://github.com/netlify-labs/react-netlify-identity/commit/61124da98761c5d765d5ddc1c0ecbbcd957e9c4b) 270 | 271 | ## v0.0.2 - 2018-12-10 272 | 273 | ### Commits 274 | 275 | - init create-react-library@2.6.7 [`2ae1fc1`](https://github.com/netlify-labs/react-netlify-identity/commit/2ae1fc1b5bd3d868a05bbea02c8611699714abf8) 276 | - init [`acdf2e2`](https://github.com/netlify-labs/react-netlify-identity/commit/acdf2e29850e6c6a18ac1c33ae3aa373cd878e9e) 277 | - init [`71ba046`](https://github.com/netlify-labs/react-netlify-identity/commit/71ba046d3320807cb411dd270973cd9481bd7486) 278 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SEEKING MAINTAINERS 2 | 3 | i've [left Netlify](https://www.swyx.io/writing/farewell-netlify/). obviously i still care about netlify but im gonna have other stuff to do and wont be able to give this my full attention. pls open an issue if you use this and want to take over. post your npm username too. i'd love to find a good home for this project. 4 | 5 | --- 6 | 7 | # react-netlify-identity 8 | 9 | > [Netlify Identity](https://www.netlify.com/docs/identity/?utm_source=github&utm_medium=swyx-RNI&utm_campaign=devex) + React Hooks, with Typescript. Bring your own UI! 10 | 11 | [![NPM](https://img.shields.io/npm/v/react-netlify-identity.svg)](https://www.npmjs.com/package/react-netlify-identity) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 12 | 13 | Use [Netlify Identity](https://www.netlify.com/docs/identity/?utm_source=github&utm_medium=swyx-RNI&utm_campaign=devex) easier with React! This is a thin wrapper over the [gotrue-js](https://github.com/netlify/gotrue-js) library for easily accessing Netlify Identity functionality in your app, with React Context and Hooks. Types are provided. 14 | 15 | Three demos: 16 | 17 | - [a full demo here](https://netlify-gotrue-in-react.netlify.com/) with [source code](https://github.com/netlify/create-react-app-lambda/tree/reachRouterAndGoTrueDemo/src) 18 | - [example with Reach Router](https://github.com/sw-yx/react-netlify-identity/tree/master/examples/example-reach-router) with [a demo hosted here](https://react-netlify-identity.netlify.com) and [deployed logs here](https://app.netlify.com/sites/react-netlify-identity/deploys) 19 | - [example with React Router](https://github.com/sw-yx/react-netlify-identity/tree/master/examples/example-react-router) with [a demo hosted here](https://react-netlify-identity-example.netlify.com) 20 | 21 | **This library is not officially maintained by Netlify.** This is written by swyx for his own use (and others with like minds 😎) and will be maintained as a personal project unless formally adopted by Netlify. See below for official alternatives. 22 | 23 | ## Blogposts 24 | 25 | - [Add Netlify Identity Authentication to any React App in 5 minutes with React Context, Hooks and Suspense](https://dev.to/swyx/add-netlify-identity-authentication-to-any-react-app-in-5-minutes-with-react-context-hooks-and-suspense-5gci) 26 | 27 | ## List of Alternatives 28 | 29 | **Lowest level JS Library**: If you want to use the official Javascript bindings to GoTrue, Netlify's underlying Identity service written in Go, use https://github.com/netlify/gotrue-js 30 | 31 | **React bindings**: If you want a thin wrapper over Gotrue-js for React, `react-netlify-identity` is a "headless" library, meaning there is no UI exported and you will write your own UI to work with the authentication. https://github.com/sw-yx/react-netlify-identity. If you want a drop-in UI, there is yet another library that wraps `react-netlify-identity`: https://github.com/sw-yx/react-netlify-identity-widget 32 | 33 | **High level overlay**: If you want a "widget" overlay that gives you a nice UI out of the box, with a somewhat larger bundle, check https://github.com/netlify/netlify-identity-widget 34 | 35 | **High level popup**: If you want a popup window approach also with a nice UI out of the box, and don't mind the popup flow, check https://github.com/netlify/netlify-auth-providers 36 | 37 | ## Typescript 38 | 39 | Library is written in Typescript. File an issue if you find any problems. 40 | 41 | ## Install 42 | 43 | ```bash 44 | yarn add react-netlify-identity 45 | ``` 46 | 47 | ## Usage 48 | 49 | ⚠️ **Important:** You will need to have an active Netlify site running with Netlify Identity turned on. [Click here for instructions](https://www.netlify.com/docs/identity/#getting-started?utm_source=github&utm_medium=swyx-RNI&utm_campaign=devex) to get started/double check that it is on. We will need your site's url (e.g. `https://mysite.netlify.com`) to initialize `IdentityContextProvider`. 50 | 51 | **When you call useIdentityContext()**, you can destructure these variables and methods: 52 | 53 | - `user: User` 54 | - `setUser`: directly set the user object. Not advised; use carefully!! mostly you should use the methods below 55 | - `isConfirmedUser: boolean`: if they have confirmed their email 56 | - `isLoggedIn: boolean`: if the user is logged in 57 | - `signupUser(email: string, password: string, data: Object, directLogin: boolean = true)` 58 | - `directLogin` will internally set state to the newly created user 59 | - `isConfirmedUser` will be false 60 | - `isLoggedIn` will be true 61 | - setting `directLogin` to false won't trigger the state 62 | - `loginUser(email: string, password: string, remember: boolean = true)` - we default the `remember` term to `true` since you'll usually want to remember the session in localStorage. set it to false if you need to 63 | - `logoutUser()` 64 | - `requestPasswordRecovery(email: string)` 65 | - `updateUser(fields: object)`: see [updateUser @ gotrue-js](https://github.com/netlify/gotrue-js#update-a-user) 66 | - `getFreshJWT()` 67 | - `authedFetch(endpoint: string, obj: RequestInit = {})` a thin axios-like wrapper over `fetch` that has the user's JWT attached, for convenience pinging Netlify Functions with Netlify Identity 68 | - `recoverAccount(remember?: boolean)`: verifies and consumes the recovery token caught by `runRoutes`, sets user on success 69 | - `param: TokenParam` 70 | - a token exposing Netlify tokens a dev has to implement the actions for; namely `invite`, `recovery`, `email_change` and `access_denied` 71 | - **important:** tokens this package exposes no methods for are automatically handled and will not be passed down - see [runRoutes implementation](https://github.com/sw-yx/react-netlify-identity/master/src/runRoutes.tsx) 72 | - if you don't want this behaviour (added [here](https://github.com/sw-yx/react-netlify-identity/issues/12) in v.0.1.8), pass `runRoutes={false}` to the exposed hook 73 | - for further reference, please check the [type definition](https://github.com/sw-yx/react-netlify-identity/tree/master/src/token.ts) 74 | - an example implementation for a Recovery process can be found below 75 | - `verifyToken()` 76 | - consumes & verifies TokenParam based on the type and tries to retrieve a valid user object 77 | - devs duty to show password field to afterwards call `signupUser(user.email, newPassword)` 78 | 79 | ```tsx 80 | import React from 'react'; 81 | 82 | import { IdentityContextProvider } from 'react-netlify-identity'; 83 | 84 | function App() { 85 | const url = 'https://your-identity-instance.netlify.com/'; // supply the url of your Netlify site instance. VERY IMPORTANT. no point putting in env var since this is public anyway 86 | return ( 87 | 88 | {/* rest of your app */} 89 | 90 | ); 91 | } 92 | ``` 93 | 94 |
95 | 96 |

97 | Click for More Example code 98 |

99 |
100 | 101 | ```tsx 102 | import { useIdentityContext } from 'react-netlify-identity'; 103 | 104 | // log in/sign up example 105 | function Login() { 106 | const { loginUser, signupUser } = useIdentityContext(); 107 | const formRef = React.useRef(); 108 | const [msg, setMsg] = React.useState(''); 109 | 110 | const signup = () => { 111 | const email = formRef.current.email.value; 112 | const password = formRef.current.password.value; 113 | 114 | signupUser(email, password) 115 | .then(user => { 116 | console.log('Success! Signed up', user); 117 | navigate('/dashboard'); 118 | }) 119 | .catch(err => console.error(err) || setMsg('Error: ' + err.message)); 120 | }; 121 | 122 | return ( 123 |
{ 126 | e.preventDefault(); 127 | const email = e.target.email.value; 128 | const password = e.target.password.value; 129 | load(loginUser(email, password, true)) 130 | .then(user => { 131 | console.log('Success! Logged in', user); 132 | navigate('/dashboard'); 133 | }) 134 | .catch(err => console.error(err) || setMsg('Error: ' + err.message)); 135 | }} 136 | > 137 |
138 | 142 |
143 |
144 | 148 |
149 |
150 | 151 | 152 | {msg &&
{msg}
} 153 |
154 |
155 | ); 156 | } 157 | 158 | // log out user 159 | function Logout() { 160 | const { logoutUser } = useIdentityContext(); 161 | return ; 162 | } 163 | 164 | // check `identity.user` in a protected route 165 | function PrivateRoute(props) { 166 | const identity = useIdentityContext(); 167 | let { as: Comp, ...rest } = props; 168 | return identity.user ? ( 169 | 170 | ) : ( 171 |
172 |

You are trying to view a protected page. Please log in

173 | 174 |
175 | ); 176 | } 177 | 178 | // check if user has confirmed their email 179 | // use authedFetch API to make a request to Netlify Function with the user's JWT token, 180 | // letting your function use the `user` object 181 | function Dashboard() { 182 | const { isConfirmedUser, authedFetch } = useIdentityContext(); 183 | const [msg, setMsg] = React.useState('Click to load something'); 184 | const handler = () => { 185 | authedFetch.get('/.netlify/functions/authEndPoint').then(setMsg); 186 | }; 187 | return ( 188 |
189 |

This is a Protected Dashboard!

190 | {!isConfirmedUser && ( 191 |
192 |           You have not confirmed your email. Please confirm it before you ping
193 |           the API.
194 |         
195 | )} 196 |
197 |
198 |

You can try pinging our authenticated API here.

199 |

200 | If you are logged in, you should be able to see a `user` info here. 201 |

202 | 203 |
{JSON.stringify(msg, null, 2)}
204 |
205 |
206 | ); 207 | } 208 | ``` 209 | 210 |
211 | 212 |
213 | 214 |

215 | How to handle a Recovery Action (since v0.2) 216 |

217 |
218 | 219 | Of course you can alternatively inline this logic into app. 220 | 221 | ```tsx 222 | import { useIdentityContext } from 'react-netlify-identity'; 223 | import { 224 | BrowserRouter as Router, 225 | Switch, 226 | Route, 227 | useLocation, 228 | useHistory, 229 | } from 'react-router-dom'; 230 | 231 | export default function App() { 232 | const { isLoggedIn } = useIdentityContext(); 233 | 234 | return ( 235 | 236 | 237 | 238 | {isLoggedIn ? ( 239 | <> 240 | 241 | } /> 242 | 243 | ) : ( 244 | <> 245 | 246 | 247 | 248 | {/* etc */} 249 | 250 | } /> 251 | 252 | )} 253 | 254 | 255 | ); 256 | } 257 | 258 | function CatchNetlifyRecoveryNullComponent() { 259 | const { 260 | param: { token, type }, 261 | } = useIdentityContext(); 262 | const { replace } = useHistory(); 263 | const { pathname } = useLocation(); 264 | 265 | // important to check for the current pathname here because else you land 266 | // in a infinite loop 267 | if (token && type === 'recovery' && pathname === '/') { 268 | replace(`/recovery`, { token }); 269 | } 270 | 271 | return null; 272 | } 273 | 274 | function RecoveryPage() { 275 | const { 276 | location: { state }, 277 | } = useHistory(); 278 | // this state _might_ not be needed, it was needed in my specific implementation 279 | const [token] = useState(state?.token); 280 | 281 | return null; // set new password in a form and call updateUser 282 | } 283 | ``` 284 | 285 |
286 | 287 | ## Lower level API: `useNetlifyIdentity` 288 | 289 | If you'd like to handle your own context yourself, you can use this library as a hook as well: 290 | 291 | ```tsx 292 | function useNetlifyIdentity( 293 | url: string, 294 | onAuthChange: authChangeParam = () => {}, 295 | enableRunRoutes: boolean = true 296 | ): ReactNetlifyIdentityAPI; 297 | ``` 298 | 299 | ## License 300 | 301 | MIT © [sw-yx](https://github.com/sw-yx) 302 | -------------------------------------------------------------------------------- /examples/example-reach-router/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .cache 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/example-reach-router/README.md: -------------------------------------------------------------------------------- 1 | #Netlify Identity + Reach Router by wrapping the goTrue API in a React Hook. 2 | 3 | [Deployed site here](https://unruffled-roentgen-04c3b8.netlify.com/) 4 | 5 | this is a demo of using Netlify Identity with Reach Router by wrapping the goTrue API in a React Hook. 6 | 7 | ⚠️Make sure Netlify Identity is enabled!!! or demo wont work 8 | 9 | Reach Router is used to show authentication as per [Ryan Florence](https://twitter.com/ryanflorence/status/1060361144701833216). 10 | 11 | If you want to fork/deploy this yourself on Netlfiy, click this: 12 | 13 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/netlify/create-react-app-lambda/tree/reachRouterAndGoTrueDemo&stack=cms) 14 | -------------------------------------------------------------------------------- /examples/example-reach-router/functions/async-dadjoke/async-dadjoke.js: -------------------------------------------------------------------------------- 1 | // example of async handler using async-await 2 | // https://github.com/netlify/netlify-lambda/issues/43#issuecomment-444618311 3 | 4 | const fetch = require("node-fetch") 5 | exports.handler = async function handler(event, context) { 6 | try { 7 | const response = await fetch("https://icanhazdadjoke.com", { headers: { Accept: "application/json" } }) 8 | if (!response.ok) { 9 | // NOT res.status >= 200 && res.status < 300 10 | return { statusCode: response.status, body: response.statusText } 11 | } 12 | const data = await response.json() 13 | 14 | return { 15 | statusCode: 200, 16 | body: JSON.stringify({ msg: data.joke }) 17 | } 18 | } catch (err) { 19 | console.log(err) // output to netlify function log 20 | return { 21 | statusCode: 500, 22 | body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/example-reach-router/functions/async-dadjoke/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "async-dadjoke.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "node-fetch": "^2.4.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-reach-router/functions/authEndPoint/authEndPoint.js: -------------------------------------------------------------------------------- 1 | // example of async handler using async-await 2 | // https://github.com/netlify/netlify-lambda/issues/43#issuecomment-444618311 3 | 4 | const fetch = require("node-fetch") 5 | exports.handler = async function handler(event, context) { 6 | if (!context.clientContext && !context.clientContext.identity) { 7 | return { 8 | statusCode: 500, 9 | body: JSON.stringify({ 10 | msg: 11 | "No identity instance detected. Did you enable it? Also, Netlify Identity is not supported on local dev yet." 12 | }) // Could be a custom message or object i.e. JSON.stringify(err) 13 | } 14 | } 15 | const { identity, user } = context.clientContext 16 | try { 17 | const response = await fetch("https://api.chucknorris.io/jokes/random") 18 | if (!response.ok) { 19 | // NOT res.status >= 200 && res.status < 300 20 | return { statusCode: response.status, body: response.statusText } 21 | } 22 | const data = await response.json() 23 | 24 | return { 25 | statusCode: 200, 26 | body: JSON.stringify({ identity, user, msg: data.value }) 27 | } 28 | } catch (err) { 29 | console.log(err) // output to netlify function log 30 | return { 31 | statusCode: 500, 32 | body: JSON.stringify({ msg: err.message }) // Could be a custom message or object i.e. JSON.stringify(err) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/example-reach-router/functions/authEndPoint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "async-dadjoke.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "node-fetch": "^2.4.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-reach-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Playground 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/example-reach-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "start": "parcel index.html", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "@reach/router": "^1.2.1", 12 | "react": "^15.0.0 || ^16.0.0", 13 | "react-app-polyfill": "^1.0.1", 14 | "react-dom": "^15.0.0 || ^16.0.0", 15 | "react-netlify-identity": "^0.1.8" 16 | }, 17 | "alias": { 18 | "react": "../node_modules/react", 19 | "react-dom": "../node_modules/react-dom/profiling", 20 | "react-dom$": "../node_modules/react-dom/profiling", 21 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling" 22 | }, 23 | "devDependencies": { 24 | "@types/reach__router": "^1.2.4", 25 | "@types/react": "^16.8.15", 26 | "@types/react-dom": "^16.8.4", 27 | "parcel": "^1.12.3", 28 | "typescript": "^3.4.5" 29 | }, 30 | "browserslist": [ 31 | "last 1 chrome version", 32 | "not dead" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /examples/example-reach-router/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /examples/example-reach-router/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netlify-labs/react-netlify-identity/9b879404194300851cf7652fd8544747dd01291e/examples/example-reach-router/public/favicon.ico -------------------------------------------------------------------------------- /examples/example-reach-router/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 22 | React App 23 | 24 | 25 | 26 |
27 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/example-reach-router/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-reach-router/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .title { 6 | color: #d44b15; 7 | text-align: center; 8 | background-color: #e5e4e4; 9 | font-family: Lato, serif; 10 | margin-top: 0px; 11 | display: flex; 12 | 13 | align-items: center; 14 | justify-content: center; 15 | } 16 | 17 | .title .italic { 18 | color: #fff; 19 | font-size: 2em; 20 | font-style: italic; 21 | vertical-align: bottom; 22 | margin: 0 -0.3em; 23 | } 24 | pre { 25 | text-align: left; 26 | overflow-x: scroll; 27 | overflow-y: hidden; 28 | } 29 | 30 | form { 31 | text-align: left; 32 | margin: 0 auto; 33 | display: inline-block; 34 | } 35 | form > div { 36 | margin-bottom: 1rem; 37 | } 38 | 39 | nav { 40 | background: #ffe259; /* fallback for old browsers */ 41 | background: -webkit-linear-gradient( 42 | to right, 43 | #ffa751, 44 | #ffe259 45 | ); /* Chrome 10-25, Safari 5.1-6 */ 46 | background: linear-gradient( 47 | to right, 48 | #ffa751, 49 | #ffe259 50 | ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ 51 | padding: 1rem; 52 | font-family: Lato, serif; 53 | font-weight: bold; 54 | margin-bottom: 1rem; 55 | } 56 | 57 | nav a { 58 | color: brown; 59 | text-decoration: none; 60 | } 61 | 62 | /* http://tobiasahlin.com/spinkit/ */ 63 | 64 | .sk-folding-cube { 65 | margin: 20px auto; 66 | width: 40px; 67 | height: 40px; 68 | position: relative; 69 | -webkit-transform: rotateZ(45deg); 70 | transform: rotateZ(45deg); 71 | } 72 | 73 | .sk-folding-cube .sk-cube { 74 | float: left; 75 | width: 50%; 76 | height: 50%; 77 | position: relative; 78 | -webkit-transform: scale(1.1); 79 | -ms-transform: scale(1.1); 80 | transform: scale(1.1); 81 | } 82 | .sk-folding-cube .sk-cube:before { 83 | content: ''; 84 | position: absolute; 85 | top: 0; 86 | left: 0; 87 | width: 100%; 88 | height: 100%; 89 | background-color: #333; 90 | -webkit-animation: sk-foldCubeAngle 2.4s infinite linear both; 91 | animation: sk-foldCubeAngle 2.4s infinite linear both; 92 | -webkit-transform-origin: 100% 100%; 93 | -ms-transform-origin: 100% 100%; 94 | transform-origin: 100% 100%; 95 | } 96 | .sk-folding-cube .sk-cube2 { 97 | -webkit-transform: scale(1.1) rotateZ(90deg); 98 | transform: scale(1.1) rotateZ(90deg); 99 | } 100 | .sk-folding-cube .sk-cube3 { 101 | -webkit-transform: scale(1.1) rotateZ(180deg); 102 | transform: scale(1.1) rotateZ(180deg); 103 | } 104 | .sk-folding-cube .sk-cube4 { 105 | -webkit-transform: scale(1.1) rotateZ(270deg); 106 | transform: scale(1.1) rotateZ(270deg); 107 | } 108 | .sk-folding-cube .sk-cube2:before { 109 | -webkit-animation-delay: 0.3s; 110 | animation-delay: 0.3s; 111 | } 112 | .sk-folding-cube .sk-cube3:before { 113 | -webkit-animation-delay: 0.6s; 114 | animation-delay: 0.6s; 115 | } 116 | .sk-folding-cube .sk-cube4:before { 117 | -webkit-animation-delay: 0.9s; 118 | animation-delay: 0.9s; 119 | } 120 | @-webkit-keyframes sk-foldCubeAngle { 121 | 0%, 122 | 10% { 123 | -webkit-transform: perspective(140px) rotateX(-180deg); 124 | transform: perspective(140px) rotateX(-180deg); 125 | opacity: 0; 126 | } 127 | 25%, 128 | 75% { 129 | -webkit-transform: perspective(140px) rotateX(0deg); 130 | transform: perspective(140px) rotateX(0deg); 131 | opacity: 1; 132 | } 133 | 90%, 134 | 100% { 135 | -webkit-transform: perspective(140px) rotateY(180deg); 136 | transform: perspective(140px) rotateY(180deg); 137 | opacity: 0; 138 | } 139 | } 140 | 141 | @keyframes sk-foldCubeAngle { 142 | 0%, 143 | 10% { 144 | -webkit-transform: perspective(140px) rotateX(-180deg); 145 | transform: perspective(140px) rotateX(-180deg); 146 | opacity: 0; 147 | } 148 | 25%, 149 | 75% { 150 | -webkit-transform: perspective(140px) rotateX(0deg); 151 | transform: perspective(140px) rotateX(0deg); 152 | opacity: 1; 153 | } 154 | 90%, 155 | 100% { 156 | -webkit-transform: perspective(140px) rotateY(180deg); 157 | transform: perspective(140px) rotateY(180deg); 158 | opacity: 0; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /examples/example-reach-router/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Router, Link, navigate } from '@reach/router'; 3 | import './App.css'; 4 | import { 5 | useIdentityContext, 6 | IdentityContextProvider, 7 | // Settings, 8 | } from 'react-netlify-identity'; 9 | import useLoading from './useLoading'; 10 | 11 | type MaybePathProps = { path?: string }; 12 | function PrivateRoute( 13 | props: React.PropsWithoutRef< 14 | MaybePathProps & { as: React.ComponentType } 15 | > 16 | ) { 17 | const identity = useIdentityContext(); 18 | let { as: Comp } = props; 19 | return identity.user ? ( 20 | 21 | ) : ( 22 |
23 |

You are trying to view a protected page. Please log in

24 | 25 |
26 | ); 27 | } 28 | 29 | // eslint-disable-next-line 30 | function Login({ }: MaybePathProps) { 31 | const { 32 | loginUser, 33 | signupUser, 34 | settings, 35 | loginProvider, 36 | } = useIdentityContext(); 37 | const formRef = React.useRef(null!); 38 | const [msg, setMsg] = React.useState(''); 39 | const [isLoading, load] = useLoading(); 40 | const signup = () => { 41 | const email = formRef.current.email.value; 42 | const password = formRef.current.password.value; 43 | load( 44 | signupUser(email, password, { 45 | data: 'signed up thru react-netlify-identity', 46 | }) 47 | ) 48 | .then(user => { 49 | console.log('Success! Signed up', user); 50 | navigate('/dashboard'); 51 | }) 52 | .catch(err => void console.error(err) || setMsg('Error: ' + err.message)); 53 | }; 54 | return ( 55 |
{ 58 | e.preventDefault(); 59 | const email = formRef.current.email.value; 60 | const password = formRef.current.password.value; 61 | load(loginUser(email, password)) 62 | .then(user => { 63 | console.log('Success! Logged in', user); 64 | navigate('/dashboard'); 65 | }) 66 | .catch( 67 | err => void console.error(err) || setMsg('Error: ' + err.message) 68 | ); 69 | }} 70 | > 71 |
72 | 76 |
77 |
78 | 82 |
83 | {isLoading ? ( 84 | 85 | ) : ( 86 |
87 | 88 | 89 | {msg &&
{msg}
} 90 |
91 | )} 92 | {settings &&
{JSON.stringify(settings, null, 2)}
} 93 | {settings && settings.external.bitbucket && ( 94 | 97 | )} 98 | {settings && settings.external.github && ( 99 | 106 | )} 107 | {settings && settings.external.gitlab && ( 108 | 115 | )} 116 | {settings && settings.external.google && ( 117 | 124 | )} 125 | 126 | ); 127 | } 128 | 129 | // eslint-disable-next-line 130 | function Home({ }: MaybePathProps) { 131 | return ( 132 |
133 |

Welcome to the Home page!

134 |

135 | this is a Public Page, not behind an authentication wall 136 |

137 |
138 |
139 | 142 | Deploy to Netlify 146 | 147 |
148 | This demo is{' '} 149 | 150 | Open Source. 151 | {' '} 152 |
153 |
154 | ); 155 | } 156 | // eslint-disable-next-line 157 | function About({ }: MaybePathProps) { 158 | return
About
; 159 | } 160 | // eslint-disable-next-line 161 | function Dashboard({ }: MaybePathProps) { 162 | const props = useIdentityContext(); 163 | const { isConfirmedUser, authedFetch } = props; 164 | const [isLoading, load] = useLoading(); 165 | const [msg, setMsg] = React.useState('Click to load something'); 166 | const handler = () => { 167 | load(authedFetch.get('/.netlify/functions/authEndPoint')).then(setMsg); 168 | }; 169 | return ( 170 |
171 |

This is a Protected Dashboard!

172 | {!isConfirmedUser && ( 173 |
174 |           You have not confirmed your email. Please confirm it before you ping
175 |           the API.
176 |         
177 | )} 178 |
179 |
180 |

You can try pinging our authenticated API here.

181 |

182 | If you are logged in, you should be able to see a `user` info here. 183 |

184 | 185 | {isLoading ? :
{JSON.stringify(msg, null, 2)}
} 186 |
187 |
188 | ); 189 | } 190 | 191 | function Spinner() { 192 | return ( 193 |
194 |
195 |
196 |
197 |
198 |
199 | ); 200 | } 201 | function Nav() { 202 | const { isLoggedIn } = useIdentityContext(); 203 | return ( 204 | 211 | ); 212 | } 213 | function Logout() { 214 | const { logoutUser } = useIdentityContext(); 215 | return ; 216 | } 217 | 218 | function App() { 219 | // TODO: SUPPLY A URL EITHER FROM ENVIRONMENT VARIABLES OR SOME OTHER STRATEGY 220 | // e.g. 'https://unruffled-roentgen-04c3b8.netlify.com' 221 | const domainToUse = 222 | new URL(window.location.origin).hostname === 'localhost' 223 | ? 'http://react-netlify-identity.netlify.com' 224 | : window.location.origin; 225 | const [url, setUrl] = React.useState(domainToUse); 226 | return ( 227 | 228 |
229 |
230 |

231 | Netlify Identity 232 | & Reach Router 233 |

234 | 259 |
260 |
268 |
last updated react-netlify-identity v0.1.7
269 |
270 | ); 271 | } 272 | 273 | export default App; 274 | -------------------------------------------------------------------------------- /examples/example-reach-router/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /examples/example-reach-router/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /examples/example-reach-router/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/example-reach-router/src/useLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | export default function useLoading() { 4 | const [isLoading, setState] = React.useState(false) 5 | const mount = React.useRef(false) 6 | React.useEffect(() => { 7 | mount.current = true 8 | return () => void (mount.current = false) 9 | }, []) 10 | function load(aPromise: Promise) { 11 | setState(true) 12 | return aPromise.finally(() => mount.current && setState(false)) 13 | } 14 | return [isLoading, load] as [boolean, (aPromise: Promise) => Promise] 15 | } 16 | -------------------------------------------------------------------------------- /examples/example-reach-router/src/useLocalState.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | const noop: Function = () => {} 4 | export default function useLocalStorage(key: string, optionalCallback = noop) { 5 | const [state, setState] = React.useState(null) 6 | React.useEffect(() => { 7 | // chose to make this async 8 | const existingValue = localStorage.getItem(key) 9 | if (existingValue) { 10 | const parsedValue = JSON.parse(existingValue) 11 | setState(parsedValue) 12 | optionalCallback(parsedValue) 13 | } 14 | }, []) 15 | const removeItem = () => { 16 | setState(null) 17 | localStorage.removeItem(key) 18 | optionalCallback(null) 19 | } 20 | const setItem = (obj: any) => { 21 | setState(obj) 22 | localStorage.setItem(key, JSON.stringify(obj)) 23 | optionalCallback(obj) 24 | } 25 | return [state, setItem, removeItem] 26 | } 27 | -------------------------------------------------------------------------------- /examples/example-reach-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": false, 4 | "target": "es5", 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "noImplicitAny": false, 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "removeComments": true, 12 | "strictNullChecks": true, 13 | "preserveConstEnums": true, 14 | "sourceMap": true, 15 | "lib": ["es2015", "es2016", "dom"], 16 | "baseUrl": ".", 17 | "types": ["node"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/example-react-router/README.md: -------------------------------------------------------------------------------- 1 | # React Netlify Identity Example 2 | 3 | This is an example of using `react-netlify-identity` with: 4 | 5 | - TypeScript 6 | - React 7 | - React Router 8 | - Netlify Identity 9 | 10 | Additionally, styling with: 11 | 12 | - styled-components 13 | 14 | Deployed demo site [here](https://react-netlify-identity-example.netlify.com/). 15 | -------------------------------------------------------------------------------- /examples/example-react-router/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-netlify-identity-example", 3 | "version": "1.0.0", 4 | "author": "Lewis Llobera ", 5 | "license": "MIT", 6 | "dependencies": { 7 | "react": "^16.10.1", 8 | "react-dom": "^16.10.1", 9 | "react-netlify-identity": "^0.1.9", 10 | "react-router-dom": "^5.1.2" 11 | }, 12 | "devDependencies": { 13 | "@types/react": "^16.9.4", 14 | "@types/react-dom": "^16.9.1", 15 | "@types/react-router-dom": "^5.1.0", 16 | "styled-components": "^4.4.0", 17 | "typescript": "^3.6.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/example-react-router/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Redirect, Route, Switch } from 'react-router-dom'; 3 | import { 4 | IdentityContextProvider, 5 | useIdentityContext, 6 | } from 'react-netlify-identity'; 7 | 8 | import { GlobalStyles } from './components'; 9 | import { CreateAccount, Home, LogIn, Welcome } from './views'; 10 | 11 | interface Props { 12 | component: React.FunctionComponent; 13 | exact?: boolean; 14 | path: string; 15 | } 16 | 17 | const PublicRoute: React.FunctionComponent = (props: Props) => { 18 | const { isLoggedIn } = useIdentityContext(); 19 | return isLoggedIn ? : ; 20 | }; 21 | 22 | const PrivateRoute: React.FunctionComponent = (props: Props) => { 23 | const { isLoggedIn } = useIdentityContext(); 24 | return isLoggedIn ? : ; 25 | }; 26 | 27 | export const App: React.FunctionComponent = () => { 28 | const url = 'https://react-netlify-identity-example.netlify.com'; 29 | 30 | return ( 31 | <> 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /examples/example-react-router/src/assets/googleLogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | btn_google_dark_normal_ios 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/example-react-router/src/assets/index.ts: -------------------------------------------------------------------------------- 1 | import googleLogo from './googleLogo.svg'; 2 | 3 | export { googleLogo }; 4 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/AuthOption/AuthOption.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const AuthOption = styled.div` 4 | align-items: center; 5 | display: flex; 6 | flex-direction: column; 7 | margin: 0 auto 10vh auto; 8 | width: 100%; 9 | `; 10 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/AuthOption/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthOption } from './AuthOption.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/AuthText/AuthText.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const AuthText = styled.p` 4 | border-bottom: 1px solid lightgray; 5 | display: block; 6 | font-weight: 700; 7 | line-height: 1.4; 8 | margin-bottom: 3vh; 9 | text-align: center; 10 | width: 100%; 11 | `; 12 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/AuthText/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthText } from './AuthText.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Button/Button.styles.ts: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components'; 2 | 3 | import { BREAKPOINT } from '../../constants/constants'; 4 | 5 | const animation = keyframes` 6 | from { 7 | transform: scale(1); 8 | } 9 | to { 10 | transform: scale(0.9); 11 | } 12 | `; 13 | 14 | interface Props { 15 | secondary?: boolean; 16 | } 17 | 18 | export const Button = styled.button` 19 | align-items: center; 20 | background-color: ${(props: Props): string => 21 | props.secondary ? 'transparent' : 'var(--color-accent)'}; 22 | border-radius: var(--radius-l); 23 | color: var(--color-dark); 24 | display: flex; 25 | font-weight: 700; 26 | height: 52px; 27 | justify-content: center; 28 | margin: 0 auto; 29 | transition: 0.5s; 30 | width: 300px; 31 | 32 | &:disabled { 33 | cursor: not-allowed; 34 | opacity: 0.5; 35 | } 36 | 37 | :active { 38 | animation: ${animation} 0.3s cubic-bezier(0.19, 1, 0.22, 1); 39 | } 40 | 41 | @media (max-width: ${BREAKPOINT}px) { 42 | width: 280px; 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | export { Button } from './Button.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/ButtonGoogle/ButtonGoogle.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { Button } from '../../components'; 4 | 5 | export const Logo = styled.img` 6 | margin-right: 24px; 7 | `; 8 | 9 | export const StyledButton = styled(Button)` 10 | background-color: #4285f4; 11 | background-image: none; 12 | color: #ffffff; 13 | `; 14 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/ButtonGoogle/ButtonGoogle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useIdentityContext } from 'react-netlify-identity'; 3 | 4 | import { Logo, StyledButton } from './ButtonGoogle.styles'; 5 | import { googleLogo } from '../../assets'; 6 | 7 | interface Props { 8 | children: string; 9 | } 10 | 11 | export const ButtonGoogle: React.FunctionComponent = (props: Props) => { 12 | const { loginProvider } = useIdentityContext(); 13 | 14 | const logInWithGoogle = (): void => { 15 | loginProvider('google'); 16 | }; 17 | 18 | return ( 19 | 20 | 21 | {props.children} 22 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/ButtonGoogle/index.ts: -------------------------------------------------------------------------------- 1 | export { ButtonGoogle } from './ButtonGoogle'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Container/Container.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { PAGE_WIDTH } from '../../constants/constants'; 4 | 5 | interface Props { 6 | between?: boolean; 7 | paddingBottom?: string; 8 | partial?: boolean; 9 | row?: boolean; 10 | } 11 | 12 | export const Container = styled.div` 13 | align-items: center; 14 | display: flex; 15 | flex-direction: ${(props: Props): string => (props.row ? 'row' : 'column')}; 16 | justify-content: ${(props: Props): string => 17 | props.between ? 'space-between' : 'center'}; 18 | height: ${(props: Props): string => (props.partial ? '80vh' : '100%')}; 19 | margin-left: auto; 20 | margin-right: auto; 21 | max-width: ${PAGE_WIDTH}px; 22 | padding-bottom: ${(props: Props): string => { 23 | switch (props.paddingBottom) { 24 | case 'large': 25 | return '200px'; 26 | case 'medium': 27 | return '120px'; 28 | default: 29 | return 'auto'; 30 | } 31 | }}; 32 | padding-left: var(--padding); 33 | padding-right: var(--padding); 34 | width: 100%; 35 | `; 36 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Container/index.ts: -------------------------------------------------------------------------------- 1 | export { Container } from './Container.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Form/Form.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { BREAKPOINT } from '../../constants/constants'; 4 | 5 | interface Props { 6 | narrow?: boolean; 7 | } 8 | 9 | export const Form = styled.form` 10 | backdrop-filter: blur(2px); 11 | background-color: var(--color-background-translucent); 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: center; 15 | width: ${(props: Props): string => (props.narrow ? '60%' : '100%')}; 16 | 17 | @media (max-width: ${BREAKPOINT}px) { 18 | width: 100%; 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Form/index.ts: -------------------------------------------------------------------------------- 1 | export { Form } from './Form.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/GlobalStyles/GlobalStyles.styles.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | 3 | import { BREAKPOINT } from '../../constants/constants'; 4 | 5 | export const GlobalStyles = createGlobalStyle` 6 | :root { 7 | --color-accent: hsl(165, 100%, 50%); 8 | --color-dark: hsl(0, 0%, 10%); 9 | --color-error: hsl(343, 100%, 45%); 10 | --color-light: hsl(70, 0%, 95%); 11 | --color-light-translucent: hsla(70, 0%, 95%, 0.85); 12 | --font-size-xxs: 15px; 13 | --font-size-xs: 17px; 14 | --font-size-s: 18px; 15 | --font-size-m: 22px; 16 | --font-size-l: 30px; 17 | --font-size-xl: 46px; 18 | --font-size-xxl: 60px; 19 | --padding: 4%; 20 | --radius-l: 10px; 21 | --radius-m: 6px; 22 | } 23 | 24 | html 25 | body { 26 | background-color: var(--color-light); 27 | color: var(--color-dark); 28 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 29 | font-size: var(--font-size-s); 30 | padding-top: 80px; 31 | -webkit-touch-callout: none; 32 | -webkit-user-select: none; 33 | 34 | @media (max-width: ${BREAKPOINT}px) { 35 | font-size: var(--font-size-xs); 36 | } 37 | } 38 | 39 | a { 40 | color: inherit; 41 | text-decoration: none; 42 | } 43 | 44 | [contenteditable] { 45 | user-select: text; 46 | } 47 | 48 | /* CSS Reset */ 49 | 50 | * { 51 | box-sizing: border-box; 52 | -webkit-box-sizing: border-box; 53 | -webkit-tap-highlight-color: hsla(0,0%,0%,0); 54 | -webkit-tap-highlight-color: transparent; 55 | } 56 | 57 | html, 58 | body { 59 | margin: 0; 60 | height: 100%; 61 | width: 100%; 62 | -webkit-font-smoothing: antialiased; 63 | -moz-osx-font-smoothing: grayscale; 64 | } 65 | 66 | h1, 67 | h2, 68 | h3, 69 | h4, 70 | h5, 71 | h6, 72 | p { 73 | font-size: inherit; 74 | font-weight: inherit; 75 | margin: 0; 76 | margin-block-end: 0; 77 | margin-block-start: 0; 78 | margin-inline-end: 0; 79 | margin-inline-start: 0; 80 | -webkit-margin-after: 0; 81 | -webkit-margin-before: 0; 82 | -webkit-margin-end: 0; 83 | -webkit-margin-start: 0; 84 | } 85 | 86 | button { 87 | background-color: inherit; 88 | border: none; 89 | color: inherit; 90 | cursor: pointer; 91 | display: inline-block; 92 | font-family: inherit; 93 | font-size: inherit; 94 | margin: 0; 95 | padding: 0; 96 | text-align: center; 97 | text-decoration: none; 98 | -webkit-appearance: none; 99 | -moz-appearance: none; 100 | } 101 | 102 | button:focus { 103 | outline: 0; 104 | } 105 | 106 | input { 107 | border-color: initial; 108 | border-image: initial; 109 | border-style: none; 110 | color: inherit; 111 | display: block; 112 | font: inherit; 113 | padding: 10px; 114 | margin: 0; 115 | width: 100%; 116 | } 117 | 118 | input:focus { 119 | outline: 0; 120 | } 121 | 122 | input:not([type=checkbox]):not([type=radio]) { 123 | appearance: none; 124 | } 125 | 126 | ::-webkit-scrollbar { 127 | display: none; 128 | } 129 | `; 130 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/GlobalStyles/index.ts: -------------------------------------------------------------------------------- 1 | export { GlobalStyles } from './GlobalStyles.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Header/Header.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { BREAKPOINT } from '../../constants/constants'; 4 | 5 | export const FixedBar = styled.div` 6 | background-color: var(--color-light-translucent); 7 | backdrop-filter: blur(2px); 8 | height: 60px; 9 | left: 0; 10 | padding: 10px 0; 11 | position: fixed; 12 | right: 0; 13 | top: 0; 14 | z-index: 2; 15 | `; 16 | 17 | export const Title = styled.h1` 18 | font-size: var(--font-size-xl); 19 | font-weight: 700; 20 | margin-top: -5px; 21 | 22 | @media (max-width: ${BREAKPOINT}px) { 23 | font-size: var(--font-size-l); 24 | } 25 | `; 26 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { FixedBar, Title } from './Header.styles'; 4 | import { Container } from '../../components'; 5 | 6 | interface Props { 7 | name?: string; 8 | } 9 | 10 | export const Header: React.FunctionComponent = (props: Props) => { 11 | if (props.name) { 12 | return ( 13 | 14 | 15 | {props.name} 16 | 17 | 18 | ); 19 | } else { 20 | return ( 21 | 22 | 23 | Home 24 | 25 | 26 | ); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from './Header'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Input/Input.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const Input = styled.input` 4 | background-color: white; 5 | border: 3px solid transparent; 6 | border-radius: var(--radius-m); 7 | height: 50px; 8 | margin: 0 auto 15px auto; 9 | padding: 0px 20px; 10 | 11 | :focus { 12 | border: 3px solid var(--color-accent-light); 13 | transition: border 0.2s ease-in; 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Input/index.ts: -------------------------------------------------------------------------------- 1 | export { Input } from './Input.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Label/Label.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { BREAKPOINT } from '../../constants/constants'; 4 | 5 | export const Label = styled.label` 6 | color: var(--color-text-lighter); 7 | font-size: var(--font-size-s); 8 | margin-bottom: 3px; 9 | 10 | @media (max-width: ${BREAKPOINT}px) { 11 | font-size: var(--font-size-xs); 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/Label/index.ts: -------------------------------------------------------------------------------- 1 | export { Label } from './Label.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/TextError/TextError.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | export const TextError = styled.p` 4 | color: var(--color-error); 5 | font-weight: 700; 6 | line-height: 1.4; 7 | margin-top: 3vh; 8 | text-align: center; 9 | `; 10 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/TextError/index.ts: -------------------------------------------------------------------------------- 1 | export { TextError } from './TextError.styles'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { AuthOption } from './AuthOption'; 2 | export { AuthText } from './AuthText'; 3 | export { Button } from './Button'; 4 | export { ButtonGoogle } from './ButtonGoogle'; 5 | export { Container } from './Container'; 6 | export { Form } from './Form'; 7 | export { GlobalStyles } from './GlobalStyles'; 8 | export { Header } from './Header'; 9 | export { Input } from './Input'; 10 | export { Label } from './Label'; 11 | export { TextError } from './TextError'; 12 | -------------------------------------------------------------------------------- /examples/example-react-router/src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const BREAKPOINT = 600; 2 | 3 | export const PAGE_WIDTH = 800; 4 | -------------------------------------------------------------------------------- /examples/example-react-router/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /examples/example-react-router/src/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React Netlify Identity Example 7 | 11 | 12 | 13 | 14 |
15 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /examples/example-react-router/src/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: / 4 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/CreateAccount/CreateAccount.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { BREAKPOINT } from '../../constants/constants'; 4 | 5 | export const PasswordTip = styled.span` 6 | color: var(--color-dark-lighter); 7 | font-size: var(--font-size-xs); 8 | 9 | @media (max-width: ${BREAKPOINT}px) { 10 | font-size: var(--font-size-xxs); 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/CreateAccount/CreateAccount.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import { useIdentityContext } from 'react-netlify-identity'; 4 | 5 | import { PasswordTip } from './CreateAccount.styles'; 6 | import { 7 | AuthOption, 8 | AuthText, 9 | Button, 10 | ButtonGoogle, 11 | Container, 12 | Form, 13 | Input, 14 | Label, 15 | Header, 16 | TextError, 17 | } from '../../components'; 18 | 19 | export const CreateAccount: React.FunctionComponent = () => { 20 | const { loginUser, signupUser } = useIdentityContext(); 21 | const [error, setError] = useState(false); 22 | const emailInput = useRef(null!); 23 | const passwordInput = useRef(null!); 24 | const signUpButton = useRef(null!); 25 | 26 | useEffect(() => { 27 | signUpButton.current.disabled = true; 28 | }, [emailInput, passwordInput]); 29 | 30 | const passwordPattern = /^.{6,}$/; 31 | 32 | const handleChange = (): void => { 33 | const email = emailInput.current.value; 34 | const password = passwordInput.current.value; 35 | if (email && passwordPattern.test(password)) { 36 | signUpButton.current.disabled = false; 37 | } else { 38 | signUpButton.current.disabled = true; 39 | } 40 | }; 41 | 42 | const signUp = (event: React.FormEvent): void => { 43 | event.preventDefault(); 44 | const email = emailInput.current.value; 45 | const password = passwordInput.current.value; 46 | signupUser(email, password, {}) 47 | .then(() => { 48 | loginUser(email, password, true); 49 | ; 50 | }) 51 | .catch((error) => { 52 | setError(true); 53 | console.log(error); 54 | }); 55 | }; 56 | 57 | return ( 58 | <> 59 |
60 | 61 | 62 | Sign up with email: 63 |
64 | 65 | 71 | 74 | 80 | {error ? ( 81 | 82 | The email and/or password seems to be incorrect. Please check it 83 | and try again. 84 | 85 | ) : null} 86 | 89 |
90 |
91 | 92 | Or sign up with Google: 93 | Sign up with Google 94 | 95 |
96 | 97 | ); 98 | }; 99 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/CreateAccount/index.ts: -------------------------------------------------------------------------------- 1 | export { CreateAccount } from './CreateAccount'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/Home/Home.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { Button } from '../../components'; 4 | 5 | export const LogOutButton = styled(Button)` 6 | color: var(--color-error); 7 | margin-top: 10vh; 8 | `; 9 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/Home/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useIdentityContext } from 'react-netlify-identity'; 3 | import { Redirect } from 'react-router-dom'; 4 | 5 | import { Container, Header } from '../../components'; 6 | import { LogOutButton } from './Home.styles'; 7 | 8 | export const Home: React.FunctionComponent = () => { 9 | const { user, logoutUser } = useIdentityContext(); 10 | 11 | const logOut = (): void => { 12 | logoutUser(); 13 | ; 14 | }; 15 | 16 | return ( 17 | <> 18 |
19 | 20 |

21 | Hello {JSON.stringify(user!.email)} 22 |

23 | 24 | Log out 25 | 26 |
27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/Home/index.ts: -------------------------------------------------------------------------------- 1 | export { Home } from './Home'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/LogIn/LogIn.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { Redirect } from 'react-router-dom'; 3 | import { useIdentityContext } from 'react-netlify-identity'; 4 | 5 | import { 6 | AuthOption, 7 | AuthText, 8 | Button, 9 | ButtonGoogle, 10 | Container, 11 | Form, 12 | Input, 13 | Label, 14 | Header, 15 | TextError, 16 | } from '../../components'; 17 | 18 | export const LogIn: React.FunctionComponent = () => { 19 | const { loginUser } = useIdentityContext(); 20 | const [error, setError] = useState(false); 21 | const emailInput = useRef(null!); 22 | const passwordInput = useRef(null!); 23 | const logInButton = useRef(null!); 24 | 25 | useEffect(() => { 26 | logInButton.current.disabled = true; 27 | }, [emailInput, passwordInput]); 28 | 29 | const handleChange = (): void => { 30 | const email = emailInput.current.value; 31 | const password = passwordInput.current.value; 32 | if (email && password) { 33 | logInButton.current.disabled = false; 34 | } else { 35 | logInButton.current.disabled = true; 36 | } 37 | }; 38 | 39 | const logIn = (event: React.FormEvent): void => { 40 | event.preventDefault(); 41 | const email = emailInput.current.value; 42 | const password = passwordInput.current.value; 43 | loginUser(email, password, true) 44 | .then(() => { 45 | ; 46 | }) 47 | .catch((error) => { 48 | setError(true); 49 | console.log(error); 50 | }); 51 | }; 52 | 53 | return ( 54 | <> 55 |
56 | 57 | 58 | Log in with email: 59 |
60 | 61 | 67 | 68 | 74 | {error ? ( 75 | 76 | The email and/or password seems to be incorrect. Please check it 77 | and try again. 78 | 79 | ) : null} 80 | 83 |
84 |
85 | 86 | Or log in with Google: 87 | Log in with Google 88 | 89 |
90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/LogIn/index.ts: -------------------------------------------------------------------------------- 1 | export { LogIn } from './LogIn'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/Welcome/Welcome.styles.ts: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | import { BREAKPOINT } from '../../constants/constants'; 4 | 5 | export const Intro = styled.h1` 6 | font-weight: 700; 7 | font-size: var(--font-size-xxl); 8 | margin-bottom: 2vh; 9 | text-align: center; 10 | 11 | @media (max-width: ${BREAKPOINT}px) { 12 | font-size: var(--font-size-xl); 13 | } 14 | `; 15 | 16 | export const Layout = styled.div` 17 | display: flex; 18 | justify-content: center; 19 | width: 100%; 20 | `; 21 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/Welcome/Welcome.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import { Intro, Layout } from './Welcome.styles'; 5 | import { Button, Container } from '../../components'; 6 | 7 | export const Welcome: React.FunctionComponent = () => { 8 | return ( 9 | <> 10 | 11 | React Netlify Identity Example 12 |

13 | This is made with: TypeScript, React, React Router, Netlify Identity, 14 | and styled-components. 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/Welcome/index.ts: -------------------------------------------------------------------------------- 1 | export { Welcome } from './Welcome'; 2 | -------------------------------------------------------------------------------- /examples/example-react-router/src/views/index.tsx: -------------------------------------------------------------------------------- 1 | export { CreateAccount } from './CreateAccount'; 2 | export { Home } from './Home'; 3 | export { LogIn } from './LogIn'; 4 | export { Welcome } from './Welcome'; 5 | -------------------------------------------------------------------------------- /examples/example-react-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "isolatedModules": true, 6 | "jsx": "react", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "es5" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | # example netlify.toml 2 | [build] 3 | command = "cd example && mkdir dist && cp public/_redirects dist/_redirects && yarn && SKIP_PREFLIGHT_CHECK=true yarn build" 4 | functions = "example/functions" 5 | publish = "example/dist" 6 | 7 | ## Uncomment to use this redirect for Single Page Applications like create-react-app. 8 | ## Not needed for static site generators. 9 | #[[redirects]] 10 | # from = "/*" 11 | # to = "/index.html" 12 | # status = 200 13 | 14 | ## (optional) Settings for Netlify Dev 15 | ## https://github.com/netlify/netlify-dev-plugin#project-detection 16 | #[dev] 17 | # command = "yarn start" # Command to start your dev server 18 | # port = 3000 # Port that the dev server will be listening on 19 | # publish = "dist" # Folder with the static content for _redirect file 20 | 21 | ## more info on configuring this file: https://www.netlify.com/docs/netlify-toml-reference/ 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-netlify-identity", 3 | "version": "0.2.7", 4 | "description": "use netlify identity with react", 5 | "author": "sw-yx", 6 | "license": "MIT", 7 | "repository": "https://www.github.com/sw-yx/react-netlify-identity", 8 | "main": "dist/index.js", 9 | "umd:main": "dist/react-netlify-identity.umd.production.js", 10 | "module": "dist/react-netlify-identity.esm.js", 11 | "typings": "dist/index.d.ts", 12 | "jsnext:main": "dist/index.es.js", 13 | "engines": { 14 | "node": ">=8", 15 | "npm": ">=5" 16 | }, 17 | "scripts": { 18 | "start": "tsdx watch", 19 | "build": "tsdx build", 20 | "unused_prepare": "yarn run build", 21 | "version": "yarn run build && auto-changelog -p --template keepachangelog && git add .", 22 | "prepublishOnly": "git push && git push --tags && gh-release", 23 | "predeploy": "cd example && yarn install && yarn run build", 24 | "deploy": "gh-pages -d example/build" 25 | }, 26 | "dependencies": { 27 | "gotrue-js": "^0.9.25" 28 | }, 29 | "peerDependencies": { 30 | "react": "^15.0.0 || ^16.0.0", 31 | "react-dom": "^15.0.0 || ^16.0.0" 32 | }, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "pretty-quick --staged" 36 | } 37 | }, 38 | "prettier": { 39 | "printWidth": 80, 40 | "semi": true, 41 | "singleQuote": true, 42 | "trailingComma": "es5" 43 | }, 44 | "devDependencies": { 45 | "@svgr/rollup": "^5.0.1", 46 | "@types/jest": "^24.0.25", 47 | "@types/react": "^16.9.17", 48 | "@types/react-dom": "^16.9.4", 49 | "auto-changelog": "^1.16.2", 50 | "gh-release": "^3.5.0", 51 | "husky": "^4.0.9", 52 | "prettier": "^1.19.1", 53 | "pretty-quick": "^2.0.1", 54 | "react": "^16.12.0", 55 | "react-dom": "^16.12.0", 56 | "tsdx": "^0.12.3", 57 | "typescript": "^3.7.4" 58 | }, 59 | "files": [ 60 | "dist" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | useState, 3 | useMemo, 4 | useEffect, 5 | createContext, 6 | useContext, 7 | useCallback, 8 | // types 9 | ReactNode, 10 | } from 'react'; 11 | 12 | import GoTrue, { 13 | User as GoTrueUser, 14 | Settings as GoTrueSettings, 15 | } from 'gotrue-js'; 16 | import { runRoutes } from './runRoutes'; 17 | import { TokenParam, defaultParam } from './token'; 18 | 19 | type authChangeParam = (user?: User) => string | void; 20 | 21 | export type Settings = GoTrueSettings; 22 | export type User = GoTrueUser; 23 | type Provider = 'bitbucket' | 'github' | 'gitlab' | 'google'; 24 | 25 | const defaultSettings = { 26 | autoconfirm: false, 27 | disable_signup: false, 28 | external: { 29 | bitbucket: false, 30 | email: true, 31 | facebook: false, 32 | github: false, 33 | gitlab: false, 34 | google: false, 35 | }, 36 | }; 37 | 38 | const errors = { 39 | noUserFound: 'No current user found - are you logged in?', 40 | noUserTokenFound: 'no user token found', 41 | tokenMissingOrInvalid: 'either no token found or invalid for this purpose', 42 | }; 43 | 44 | export type ReactNetlifyIdentityAPI = { 45 | user: User | undefined; 46 | /** not meant for normal use! you should mostly use one of the other exported methods to update the user instance */ 47 | setUser: (_user: GoTrueUser | undefined) => GoTrueUser | undefined; 48 | isConfirmedUser: boolean; 49 | isLoggedIn: boolean; 50 | signupUser: ( 51 | email: string, 52 | password: string, 53 | data: Object, 54 | directLogin?: boolean 55 | ) => Promise; 56 | loginUser: ( 57 | email: string, 58 | password: string, 59 | remember?: boolean 60 | ) => Promise; 61 | logoutUser: () => Promise; 62 | requestPasswordRecovery: (email: string) => Promise; 63 | recoverAccount: (remember?: boolean) => Promise; 64 | updateUser: (fields: object) => Promise; 65 | getFreshJWT: () => Promise | undefined; 66 | authedFetch: { 67 | get: (endpoint: string, obj?: RequestInit) => Promise; 68 | post: (endpoint: string, obj?: RequestInit) => Promise; 69 | put: (endpoint: string, obj?: RequestInit) => Promise; 70 | delete: (endpoint: string, obj?: RequestInit) => Promise; 71 | }; 72 | _goTrueInstance: GoTrue; 73 | _url: string; 74 | loginProvider: (provider: Provider) => void; 75 | acceptInviteExternalUrl: ( 76 | provider: Provider, 77 | autoRedirect: boolean 78 | ) => string | undefined; 79 | settings: Settings; 80 | param: TokenParam; 81 | verifyToken: () => Promise; 82 | }; 83 | 84 | const [_useIdentityContext, _IdentityCtxProvider] = createCtx< 85 | ReactNetlifyIdentityAPI 86 | >(); 87 | export const useIdentityContext = _useIdentityContext; // we dont want to expose _IdentityCtxProvider 88 | 89 | /** most people should use this provider directly */ 90 | export function IdentityContextProvider({ 91 | url, 92 | children, 93 | onAuthChange = () => {}, 94 | }: { 95 | url: string; 96 | children: ReactNode; 97 | onAuthChange?: authChangeParam; 98 | }) { 99 | /******** SETUP */ 100 | if (!url || !validateUrl(url)) { 101 | // just a safety check in case a JS user tries to skip this 102 | throw new Error( 103 | 'invalid netlify instance URL: ' + 104 | url + 105 | '. Please check the docs for proper usage or file an issue.' 106 | ); 107 | } 108 | const identity = useNetlifyIdentity(url, onAuthChange); 109 | return ( 110 | <_IdentityCtxProvider value={identity}>{children} 111 | ); 112 | } 113 | 114 | /** some people may want to use this as a hook and bring their own contexts */ 115 | export function useNetlifyIdentity( 116 | url: string, 117 | onAuthChange: authChangeParam = () => {}, 118 | enableRunRoutes: boolean = true 119 | ): ReactNetlifyIdentityAPI { 120 | const goTrueInstance = useMemo( 121 | () => 122 | new GoTrue({ 123 | APIUrl: `${url}/.netlify/identity`, 124 | setCookie: true, 125 | }), 126 | [url] 127 | ); 128 | 129 | /******* STATE and EFFECTS */ 130 | 131 | const [user, setUser] = useState( 132 | goTrueInstance.currentUser() || undefined 133 | ); 134 | 135 | const _setUser = useCallback( 136 | (_user: User | undefined) => { 137 | setUser(_user); 138 | onAuthChange(_user); // if someone's subscribed to auth changes, let 'em know 139 | return _user; // so that we can continue chaining 140 | }, 141 | [onAuthChange] 142 | ); 143 | 144 | const [param, setParam] = useState(defaultParam); 145 | 146 | useEffect(() => { 147 | if (enableRunRoutes) { 148 | const param = runRoutes(goTrueInstance, _setUser); 149 | 150 | if (param.token || param.error) { 151 | setParam(param); 152 | } 153 | } 154 | }, []); 155 | 156 | const [settings, setSettings] = useState(defaultSettings); 157 | 158 | useEffect(() => { 159 | goTrueInstance.settings 160 | .bind(goTrueInstance)() 161 | .then(x => setSettings(x)); 162 | }, []); 163 | 164 | /******* OPERATIONS */ 165 | // make sure the Registration preferences under Identity settings in your Netlify dashboard are set to Open. 166 | // https://react-netlify-identity.netlify.com/login#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTY0ODY3MjEsInN1YiI6ImNiZjY5MTZlLTNlZGYtNGFkNS1iOTYzLTQ4ZTY2NDcyMDkxNyIsImVtYWlsIjoic2hhd250aGUxQGdtYWlsLmNvbSIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImdpdGh1YiJ9LCJ1c2VyX21ldGFkYXRhIjp7ImF2YXRhcl91cmwiOiJodHRwczovL2F2YXRhcnMxLmdpdGh1YnVzZXJjb250ZW50LmNvbS91LzY3NjQ5NTc_dj00IiwiZnVsbF9uYW1lIjoic3d5eCJ9fQ.E8RrnuCcqq-mLi1_Q5WHJ-9THIdQ3ha1mePBKGhudM0&expires_in=3600&refresh_token=OyA_EdRc7WOIVhY7RiRw5w&token_type=bearer 167 | /******* external oauth */ 168 | 169 | /** 170 | * @see https://github.com/netlify/gotrue-js/blob/master/src/index.js#L71 171 | */ 172 | const loginProvider = useCallback( 173 | (provider: Provider) => { 174 | const url = goTrueInstance.loginExternalUrl(provider); 175 | window.location.href = url; 176 | }, 177 | [goTrueInstance] 178 | ); 179 | 180 | /** 181 | * @see https://github.com/netlify/gotrue-js/blob/master/src/index.js#L92 182 | */ 183 | const acceptInviteExternalUrl = useCallback( 184 | (provider: Provider, autoRedirect: boolean = true) => { 185 | if (!param.token || param.type !== 'invite') { 186 | console.error(errors.tokenMissingOrInvalid); 187 | return; 188 | } 189 | 190 | const url = goTrueInstance.acceptInviteExternalUrl(provider, param.token); 191 | // clean up consumed token 192 | setParam(defaultParam); 193 | 194 | if (autoRedirect) { 195 | window.location.href = url; 196 | return; 197 | } 198 | 199 | return url; 200 | }, 201 | [goTrueInstance, param] 202 | ); 203 | 204 | /** 205 | * @see https://github.com/netlify/gotrue-js/blob/master/src/index.js#L123 206 | */ 207 | const verifyToken = useCallback(() => { 208 | if (!param.type || !param.token) { 209 | return Promise.reject(errors.tokenMissingOrInvalid); 210 | } 211 | 212 | return goTrueInstance.verify(param.type, param.token).then(user => { 213 | // cleanup consumed token 214 | setParam(defaultParam); 215 | 216 | return user; 217 | }); 218 | }, [goTrueInstance, param]); 219 | 220 | /******* email auth */ 221 | /** 222 | * @see https://github.com/netlify/gotrue-js/blob/master/src/index.js#L50 223 | */ 224 | const signupUser = useCallback( 225 | ( 226 | email: string, 227 | password: string, 228 | data: Object, 229 | directLogin: boolean = true 230 | ) => 231 | goTrueInstance.signup(email, password, data).then(user => { 232 | if (directLogin) { 233 | return _setUser(user); 234 | } 235 | 236 | return user; 237 | }), 238 | [goTrueInstance, _setUser] 239 | ); 240 | 241 | /** 242 | * @see https://github.com/netlify/gotrue-js/blob/master/src/index.js#L57 243 | */ 244 | const loginUser = useCallback( 245 | (email: string, password: string, remember: boolean = true) => 246 | goTrueInstance.login(email, password, remember).then(_setUser), 247 | [goTrueInstance, _setUser] 248 | ); 249 | 250 | /** 251 | * @see https://github.com/netlify/gotrue-js/blob/master/src/index.js#L80 252 | */ 253 | const requestPasswordRecovery = useCallback( 254 | (email: string) => goTrueInstance.requestPasswordRecovery(email), 255 | [goTrueInstance] 256 | ); 257 | 258 | /** 259 | * @see https://github.com/netlify/gotrue-js/blob/master/src/index.js#L87 260 | */ 261 | const recoverAccount = useCallback( 262 | (remember?: boolean) => { 263 | if (!param.token || param.type !== 'recovery') { 264 | return Promise.reject(errors.tokenMissingOrInvalid); 265 | } 266 | 267 | return goTrueInstance 268 | .recover(param.token, remember) 269 | .then(user => { 270 | return _setUser(user); 271 | }) 272 | .finally(() => { 273 | // clean up consumed token 274 | setParam(defaultParam); 275 | }); 276 | }, 277 | [goTrueInstance, _setUser, param] 278 | ); 279 | 280 | /** 281 | * @see https://github.com/netlify/gotrue-js/blob/master/src/user.js#L54 282 | */ 283 | const updateUser = useCallback( 284 | (fields: object) => { 285 | if (!user) { 286 | return Promise.reject(errors.noUserFound); 287 | } 288 | 289 | return user! 290 | .update(fields) // e.g. { email: "example@example.com", password: "password" } 291 | .then(_setUser); 292 | }, 293 | [user] 294 | ); 295 | 296 | /** 297 | * @see https://github.com/netlify/gotrue-js/blob/master/src/user.js#L63 298 | */ 299 | const getFreshJWT = useCallback(() => { 300 | if (!user) { 301 | return Promise.reject(errors.noUserFound); 302 | } 303 | 304 | return user.jwt(); 305 | }, [user]); 306 | 307 | /** 308 | * @see https://github.com/netlify/gotrue-js/blob/master/src/user.js#L71 309 | */ 310 | const logoutUser = useCallback(() => { 311 | if (!user) { 312 | return Promise.reject(errors.noUserFound); 313 | } 314 | 315 | return user.logout().then(() => _setUser(undefined)); 316 | }, [user]); 317 | 318 | const genericAuthedFetch = (method: RequestInit['method']) => ( 319 | endpoint: string, 320 | options: RequestInit = {} 321 | ) => { 322 | if (!user?.token?.access_token) { 323 | return Promise.reject(errors.noUserTokenFound); 324 | } 325 | 326 | const defaultObj = { 327 | headers: { 328 | Accept: 'application/json', 329 | 'Content-Type': 'application/json', 330 | Authorization: 'Bearer ' + user.token.access_token, 331 | }, 332 | }; 333 | const finalObj = Object.assign(defaultObj, { method }, options); 334 | 335 | return fetch(endpoint, finalObj).then(res => 336 | finalObj.headers['Content-Type'] === 'application/json' ? res.json() : res 337 | ); 338 | }; 339 | 340 | const authedFetch = { 341 | get: genericAuthedFetch('GET'), 342 | post: genericAuthedFetch('POST'), 343 | put: genericAuthedFetch('PUT'), 344 | delete: genericAuthedFetch('DELETE'), 345 | }; 346 | 347 | /******* hook API */ 348 | return { 349 | user, 350 | /** not meant for normal use! you should mostly use one of the other exported methods to update the user instance */ 351 | setUser: _setUser, 352 | isConfirmedUser: !!(user && user.confirmed_at), 353 | isLoggedIn: !!user, 354 | signupUser, 355 | loginUser, 356 | logoutUser, 357 | requestPasswordRecovery, 358 | recoverAccount, 359 | updateUser, 360 | getFreshJWT, 361 | authedFetch, 362 | _goTrueInstance: goTrueInstance, 363 | _url: url, 364 | loginProvider, 365 | acceptInviteExternalUrl, 366 | settings, 367 | param, 368 | verifyToken, 369 | }; 370 | } 371 | 372 | /** 373 | * 374 | * 375 | * Utils 376 | * 377 | */ 378 | 379 | function validateUrl(value: string) { 380 | return /^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i.test( 381 | value 382 | ); 383 | } 384 | 385 | // lazy initialize contexts without providing a Nullable type upfront 386 | function createCtx
() { 387 | const ctx = createContext(undefined); 388 | function useCtx() { 389 | const c = useContext(ctx); 390 | if (!c) throw new Error('useCtx must be inside a Provider with a value'); 391 | return c; 392 | } 393 | return [useCtx, ctx.Provider] as const; 394 | } 395 | 396 | // // Deprecated for now 397 | // interface NIProps { 398 | // children: any 399 | // url: string 400 | // onAuthChange?: authChangeParam 401 | // } 402 | // export default function NetlifyIdentity({ children, url, onAuthChange }: NIProps) { 403 | // return children(useNetlifyIdentity(url, onAuthChange)) 404 | // } 405 | -------------------------------------------------------------------------------- /src/runRoutes.tsx: -------------------------------------------------------------------------------- 1 | import GoTrue, { User } from 'gotrue-js'; 2 | import { TokenParam, defaultParam } from './token'; 3 | 4 | /** 5 | * This code runs on every rerender so keep it light 6 | * keep checking the current route and do logic based on the route 7 | * as dictated by netlify identity's communication with us via hashes 8 | */ 9 | 10 | const routes = /(confirmation|invite|recovery|email_change|access)_token=([^&]+)/; 11 | const errorRoute = /error=access_denied&error_description=403/; 12 | 13 | const reduceHashToKeyValue = (hash: string): { [key: string]: string } => 14 | hash.split('&').reduce((carry, pair) => { 15 | const [key, value] = pair.split('='); 16 | 17 | return { ...carry, [key]: value }; 18 | }, {}); 19 | 20 | const hashReplace = /^#\/?/; 21 | 22 | export function runRoutes( 23 | gotrue: GoTrue, 24 | setUser: (value: User) => User | undefined, 25 | remember = true 26 | ): TokenParam { 27 | // early terminate if no hash 28 | // also accounts for document.cookie further down 29 | if (!document?.location?.hash) { 30 | return defaultParam; 31 | } 32 | 33 | const hash = document.location.hash.replace(hashReplace, ''); 34 | 35 | try { 36 | history.pushState( 37 | '', 38 | document.title, 39 | window.location.pathname + window.location.search 40 | ); 41 | } catch (_) { 42 | window.location.href.substr(0, window.location.href.indexOf('#')); 43 | } 44 | 45 | // earliest possible bail on any match 46 | if (hash.match(errorRoute)) { 47 | return { 48 | ...defaultParam, 49 | error: 'access_denied', 50 | status: 403, 51 | }; 52 | } 53 | 54 | const matchesActionHashes = hash.match(routes); 55 | 56 | if (matchesActionHashes) { 57 | const params = reduceHashToKeyValue(hash); 58 | 59 | if (params.confirmation_token) { 60 | gotrue 61 | .confirm(params.confirmation_token, remember) 62 | .then(setUser) 63 | .catch(console.error); 64 | 65 | // dont notify dev as this package does not export its own method for this 66 | return defaultParam; 67 | } 68 | 69 | if (params.access_token) { 70 | document.cookie = `nf_jwt=${params.access_token}`; 71 | 72 | gotrue 73 | .createUser(params, remember) 74 | .then(setUser) 75 | .catch(console.error); 76 | 77 | // also dont notify dev here for the same reasons as above 78 | return defaultParam; 79 | } 80 | 81 | // pass responsibility to dev in all other cases 82 | return { 83 | ...defaultParam, 84 | type: matchesActionHashes[1] as TokenParam['type'], 85 | token: matchesActionHashes[2], 86 | }; 87 | } 88 | 89 | return defaultParam; 90 | } 91 | -------------------------------------------------------------------------------- /src/token.ts: -------------------------------------------------------------------------------- 1 | export type TokenParam = { 2 | token: string | undefined; 3 | type: 'invite' | 'recovery' | 'email_change' | undefined; 4 | error: 'access_denied' | undefined; 5 | status: 403 | undefined; 6 | }; 7 | 8 | export const defaultParam: TokenParam = { 9 | token: undefined, 10 | type: undefined, 11 | error: undefined, 12 | status: undefined, 13 | }; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src", "types"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | "declaration": true, 9 | "sourceMap": true, 10 | "rootDir": "./", 11 | "strict": true, 12 | "noImplicitAny": true, 13 | "strictNullChecks": true, 14 | "strictFunctionTypes": true, 15 | "strictPropertyInitialization": true, 16 | "noImplicitThis": true, 17 | "alwaysStrict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noImplicitReturns": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "moduleResolution": "node", 23 | "baseUrl": "./", 24 | "paths": { 25 | "*": ["src/*", "node_modules/*"] 26 | }, 27 | "jsx": "react", 28 | "esModuleInterop": true 29 | } 30 | } 31 | --------------------------------------------------------------------------------