Skip to content

Instantly share code, notes, and snippets.

@kepi74
Last active January 22, 2020 15:38
Show Gist options
  • Save kepi74/61f84f05d7d704c72ec1bb902d6617b4 to your computer and use it in GitHub Desktop.
Save kepi74/61f84f05d7d704c72ec1bb902d6617b4 to your computer and use it in GitHub Desktop.
Code for Vue.js meetup
function getEffects(list, payload) {
if (list && list.length) {
return list.map((type) => {
if (typeof type === 'string') {
return put({
type,
payload,
});
}
return put(type);
});
}
return [];
}
export function* apiFetch(action) {
const { promise = {} } = action.payload;
const { resolve = null, reject = null } = promise;
yield all(
getEffects(action.payload.onStart, {
meta: action.payload,
}),
);
try {
const response = yield api(action.payload);
yield all(
getEffects(
action.payload.onSuccess,
{
...response,
meta: action.payload,
},
),
);
if (resolve !== null) {
resolve(response.data);
}
} catch (error) {
yield all(
getEffects(
action.payload.onError,
{
...error,
meta: action.payload,
},
),
);
if (reject !== null) {
reject(error.response);
}
}
yield all(
getEffects(action.payload.onFinish, {
meta: action.payload,
}),
);
}
function* apiFetchWatch() {
yield takeEvery('api/fetch', apiFetch);
}
const devSagas = process.env.NODE_ENV === 'development' ? [logActions] : [];
export default [
fetchApi,
refreshToken,
...devSagas,
];
const tokensSelector = state => pathOr({}, ['auth', 'tokens'], state);
const keepMeSignedInSelector = state => pathOr(false, ['auth', 'keepMeSignedIn'], state);
const isTokenExpired = token => !isFuture(parseISO(token));
const isTokenValid = token => isFuture(parseISO(token));
const wait = (ms, result = true) => new Promise(resolve => setTimeout(() => resolve(result), ms));
function* refreshToken() {
const tokens = yield select(tokensSelector);
try {
const response = yield api({
method: 'POST',
url: '/refresh',
headers: {
'Content-Type': 'application/json',
'X-Refresh-Token': `Bearer ${tokens.refresh}`,
},
});
yield put({
type: 'auth/setTokens',
payload: response,
});
return response;
} catch (err) {
yield all([
put({ type: 'auth/resetState' }),
put({
type: 'flashMessages/addMessage',
kind: 'error',
key: 'refreshTokenError',
}),
]);
return {};
}
}
export default function* refreshTokenWorkflow() {
while (true) {
let tokens = yield select(tokensSelector);
let keepMeSignedIn = yield select(keepMeSignedInSelector);
tokens = yield select(tokensSelector);
keepMeSignedIn = yield select(keepMeSignedInSelector);
if (isTokenExpired(tokens.access_expires_at) && keepMeSignedIn === false) {
/**
* User have expired access token and keep me signed in is not enabled
* -> reset auth state to not persist obsolete data
*/
yield put({ type: 'auth/resetState' });
}
if (isTokenExpired(tokens.refresh_expires_at) && keepMeSignedIn === true) {
/**
* User have expired refresh token and keep me signed in is enabled
* -> reset auth state to not persit obsolete data
*/
yield put({ type: 'auth/resetState' });
}
if (
isTokenExpired(tokens.access_expires_at)
&& isTokenValid(tokens.refresh_expires_at)
&& keepMeSignedIn === true
) {
/**
* User have expired access token, valid refresh token and keep me signed in is enabled
* -> try to refresh token
*/
yield call(refreshToken);
tokens = yield select(tokensSelector);
keepMeSignedIn = yield select(keepMeSignedInSelector);
}
if (tokens.access === null || tokens.access === undefined) {
/**
* User is not signed in
* -> wait for signIn / signUp action
*/
yield race({
signIn: take('auth/signInSuccess'),
signUp: take('signUp/signUpSuccess'),
});
yield call(wait, 100); // wait for tokens to be stored in state
tokens = yield select(tokensSelector);
keepMeSignedIn = yield select(keepMeSignedInSelector);
}
let userSignedOut = false;
while (!userSignedOut) {
/**
* Wait for access token expiration or user signOut action
*/
const { expired } = yield race({
expired: call(wait,
differenceInMilliseconds(parseISO(tokens.access_expires_at), new Date())),
signOut: take('auth/resetState'),
});
if (expired && keepMeSignedIn) {
/**
* Try to refresh access token for user with expired one and keep me signed in enabled
*/
const response = yield call(refreshToken);
const accessToken = pathOr(undefined, ['data', 'access'], response);
userSignedOut = accessToken === undefined;
} else {
/**
* User was signed out or access token was expired
*/
yield all([
call(wait, 100), // slight delay to prevent calling it multiple times
put({ type: 'auth/signOut' }),
]);
userSignedOut = true;
}
tokens = yield select(tokensSelector);
keepMeSignedIn = yield select(keepMeSignedInSelector);
}
}
}
// actions
getCompanyData({ commit, rootGetters }, { id, partyType }) {
commit(
'api/fetch',
{
url: `/services/company-lookups/${String(id).replace(/\s/g, '')}`,
method: 'GET',
headers: rootGetters['auth/getHeaders'],
onSuccess: [`invoiceForm/set${partyType}CompanyData`],
onError: [
{
type: 'invoiceForm/companyDataError',
partyType,
},
],
},
{ root: true },
);
},
// mutations
setSellerCompanyData: (state, action) => {
state.seller_name = action.payload.data.name;
state.seller_tax_number = action.payload.data.dic;
state.seller_address_street = action.payload.data.address_street;
state.seller_address_city = action.payload.data.address_city;
state.seller_address_postcode = action.payload.data.address_postcode;
state.seller_company_number_error = false;
},
companyDataError: (state, { partyType }) => {
const key = `${partyType.toLowerCase()}_company_number_error`;
state[key] = true;
},
Vue.use(Vuex);
const sagaPlugin = createSagaPlugin();
const debug = process.env.NODE_ENV !== 'production';
const loggerPlugin = debug ? [createLogger()] : [];
const globalPlugins = [
createPersistedState({ paths: ['auth', 'flashMessages'] }),
sagaPlugin,
];
const store = new Vuex.Store({
modules: {
api,
invoiceForm,
// ... next stores
},
plugins: [...globalPlugins, ...loggerPlugin],
});
sagas.forEach(sagaPlugin.run);
/**
* Forked & updated code of package `vuex-redux-saga`
* https://github.com/xanf/vuex-redux-saga
*/
import { runSaga, stdChannel } from 'redux-saga';
const channel = stdChannel();
const isFunc = f => typeof f === 'function';
const noop = () => undefined;
export default (options = {}) => {
const { sagaMonitor } = options;
if (sagaMonitor) {
sagaMonitor.effectTriggered = sagaMonitor.effectTriggered || noop;
sagaMonitor.effectResolved = sagaMonitor.effectResolved || noop;
sagaMonitor.effectRejected = sagaMonitor.effectRejected || noop;
sagaMonitor.effectCancelled = sagaMonitor.effectCancelled || noop;
sagaMonitor.actionDispatched = sagaMonitor.actionDispatched || noop;
}
if (options.logger && !isFunc(options.logger)) {
throw new Error('`options.logger` passed to the Saga plugin is not a function!');
}
if (options.onError && !isFunc(options.onError)) {
throw new Error('`options.onError` passed to the Saga plugin is not a function!');
}
if (options.emitter) {
throw new Error('`options.emitter` is not yet supported by Saga plugin!');
}
let store;
const sagaPlugin = (_store) => {
store = _store;
store.subscribe((mutation) => {
channel.put(mutation);
});
/**
* For listening to actions, instead of mutations use:
* store.subscribeAction((action) => {
* channel.put(action);
* });
*/
};
sagaPlugin.run = (saga, ...args) => {
if (!store) {
throw new Error('Before running a Saga, you must add Saga plugin to vuex store');
}
if (!isFunc(saga)) {
throw new Error(
'`sagaPlugin.run(saga, ...args)`: saga argument must be a Generator function',
);
}
runSaga(
{
channel,
dispatch: output => store.dispatch(output),
getState: () => store.state,
logger: options.logger,
sagaMonitor,
onError: options.onError,
},
saga,
...args,
);
};
return sagaPlugin;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment