Skip to content

Commit c761cd2

Browse files
author
Domagoj Rukavina
authored
Merge pull request #8 from shoutem/feature/fix-import-without-configure
Added support for importing without configure
2 parents 250a47e + e39fe02 commit c761cd2

13 files changed

+347
-97
lines changed

README.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ config: {
6969
```
7070

7171
All required methods return a promise to enable reading of request or response body.
72-
You should avoid reading the body directly on provided requests and responses and instead clone
73-
them first. The library does not clone objects to avoid unnecessary overhead in cases where
72+
You should avoid reading the body directly on provided requests and responses and instead **clone
73+
them first.** The library does not clone objects to avoid unnecessary overhead in cases where
7474
reading a body is not required to provide data.
7575

7676
To configure the interceptor you should import and call `configure` function. And when you obtain
@@ -117,6 +117,10 @@ to stop fetch interception.
117117

118118
Clears all tokens from interceptor.
119119

120+
`unload()`
121+
122+
Completely unloads the library and restores initial state.
123+
120124
`isResponseUnauthorized(response)`
121125

122126
Utility method which determines if given response should be considered unauthorized.

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shoutem/fetch-token-intercept",
3-
"version": "0.2.2",
3+
"version": "0.3.0",
44
"description": "Fetch interceptor for managing refresh token flow.",
55
"main": "lib/index.js",
66
"files": [
@@ -57,5 +57,7 @@
5757
"nock": "^8.0.0",
5858
"sinon": "^1.17.4"
5959
},
60-
"dependencies": {}
60+
"dependencies": {
61+
"lodash": "^4.17.4"
62+
}
6163
}

src/AccessTokenProvider.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* a renewed version of access token at the moment. All subsequent requests will be chained
66
* to renewing fetch promise and resolved once the response is received.
77
*/
8-
export default class AccessTokenProvider {
8+
class AccessTokenProvider {
99
constructor(fetch, config) {
1010
this.fetch = fetch;
1111

@@ -29,6 +29,13 @@ export default class AccessTokenProvider {
2929
this.handleError = this.handleError.bind(this);
3030
}
3131

32+
/**
33+
* Configures access token provider
34+
*/
35+
configure(config) {
36+
this.config = { ...this.config, ...config };
37+
}
38+
3239
/**
3340
* Renews current access token with provided refresh token
3441
*/
@@ -120,3 +127,5 @@ export default class AccessTokenProvider {
120127
.catch(error => this.handleError(error, reject));
121128
}
122129
}
130+
131+
export default AccessTokenProvider;

src/FetchInterceptor.js

+98-50
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import isFunction from 'lodash/isFunction';
12
import {
23
ERROR_INVALID_CONFIG,
34
} from './const';
@@ -6,26 +7,70 @@ import TokenExpiredException from './services/TokenExpiredException';
67
import RetryCountExceededException from './services/RetryCountExceededException';
78
import AccessTokenProvider from './AccessTokenProvider';
89

10+
/**
11+
* Prepares signed request object which can be used for renewing access token
12+
*
13+
* @callback createAccessTokenRequest
14+
* @param {string} refreshToken Refresh token used to sign the request
15+
* @returns {Request} Signed request object which can be used to get access token
16+
*/
17+
18+
/**
19+
* Parses access token from access token response object
20+
*
21+
* @callback parseAccessToken
22+
* @param {Response} response Response object with access token
23+
* @returns {string} Access token parsed from response
24+
*/
25+
26+
/**
27+
* Checks whether interceptor will intercept this request or just let it pass through
28+
*
29+
* @callback shouldIntercept
30+
* @param {Request} request Request object
31+
* @returns {bool} A value indicating whether this request should be intercepted
32+
*/
33+
34+
/**
35+
* Checks whether provided response invalidates current access token
36+
*
37+
* @callback shouldInvalidateAccessToken
38+
* @param {Response} response Response object
39+
* @returns {bool} A value indicating whether token should be invalidated
40+
*/
41+
42+
/**
43+
* Adds authorization for intercepted requests
44+
*
45+
* @callback authorizeRequest
46+
* @param {Request} request Request object being intercepted
47+
* @param {string} accessToken Current access token
48+
* @returns {Request} Authorized request object
49+
*/
50+
51+
const getDefaultConfig = () => ({
52+
fetchRetryCount: 1,
53+
createAccessTokenRequest: null,
54+
shouldIntercept: () => false,
55+
shouldInvalidateAccessToken: () => false,
56+
isResponseUnauthorized: http.isResponseUnauthorized,
57+
parseAccessToken: null,
58+
authorizeRequest: null,
59+
onAccessTokenChange: null,
60+
onResponse: null,
61+
});
62+
963
/**
1064
* Provides a default implementation for intercepting fetch requests. It will try to resolve
1165
* unauthorized responses by renewing the access token and repeating the initial request.
1266
*/
13-
export default class FetchInterceptor {
67+
class FetchInterceptor {
1468
constructor(fetch) {
1569
// stores reference to vanilla fetch method
1670
this.fetch = fetch;
71+
this.accessTokenProvider = new AccessTokenProvider(this.fetch);
1772

18-
this.config = {
19-
fetchRetryCount: 1,
20-
createAccessTokenRequest: null,
21-
shouldIntercept: () => true,
22-
shouldInvalidateAccessToken: () => false,
23-
isResponseUnauthorized: http.isResponseUnauthorized,
24-
parseAccessToken: null,
25-
authorizeRequest: null,
26-
onAccessTokenChange: null,
27-
onResponse: null,
28-
};
73+
this.config = getDefaultConfig();
2974

3075
this.intercept = this.intercept.bind(this);
3176

@@ -48,37 +93,28 @@ export default class FetchInterceptor {
4893
* Configures fetch interceptor with given config object. All required properties can optionally
4994
* return a promise which will be resolved by fetch interceptor automatically.
5095
*
51-
* @param config
52-
*
53-
* (Required) Prepare fetch request for renewing new access token
54-
* createAccessTokenRequest: (refreshToken) => request,
55-
*
56-
* (Required) Parses access token from access token response
57-
* parseAccessToken: (response) => accessToken,
58-
*
59-
* (Required) Defines whether interceptor will intercept this request or just let it pass through
60-
* shouldIntercept: (request) => boolean,
61-
*
62-
* (Required) Defines whether access token will be invalidated after this response
63-
* shouldInvalidateAccessToken: (response) => boolean,
64-
*
65-
* (Required) Adds authorization for intercepted requests
66-
* authorizeRequest: (request, accessToken) => authorizedRequest,
67-
*
68-
* Checks if response should be considered unauthorized (by default only 401 responses are
69-
* considered unauthorized. Override this method if you need to trigger token renewal for
70-
* other response statuses.
71-
* isResponseUnauthorized: (response) => boolean,
72-
*
73-
* Number of retries after initial request was unauthorized
74-
* fetchRetryCount: 1,
75-
*
76-
* Event invoked when access token has changed
77-
* onAccessTokenChange: null,
78-
*
79-
* Event invoked when response is resolved
80-
* onResponse: null,
81-
*
96+
* @param {object} config
97+
* @param {createAccessTokenRequest} config.createAccessTokenRequest
98+
* Prepare fetch request for renewing new access token
99+
* @param {parseAccessToken} config.parseAccessToken
100+
* Parses access token from access token response
101+
* @param {shouldIntercept} config.shouldIntercept
102+
* Defines whether interceptor will intercept this request or just let it pass through
103+
* @param {shouldInvalidateAccessToken} config.shouldInvalidateAccessToken
104+
* Defines whether access token will be invalidated after this response
105+
* @param {authorizeRequest} config.authorizeRequest
106+
* Adds authorization for intercepted requests
107+
* @param {function} [config.isResponseUnauthorized=null]
108+
* Checks if response should be considered unauthorized (by default only 401 responses are
109+
* considered unauthorized. Override this method if you need to trigger token renewal for
110+
* other response statuses.
111+
* @param {number} [config.fetchRetryCount=1]
112+
* Number of retries after initial request was unauthorized
113+
* @param {number} [config.onAccessTokenChange=null]
114+
* Event invoked when access token has changed
115+
* @param {number} [config.onResponse=null]
116+
* Event invoked when response is resolved
117+
* </pre>
82118
*/
83119
configure(config) {
84120
this.config = { ...this.config, ...config };
@@ -87,13 +123,13 @@ export default class FetchInterceptor {
87123
throw new Error(ERROR_INVALID_CONFIG);
88124
}
89125

90-
this.accessTokenProvider = new AccessTokenProvider(this.fetch, this.config);
126+
this.accessTokenProvider.configure(this.config);
91127
}
92128

93129
/**
94130
* Authorizes fetch interceptor with given refresh token
95-
* @param refreshToken
96-
* @param accessToken
131+
* @param {string} refreshToken Refresh token
132+
* @param {string} accessToken Access token
97133
*/
98134
authorize(refreshToken, accessToken) {
99135
this.accessTokenProvider.authorize(refreshToken, accessToken);
@@ -114,6 +150,15 @@ export default class FetchInterceptor {
114150
this.accessTokenProvider.clear();
115151
}
116152

153+
/**
154+
* Clears current authorization and restores default configuration, e.g. interceptor
155+
* will stop intercepting requests.
156+
*/
157+
unload() {
158+
this.clear();
159+
this.config = getDefaultConfig();
160+
}
161+
117162
/**
118163
* Main intercept method, you should chain this inside wrapped fetch call
119164
* @param args Args initially provided to fetch method
@@ -124,10 +169,11 @@ export default class FetchInterceptor {
124169
}
125170

126171
isConfigValid() {
127-
return this.config.shouldIntercept &&
128-
this.config.authorizeRequest &&
129-
this.config.createAccessTokenRequest &&
130-
this.config.parseAccessToken;
172+
return this.config.shouldIntercept && isFunction(this.config.shouldIntercept) &&
173+
this.config.authorizeRequest && isFunction(this.config.authorizeRequest) &&
174+
this.config.isResponseUnauthorized && isFunction(this.config.isResponseUnauthorized) &&
175+
this.config.createAccessTokenRequest && isFunction(this.config.createAccessTokenRequest) &&
176+
this.config.parseAccessToken && isFunction(this.config.parseAccessToken);
131177
}
132178

133179
resolveIntercept(resolve, reject, ...args) {
@@ -357,3 +403,5 @@ export default class FetchInterceptor {
357403
throw new Error(error);
358404
}
359405
}
406+
407+
export default FetchInterceptor;

src/index.js

+46-15
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import {
2-
isReactNative,
3-
isWorker,
4-
isWeb,
5-
isNode,
2+
resolveEnvironment,
63
} from './services/environment';
74
import { isResponseUnauthorized } from './services/http';
85
import FetchInterceptor from './FetchInterceptor';
96

107
let interceptor = null;
8+
let environment = null;
119

1210
export function attach(env) {
1311
if (!env.fetch) {
@@ -28,38 +26,71 @@ export function attach(env) {
2826
env.fetch = fetchWrapper(env.fetch);
2927
}
3028

31-
function init() {
32-
if (isReactNative()) {
33-
attach(global);
34-
} else if (isWorker()) {
35-
attach(self);
36-
} else if (isWeb()) {
37-
attach(window);
38-
} else if (isNode()) {
39-
attach(global);
40-
} else {
29+
function initialize() {
30+
environment = resolveEnvironment();
31+
if (!environment) {
4132
throw new Error('Unsupported environment for fetch-token-intercept');
4233
}
34+
35+
attach(environment);
4336
}
4437

38+
/**
39+
* Initializes and configures interceptor
40+
* @param config Configuration object
41+
* @see FetchInterceptor#configure
42+
*/
4543
export function configure(config) {
44+
if (!interceptor) {
45+
initialize();
46+
}
47+
4648
interceptor.configure(config);
4749
}
4850

51+
/**
52+
* Initializes tokens which will be used by interceptor
53+
* @param args
54+
* @see FetchInterceptor#authorize
55+
*/
4956
export function authorize(...args) {
5057
interceptor.authorize(...args);
5158
}
5259

60+
/**
61+
* Returns current set of tokens used by interceptor
62+
* @returns {{accessToken: string, refreshToken: string}|*}
63+
*/
5364
export function getAuthorization() {
5465
return interceptor.getAuthorization();
5566
}
5667

68+
/**
69+
* Clears authorization tokens from interceptor
70+
*/
5771
export function clear() {
5872
return interceptor.clear();
5973
}
6074

75+
/**
76+
* Gets a value indicating whether interceptor is currently active
77+
* @returns {boolean}
78+
*/
79+
export function isActive() {
80+
return !!interceptor;
81+
}
82+
83+
/**
84+
* Removes interceptor and restores default behaviour
85+
*/
86+
export function unload() {
87+
if (interceptor) {
88+
interceptor.unload();
89+
}
90+
}
91+
6192
export {
6293
isResponseUnauthorized,
6394
};
6495

65-
init();
96+
initialize();

src/services/RetryCountExceededException.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default class RetryCountExceededException extends Error {
55
this.requestContext = requestContext;
66

77
// Use V8's native method if available, otherwise fallback
8-
if ("captureStackTrace" in Error) {
8+
if ('captureStackTrace' in Error) {
99
Error.captureStackTrace(this, RetryCountExceededException);
1010
} else {
1111
this.stack = (new Error()).stack;

src/services/TokenExpiredException.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default class TokenExpiredException extends Error {
55
this.name = this.constructor.name;
66

77
// Use V8's native method if available, otherwise fallback
8-
if ("captureStackTrace" in Error) {
8+
if ('captureStackTrace' in Error) {
99
Error.captureStackTrace(this, TokenExpiredException);
1010
} else {
1111
this.stack = (new Error()).stack;

0 commit comments

Comments
 (0)