Last active
January 22, 2020 15:38
-
-
Save kepi74/61f84f05d7d704c72ec1bb902d6617b4 to your computer and use it in GitHub Desktop.
Code for Vue.js meetup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const devSagas = process.env.NODE_ENV === 'development' ? [logActions] : []; | |
export default [ | |
fetchApi, | |
refreshToken, | |
...devSagas, | |
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}, |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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