Skip to content

Commit aa5483d

Browse files
Merge pull request #23511 from Expensify/tgolen-mapbox-tokenmanager
Create a token manager library for MapBox
2 parents 6c22eb3 + 29c6f35 commit aa5483d

File tree

2 files changed

+111
-0
lines changed

2 files changed

+111
-0
lines changed

src/ONYXKEYS.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,9 @@ export default {
243243
// Experimental memory only Onyx mode flag
244244
IS_USING_MEMORY_ONLY_KEYS: 'isUsingMemoryOnlyKeys',
245245

246+
// The access token to be used with the Mapbox library
247+
MAPBOX_ACCESS_TOKEN: 'mapboxAccessToken',
248+
246249
ONYX_UPDATES: {
247250
// The ID of the last Onyx update that was applied to this client
248251
LAST_UPDATE_ID: 'onyxUpdatesLastUpdateID',

src/libs/actions/MapboxToken.js

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import _ from 'underscore';
2+
import moment from 'moment';
3+
import Onyx from 'react-native-onyx';
4+
import {AppState} from 'react-native';
5+
import lodashGet from 'lodash/get';
6+
import ONYXKEYS from '../../ONYXKEYS';
7+
import * as API from '../API';
8+
import CONST from '../../CONST';
9+
10+
let authToken;
11+
Onyx.connect({
12+
key: ONYXKEYS.SESSION,
13+
callback: (val) => {
14+
authToken = lodashGet(val, 'authToken', null);
15+
},
16+
});
17+
18+
let connectionID;
19+
let currentToken;
20+
let refreshTimeoutID;
21+
const REFRESH_INTERVAL = 1000 * 60 * 25;
22+
23+
const setExpirationTimer = () => {
24+
console.debug('[MapboxToken] refreshing token on an interval', REFRESH_INTERVAL, 'ms');
25+
26+
// Cancel any previous timeouts so that there is only one request to get a token at a time.
27+
clearTimeout(refreshTimeoutID);
28+
29+
// Refresh the token every 25 minutes
30+
refreshTimeoutID = setTimeout(() => {
31+
// If the user has logged out while the timer was running, skip doing anything when this callback runs
32+
if (!authToken) {
33+
console.debug('[MapboxToken] Skipping the fetch of a new token because user signed out');
34+
return;
35+
}
36+
console.debug(`[MapboxToken] Fetching a new token after waiting ${REFRESH_INTERVAL / 1000 / 60} minutes`);
37+
API.read('GetMapboxAccessToken');
38+
}, REFRESH_INTERVAL);
39+
};
40+
41+
const hasTokenExpired = () => moment().isAfter(currentToken.expiration);
42+
43+
const clearToken = () => {
44+
console.debug('[MapboxToken] Deleting the token stored in Onyx');
45+
46+
// Use Onyx.set() to delete the key from Onyx, which will trigger a new token to be retrieved from the API.
47+
Onyx.set(ONYXKEYS.MAPBOX_ACCESS_TOKEN, null);
48+
};
49+
50+
const init = () => {
51+
if (connectionID) {
52+
console.debug('[MapboxToken] init() is already listening to Onyx so returning early');
53+
return;
54+
}
55+
56+
// When the token changes in Onyx, the expiration needs to be checked so a new token can be retrieved.
57+
connectionID = Onyx.connect({
58+
key: ONYXKEYS.MAPBOX_ACCESS_TOKEN,
59+
/**
60+
* @param {Object} token
61+
* @param {String} token.token
62+
* @param {String} token.expiration
63+
* @param {String[]} [token.errors]
64+
*/
65+
callback: (token) => {
66+
// If the user has logged out, don't do anything and ignore changes to the access token
67+
if (!authToken) {
68+
console.debug('[MapboxToken] Ignoring changes to token because user signed out');
69+
return;
70+
}
71+
72+
// If the token is falsy or an empty object, the token needs to be retrieved from the API.
73+
// The API sets a token in Onyx with a 30 minute expiration.
74+
if (_.isEmpty(token)) {
75+
console.debug('[MapboxToken] Token does not exist so fetching one');
76+
API.read('GetMapboxAccessToken');
77+
return;
78+
}
79+
80+
// Store the token in a place where the AppState callback can also access it.
81+
currentToken = token;
82+
83+
if (hasTokenExpired()) {
84+
console.debug('[MapboxToken] Token has expired after reading from Onyx');
85+
clearToken();
86+
return;
87+
}
88+
89+
console.debug('[MapboxToken] Token is valid, setting up refresh');
90+
setExpirationTimer();
91+
},
92+
});
93+
94+
AppState.addEventListener('change', (nextAppState) => {
95+
// Skip getting a new token if:
96+
// - The app state is not changing to active
97+
// - There is no current token (which means it's not been fetch yet for the first time)
98+
// - The token hasn't expired yet (this would just be a waste of an API call)
99+
// - There is no authToken (which means the user has logged out)
100+
if (nextAppState !== CONST.APP_STATE.ACTIVE || !currentToken || !hasTokenExpired() || !authToken) {
101+
return;
102+
}
103+
console.debug('[MapboxToken] Token is expired after app became active');
104+
clearToken();
105+
});
106+
};
107+
108+
export default init;

0 commit comments

Comments
 (0)