@tasoskakour/react-use-oauth2 2.0.2 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,36 +2,38 @@
2
2
 
3
3
  ![gh workflow](https://img.shields.io/github/actions/workflow/status/tasoskakour/react-use-oauth2/ci-cd.yml?branch=master) [![npm](https://img.shields.io/npm/v/@tasoskakour/react-use-oauth2.svg?style=svg&logo=npm&label=)](https://www.npmjs.com/package/@tasoskakour/react-use-oauth2)
4
4
 
5
- > 💎 A custom React hook that makes OAuth2 authorization simple. Both for **Implicit Grant** and **Authorization Code** flows.
5
+ > 💎 A custom React hook that makes OAuth2 authorization simple supporting both **Authorization Code** and **Implicit Grant** flows.
6
6
 
7
- ## Features
7
+ ---
8
8
 
9
- - Usage with both `Implicit` and `Authorization Code` grant flows.
10
- - Seamlessly **exchanges code for token** via your backend API URL, for authorization code grant flows.
11
- - Works with **Popup** authorization.
12
- - Provides data and loading/error states via a hook.
13
- - **Persists data** to localStorage and automatically syncs auth state between tabs and/or browser windows.
9
+ ## Features
14
10
 
15
- ## Install
11
+ - Supports **Authorization Code** and **Implicit Grant** flows.
12
+ - **Exchanges code for token** via your backend automatically (for Authorization Code flow).
13
+ - Handles **popup-based** authorization.
14
+ - Provides **data**, **loading**, and **error** states.
15
+ - **Persists auth state** to `localStorage` and **syncs across tabs**.
16
16
 
17
- _Requires `react@18.0.0` or higher_
17
+ ---
18
18
 
19
- ```console
20
- yarn add @tasoskakour/react-use-oauth2
21
- ```
19
+ ## 📦 Installation
22
20
 
23
- or
21
+ _Requires `react@18` or higher._
24
22
 
25
- ```console
26
- npm i @tasoskakour/react-use-oauth2
23
+ ```bash
24
+ npm install @tasoskakour/react-use-oauth2
25
+ # or
26
+ yarn add @tasoskakour/react-use-oauth2
27
27
  ```
28
28
 
29
- ## Usage example
29
+ ---
30
+
31
+ ## 🚀 Usage Example
30
32
 
31
- *For authorization code flow:*
33
+ **Authorization Code Flow Example:**
32
34
 
33
- ```js
34
- import { OAuth2Popup, useOAuth2 } from "@tasoskakour/react-use-oauth2";
35
+ ```tsx
36
+ import { OAuthPopup, useOAuth2 } from "@tasoskakour/react-use-oauth2";
35
37
  import { BrowserRouter, Routes, Route } from "react-router-dom";
36
38
 
37
39
  const Home = () => {
@@ -45,129 +47,163 @@ const Home = () => {
45
47
  url: "https://your-backend/token",
46
48
  method: "POST",
47
49
  },
50
+ state: {
51
+ foo: 'bar',
52
+ customInfo: 'something',
53
+ },
48
54
  onSuccess: (payload) => console.log("Success", payload),
49
- onError: (error_) => console.log("Error", error_)
55
+ onError: (error_) => console.log("Error", error_),
50
56
  });
51
57
 
52
58
  const isLoggedIn = Boolean(data?.access_token); // or whatever...
53
59
 
54
- if (error) {
55
- return <div>Error</div>;
56
- }
57
-
58
- if (loading) {
59
- return <div>Loading...</div>;
60
- }
61
60
 
62
- if (isLoggedIn) {
63
- return (
64
- <div>
65
- <pre>{JSON.stringify(data)}</pre>
66
- <button onClick={logout}>Logout</button>
67
- </div>
68
- )
69
- }
70
-
71
- return (
72
- <button style={{ margin: "24px" }} type="button" onClick={() => getAuth()}>
73
- Login
74
- </button>
61
+ if (loading) return <div>Loading...</div>;
62
+ if (error) return <div>Error</div>;
63
+ if (isLoggedIn) return (
64
+ <div>
65
+ <pre>{JSON.stringify(data)}</pre>
66
+ <button onClick={logout}>Logout</button>
67
+ </div>
75
68
  );
76
- };
77
69
 
78
- const App = () => {
79
70
  return (
80
- <BrowserRouter>
81
- <Routes>
82
- <Route element={<OAuthPopup />} path="/callback" />
83
- <Route element={<Home />} path="/" />
84
- </Routes>
85
- </BrowserRouter>
71
+ <button onClick={getAuth}>Login</button>
86
72
  );
87
73
  };
88
- ```
89
74
 
90
- ##### Example with `exchangeCodeForTokenQueryFn`
91
-
92
- You can also use `exchangeCodeForTokenQueryFn` if you want full control over your query to your backend, e.g if you must send your data as form-urlencoded:
93
- ```js
94
-
95
- const { ... } = useOAuth2({
96
- // ...
97
- // Instead of exchangeCodeForTokenQuery (e.g sending form-urlencoded or similar)...
98
- exchangeCodeForTokenQueryFn: async (callbackParameters) => {
99
- const formBody = [];
100
- for (const key in callbackParameters) {
101
- formBody.push(
102
- `${encodeURIComponent(key)}=${encodeURIComponent(callbackParameters[key])}`
103
- );
104
- }
105
- const response = await fetch(`YOUR_BACKEND_URL`, {
106
- method: 'POST',
107
- body: formBody.join('&'),
108
- headers: {
109
- 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
110
- },
111
- });
112
- if (!response.ok) throw new Error('Failed');
113
- const tokenData = await response.json();
114
- return tokenData;
115
- },.
116
- // ...
117
- })
118
-
75
+ const App = () => (
76
+ <BrowserRouter>
77
+ <Routes>
78
+ <Route path="/" element={<Home />} />
79
+ <Route path="/callback" element={<OAuthPopup />} />
80
+ </Routes>
81
+ </BrowserRouter>
82
+ );
119
83
  ```
120
84
 
121
- ### What is the purpose of `exchangeCodeForTokenQuery` for Authorization Code flows?
85
+ ---
86
+
87
+ ## 📚 Concepts
88
+
89
+ ### 🔹 What is `exchangeCodeForTokenQuery`?
90
+
91
+ When using the **Authorization Code** flow, after receiving an authorization `code`, you must **exchange** it for an **access token**.
92
+ You typically do this **server-side** because it requires your OAuth client secret.
122
93
 
123
- Generally when we're working with authorization code flows, we need to *immediately* **exchange** the retrieved *code* with an actual *access token*, after a successful authorization. Most of the times this is needed for back-end apps, but there are many use cases this is useful for front-end apps as well.
94
+ The `exchangeCodeForTokenQuery` object lets you specify:
95
+ - `url`: Your backend endpoint that performs the code-to-token exchange.
96
+ - `method`: HTTP method (default: `POST`).
97
+ - `headers`: Optional custom headers.
124
98
 
125
- In order for the flow to be accomplished, the 3rd party provider we're authorizing against (e.g Google, Facebook etc), will provide an API call (e.g for Google is `https://oauth2.googleapis.com/token`) that we need to hit in order to exchange the code for an access token. However, this call requires the `client_secret` of your 3rd party app as a parameter to work - a secret that you cannot expose to your front-end app.
99
+ The backend must call the OAuth provider's token endpoint securely.
126
100
 
127
- That's why you need to proxy this call to your back-end and with `exchangeCodeForTokenQuery` object you can provide the schematics of your call e.g `url`, `method` etc. The request parameters that will get passed along as **query parameters** are `{ code, client_id, grant_type, redirect_uri, state }`. By default this will be a **POST** request but you can change it with the `method` property.
101
+ [More about exchanging authorization codes ➡️ (Google docs)](https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code)
128
102
 
103
+ ---
129
104
 
130
- You can read more about "Exchanging authorization code for refresh and access tokens" in [Google OAuth2 documentation](https://developers.google.com/identity/protocols/oauth2/web-server#exchange-authorization-code).
105
+ ### 🔹 Alternative: `exchangeCodeForTokenQueryFn`
106
+
107
+ If you need **full control** (e.g., sending `application/x-www-form-urlencoded` body),
108
+ you can use `exchangeCodeForTokenQueryFn`, a custom async function that manually exchanges the code.
109
+
110
+ ```tsx
111
+ const { getAuth } = useOAuth2({
112
+ exchangeCodeForTokenQueryFn: async (callbackParameters) => {
113
+ const formBody = Object.entries(callbackParameters)
114
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
115
+ .join("&");
116
+
117
+ const response = await fetch(`YOUR_BACKEND_URL`, {
118
+ method: 'POST',
119
+ headers: {
120
+ 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
121
+ },
122
+ body: formBody,
123
+ });
124
+
125
+ if (!response.ok) throw new Error('Failed to exchange code');
126
+ return response.json();
127
+ },
128
+ });
129
+ ```
131
130
 
132
- ### What's the alternative option `exchangeCodeForTokenQueryFn`?
131
+ ---
133
132
 
134
- There could be certain cases where `exchangeCodeForTokenQuery` is not enough and you want full control over how you send the request to your backend. For example you may want to send it as a urlencoded form. With this property you can define your callback function which takes `callbackParameters: object` as a parameter (which includes whatever returned from OAuth2 callback e.g `code, scope, state` etc) and must return a promise with a valid object which will contain all the token data state e.g `access_token, expires_in` etc.
133
+ ### 🔹 What about Implicit Grant flows?
135
134
 
136
- ### What's the case with Implicit Grant flows?
135
+ In an **Implicit Grant** (`responseType: 'token'`), the 3rd-party provider sends back the `access_token` **directly** — no server exchange needed.
137
136
 
138
- With an implicit grant flow things are much simpler as the 3rd-party provider immediately returns the `access_token` to the callback request so there's no need to make any action after that. Just set `responseType=token` to use this flow.
137
+ ---
139
138
 
140
- ### Data persistence
139
+ ### 🔹 Passing custom state
141
140
 
142
- After a successful authorization, data will get persisted to **localStorage** and the state will automatically sync to all tabs/pages of the browser. The storage key the data will be written to will be: `{responseType}-{authorizeUrl}-{clientId}-{scope}`.
141
+ You can also pass a **custom `state`** object into `useOAuth2`, which will:
142
+ - Be securely wrapped under a random key.
143
+ - Be sent during the authorization request.
144
+ - Be automatically extracted and sent back to your backend during token exchange.
143
145
 
144
- If you want to re-trigger the authorization flow just call `getAuth()` function again.
146
+ Example:
145
147
 
146
- **Note**: In case localStorage is throwing an error (e.g user has disabled it) then you can use the `isPersistent` property which - for this case - will be false. Useful if you want to notify the user that the data is only stored in-memory.
148
+ ```tsx
149
+ state: {
150
+ visitedPage: '/checkout',
151
+ customParam: 'something',
152
+ }
153
+ ```
147
154
 
148
- ## API
155
+ Safely preserves context like `{ visitedPage: '/checkout', customInfo: 'xyz' }` across OAuth.
149
156
 
150
- - `function useOAuth2(options): {data, loading, error, getAuth}`
157
+ ---
151
158
 
152
- This is the hook that makes this package to work. `Options` is an object that contains the properties below
159
+ ## 🧠 Data Persistence
153
160
 
154
- - `authorizeUrl` (string): The 3rd party authorization URL (e.g https://accounts.google.com/o/oauth2/v2/auth).
155
- - `clientId` (string): The OAuth2 client id of your application.
156
- - `redirectUri` (string): Determines where the 3rd party API server redirects the user after the user completes the authorization flow. In our [example](#usage-example) the Popup is rendered on that redirectUri.
157
- - `scope` (string - _optional_): A list of scopes depending on your application needs.
158
- - `responseType` (string): Can be either **code** for _code authorization grant_ or **token** for _implicit grant_.
159
- - `extraQueryParameters` (object - _optional_): An object of extra parameters that you'd like to pass to the query part of the authorizeUrl, e.g {audience: "xyz"}.
160
- - `exchangeCodeForTokenQuery` (object): This property is only required when using _code authorization grant_ flow (responseType = code). It's properties are:
161
- - `url` (string - _required_) It specifies the API URL of your server that will get called immediately after the user completes the authorization flow. Read more [here](#what-is-the-purpose-of-exchangecodefortokenserverurl-for-authorization-code-flows).
162
- - `method` (string - _required_): Specifies the HTTP method that will be used for the code-for-token exchange to your server. Defaults to **POST**
163
- - `headers` (object - _optional_): An object of extra parameters that will be used for the code-for-token exchange to your server.
164
- - `exchangeCodeForTokenQueryFn` function(callbackParameters) => Promise\<Object\>: **Instead of using** `exchangeCodeForTokenQuery` to describe the query, you can take full control and provide query function yourself. `callbackParameters` will contain everything returned from the OAUth2 callback e.g `code, state` etc. You must return a promise with a valid object that will represent your final state - data of the auth procedure.
165
- - **onSuccess** (function): Called after a complete successful authorization flow.
166
- - **onError** (function): Called when an error occurs.
161
+ - After login, auth data persists to `localStorage`.
162
+ - Auto-syncs across tabs/windows.
163
+ - The storage key format is:
164
+ ```
165
+ {responseType}-{authorizeUrl}-{clientId}-{scope}
166
+ ```
167
+ - If you want to re-trigger the authorization flow just call `getAuth()` function again.
168
+ - If localStorage is disabled (e.g. by browser settings), the hook falls back to in-memory storage and sets `isPersistent = false`.
167
169
 
168
- **Returns**:
170
+ ---
169
171
 
170
- - `data` (object): Consists of the retrieved auth data and generally will have the shape of `{access_token, token_type, expires_in}` (check [Typescript](#typescript) usage for providing custom shape). If you're using `responseType: code` and `exchangeCodeForTokenQueryFn` this object will contain whatever you returnn from your query function.
172
+ ## 🛠 API
173
+
174
+ ```tsx
175
+ const {
176
+ data,
177
+ loading,
178
+ error,
179
+ getAuth,
180
+ logout,
181
+ isPersistent
182
+ } = useOAuth2(options);
183
+ ```
184
+
185
+ **Options:**
186
+
187
+ | Option | Type | Description |
188
+ |:---|:---|:---|
189
+ | `authorizeUrl` | string | OAuth provider authorization URL (e.g https://accounts.google.com/o/oauth2/v2/auth) |
190
+ | `clientId` | string | Your app's client ID |
191
+ | `redirectUri` | string | Callback URL after authorization |
192
+ | `scope` | string _(optional)_ | Space-separated OAuth scopes |
193
+ | `responseType` | `'code' \| 'token'` | Authorization Code or Implicit Grant flow |
194
+ | `state` | `Record<string, any> \| null \| undefined` | Custom state object (optional) |
195
+ | `extraQueryParameters` | object _(optional)_ | An object of extra parameters that you'd like to pass to the query part of the authorizeUrl, e.g {audience: "xyz"} |
196
+ | `exchangeCodeForTokenQuery` | object | This property is only required when using code authorization grant flow (responseType = code). Its properties are listed below |
197
+ | `exchangeCodeForTokenQuery.url` | string _(required)_ | It specifies the API URL of your server that will get called immediately after the user completes the authorization flow. Read more [here](#-concepts) |
198
+ | `exchangeCodeForTokenQuery.method` | string _(required)_ | Specifies the HTTP method that will be used for the code-for-token exchange to your server. Defaults to **POST** |
199
+ | `exchangeCodeForTokenQuery.headers` | object _(optional)_ | An object of extra parameters that will be used for the code-for-token exchange to your server. |
200
+ | `exchangeCodeForTokenQueryFn` | `function(callbackParameters) => Promise<Object>` _(optional)_ | **Instead of using** `exchangeCodeForTokenQuery` to describe the query, you can take full control and provide query function yourself. `callbackParameters` will contain everything returned from the OAUth2 callback e.g `code, state` etc. You must return a promise with a valid object that will represent your final state - data of the auth procedure. |
201
+ | `onSuccess` | function | Called after a complete successful authorization flow. |
202
+ | `onError` | function | Called when an error occurs. |
203
+
204
+ **Returned fields:**
205
+
206
+ - `data` (object): Consists of the retrieved auth data and generally will have the shape of `{access_token, token_type, expires_in}` (check [Typescript](#-typescript-support) usage for providing custom shape). If you're using `responseType: code` and `exchangeCodeForTokenQueryFn` this object will contain whatever you return from your query function.
171
207
  - `loading` (boolean): Is set to true while the authorization is taking place.
172
208
  - `error` (string): Is set when an error occurs.
173
209
  - `getAuth` (function): Call this function to trigger the authorization flow.
@@ -178,13 +214,15 @@ This is the hook that makes this package to work. `Options` is an object that co
178
214
 
179
215
  - `function OAuthPopup(props)`
180
216
 
181
- This is the component that will be rendered as a window Popup for as long as the authorization is taking place. You need to render this in a place where it does not disrupt the user flow. An ideal place is inside a `Route` component of `react-router-dom` as seen in the [usage example](#usage-example).
217
+ This is the component that will be rendered as a window Popup for as long as the authorization is taking place. You need to render this in a place where it does not disrupt the user flow. An ideal place is inside a `Route` component of `react-router-dom` as seen in the [usage example](#-usage-example).
182
218
 
183
219
  Props consists of:
184
220
 
185
221
  - `Component` (ReactElement - _optional_): You can optionally set a custom component to be rendered inside the Popup. By default it just displays a "Loading..." message.
186
222
 
187
- ### Typescript
223
+ ---
224
+
225
+ ## 📜 TypeScript Support
188
226
 
189
227
  The `useOAuth2` function identity is:
190
228
 
@@ -217,20 +255,15 @@ type MyCustomShapeData = {
217
255
  const {data, ...} = useOAuth2<MyCustomShapeData>({...});
218
256
  ```
219
257
 
220
- ### Migrating to v2.0.0 (2024-03-05)
221
-
222
- Please follow the steps below to migrate to `v2.0.0`:
223
-
224
- - **DEPRECATED properties**: `exchangeCodeForTokenServerURL`, `exchangeCodeForTokenMethod`, `exchangeCodeForTokenHeaders`
225
- - **INTRODUCED NEW PROPERTY**: `exchangeCodeForTokenQuery`
226
- - `exchangeCodeForTokenQuery` just combines all the above deprecated properties, e.g you can use it like: `exchangeCodeForTokenQuery: { url:"...", method:"POST", headers:{} }`
227
-
228
- ### Tests
258
+ ---
229
259
 
230
- You can run tests by calling
260
+ ### 🧪 Running Tests
231
261
 
232
- ```console
262
+ ```bash
233
263
  npm test
234
264
  ```
235
265
 
236
- It will start a react-app (:3000) and back-end server (:3001) and then it will run the tests with jest & puppeteer.
266
+ - Spins up a React app (`localhost:3000`) and a mock server (`localhost:3001`).
267
+ - Runs E2E tests using Jest + Puppeteer.
268
+ - Covers popup opening, redirection, token exchange, custom state handling, logout flow.
269
+
package/dist/cjs/index.js CHANGED
@@ -1 +1 @@
1
- "use strict";var e=require("react/jsx-runtime"),r=require("react"),n=function(){return n=Object.assign||function(e){for(var r,n=1,t=arguments.length;n<t;n++)for(var o in r=arguments[n])Object.prototype.hasOwnProperty.call(r,o)&&(e[o]=r[o]);return e},n.apply(this,arguments)};function t(e,r,n,t){return new(n||(n=Promise))((function(o,a){function u(e){try{c(t.next(e))}catch(e){a(e)}}function i(e){try{c(t.throw(e))}catch(e){a(e)}}function c(e){var r;e.done?o(e.value):(r=e.value,r instanceof n?r:new n((function(e){e(r)}))).then(u,i)}c((t=t.apply(e,r||[])).next())}))}function o(e,r){var n,t,o,a,u={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function i(i){return function(c){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,i[0]&&(u=0)),u;)try{if(n=1,t&&(o=2&i[0]?t.return:i[0]?t.throw||((o=t.return)&&o.call(t),0):t.next)&&!(o=o.call(t,i[1])).done)return o;switch(t=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return u.label++,{value:i[1],done:!1};case 5:u.label++,t=i[1],i=[0];continue;case 7:i=u.ops.pop(),u.trys.pop();continue;default:if(!(o=u.trys,(o=o.length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){u=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){u.label=i[1];break}if(6===i[0]&&u.label<o[1]){u.label=o[1],o=i;break}if(o&&u.label<o[2]){u.label=o[2],u.ops.push(i);break}o[2]&&u.ops.pop(),u.trys.pop();continue}i=r.call(e,u)}catch(e){i=[6,e],t=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,c])}}}"function"==typeof SuppressedError&&SuppressedError;var a="react-use-oauth2-state-key",u="react-use-oauth2-response",i=["GET","POST","PUT","PATCH"],c=function(e){return new URLSearchParams(e).toString()},s=function(e){var r=new URLSearchParams(e);return Object.fromEntries(r.entries())},l=function(e,r){return e.postMessage(r)},d=function(e,r,n){clearInterval(e.current),r.current&&"function"==typeof r.current.close&&function(e){var r;null===(r=e.current)||void 0===r||r.close()}(r),sessionStorage.removeItem(a),window.removeEventListener("message",n)},f=function(e,r,t,o,a){var u=e.split("?")[0],i=s(e.split("?")[1]);return"".concat(u,"?").concat(c(n(n({},i),{client_id:r,grant_type:"authorization_code",code:t,redirect_uri:o,state:a})))},p=!1;const h=new Map;function v(e,n){if(void 0===r.useSyncExternalStore)throw new TypeError('You are using React 17 or below. Install with "npm install use-local-storage-state@17".');const[t]=r.useState(null==n?void 0:n.defaultValue);if("undefined"==typeof window)return[t,()=>{},{isPersistent:!0,removeItem:()=>{}}];const o=null==n?void 0:n.serializer;return function(e,n,t=!0,o=g,a=JSON.stringify){h.has(e)||void 0===n||null!==m((()=>localStorage.getItem(e)))||m((()=>localStorage.setItem(e,a(n))));const u=r.useRef({item:null,parsed:n}),i=r.useSyncExternalStore(r.useCallback((r=>{const n=n=>{e===n&&r()};return w.add(n),()=>{w.delete(n)}}),[e]),(()=>{var r;const t=null!==(r=m((()=>localStorage.getItem(e))))&&void 0!==r?r:null;if(h.has(e))u.current={item:t,parsed:h.get(e)};else if(t!==u.current.item){let e;try{e=null===t?n:o(t)}catch(r){e=n}u.current={item:t,parsed:e}}return u.current.parsed}),(()=>n)),c=r.useCallback((r=>{const n=r instanceof Function?r(u.current.parsed):r;try{localStorage.setItem(e,a(n)),h.delete(e)}catch(r){h.set(e,n)}y(e)}),[e,a]);return r.useEffect((()=>{if(!t)return;const r=r=>{r.storageArea===m((()=>localStorage))&&r.key===e&&y(e)};return window.addEventListener("storage",r),()=>window.removeEventListener("storage",r)}),[e,t]),r.useMemo((()=>[i,c,{isPersistent:i===n||!h.has(e),removeItem(){m((()=>localStorage.removeItem(e))),h.delete(e),y(e)}}]),[e,c,i,n])}(e,t,null==n?void 0:n.storageSync,null==o?void 0:o.parse,null==o?void 0:o.stringify)}const w=new Set;function y(e){for(const r of[...w])r(e)}function g(e){return"undefined"===e?void 0:JSON.parse(e)}function m(e){try{return e()}catch(e){return}}exports.OAuthPopup=function(t){var o=t.Component,i=void 0===o?e.jsx("div",{style:{margin:"12px"},"data-testid":"popup-loading",children:"Loading..."}):o;return r.useEffect((function(){if(!p){p=!0;var e=n(n({},s(window.location.search.split("?")[1])),s(window.location.hash.split("#")[1])),r=null==e?void 0:e.state,t=null==e?void 0:e.error,o=null===window||void 0===window?void 0:window.opener;if(!function(e){return null!=e}(o))throw new Error("No window opener");var i,c,d=r&&(i=o.sessionStorage,c=r,i.getItem(a)===c);if(!t&&d)l(o,{type:u,payload:e});else{var f=t?decodeURI(t):"".concat(d?"OAuth error: An error has occured.":"OAuth error: State mismatch.");l(o,{type:u,error:f})}}}),[]),i},exports.useOAuth2=function(e){var s=e.authorizeUrl,l=e.clientId,p=e.redirectUri,h=e.scope,w=void 0===h?"":h,y=e.responseType,g=e.extraQueryParameters,m=void 0===g?{}:g,b=e.onSuccess,S=e.onError;!function(e){var r=e.authorizeUrl,n=e.clientId,t=e.redirectUri,o=e.responseType,a=e.extraQueryParameters,u=void 0===a?{}:a,c=e.onSuccess,s=e.onError;if(!(r&&n&&t&&o))throw new Error("Missing required props for useOAuth2. Required props are: {authorizeUrl, clientId, redirectUri, responseType}");if("code"===o&&!e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQueryFn)throw new Error('Either `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn` is required for responseType of "code" for useOAuth2.');if("code"===o&&e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQuery.url)throw new Error("Value `exchangeCodeForTokenQuery.url` is missing.");if("code"===o&&e.exchangeCodeForTokenQuery&&!["GET","POST","PUT","PATCH"].includes(e.exchangeCodeForTokenQuery.method))throw new Error("Invalid `exchangeCodeForTokenQuery.method` value. It can be one of ".concat(i.join(", "),"."));if("object"!=typeof u)throw new TypeError("extraQueryParameters must be an object for useOAuth2.");if(c&&"function"!=typeof c)throw new TypeError("onSuccess callback must be a function for useOAuth2.");if(s&&"function"!=typeof s)throw new TypeError("onError callback must be a function for useOAuth2.")}(e);var T=r.useRef(m),E=r.useRef(),x=r.useRef(),k=r.useRef("code"===y&&e.exchangeCodeForTokenQuery),C=r.useRef("code"===y&&e.exchangeCodeForTokenQueryFn),P=r.useState({loading:!1,error:null}),I=P[0],O=I.loading,F=I.error,A=P[1],Q=v("".concat(y,"-").concat(s,"-").concat(l,"-").concat(w),{defaultValue:null}),U=Q[0],R=Q[1],j=Q[2],L=j.removeItem,_=j.isPersistent,q=r.useCallback((function(){A({loading:!0,error:null});var e,r,i,h,v,g=(e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",r=new Uint8Array(40),window.crypto.getRandomValues(r),r=r.map((function(r){return e.codePointAt(r%62)})),String.fromCharCode.apply(null,r));function m(e){var r,n,a,i,c;return t(this,void 0,void 0,(function(){var t,s,h,v,w;return o(this,(function(o){switch(o.label){case 0:if((null===(r=null==e?void 0:e.data)||void 0===r?void 0:r.type)!==u)return[2];o.label=1;case 1:return o.trys.push([1,13,16,17]),"error"in e.data?(t=(null===(n=e.data)||void 0===n?void 0:n.error)||"Unknown Error occured.",A({loading:!1,error:t}),S?[4,S(t)]:[3,3]):[3,4];case 2:o.sent(),o.label=3;case 3:return[3,12];case 4:return s=null===(a=null==e?void 0:e.data)||void 0===a?void 0:a.payload,"code"!==y?[3,10]:(h=C.current,v=k.current,h&&"function"==typeof h?[4,h(null===(i=e.data)||void 0===i?void 0:i.payload)]:[3,6]);case 5:return s=o.sent(),[3,10];case 6:return v?[4,fetch(f(v.url,l,null==s?void 0:s.code,p,g),{method:null!==(c=v.method)&&void 0!==c?c:"POST",headers:v.headers||{}})]:[3,9];case 7:return[4,o.sent().json()];case 8:return s=o.sent(),[3,10];case 9:throw new Error("useOAuth2: You must provide `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn`");case 10:return A({loading:!1,error:null}),R(s),b?[4,b(s)]:[3,12];case 11:o.sent(),o.label=12;case 12:return[3,17];case 13:return w=o.sent(),console.error(w),A({loading:!1,error:w.toString()}),S?[4,S(w.toString())]:[3,15];case 14:o.sent(),o.label=15;case 15:return[3,17];case 16:return d(x,E,m),[7];case 17:return[2]}}))}))}return function(e,r){e.setItem(a,r)}(sessionStorage,g),E.current=(i=function(e,r,t,o,a,u,i){void 0===i&&(i={});var s=c(n({response_type:u,client_id:r,redirect_uri:t,scope:o,state:a},i));return"".concat(e,"?").concat(s)}(s,l,p,w,g,y,T.current),h=window.outerHeight/2+window.screenY-350,v=window.outerWidth/2+window.screenX-300,window.open(i,"OAuth2 Popup","height=".concat(700,",width=").concat(600,",top=").concat(h,",left=").concat(v))),window.addEventListener("message",m),x.current=setInterval((function(){var e,r,t;(!(null===(e=E.current)||void 0===e?void 0:e.window)||(null===(t=null===(r=E.current)||void 0===r?void 0:r.window)||void 0===t?void 0:t.closed))&&(A((function(e){return n(n({},e),{loading:!1})})),console.warn("Warning: Popup was closed before completing authentication."),d(x,E,m))}),250),function(){window.removeEventListener("message",m),x.current&&clearInterval(x.current)}}),[s,l,p,w,y,b,S,A,R]);return{data:U,loading:O,error:F,getAuth:q,logout:r.useCallback((function(){L(),A({loading:!1,error:null})}),[L]),isPersistent:_}};
1
+ "use strict";var e=require("react/jsx-runtime"),r=require("react"),n=function(){return n=Object.assign||function(e){for(var r,n=1,t=arguments.length;n<t;n++)for(var o in r=arguments[n])Object.prototype.hasOwnProperty.call(r,o)&&(e[o]=r[o]);return e},n.apply(this,arguments)};function t(e,r,n,t){return new(n||(n=Promise))((function(o,a){function u(e){try{c(t.next(e))}catch(e){a(e)}}function i(e){try{c(t.throw(e))}catch(e){a(e)}}function c(e){var r;e.done?o(e.value):(r=e.value,r instanceof n?r:new n((function(e){e(r)}))).then(u,i)}c((t=t.apply(e,r||[])).next())}))}function o(e,r){var n,t,o,a,u={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function i(i){return function(c){return function(i){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,i[0]&&(u=0)),u;)try{if(n=1,t&&(o=2&i[0]?t.return:i[0]?t.throw||((o=t.return)&&o.call(t),0):t.next)&&!(o=o.call(t,i[1])).done)return o;switch(t=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return u.label++,{value:i[1],done:!1};case 5:u.label++,t=i[1],i=[0];continue;case 7:i=u.ops.pop(),u.trys.pop();continue;default:if(!(o=u.trys,(o=o.length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){u=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]<o[3])){u.label=i[1];break}if(6===i[0]&&u.label<o[1]){u.label=o[1],o=i;break}if(o&&u.label<o[2]){u.label=o[2],u.ops.push(i);break}o[2]&&u.ops.pop(),u.trys.pop();continue}i=r.call(e,u)}catch(e){i=[6,e],t=0}finally{n=o=0}if(5&i[0])throw i[1];return{value:i[0]?i[1]:void 0,done:!0}}([i,c])}}}"function"==typeof SuppressedError&&SuppressedError;var a="react-use-oauth2-state-key",u="react-use-oauth2-response",i=["GET","POST","PUT","PATCH"],c=function(e){return new URLSearchParams(e).toString()},s=function(e){var r=new URLSearchParams(e);return Object.fromEntries(r.entries())},l=function(e,r){return e.postMessage(r)},d=function(e,r,n){clearInterval(e.current),r.current&&"function"==typeof r.current.close&&function(e){var r;null===(r=e.current)||void 0===r||r.close()}(r),sessionStorage.removeItem(a),window.removeEventListener("message",n)},f=function(e){try{var r=JSON.parse(e),n=Object.values(r)[0];return null!=n?n:{}}catch(e){return{}}},p=function(e,r,t,o,a){var u=e.split("?")[0],i=s(e.split("?")[1]);return"".concat(u,"?").concat(c(n(n({},i),{client_id:r,grant_type:"authorization_code",code:t,redirect_uri:o,state:JSON.stringify(f(a))})))},h=!1;const v=new Map;function w(e,n){if(void 0===r.useSyncExternalStore)throw new TypeError('You are using React 17 or below. Install with "npm install use-local-storage-state@17".');const[t]=r.useState(null==n?void 0:n.defaultValue);if("undefined"==typeof window)return[t,()=>{},{isPersistent:!0,removeItem:()=>{}}];const o=null==n?void 0:n.serializer;return function(e,n,t=!0,o=m,a=JSON.stringify){v.has(e)||void 0===n||null!==S((()=>localStorage.getItem(e)))||S((()=>localStorage.setItem(e,a(n))));const u=r.useRef({item:null,parsed:n}),i=r.useSyncExternalStore(r.useCallback((r=>{const n=n=>{e===n&&r()};return y.add(n),()=>{y.delete(n)}}),[e]),(()=>{var r;const t=null!==(r=S((()=>localStorage.getItem(e))))&&void 0!==r?r:null;if(v.has(e))u.current={item:t,parsed:v.get(e)};else if(t!==u.current.item){let e;try{e=null===t?n:o(t)}catch(r){e=n}u.current={item:t,parsed:e}}return u.current.parsed}),(()=>n)),c=r.useCallback((r=>{const n=r instanceof Function?r(u.current.parsed):r;try{localStorage.setItem(e,a(n)),v.delete(e)}catch(r){v.set(e,n)}g(e)}),[e,a]);return r.useEffect((()=>{if(!t)return;const r=r=>{r.storageArea===S((()=>localStorage))&&r.key===e&&g(e)};return window.addEventListener("storage",r),()=>window.removeEventListener("storage",r)}),[e,t]),r.useMemo((()=>[i,c,{isPersistent:i===n||!v.has(e),removeItem(){S((()=>localStorage.removeItem(e))),v.delete(e),g(e)}}]),[e,c,i,n])}(e,t,null==n?void 0:n.storageSync,null==o?void 0:o.parse,null==o?void 0:o.stringify)}const y=new Set;function g(e){for(const r of[...y])r(e)}function m(e){return"undefined"===e?void 0:JSON.parse(e)}function S(e){try{return e()}catch(e){return}}exports.OAuthPopup=function(t){var o=t.Component,i=void 0===o?e.jsx("div",{style:{margin:"12px"},"data-testid":"popup-loading",children:"Loading..."}):o;return r.useEffect((function(){if(!h){h=!0;var e=n(n({},s(window.location.search.split("?")[1])),s(window.location.hash.split("#")[1])),r=null==e?void 0:e.state,t=null==e?void 0:e.error,o=null===window||void 0===window?void 0:window.opener;if(!function(e){return null!=e}(o))throw new Error("No window opener");var i,c,d=r&&(i=o.sessionStorage,c=r,i.getItem(a)===c);if(!t&&d)l(o,{type:u,payload:e});else{var f=t?decodeURI(t):"".concat(d?"OAuth error: An error has occured.":"OAuth error: State mismatch.");l(o,{type:u,error:f})}}}),[]),i},exports.useOAuth2=function(e){var s=e.authorizeUrl,l=e.clientId,f=e.redirectUri,h=e.scope,v=void 0===h?"":h,y=e.state,g=e.responseType,m=e.extraQueryParameters,S=void 0===m?{}:m,b=e.onSuccess,T=e.onError;!function(e){var r=e.authorizeUrl,n=e.clientId,t=e.redirectUri,o=e.state,a=e.responseType,u=e.extraQueryParameters,c=void 0===u?{}:u,s=e.onSuccess,l=e.onError;if(!(r&&n&&t&&a))throw new Error("Missing required props for useOAuth2. Required props are: {authorizeUrl, clientId, redirectUri, responseType}");if("code"===a&&!e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQueryFn)throw new Error('Either `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn` is required for responseType of "code" for useOAuth2.');if("code"===a&&e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQuery.url)throw new Error("Value `exchangeCodeForTokenQuery.url` is missing.");if("code"===a&&e.exchangeCodeForTokenQuery&&!["GET","POST","PUT","PATCH"].includes(e.exchangeCodeForTokenQuery.method))throw new Error("Invalid `exchangeCodeForTokenQuery.method` value. It can be one of ".concat(i.join(", "),"."));if("object"!=typeof c||null===c)throw new TypeError("extraQueryParameters must be a plain object for useOAuth2.");if(null!=o&&("object"!=typeof o||Array.isArray(o)))throw new TypeError("The `state` prop for useOAuth2 must be a plain JSON object if provided.");if(s&&"function"!=typeof s)throw new TypeError("onSuccess callback must be a function for useOAuth2.");if(l&&"function"!=typeof l)throw new TypeError("onError callback must be a function for useOAuth2.")}(e);var E=r.useRef(S),x=r.useRef(),O=r.useRef(),k=r.useRef("code"===g&&e.exchangeCodeForTokenQuery),C=r.useRef("code"===g&&e.exchangeCodeForTokenQueryFn),P=r.useState({loading:!1,error:null}),A=P[0],I=A.loading,F=A.error,Q=P[1],U=w("".concat(g,"-").concat(s,"-").concat(l,"-").concat(v),{defaultValue:null}),j=U[0],R=U[1],N=U[2],J=N.removeItem,L=N.isPersistent,_=null!=y?y:{},q=JSON.stringify(_),z=r.useCallback((function(){Q({loading:!0,error:null});var e,r,i,h=function(e){var r,n,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",o=new Uint8Array(40);window.crypto.getRandomValues(o),o=o.map((function(e){return t.codePointAt(e%62)}));var a=String.fromCharCode.apply(null,o);try{return JSON.stringify(((r={})[a]=JSON.parse(null!=e?e:"{}"),r))}catch(e){return JSON.stringify(((n={})[a]={},n))}}(q);function w(e){var r,n,a,i,c;return t(this,void 0,void 0,(function(){var t,s,v,y,m;return o(this,(function(o){switch(o.label){case 0:if((null===(r=null==e?void 0:e.data)||void 0===r?void 0:r.type)!==u)return[2];o.label=1;case 1:return o.trys.push([1,13,16,17]),"error"in e.data?(t=(null===(n=e.data)||void 0===n?void 0:n.error)||"Unknown Error occured.",Q({loading:!1,error:t}),T?[4,T(t)]:[3,3]):[3,4];case 2:o.sent(),o.label=3;case 3:return[3,12];case 4:return s=null===(a=null==e?void 0:e.data)||void 0===a?void 0:a.payload,"code"!==g?[3,10]:(v=C.current,y=k.current,v&&"function"==typeof v?[4,v(null===(i=e.data)||void 0===i?void 0:i.payload)]:[3,6]);case 5:return s=o.sent(),[3,10];case 6:return y?[4,fetch(p(y.url,l,null==s?void 0:s.code,f,h),{method:null!==(c=y.method)&&void 0!==c?c:"POST",headers:y.headers||{}})]:[3,9];case 7:return[4,o.sent().json()];case 8:return s=o.sent(),[3,10];case 9:throw new Error("useOAuth2: You must provide `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn`");case 10:return Q({loading:!1,error:null}),R(s),b?[4,b(s)]:[3,12];case 11:o.sent(),o.label=12;case 12:return[3,17];case 13:return m=o.sent(),console.error(m),Q({loading:!1,error:m.toString()}),T?[4,T(m.toString())]:[3,15];case 14:o.sent(),o.label=15;case 15:return[3,17];case 16:return d(O,x,w),[7];case 17:return[2]}}))}))}return function(e,r){e.setItem(a,r)}(sessionStorage,h),x.current=(e=function(e,r,t,o,a,u,i){void 0===i&&(i={});var s=c(n({response_type:u,client_id:r,redirect_uri:t,scope:o,state:a},i));return"".concat(e,"?").concat(s)}(s,l,f,v,h,g,E.current),r=window.outerHeight/2+window.screenY-350,i=window.outerWidth/2+window.screenX-300,window.open(e,"OAuth2 Popup","height=".concat(700,",width=").concat(600,",top=").concat(r,",left=").concat(i))),window.addEventListener("message",w),O.current=setInterval((function(){var e,r,t;(!(null===(e=x.current)||void 0===e?void 0:e.window)||(null===(t=null===(r=x.current)||void 0===r?void 0:r.window)||void 0===t?void 0:t.closed))&&(Q((function(e){return n(n({},e),{loading:!1})})),console.warn("Warning: Popup was closed before completing authentication."),d(O,x,w))}),250),function(){window.removeEventListener("message",w),O.current&&clearInterval(O.current)}}),[s,l,f,v,g,b,T,Q,R,q]);return{data:j,loading:I,error:F,getAuth:z,logout:r.useCallback((function(){J(),Q({loading:!1,error:null})}),[J]),isPersistent:L}};
@@ -4,7 +4,7 @@ export declare const queryToObject: (query: string) => {
4
4
  [k: string]: string;
5
5
  };
6
6
  export declare const formatAuthorizeUrl: (authorizeUrl: string, clientId: string, redirectUri: string, scope: string, state: string, responseType: TOauth2Props['responseType'], extraQueryParameters?: TOauth2Props['extraQueryParameters']) => string;
7
- export declare const generateState: () => string;
7
+ export declare const generateState: (customStateString?: string | undefined) => string;
8
8
  export declare const saveState: (storage: Storage, state: string) => void;
9
9
  export declare const removeState: (storage: Storage) => void;
10
10
  export declare const checkState: (storage: Storage, receivedState: string) => boolean;
@@ -13,4 +13,5 @@ export declare const closePopup: (popupRef: React.MutableRefObject<Window | null
13
13
  export declare const isWindowOpener: (opener: Window | null) => opener is Window;
14
14
  export declare const openerPostMessage: (opener: Window, message: TMessageData) => void;
15
15
  export declare const cleanup: (intervalRef: React.MutableRefObject<string | number | NodeJS.Timeout | undefined>, popupRef: React.MutableRefObject<Window | null | undefined>, handleMessageListener: any) => void;
16
+ export declare const extractCustomState: (state: string) => any;
16
17
  export declare const formatExchangeCodeForTokenServerURL: (serverUrl: string, clientId: string, code: string, redirectUri: string, state: string) => string;
@@ -25,6 +25,7 @@ export type TOauth2Props<TData = TAuthTokenPayload> = {
25
25
  authorizeUrl: string;
26
26
  clientId: string;
27
27
  redirectUri: string;
28
+ state?: Record<string, any> | null;
28
29
  scope?: string;
29
30
  extraQueryParameters?: Record<string, any>;
30
31
  onError?: (error: string) => void;
package/dist/esm/index.js CHANGED
@@ -1 +1 @@
1
- import{jsx as e}from"react/jsx-runtime";import{useEffect as r,useSyncExternalStore as n,useState as o,useRef as t,useCallback as a,useMemo as i}from"react";var c=function(){return c=Object.assign||function(e){for(var r,n=1,o=arguments.length;n<o;n++)for(var t in r=arguments[n])Object.prototype.hasOwnProperty.call(r,t)&&(e[t]=r[t]);return e},c.apply(this,arguments)};function u(e,r,n,o){return new(n||(n=Promise))((function(t,a){function i(e){try{u(o.next(e))}catch(e){a(e)}}function c(e){try{u(o.throw(e))}catch(e){a(e)}}function u(e){var r;e.done?t(e.value):(r=e.value,r instanceof n?r:new n((function(e){e(r)}))).then(i,c)}u((o=o.apply(e,r||[])).next())}))}function s(e,r){var n,o,t,a,i={label:0,sent:function(){if(1&t[0])throw t[1];return t[1]},trys:[],ops:[]};return a={next:c(0),throw:c(1),return:c(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function c(c){return function(u){return function(c){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,c[0]&&(i=0)),i;)try{if(n=1,o&&(t=2&c[0]?o.return:c[0]?o.throw||((t=o.return)&&t.call(o),0):o.next)&&!(t=t.call(o,c[1])).done)return t;switch(o=0,t&&(c=[2&c[0],t.value]),c[0]){case 0:case 1:t=c;break;case 4:return i.label++,{value:c[1],done:!1};case 5:i.label++,o=c[1],c=[0];continue;case 7:c=i.ops.pop(),i.trys.pop();continue;default:if(!(t=i.trys,(t=t.length>0&&t[t.length-1])||6!==c[0]&&2!==c[0])){i=0;continue}if(3===c[0]&&(!t||c[1]>t[0]&&c[1]<t[3])){i.label=c[1];break}if(6===c[0]&&i.label<t[1]){i.label=t[1],t=c;break}if(t&&i.label<t[2]){i.label=t[2],i.ops.push(c);break}t[2]&&i.ops.pop(),i.trys.pop();continue}c=r.call(e,i)}catch(e){c=[6,e],o=0}finally{n=t=0}if(5&c[0])throw c[1];return{value:c[0]?c[1]:void 0,done:!0}}([c,u])}}}"function"==typeof SuppressedError&&SuppressedError;var l="react-use-oauth2-state-key",d="react-use-oauth2-response",f=["GET","POST","PUT","PATCH"],p=function(e){return new URLSearchParams(e).toString()},h=function(e){var r=new URLSearchParams(e);return Object.fromEntries(r.entries())},v=function(e,r){return e.postMessage(r)},w=function(e,r,n){clearInterval(e.current),r.current&&"function"==typeof r.current.close&&function(e){var r;null===(r=e.current)||void 0===r||r.close()}(r),sessionStorage.removeItem(l),window.removeEventListener("message",n)},g=function(e,r,n,o,t){var a=e.split("?")[0],i=h(e.split("?")[1]);return"".concat(a,"?").concat(p(c(c({},i),{client_id:r,grant_type:"authorization_code",code:n,redirect_uri:o,state:t})))},y=!1,m=function(n){var o=n.Component,t=void 0===o?e("div",{style:{margin:"12px"},"data-testid":"popup-loading",children:"Loading..."}):o;return r((function(){if(!y){y=!0;var e=c(c({},h(window.location.search.split("?")[1])),h(window.location.hash.split("#")[1])),r=null==e?void 0:e.state,n=null==e?void 0:e.error,o=null===window||void 0===window?void 0:window.opener;if(!function(e){return null!=e}(o))throw new Error("No window opener");var t,a,i=r&&(t=o.sessionStorage,a=r,t.getItem(l)===a);if(!n&&i)v(o,{type:d,payload:e});else{var u=n?decodeURI(n):"".concat(i?"OAuth error: An error has occured.":"OAuth error: State mismatch.");v(o,{type:d,error:u})}}}),[]),t};const b=new Map;function T(e,c){if(void 0===n)throw new TypeError('You are using React 17 or below. Install with "npm install use-local-storage-state@17".');const[u]=o(null==c?void 0:c.defaultValue);if("undefined"==typeof window)return[u,()=>{},{isPersistent:!0,removeItem:()=>{}}];const s=null==c?void 0:c.serializer;return function(e,o,c=!0,u=x,s=JSON.stringify){b.has(e)||void 0===o||null!==k((()=>localStorage.getItem(e)))||k((()=>localStorage.setItem(e,s(o))));const l=t({item:null,parsed:o}),d=n(a((r=>{const n=n=>{e===n&&r()};return S.add(n),()=>{S.delete(n)}}),[e]),(()=>{var r;const n=null!==(r=k((()=>localStorage.getItem(e))))&&void 0!==r?r:null;if(b.has(e))l.current={item:n,parsed:b.get(e)};else if(n!==l.current.item){let e;try{e=null===n?o:u(n)}catch(r){e=o}l.current={item:n,parsed:e}}return l.current.parsed}),(()=>o)),f=a((r=>{const n=r instanceof Function?r(l.current.parsed):r;try{localStorage.setItem(e,s(n)),b.delete(e)}catch(r){b.set(e,n)}E(e)}),[e,s]);return r((()=>{if(!c)return;const r=r=>{r.storageArea===k((()=>localStorage))&&r.key===e&&E(e)};return window.addEventListener("storage",r),()=>window.removeEventListener("storage",r)}),[e,c]),i((()=>[d,f,{isPersistent:d===o||!b.has(e),removeItem(){k((()=>localStorage.removeItem(e))),b.delete(e),E(e)}}]),[e,f,d,o])}(e,u,null==c?void 0:c.storageSync,null==s?void 0:s.parse,null==s?void 0:s.stringify)}const S=new Set;function E(e){for(const r of[...S])r(e)}function x(e){return"undefined"===e?void 0:JSON.parse(e)}function k(e){try{return e()}catch(e){return}}var I=function(e){var r=e.authorizeUrl,n=e.clientId,i=e.redirectUri,h=e.scope,v=void 0===h?"":h,y=e.responseType,m=e.extraQueryParameters,b=void 0===m?{}:m,S=e.onSuccess,E=e.onError;!function(e){var r=e.authorizeUrl,n=e.clientId,o=e.redirectUri,t=e.responseType,a=e.extraQueryParameters,i=void 0===a?{}:a,c=e.onSuccess,u=e.onError;if(!(r&&n&&o&&t))throw new Error("Missing required props for useOAuth2. Required props are: {authorizeUrl, clientId, redirectUri, responseType}");if("code"===t&&!e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQueryFn)throw new Error('Either `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn` is required for responseType of "code" for useOAuth2.');if("code"===t&&e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQuery.url)throw new Error("Value `exchangeCodeForTokenQuery.url` is missing.");if("code"===t&&e.exchangeCodeForTokenQuery&&!["GET","POST","PUT","PATCH"].includes(e.exchangeCodeForTokenQuery.method))throw new Error("Invalid `exchangeCodeForTokenQuery.method` value. It can be one of ".concat(f.join(", "),"."));if("object"!=typeof i)throw new TypeError("extraQueryParameters must be an object for useOAuth2.");if(c&&"function"!=typeof c)throw new TypeError("onSuccess callback must be a function for useOAuth2.");if(u&&"function"!=typeof u)throw new TypeError("onError callback must be a function for useOAuth2.")}(e);var x=t(b),k=t(),I=t(),P=t("code"===y&&e.exchangeCodeForTokenQuery),C=t("code"===y&&e.exchangeCodeForTokenQueryFn),F=o({loading:!1,error:null}),O=F[0],Q=O.loading,A=O.error,U=F[1],j=T("".concat(y,"-").concat(r,"-").concat(n,"-").concat(v),{defaultValue:null}),L=j[0],R=j[1],_=j[2],z=_.removeItem,V=_.isPersistent,q=a((function(){U({loading:!0,error:null});var e,o,t,a,f,h=(e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",o=new Uint8Array(40),window.crypto.getRandomValues(o),o=o.map((function(r){return e.codePointAt(r%62)})),String.fromCharCode.apply(null,o));function m(e){var r,o,t,a,c;return u(this,void 0,void 0,(function(){var u,l,f,p,v;return s(this,(function(s){switch(s.label){case 0:if((null===(r=null==e?void 0:e.data)||void 0===r?void 0:r.type)!==d)return[2];s.label=1;case 1:return s.trys.push([1,13,16,17]),"error"in e.data?(u=(null===(o=e.data)||void 0===o?void 0:o.error)||"Unknown Error occured.",U({loading:!1,error:u}),E?[4,E(u)]:[3,3]):[3,4];case 2:s.sent(),s.label=3;case 3:return[3,12];case 4:return l=null===(t=null==e?void 0:e.data)||void 0===t?void 0:t.payload,"code"!==y?[3,10]:(f=C.current,p=P.current,f&&"function"==typeof f?[4,f(null===(a=e.data)||void 0===a?void 0:a.payload)]:[3,6]);case 5:return l=s.sent(),[3,10];case 6:return p?[4,fetch(g(p.url,n,null==l?void 0:l.code,i,h),{method:null!==(c=p.method)&&void 0!==c?c:"POST",headers:p.headers||{}})]:[3,9];case 7:return[4,s.sent().json()];case 8:return l=s.sent(),[3,10];case 9:throw new Error("useOAuth2: You must provide `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn`");case 10:return U({loading:!1,error:null}),R(l),S?[4,S(l)]:[3,12];case 11:s.sent(),s.label=12;case 12:return[3,17];case 13:return v=s.sent(),console.error(v),U({loading:!1,error:v.toString()}),E?[4,E(v.toString())]:[3,15];case 14:s.sent(),s.label=15;case 15:return[3,17];case 16:return w(I,k,m),[7];case 17:return[2]}}))}))}return function(e,r){e.setItem(l,r)}(sessionStorage,h),k.current=(t=function(e,r,n,o,t,a,i){void 0===i&&(i={});var u=p(c({response_type:a,client_id:r,redirect_uri:n,scope:o,state:t},i));return"".concat(e,"?").concat(u)}(r,n,i,v,h,y,x.current),a=window.outerHeight/2+window.screenY-350,f=window.outerWidth/2+window.screenX-300,window.open(t,"OAuth2 Popup","height=".concat(700,",width=").concat(600,",top=").concat(a,",left=").concat(f))),window.addEventListener("message",m),I.current=setInterval((function(){var e,r,n;(!(null===(e=k.current)||void 0===e?void 0:e.window)||(null===(n=null===(r=k.current)||void 0===r?void 0:r.window)||void 0===n?void 0:n.closed))&&(U((function(e){return c(c({},e),{loading:!1})})),console.warn("Warning: Popup was closed before completing authentication."),w(I,k,m))}),250),function(){window.removeEventListener("message",m),I.current&&clearInterval(I.current)}}),[r,n,i,v,y,S,E,U,R]);return{data:L,loading:Q,error:A,getAuth:q,logout:a((function(){z(),U({loading:!1,error:null})}),[z]),isPersistent:V}};export{m as OAuthPopup,I as useOAuth2};
1
+ import{jsx as e}from"react/jsx-runtime";import{useEffect as r,useSyncExternalStore as n,useState as t,useRef as o,useCallback as a,useMemo as i}from"react";var u=function(){return u=Object.assign||function(e){for(var r,n=1,t=arguments.length;n<t;n++)for(var o in r=arguments[n])Object.prototype.hasOwnProperty.call(r,o)&&(e[o]=r[o]);return e},u.apply(this,arguments)};function c(e,r,n,t){return new(n||(n=Promise))((function(o,a){function i(e){try{c(t.next(e))}catch(e){a(e)}}function u(e){try{c(t.throw(e))}catch(e){a(e)}}function c(e){var r;e.done?o(e.value):(r=e.value,r instanceof n?r:new n((function(e){e(r)}))).then(i,u)}c((t=t.apply(e,r||[])).next())}))}function s(e,r){var n,t,o,a,i={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:u(0),throw:u(1),return:u(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function u(u){return function(c){return function(u){if(n)throw new TypeError("Generator is already executing.");for(;a&&(a=0,u[0]&&(i=0)),i;)try{if(n=1,t&&(o=2&u[0]?t.return:u[0]?t.throw||((o=t.return)&&o.call(t),0):t.next)&&!(o=o.call(t,u[1])).done)return o;switch(t=0,o&&(u=[2&u[0],o.value]),u[0]){case 0:case 1:o=u;break;case 4:return i.label++,{value:u[1],done:!1};case 5:i.label++,t=u[1],u=[0];continue;case 7:u=i.ops.pop(),i.trys.pop();continue;default:if(!(o=i.trys,(o=o.length>0&&o[o.length-1])||6!==u[0]&&2!==u[0])){i=0;continue}if(3===u[0]&&(!o||u[1]>o[0]&&u[1]<o[3])){i.label=u[1];break}if(6===u[0]&&i.label<o[1]){i.label=o[1],o=u;break}if(o&&i.label<o[2]){i.label=o[2],i.ops.push(u);break}o[2]&&i.ops.pop(),i.trys.pop();continue}u=r.call(e,i)}catch(e){u=[6,e],t=0}finally{n=o=0}if(5&u[0])throw u[1];return{value:u[0]?u[1]:void 0,done:!0}}([u,c])}}}"function"==typeof SuppressedError&&SuppressedError;var l="react-use-oauth2-state-key",d="react-use-oauth2-response",f=["GET","POST","PUT","PATCH"],p=function(e){return new URLSearchParams(e).toString()},h=function(e){var r=new URLSearchParams(e);return Object.fromEntries(r.entries())},v=function(e,r){return e.postMessage(r)},w=function(e,r,n){clearInterval(e.current),r.current&&"function"==typeof r.current.close&&function(e){var r;null===(r=e.current)||void 0===r||r.close()}(r),sessionStorage.removeItem(l),window.removeEventListener("message",n)},y=function(e){try{var r=JSON.parse(e),n=Object.values(r)[0];return null!=n?n:{}}catch(e){return{}}},g=function(e,r,n,t,o){var a=e.split("?")[0],i=h(e.split("?")[1]);return"".concat(a,"?").concat(p(u(u({},i),{client_id:r,grant_type:"authorization_code",code:n,redirect_uri:t,state:JSON.stringify(y(o))})))},m=!1,S=function(n){var t=n.Component,o=void 0===t?e("div",{style:{margin:"12px"},"data-testid":"popup-loading",children:"Loading..."}):t;return r((function(){if(!m){m=!0;var e=u(u({},h(window.location.search.split("?")[1])),h(window.location.hash.split("#")[1])),r=null==e?void 0:e.state,n=null==e?void 0:e.error,t=null===window||void 0===window?void 0:window.opener;if(!function(e){return null!=e}(t))throw new Error("No window opener");var o,a,i=r&&(o=t.sessionStorage,a=r,o.getItem(l)===a);if(!n&&i)v(t,{type:d,payload:e});else{var c=n?decodeURI(n):"".concat(i?"OAuth error: An error has occured.":"OAuth error: State mismatch.");v(t,{type:d,error:c})}}}),[]),o};const b=new Map;function T(e,u){if(void 0===n)throw new TypeError('You are using React 17 or below. Install with "npm install use-local-storage-state@17".');const[c]=t(null==u?void 0:u.defaultValue);if("undefined"==typeof window)return[c,()=>{},{isPersistent:!0,removeItem:()=>{}}];const s=null==u?void 0:u.serializer;return function(e,t,u=!0,c=x,s=JSON.stringify){b.has(e)||void 0===t||null!==k((()=>localStorage.getItem(e)))||k((()=>localStorage.setItem(e,s(t))));const l=o({item:null,parsed:t}),d=n(a((r=>{const n=n=>{e===n&&r()};return E.add(n),()=>{E.delete(n)}}),[e]),(()=>{var r;const n=null!==(r=k((()=>localStorage.getItem(e))))&&void 0!==r?r:null;if(b.has(e))l.current={item:n,parsed:b.get(e)};else if(n!==l.current.item){let e;try{e=null===n?t:c(n)}catch(r){e=t}l.current={item:n,parsed:e}}return l.current.parsed}),(()=>t)),f=a((r=>{const n=r instanceof Function?r(l.current.parsed):r;try{localStorage.setItem(e,s(n)),b.delete(e)}catch(r){b.set(e,n)}O(e)}),[e,s]);return r((()=>{if(!u)return;const r=r=>{r.storageArea===k((()=>localStorage))&&r.key===e&&O(e)};return window.addEventListener("storage",r),()=>window.removeEventListener("storage",r)}),[e,u]),i((()=>[d,f,{isPersistent:d===t||!b.has(e),removeItem(){k((()=>localStorage.removeItem(e))),b.delete(e),O(e)}}]),[e,f,d,t])}(e,c,null==u?void 0:u.storageSync,null==s?void 0:s.parse,null==s?void 0:s.stringify)}const E=new Set;function O(e){for(const r of[...E])r(e)}function x(e){return"undefined"===e?void 0:JSON.parse(e)}function k(e){try{return e()}catch(e){return}}var I=function(e){var r=e.authorizeUrl,n=e.clientId,i=e.redirectUri,h=e.scope,v=void 0===h?"":h,y=e.state,m=e.responseType,S=e.extraQueryParameters,b=void 0===S?{}:S,E=e.onSuccess,O=e.onError;!function(e){var r=e.authorizeUrl,n=e.clientId,t=e.redirectUri,o=e.state,a=e.responseType,i=e.extraQueryParameters,u=void 0===i?{}:i,c=e.onSuccess,s=e.onError;if(!(r&&n&&t&&a))throw new Error("Missing required props for useOAuth2. Required props are: {authorizeUrl, clientId, redirectUri, responseType}");if("code"===a&&!e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQueryFn)throw new Error('Either `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn` is required for responseType of "code" for useOAuth2.');if("code"===a&&e.exchangeCodeForTokenQuery&&!e.exchangeCodeForTokenQuery.url)throw new Error("Value `exchangeCodeForTokenQuery.url` is missing.");if("code"===a&&e.exchangeCodeForTokenQuery&&!["GET","POST","PUT","PATCH"].includes(e.exchangeCodeForTokenQuery.method))throw new Error("Invalid `exchangeCodeForTokenQuery.method` value. It can be one of ".concat(f.join(", "),"."));if("object"!=typeof u||null===u)throw new TypeError("extraQueryParameters must be a plain object for useOAuth2.");if(null!=o&&("object"!=typeof o||Array.isArray(o)))throw new TypeError("The `state` prop for useOAuth2 must be a plain JSON object if provided.");if(c&&"function"!=typeof c)throw new TypeError("onSuccess callback must be a function for useOAuth2.");if(s&&"function"!=typeof s)throw new TypeError("onError callback must be a function for useOAuth2.")}(e);var x=o(b),k=o(),I=o(),P=o("code"===m&&e.exchangeCodeForTokenQuery),A=o("code"===m&&e.exchangeCodeForTokenQueryFn),C=t({loading:!1,error:null}),F=C[0],Q=F.loading,U=F.error,j=C[1],N=T("".concat(m,"-").concat(r,"-").concat(n,"-").concat(v),{defaultValue:null}),J=N[0],L=N[1],R=N[2],_=R.removeItem,z=R.isPersistent,V=null!=y?y:{},q=JSON.stringify(V),G=a((function(){j({loading:!0,error:null});var e,t,o,a=function(e){var r,n,t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",o=new Uint8Array(40);window.crypto.getRandomValues(o),o=o.map((function(e){return t.codePointAt(e%62)}));var a=String.fromCharCode.apply(null,o);try{return JSON.stringify(((r={})[a]=JSON.parse(null!=e?e:"{}"),r))}catch(e){return JSON.stringify(((n={})[a]={},n))}}(q);function f(e){var r,t,o,u,l;return c(this,void 0,void 0,(function(){var c,p,h,v,y;return s(this,(function(s){switch(s.label){case 0:if((null===(r=null==e?void 0:e.data)||void 0===r?void 0:r.type)!==d)return[2];s.label=1;case 1:return s.trys.push([1,13,16,17]),"error"in e.data?(c=(null===(t=e.data)||void 0===t?void 0:t.error)||"Unknown Error occured.",j({loading:!1,error:c}),O?[4,O(c)]:[3,3]):[3,4];case 2:s.sent(),s.label=3;case 3:return[3,12];case 4:return p=null===(o=null==e?void 0:e.data)||void 0===o?void 0:o.payload,"code"!==m?[3,10]:(h=A.current,v=P.current,h&&"function"==typeof h?[4,h(null===(u=e.data)||void 0===u?void 0:u.payload)]:[3,6]);case 5:return p=s.sent(),[3,10];case 6:return v?[4,fetch(g(v.url,n,null==p?void 0:p.code,i,a),{method:null!==(l=v.method)&&void 0!==l?l:"POST",headers:v.headers||{}})]:[3,9];case 7:return[4,s.sent().json()];case 8:return p=s.sent(),[3,10];case 9:throw new Error("useOAuth2: You must provide `exchangeCodeForTokenQuery` or `exchangeCodeForTokenQueryFn`");case 10:return j({loading:!1,error:null}),L(p),E?[4,E(p)]:[3,12];case 11:s.sent(),s.label=12;case 12:return[3,17];case 13:return y=s.sent(),console.error(y),j({loading:!1,error:y.toString()}),O?[4,O(y.toString())]:[3,15];case 14:s.sent(),s.label=15;case 15:return[3,17];case 16:return w(I,k,f),[7];case 17:return[2]}}))}))}return function(e,r){e.setItem(l,r)}(sessionStorage,a),k.current=(e=function(e,r,n,t,o,a,i){void 0===i&&(i={});var c=p(u({response_type:a,client_id:r,redirect_uri:n,scope:t,state:o},i));return"".concat(e,"?").concat(c)}(r,n,i,v,a,m,x.current),t=window.outerHeight/2+window.screenY-350,o=window.outerWidth/2+window.screenX-300,window.open(e,"OAuth2 Popup","height=".concat(700,",width=").concat(600,",top=").concat(t,",left=").concat(o))),window.addEventListener("message",f),I.current=setInterval((function(){var e,r,n;(!(null===(e=k.current)||void 0===e?void 0:e.window)||(null===(n=null===(r=k.current)||void 0===r?void 0:r.window)||void 0===n?void 0:n.closed))&&(j((function(e){return u(u({},e),{loading:!1})})),console.warn("Warning: Popup was closed before completing authentication."),w(I,k,f))}),250),function(){window.removeEventListener("message",f),I.current&&clearInterval(I.current)}}),[r,n,i,v,m,E,O,j,L,q]);return{data:J,loading:Q,error:U,getAuth:G,logout:a((function(){_(),j({loading:!1,error:null})}),[_]),isPersistent:z}};export{S as OAuthPopup,I as useOAuth2};
@@ -4,7 +4,7 @@ export declare const queryToObject: (query: string) => {
4
4
  [k: string]: string;
5
5
  };
6
6
  export declare const formatAuthorizeUrl: (authorizeUrl: string, clientId: string, redirectUri: string, scope: string, state: string, responseType: TOauth2Props['responseType'], extraQueryParameters?: TOauth2Props['extraQueryParameters']) => string;
7
- export declare const generateState: () => string;
7
+ export declare const generateState: (customStateString?: string | undefined) => string;
8
8
  export declare const saveState: (storage: Storage, state: string) => void;
9
9
  export declare const removeState: (storage: Storage) => void;
10
10
  export declare const checkState: (storage: Storage, receivedState: string) => boolean;
@@ -13,4 +13,5 @@ export declare const closePopup: (popupRef: React.MutableRefObject<Window | null
13
13
  export declare const isWindowOpener: (opener: Window | null) => opener is Window;
14
14
  export declare const openerPostMessage: (opener: Window, message: TMessageData) => void;
15
15
  export declare const cleanup: (intervalRef: React.MutableRefObject<string | number | NodeJS.Timeout | undefined>, popupRef: React.MutableRefObject<Window | null | undefined>, handleMessageListener: any) => void;
16
+ export declare const extractCustomState: (state: string) => any;
16
17
  export declare const formatExchangeCodeForTokenServerURL: (serverUrl: string, clientId: string, code: string, redirectUri: string, state: string) => string;
@@ -25,6 +25,7 @@ export type TOauth2Props<TData = TAuthTokenPayload> = {
25
25
  authorizeUrl: string;
26
26
  clientId: string;
27
27
  redirectUri: string;
28
+ state?: Record<string, any> | null;
28
29
  scope?: string;
29
30
  extraQueryParameters?: Record<string, any>;
30
31
  onError?: (error: string) => void;
package/dist/index.d.ts CHANGED
@@ -35,6 +35,7 @@ type TOauth2Props<TData = TAuthTokenPayload> = {
35
35
  authorizeUrl: string;
36
36
  clientId: string;
37
37
  redirectUri: string;
38
+ state?: Record<string, any> | null;
38
39
  scope?: string;
39
40
  extraQueryParameters?: Record<string, any>;
40
41
  onError?: (error: string) => void;
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "nodejs",
13
13
  "oauth2"
14
14
  ],
15
- "version": "2.0.2",
15
+ "version": "2.1.1",
16
16
  "description": "A React hook that handles OAuth2 authorization flow.",
17
17
  "license": "MIT",
18
18
  "homepage": "https://github.com/tasoskakour/react-use-oauth2#readme",