diff --git a/.env-example b/.env-example index 20ad094..4066979 100755 --- a/.env-example +++ b/.env-example @@ -1,4 +1,5 @@ NODE_ENV=development VUE_APP_TITLE=GrowlerApp VUE_APP_GRAPHQL_API=https://api-growlerapp.herokuapp.com/graphql -VUE_APP_GOOGLE_MAPS_KEY= \ No newline at end of file +VUE_APP_GOOGLE_MAPS_KEY= +VUE_APP_SENTRY_SLUG='' diff --git a/.npmrc b/.npmrc index 43c97e7..0ca8d2a 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ package-lock=false +save-exact=true diff --git a/package.json b/package.json index 405179b..bd741d4 100755 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "generate-icons": "icon-font-generator icons/svg/*.svg -o public/icons --prefix ico --csstp icons/css.hbs --htmltp icons/html.hbs --fontspath ./" }, "dependencies": { + "@sentry/browser": "^5.7.1", + "@sentry/integrations": "^5.7.1", "apollo-cache-inmemory": "1.6.3", "apollo-client": "2.6.4", "apollo-link-http": "1.5.16", diff --git a/src/App.vue b/src/App.vue index adc5459..b914de6 100755 --- a/src/App.vue +++ b/src/App.vue @@ -13,6 +13,7 @@ diff --git a/src/helpers/index.js b/src/helpers/index.js index 4d267fc..8b662f1 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -7,10 +7,11 @@ const isJSON = json => { if (json && typeof json === 'object' && json !== null) { return true } - } catch (err) {} - return false + } catch (error) { + return error + } } -export default { +export { isJSON } diff --git a/src/main.js b/src/main.js index f864df7..bca5c26 100755 --- a/src/main.js +++ b/src/main.js @@ -3,7 +3,8 @@ import App from './App.vue' import router from './router' import store from './store' import './registerServiceWorker' -import { apolloProvider } from './apollo' +import './services/sentry' +import { apolloProvider } from './services/apollo' Vue.config.productionTip = false diff --git a/src/router.js b/src/router.js index 2b251ae..9be62dd 100755 --- a/src/router.js +++ b/src/router.js @@ -4,20 +4,9 @@ import Home from '@/views/Home' import Splash from '@/views/Splash' import About from '@/views/About' import Store from '@/views/Store' -import user from '@/user' Vue.use(Router) -const requireUser = (to, from, next) => { - if (!user.checkUserGeoData()) { - next({ - path: '/' - }) - } else { - next() - } -} - export default new Router({ scrollBehavior () { return new Promise(resolve => { @@ -41,8 +30,7 @@ export default new Router({ { path: '/home', name: 'home', - component: Home, - beforeEnter: requireUser + component: Home }, { path: '/store/:id', diff --git a/src/apollo.js b/src/services/apollo.js similarity index 100% rename from src/apollo.js rename to src/services/apollo.js diff --git a/src/services/sentry.js b/src/services/sentry.js new file mode 100644 index 0000000..a96d51c --- /dev/null +++ b/src/services/sentry.js @@ -0,0 +1,10 @@ +import Vue from 'vue' +import * as Sentry from '@sentry/browser' +import * as Integrations from '@sentry/integrations' + +if (process.env.NODE_ENV === 'production') { + Sentry.init({ + dsn: process.env.VUE_APP_SENTRY_SLUG, + integrations: [new Integrations.Vue({ Vue, attachProps: true })] + }) +} diff --git a/src/services/user.js b/src/services/user.js new file mode 100755 index 0000000..33ef674 --- /dev/null +++ b/src/services/user.js @@ -0,0 +1,130 @@ +import { isJSON } from '@/helpers' + +/** + * Parse any errors from PositionError callback + * @param {object} error + * @return {object} + */ +const parsePositionError = error => { + switch (error.code) { + case error.PERMISSION_DENIED: + return { + code: 1, + message: 'User denied the request for geolocation.' + } + case error.POSITION_UNAVAILABLE: + return { + code: 2, + message: 'Location information is unavailable.' + } + case error.TIMEOUT: + return { + code: 3, + message: 'The request to get user location timed out.' + } + } +} + +/** + * Get user geo from device + * @return {Promise} + * https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API + */ +const getUserGeoDataFromDevice = () => { + return new Promise((resolve, reject) => { + if ('geolocation' in navigator) { + navigator.geolocation.getCurrentPosition(({ coords }) => + resolve(coords), error => reject(parsePositionError(error)) + ) + } + }) +} + +/** + * Fetch user geo data + * @return {object} user geo data + */ +const fetchUserGeoData = () => { + return new Promise(async (resolve, reject) => { + try { + let data = await getUserGeoDataFromDevice() + data = { + lat: data.latitude, + long: data.longitude + } + setUserGeoDataToStorage(data) + resolve(data) + } catch (error) { + // eslint-disable-next-line prefer-promise-reject-errors + reject(error) + } + }) +} + +/** + * Set flag if user has joined to app + */ +const setUserHasJoined = () => { + localStorage.setItem('userHasJoined', JSON.stringify(true)) +} + +/** + * Check if user has joined to app + */ +const checkHasJoined = () => { + return !!localStorage.getItem('userHasJoined') +} + +/** + * Set user geo data + * @param {object} data + */ +const setUserGeoDataToStorage = data => { + localStorage.setItem('userGeoData', JSON.stringify(data)) +} + +/** + * Get user geo data from localstorage + */ +const getUserGeoDataFromStorage = () => { + try { + return JSON.parse(localStorage.getItem('userGeoData')) + } catch (err) { + return null + } +} + +/** + * Check if user has geo data + */ +const checkUserGeoData = () => { + const data = getUserGeoDataFromStorage() + return isJSON(data) && data.lat && data.long +} + +/** + * Get user data from storage or user device + * @param {boolean} force + * @return {Promise} user geo data + */ +const getUserData = (force = false) => { + try { + if (force) { + return fetchUserGeoData() + } else { + return checkUserGeoData() + ? getUserGeoDataFromStorage() + : fetchUserGeoData() + } + } catch (error) { + return error + } +} + +export { + getUserData, + checkUserGeoData, + checkHasJoined, + setUserHasJoined, + fetchUserGeoData +} diff --git a/src/store.js b/src/store.js index b9fe184..ca675a2 100755 --- a/src/store.js +++ b/src/store.js @@ -1,6 +1,6 @@ import Vue from 'vue' import Vuex from 'vuex' -import user from '@/user' +import { getUserData } from '@/services/user' Vue.use(Vuex) @@ -16,9 +16,8 @@ export default new Vuex.Store({ setLoading (state, value) { state.loading = value }, - async setUserGeoData (state, { force }) { - const data = await user.getUserData(force) - state.userData = data + setUserGeoData (state, value) { + state.userData = value }, setMenu (state, value) { state.isMenuActive = value @@ -29,8 +28,15 @@ export default new Vuex.Store({ }, actions: { - userGeoData ({ commit }, payload) { - commit('setUserGeoData', payload) + userGeoData ({ commit }, { force }) { + return new Promise(async (resolve, reject) => { + try { + commit('setUserGeoData', await getUserData(force)) + resolve(true) + } catch (error) { + reject(error) + } + }) } } }) diff --git a/src/user.js b/src/user.js deleted file mode 100755 index b893757..0000000 --- a/src/user.js +++ /dev/null @@ -1,96 +0,0 @@ -import helpers from '@/helpers' - -/** - * Get user geo from device - * @return {Promise} - */ -const getUserGeoDataFromDevice = () => { - return new Promise((resolve, reject) => { - if (!navigator.geolocation) { - return reject( - new Error('geo error') - ) - } else { - navigator.geolocation.getCurrentPosition(result => { - const data = { - lat: result.coords.latitude, - long: result.coords.longitude - } - return resolve(data) - }) - } - }) -} - -/** - * Set flag if user has joined to app - */ -const setUserHasJoined = () => { - localStorage.setItem('userHasJoined', JSON.stringify(true)) -} - -/** - * Check if user has joined to app - */ -const checkHasJoined = () => { - return !!localStorage.getItem('userHasJoined') -} - -/** - * Set user geo data - * @param {object} data - */ -const setUserGeoDataToStorage = data => { - localStorage.setItem('userGeoData', JSON.stringify(data)) -} - -/** - * Get user geo data from localstorage - */ -const getUserGeoDataFromStorage = () => { - try { - return JSON.parse(localStorage.getItem('userGeoData')) - } catch (err) { - return null - } -} - -/** - * Check if user has geo data - */ -const checkUserGeoData = () => { - const data = getUserGeoDataFromStorage() - return helpers.isJSON(data) && data.lat && data.long -} - -/** - * Fetch user geo data - * @return {object} user geo data - */ -const fetchUserGeoData = async () => { - const data = await getUserGeoDataFromDevice() - setUserGeoDataToStorage(data) - return data -} - -/** - * Get user data from storage or user device - * @param {boolean} force - * @return {Promise} user geo data - */ -const getUserData = async (force = false) => { - if (force) { - return fetchUserGeoData() - } else { - return checkUserGeoData() - ? getUserGeoDataFromStorage() - : fetchUserGeoData() - } -} - -export default { - getUserData, - checkUserGeoData, - checkHasJoined, - setUserHasJoined -} diff --git a/src/views/Home.vue b/src/views/Home.vue index a2696ff..7d4bf4a 100755 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -1,6 +1,6 @@ @@ -58,21 +97,31 @@ import 'mobile-pull-to-refresh/dist/styles/ios/style.css' import FIND_BY_PROXIMITY from '@/graphql/FindByProximity.gql' import StoreItem from '@/components/StoreItem' import Loading from '@/components/Loading' +import vButton from '@/components/v-Button' export default { name: 'Home', components: { StoreItem, - Loading + Loading, + vButton }, mounted () { - this.refresh() + this.fetchData() + this.pullToRefresh() }, + data: () => ({ + loading: true, + stores: null, + isfetchError: null, + isGeoError: null + }), + apollo: { - findByProximity: { + stores: { query: FIND_BY_PROXIMITY, variables () { return { @@ -81,29 +130,51 @@ export default { googleMapsKey: process.env.VUE_APP_GOOGLE_MAPS_KEY } }, + $query: { + fetchPolicy: 'network-only' + }, update ({ findByProximity }) { - return findByProximity.map(store => { + this.loading = false + return findByProximity && findByProximity.map(store => { const copy = Object.assign({}, store) copy.matrix = store.matrix.find(({ mode }) => mode === 'driving') return copy }) - } + }, + error () { + this.isfetchError = true + }, + skip: true } }, methods: { - refresh () { + pullToRefresh () { const context = this pullToRefresh({ container: document.querySelector('.Home'), animates: ptrAnimatesIos, async refresh () { - context.$store.dispatch('userGeoData', { force: true }) - await context.$apollo.queries.findByProximity.refetch() + await context.fetchData(false) } }) }, + async fetchData (loading = true, reload = false) { + if (reload) window.location.reload(true) + try { + await this.$store.dispatch('userGeoData', { force: true }) + this.isGeoError = null + this.isfetchError = null + this.loading = loading + this.$apollo.queries.stores.skip = false + await this.$apollo.queries.stores.refetch() + } catch (error) { + this.loading = false + this.isGeoError = error.code + } + }, + isTouchDevice () { // https://stackoverflow.com/a/4819886/1832887 const prefixes = ' -webkit- -moz- -o- -ms- '.split(' ') @@ -159,36 +230,36 @@ export default { } } -.Home-empty { +.Home-error { text-align: center; padding-top: 4em; max-width: 280px; margin: 0 auto; } -.Home-empty h2 { +.Home-error h2 { text-transform: uppercase; line-height: 1.5; } -.Home-empty p { +.Home-error p { font-size: 1.2rem; font-family: var(--font-family-normal); } -.Home-empty-icon { +.Home-error-icon { max-width: 80px; display: inline-block; opacity: .4; transform: rotate(10deg); } @media (--lg-viewport) { - .Home-empty-icon { + .Home-error-icon { max-width: 120px; } } -.Home-empty-sep { +.Home-error-sep { display: inline-block; width: 120px; } -.Home-empty small { +.Home-error small { display: inline-block; margin-top: 1em; text-transform: uppercase; diff --git a/src/views/Splash.vue b/src/views/Splash.vue index 1296d9f..a54bf1b 100755 --- a/src/views/Splash.vue +++ b/src/views/Splash.vue @@ -26,10 +26,18 @@

Obteniendo tu ubicación...

- @@ -50,7 +58,7 @@