Better management of permissions

* Finish the ability to use custom tile server.
* Store settings whenever they are changed.
* Support new settings for audio/vibrate permission.
This commit is contained in:
Lucas Verney 2018-07-27 16:13:16 +02:00
parent 0d0a5d85d6
commit c19bd60174
9 changed files with 221 additions and 69 deletions

View File

@ -107,6 +107,14 @@ export default {
this.radiusFromAccuracy > this.markerRadius this.radiusFromAccuracy > this.markerRadius
); );
}, },
tileServer() {
const tileServerSetting = this.$store.state.settings.tileServer;
if (tileServerSetting in constants.TILE_SERVERS) {
return constants.TILE_SERVERS[tileServerSetting];
}
const firstColon = tileServerSetting.indexOf(':');
return tileServerSetting.substring(firstColon + 1);
},
}, },
data() { data() {
return { return {
@ -118,7 +126,6 @@ export default {
maxZoom: constants.MAX_ZOOM, maxZoom: constants.MAX_ZOOM,
minZoom: constants.MIN_ZOOM, minZoom: constants.MIN_ZOOM,
isRecenterButtonShown: false, isRecenterButtonShown: false,
tileServer: constants.TILE_SERVERS[this.$store.state.settings.tileServer],
unknownMarkerIcon: L.icon({ unknownMarkerIcon: L.icon({
iconAnchor: [20, 40], iconAnchor: [20, 40],
iconSize: [40, 40], iconSize: [40, 40],

View File

@ -0,0 +1,79 @@
<template>
<div>
<v-switch
class="switch"
:messages="[`<i aria-hidden='true' class='v-icon material-icons' style='vertical-align: middle;'>help</i> ${$t('permissions.preventSuspendDescription')}`]"
color="success"
:label="$t('permissions.preventSuspend')"
v-model="hasPreventSuspendPermission"
>
</v-switch>
<v-switch
class="switch"
color="success"
:label="$t('permissions.playSound')"
v-model="hasPlaySoundPermission"
>
</v-switch>
<v-switch
class="switch"
color="success"
:label="$t('permissions.vibrate')"
v-model="hasVibratePermission"
>
</v-switch>
<v-switch
class="switch"
:messages="[`<i aria-hidden='true' class='v-icon material-icons' style='vertical-align: middle;'>help</i> ${$t('permissions.geolocationDescription')}`]"
color="success"
:label="$t('permissions.geolocation')"
v-model="hasGeolocationPermission"
>
</v-switch>
</div>
</template>
<script>
export default {
computed: {
hasGeolocationPermission: {
get() {
return this.$store.state.settings.hasGeolocationPermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasGeolocationPermission', value });
},
},
hasPlaySoundPermission: {
get() {
return this.$store.state.settings.hasPlaySoundPermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasPlaySoundPermission', value });
},
},
hasPreventSuspendPermission: {
get() {
return this.$store.state.settings.hasPreventSuspendPermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasPreventSuspendPermission', value });
},
},
hasVibratePermission: {
get() {
return this.$store.state.settings.hasVibratePermission;
},
set(value) {
this.$store.dispatch('setSetting', { setting: 'hasVibratePermission', value });
},
},
},
};
</script>
<style>
.switch .v-label {
color: rgba(0,0,0,.87);
}
</style>

View File

@ -13,6 +13,8 @@ import miscIcon from '@/assets/misc.svg';
import obstacleIcon from '@/assets/obstacle.svg'; import obstacleIcon from '@/assets/obstacle.svg';
import potholeIcon from '@/assets/pothole.svg'; import potholeIcon from '@/assets/pothole.svg';
export const VERSION = '0.1';
export const REPORT_TYPES = { export const REPORT_TYPES = {
accident: { accident: {
description: 'reportLabels.accidentDescription', description: 'reportLabels.accidentDescription',

View File

@ -1,7 +1,6 @@
{ {
"about": { "about": {
"availableReportsTitle": "The available reports so far are:", "availableReportsTitle": "The available reports so far are:",
"geolocationDescription": "As of current version, your precise geolocation is handled within your device and never sent from it to any external service. The map background is downloaded on demand from the tile provider and it has then access to an estimate of the displayed position. If you refuse to share your geolocation, you can still pick a location manually but you will miss some geolocation dependent features.",
"license": "It is released under an <a href=\"https://opensource.org/licenses/MIT\">MIT license</a> (<a href=\"https://framagit.org/phyks/cyclassist\">source code</a>). Icons are based on creations from Wikimedia, Vecteezy, Pixabay or Flaticon. The map background is using tiles from <a href=\"https://carto.com/location-data-services/basemaps/\">Carto.com</a> or <a href=\"http://thunderforest.com/\">Thunderforest</a>, thanks to <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap contributors</a> and <a href=\"http://leafletjs.com/\">Leaflet</a>. Collected reports are available under <a href=\"https://opendatacommons.org/licenses/odbl/\">ODbL license</a>. Manual location picking uses the awesome API from <a href=\"https://adresse.data.gouv.fr\">adresse.data.gouv.fr</a>.", "license": "It is released under an <a href=\"https://opensource.org/licenses/MIT\">MIT license</a> (<a href=\"https://framagit.org/phyks/cyclassist\">source code</a>). Icons are based on creations from Wikimedia, Vecteezy, Pixabay or Flaticon. The map background is using tiles from <a href=\"https://carto.com/location-data-services/basemaps/\">Carto.com</a> or <a href=\"http://thunderforest.com/\">Thunderforest</a>, thanks to <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap contributors</a> and <a href=\"http://leafletjs.com/\">Leaflet</a>. Collected reports are available under <a href=\"https://opendatacommons.org/licenses/odbl/\">ODbL license</a>. Manual location picking uses the awesome API from <a href=\"https://adresse.data.gouv.fr\">adresse.data.gouv.fr</a>.",
"summary": "This app lets you track and share issues with bike lanes.", "summary": "This app lets you track and share issues with bike lanes.",
"usage": "How to use", "usage": "How to use",
@ -53,6 +52,14 @@
"retry": "Retry", "retry": "Retry",
"spaceBeforeDoublePunctuations": "" "spaceBeforeDoublePunctuations": ""
}, },
"permissions": {
"geolocation": "Geolocation",
"geolocationDescription": "As of current version, your precise geolocation is handled within your device and never sent from it to any external service. The map background is downloaded on demand from the tile provider and it has then access to an estimate of the displayed position. If you refuse to share your geolocation, you can still pick a location manually but you will miss some geolocation dependent features.",
"playSound": "Play sound",
"preventSuspend": "Prevent device from going to sleep",
"preventSuspendDescription": "When the map is displayed, the device will be prevented from going to sleep.",
"vibrate": "Vibrate"
},
"reportCard": { "reportCard": {
"Reported": "Reported" "Reported": "Reported"
}, },
@ -75,9 +82,10 @@
"potholeDescription": "A pothole in the ground." "potholeDescription": "A pothole in the ground."
}, },
"settings": { "settings": {
"customTileServer": "Custom tile server",
"customTileServerURL": "Custom tile server URL",
"customTileServerURLHint": "For example: http://tile.thunderforest.com/cycle/{z}/{x}/{y}.png",
"locale": "Language", "locale": "Language",
"preventSuspend": "Prevent device from going to sleep",
"save": "Save",
"skipOnboarding": "Skip onboarding", "skipOnboarding": "Skip onboarding",
"tileServer": "Map tiles server" "tileServer": "Map tiles server"
}, },

View File

@ -3,7 +3,7 @@ import Vue from 'vue';
import { messages, getBrowserLocales } from '@/i18n'; import { messages, getBrowserLocales } from '@/i18n';
import { storageAvailable } from '@/tools'; import { storageAvailable } from '@/tools';
import { TILE_SERVERS, DEFAULT_TILE_SERVER } from '@/constants'; import { DEFAULT_TILE_SERVER, TILE_SERVERS, VERSION } from '@/constants';
import * as types from './mutations-types'; import * as types from './mutations-types';
function loadDataFromStorage(name) { function loadDataFromStorage(name) {
@ -19,6 +19,23 @@ function loadDataFromStorage(name) {
} }
} }
function handleMigrations() {
if (!storageAvailable('localStorage')) {
return;
}
const version = loadDataFromStorage('version');
// Migration from pre-0.1 to 0.1
if (version === null) {
const preventSuspend = loadDataFromStorage('preventSuspend');
if (preventSuspend !== null) {
localStorage.setItem('hasPreventSuspendPermission', JSON.stringify(preventSuspend));
}
localStorage.removeItem('preventSuspend');
localStorage.setItem('version', JSON.stringify(VERSION));
}
}
// Load unsent reports from storage // Load unsent reports from storage
let unsentReports = null; let unsentReports = null;
if (storageAvailable('localStorage')) { if (storageAvailable('localStorage')) {
@ -27,15 +44,23 @@ if (storageAvailable('localStorage')) {
// Load settings from storage // Load settings from storage
let locale = null; let locale = null;
let preventSuspend = null; let hasGeolocationPermission = null;
let hasPlaySoundPermission = null;
let hasPreventSuspendPermission = null;
let hasVibratePermission = null;
let skipOnboarding = null; let skipOnboarding = null;
let tileServer = null; let tileServer = null;
if (storageAvailable('localStorage')) { if (storageAvailable('localStorage')) {
preventSuspend = loadDataFromStorage('preventSuspend'); handleMigrations();
hasGeolocationPermission = loadDataFromStorage('hasGeolocationPermission');
hasPlaySoundPermission = loadDataFromStorage('hasPlaySoundPermission');
hasPreventSuspendPermission = loadDataFromStorage('hasPreventSuspendPermission');
hasVibratePermission = loadDataFromStorage('hasVibratePermission');
skipOnboarding = loadDataFromStorage('skipOnboarding'); skipOnboarding = loadDataFromStorage('skipOnboarding');
tileServer = loadDataFromStorage('tileServer'); tileServer = loadDataFromStorage('tileServer');
if (!TILE_SERVERS[tileServer]) { if (tileServer && !TILE_SERVERS[tileServer] && !tileServer.startsWith('custom:')) {
tileServer = null; tileServer = null;
} }
@ -80,7 +105,18 @@ export const initialState = {
unsentReports: unsentReports || [], unsentReports: unsentReports || [],
settings: { settings: {
locale: locale || 'en', locale: locale || 'en',
preventSuspend: preventSuspend || true, hasGeolocationPermission: (
hasGeolocationPermission !== null ? hasGeolocationPermission : true
),
hasPlaySoundPermission: (
hasPlaySoundPermission !== null ? hasPlaySoundPermission : true
),
hasPreventSuspendPermission: (
hasPreventSuspendPermission !== null ? hasPreventSuspendPermission : true
),
hasVibratePermission: (
hasVibratePermission !== null ? hasVibratePermission : true
),
skipOnboarding: skipOnboarding || false, skipOnboarding: skipOnboarding || false,
tileServer: tileServer || DEFAULT_TILE_SERVER, tileServer: tileServer || DEFAULT_TILE_SERVER,
}, },

View File

@ -2,7 +2,7 @@
<v-container fluid> <v-container fluid>
<v-layout row> <v-layout row>
<v-flex xs12 sm6 offset-sm3> <v-flex xs12 sm6 offset-sm3>
<p>{{ $t('about.summary') }} <span v-html="$t('about.geolocationDescription')"></span></p> <p>{{ $t('about.summary') }} <span v-html="$t('permissions.geolocationDescription')"></span></p>
<h2 class="body-2">{{ $t('about.usage') }}</h2> <h2 class="body-2">{{ $t('about.usage') }}</h2>
<p>{{ $t('about.usageDescription') }}</p> <p>{{ $t('about.usageDescription') }}</p>

View File

@ -57,7 +57,6 @@ import i18n from '@/i18n';
import store from '@/store'; import store from '@/store';
function handlePositionError(error) { function handlePositionError(error) {
// TODO: Not translated when changing locale
store.dispatch('setLocationError', { error: error.code }); store.dispatch('setLocationError', { error: error.code });
} }
@ -220,7 +219,7 @@ export default {
this.reportLatLng = null; this.reportLatLng = null;
}, },
setNoSleep() { setNoSleep() {
if (this.$store.state.settings.preventSuspend) { if (this.$store.state.settings.hasPreventSuspendPermission) {
this.noSleep = new NoSleep(); this.noSleep = new NoSleep();
this.noSleep.enable(); this.noSleep.enable();
} }
@ -238,7 +237,11 @@ export default {
if (this.$route.name !== 'SharedMap') { if (this.$route.name !== 'SharedMap') {
// Only enable NoSleep in normal map view (with position tracking). // Only enable NoSleep in normal map view (with position tracking).
this.setNoSleep(); this.setNoSleep();
this.initializePositionWatching(); if (this.$store.state.settings.hasGeolocationPermission) {
this.initializePositionWatching();
} else {
this.$store.state.location.error = 1;
}
} }
this.$store.dispatch('fetchReports'); this.$store.dispatch('fetchReports');
}, },

View File

@ -23,9 +23,9 @@
<v-flex xs8 offset-xs2> <v-flex xs8 offset-xs2>
<h2 class="headline pa-3">{{ $t('intro.checkingPermissions') }}</h2> <h2 class="headline pa-3">{{ $t('intro.checkingPermissions') }}</h2>
<v-layout row class="white mb-3"> <v-layout row class="white">
<v-flex xs6 offset-xs3> <v-flex class="mb-3 mx-3">
<v-switch class="switch" :messages="[`<i aria-hidden='true' class='v-icon material-icons' style='vertical-align: middle;'>help</i> ${$t('about.geolocationDescription')}`]" color="success" :label="$t('geolocation.geolocation')" v-model="hasGeolocationPermission" readonly @click="handleGeolocationPermission"></v-switch> <PermissionsSwitches></PermissionsSwitches>
</v-flex> </v-flex>
</v-layout> </v-layout>
@ -44,17 +44,14 @@
<script> <script>
import ReportsDescription from '@/components/ReportsDescription.vue'; import ReportsDescription from '@/components/ReportsDescription.vue';
import PermissionsSwitches from '@/components/PermissionsSwitches.vue';
export default { export default {
components: { components: {
ReportsDescription, ReportsDescription,
PermissionsSwitches,
}, },
created() { created() {
if (navigator.permissions) {
navigator.permissions.query({ name: 'geolocation' }).then((result) => {
this.hasGeolocationPermission = (result.state === 'granted');
});
}
this.$store.dispatch('markIntroAsSeen'); this.$store.dispatch('markIntroAsSeen');
}, },
data() { data() {
@ -63,34 +60,10 @@ export default {
step = 3; step = 3;
} }
return { return {
hasGeolocationPermission: false,
step, step,
}; };
}, },
methods: { methods: {
handleGeolocationPermission() {
if (this.hasGeolocationPermission) {
// Permission already granted
return;
}
// Explicitly request the permission to the user
navigator.geolocation.getCurrentPosition(
() => {
this.hasGeolocationPermission = true;
},
(error) => {
if (error.code === 1) { // Permission denied
this.hasGeolocationPermission = false;
} else {
this.hasGeolocationPermission = true;
}
},
{
timeout: 0,
},
);
},
goToMap() { goToMap() {
if (!this.$store.state.settings.skipOnboarding) { if (!this.$store.state.settings.skipOnboarding) {
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: true }); this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: true });
@ -106,9 +79,3 @@ export default {
padding: 0; padding: 0;
} }
</style> </style>
<style>
.switch .v-label {
color: rgba(0,0,0,.87);
}
</style>

View File

@ -20,17 +20,22 @@
required required
></v-select> ></v-select>
<v-checkbox <v-text-field
:label="$t('settings.preventSuspend')" ref="shareLinkRef"
v-model="preventSuspend" :hint="$t('settings.customTileServerURLHint')"
></v-checkbox> :label="$t('settings.customTileServerURL')"
v-model="customTileServerURL"
v-if="showCustomTileServerURLField"
required
>
</v-text-field>
<v-checkbox <v-checkbox
:label="$t('settings.skipOnboarding')" :label="$t('settings.skipOnboarding')"
v-model="skipOnboarding" v-model="skipOnboarding"
></v-checkbox> ></v-checkbox>
<v-btn role="button" @click="submit">{{ $t('settings.save') }}</v-btn> <PermissionsSwitches></PermissionsSwitches>
</form> </form>
</v-flex> </v-flex>
</v-layout> </v-layout>
@ -41,24 +46,69 @@
import { TILE_SERVERS } from '@/constants'; import { TILE_SERVERS } from '@/constants';
import { AVAILABLE_LOCALES } from '@/i18n'; import { AVAILABLE_LOCALES } from '@/i18n';
import PermissionsSwitches from '@/components/PermissionsSwitches.vue';
export default { export default {
components: {
PermissionsSwitches,
},
computed: {
customTileServerURL: {
get() {
const tileServerStore = this.$store.state.settings.tileServer;
if (tileServerStore in TILE_SERVERS) {
return null;
}
const firstColon = tileServerStore.indexOf(':');
return tileServerStore.substring(firstColon + 1);
},
set(URL) {
this.$store.dispatch('setSetting', { setting: 'tileServer', value: `custom:${URL}` });
},
},
locale: {
get() {
return this.$store.state.settings.locale;
},
set(locale) {
this.$store.dispatch('setLocale', { locale });
},
},
showCustomTileServerURLField() {
return !(this.$store.state.settings.tileServer in TILE_SERVERS);
},
skipOnboarding: {
get() {
return this.$store.state.settings.skipOnboarding;
},
set(skipOnboarding) {
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: skipOnboarding });
},
},
tileServer: {
get() {
const tileServerStore = this.$store.state.settings.tileServer;
if (tileServerStore.startsWith('custom:')) {
return this.$t('settings.customTileServer');
}
return tileServerStore;
},
set(tileServer) {
if (tileServer in TILE_SERVERS) {
this.$store.dispatch('setSetting', { setting: 'tileServer', value: tileServer });
} else {
this.$store.dispatch('setSetting', { setting: 'tileServer', value: 'custom:' });
}
},
},
tileServers() {
return [].concat(Object.keys(TILE_SERVERS), this.$t('settings.customTileServer'));
},
},
data() { data() {
return { return {
i18nItems: AVAILABLE_LOCALES, i18nItems: AVAILABLE_LOCALES,
locale: this.$store.state.settings.locale,
preventSuspend: this.$store.state.settings.preventSuspend,
skipOnboarding: this.$store.state.settings.skipOnboarding,
tileServer: this.$store.state.settings.tileServer,
tileServers: Object.keys(TILE_SERVERS),
}; };
}, },
methods: {
submit() {
this.$store.dispatch('setLocale', { locale: this.locale });
this.$store.dispatch('setSetting', { setting: 'preventSuspend', value: this.preventSuspend });
this.$store.dispatch('setSetting', { setting: 'skipOnboarding', value: this.skipOnboarding });
this.$store.dispatch('setSetting', { setting: 'tileServer', value: this.tileServer });
},
},
}; };
</script> </script>