[FIX] website: properly warn users of Google API errors

Before this commit, when adding a google map snippet in the DOM, the
user was asked for its API key if not already configured, thanks to an
user-friendly dialog. However, in the case it was misconfigured (while
the editor dialog prevents some misconfiguration, configuration via the
backend allow any random key to be given), the UX was terrible: the
google map is simply removed without any notification.

Misconfiguration can be:
- Invalid API key
- "Maps JavaScript API", "Places API" or "Maps Static API" not enabled
- Billing not enabled

Now notifications/messages warn about those things and reopens the key
configuration dialog, which contains links to the gmap API documentation
and now more information.
Hopefully, this can be improved even further later. Indeed, this is
still not perfect as there is no reliable way to understand google
responses. E.g. even with the three mentioned API and billing enabled,
sometimes the google map API still returns errors indicating "not
enabled APIs" but it cannot be reproduced reliably. During my test it
was systematic for 15min at some point but now there is none, ever. Like
if there was a delay after enabling an API on the google console before
it stops sending errors... although the API works immediately.

Notice that in 15.0, this snippet is shown in debug mode only and we
encourage users to use the new "Map" snippet which does not require any
configuration.

opw-2976261

X-original-commit: d9f86840e27463ccf370a748cafc7c4be32f901f
Part-of: odoo/odoo#106103
This commit is contained in:
qsm-odoo
2022-11-21 21:18:57 +01:00
parent f0ca3f32d2
commit d5ab890f23
3 changed files with 169 additions and 58 deletions
@@ -73,22 +73,47 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({
* if not already defined.
*
* @private
* @param {boolean} [reconfigure=false]
* @param {boolean} [onlyIfUndefined=false]
* @param {boolean} [alwaysReconfigure=false]
* @param {boolean} [configureIfNecessary=false]
*/
async _configureGMapAPI({reconfigure, onlyIfUndefined}) {
async _configureGMapAPI({alwaysReconfigure, configureIfNecessary}) {
if (!alwaysReconfigure && !configureIfNecessary) {
// TODO should review, parameters are weird... only one necessary?
return false;
}
const apiKey = await new Promise(resolve => {
this.getParent().trigger_up('gmap_api_key_request', {
onSuccess: key => resolve(key),
});
});
if (!reconfigure && (apiKey || !onlyIfUndefined)) {
const apiKeyValidation = apiKey ? await this._validateGMapAPIKey(apiKey) : {
isValid: false,
message: undefined,
};
if (!alwaysReconfigure && configureIfNecessary && apiKey && apiKeyValidation.isValid) {
return false;
}
let websiteId;
this.trigger_up('context_get', {
callback: ctx => websiteId = ctx['website_id'],
});
function applyError(message) {
const $apiKeyInput = this.find('#api_key_input');
const $apiKeyHelp = this.find('#api_key_help');
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.empty().text(message);
}
const $content = $(qweb.render('website.s_google_map_modal', {
apiKey: apiKey,
}));
if (!apiKeyValidation.isValid && apiKeyValidation.message) {
applyError.call($content, apiKeyValidation.message);
}
return new Promise(resolve => {
let invalidated = false;
const dialog = new Dialog(this, {
@@ -96,55 +121,57 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({
title: _t("Google Map API Key"),
buttons: [
{text: _t("Save"), classes: 'btn-primary', click: async (ev) => {
const $apiKeyInput = dialog.$('#api_key_input');
const valueAPIKey = $apiKeyInput.val();
const $apiKeyHelp = dialog.$('#api_key_help');
const valueAPIKey = dialog.$('#api_key_input').val();
if (!valueAPIKey) {
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.text(_t("Enter an API Key"));
applyError.call(dialog.$el, _t("Enter an API Key"));
return;
}
const $button = $(ev.currentTarget);
$button.prop('disabled', true);
try {
const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${valueAPIKey}`);
if (response.status === 200) {
await this._rpc({
model: 'website',
method: 'write',
args: [
[websiteId],
{google_maps_api_key: valueAPIKey},
],
});
invalidated = true;
dialog.close();
} else {
const text = await response.text();
$apiKeyInput.addClass('is-invalid');
$apiKeyHelp.empty().text(
_t("Invalid API Key. The following error was returned by Google:")
).append($('<i/>', {
text: text,
class: 'ms-1',
}));
}
} catch {
$apiKeyHelp.text(_t("Check your connection and try again"));
} finally {
$button.prop("disabled", false);
const res = await this._validateGMapAPIKey(valueAPIKey);
if (res.isValid) {
await this._rpc({
model: 'website',
method: 'write',
args: [
[websiteId],
{google_maps_api_key: valueAPIKey},
],
});
invalidated = true;
dialog.close();
} else {
applyError.call(dialog.$el, res.message);
}
$button.prop("disabled", false);
}},
{text: _t("Cancel"), close: true}
],
$content: $(qweb.render('website.s_google_map_modal', {
apiKey: apiKey,
})),
$content: $content,
});
dialog.on('closed', this, () => resolve(invalidated));
dialog.open();
});
},
/**
* @private
*/
async _validateGMapAPIKey(key) {
try {
const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${key}`);
const isValid = (response.status === 200);
return {
isValid: isValid,
message: !isValid &&
_t("Invalid API Key. The following error was returned by Google:") + " " + (await response.text()),
};
} catch {
return {
isValid: false,
message: _t("Check your connection and try again"),
};
}
},
/**
* @override
*/
@@ -168,8 +195,8 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({
async _handleGMapRequest(ev, gmapRequestEventName) {
ev.stopPropagation();
const reconfigured = await this._configureGMapAPI({
reconfigure: ev.data.reconfigure,
onlyIfUndefined: ev.data.configureIfNecessary,
alwaysReconfigure: ev.data.reconfigure,
configureIfNecessary: ev.data.configureIfNecessary,
});
this.getParent().trigger_up(gmapRequestEventName, {
refetch: reconfigured,
@@ -324,10 +324,20 @@ const GPSPicker = InputUserValueWidget.extend({
this.trigger_up('gmap_api_request', {
editableMode: true,
configureIfNecessary: true,
onSuccess: key => resolve(!!key),
onSuccess: key => {
if (!key) {
resolve(false);
return;
}
// TODO see _notifyGMapError, this tries to trigger an error
// early but this is not consistent with new gmap keys.
this._nearbySearch('(50.854975,4.3753899)', !!key)
.then(place => resolve(!!place));
},
});
});
if (!this._gmapLoaded) {
if (!this._gmapLoaded && !this._gmapErrorNotified) {
this.trigger_up('user_value_widget_critical');
return;
}
@@ -361,17 +371,36 @@ const GPSPicker = InputUserValueWidget.extend({
*/
async setValue() {
await this._super(...arguments);
if (!this._gmapLoaded) {
return;
}
await new Promise(resolve => {
const gps = this._value;
if (this._gmapCacheGPSToPlace[gps]) {
this._gmapPlace = this._gmapCacheGPSToPlace[gps];
resolve();
return;
}
this._gmapPlace = await this._nearbySearch(this._value);
if (this._gmapPlace) {
this.inputEl.value = this._gmapPlace.formatted_address;
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {string} gps
* @param {boolean} [notify=true]
* @returns {Promise}
*/
async _nearbySearch(gps, notify = true) {
if (this._gmapCacheGPSToPlace[gps]) {
return this._gmapCacheGPSToPlace[gps];
}
const p = gps.substring(1).slice(0, -1).split(',');
const location = new google.maps.LatLng(p[0] || 0, p[1] || 0);
return new Promise(resolve => {
const service = new google.maps.places.PlacesService(document.createElement('div'));
const p = gps.substring(1).slice(0, -1).split(',');
const location = new google.maps.LatLng(p[0] || 0, p[1] || 0);
service.nearbySearch({
// Do a 'nearbySearch' followed by 'getDetails' to avoid using
// GMap Geocoder which the user may not have enabled... but
@@ -386,23 +415,59 @@ const GPSPicker = InputUserValueWidget.extend({
placeId: results[0].place_id,
fields: ['geometry', 'formatted_address'],
}, (place, status) => {
resolve();
if (status === google.maps.places.PlacesServiceStatus.OK) {
this._gmapCacheGPSToPlace[gps] = place;
this._gmapPlace = place;
resolve(place);
} else if (GMAP_CRITICAL_ERRORS.includes(status)) {
this.trigger_up('user_value_widget_critical');
if (notify) {
this._notifyGMapError();
}
resolve();
}
});
} else if (GMAP_CRITICAL_ERRORS.includes(status)) {
if (notify) {
this._notifyGMapError();
}
resolve();
} else {
resolve();
this.trigger_up('user_value_widget_critical');
}
});
});
if (this._gmapPlace) {
this.inputEl.value = this._gmapPlace.formatted_address;
},
/**
* Indicates to the user there is an error with the google map API and
* re-opens the configuration dialog. For good measures, this also notifies
* a critical error which normally removes the related snippet entirely.
*
* @private
*/
_notifyGMapError() {
// TODO this should be better to detect all errors. This is random.
// When misconfigured (wrong APIs enabled), sometimes Google throw
// errors immediately (which then reaches this code), sometimes it
// throws them later (which then induces an error log in the console
// and random behaviors).
if (this._gmapErrorNotified) {
return;
}
this._gmapErrorNotified = true;
this.displayNotification({
type: 'danger',
sticky: true,
message: _t("A Google Map error occurred. Make sure to read the key configuration popup carefully."),
});
this.trigger_up('gmap_api_request', {
editableMode: true,
reconfigure: true,
onSuccess: () => {
this._gmapErrorNotified = false;
},
});
setTimeout(() => this.trigger_up('user_value_widget_critical'));
},
//--------------------------------------------------------------------------
@@ -132,6 +132,25 @@
Enable billing on your Google Project
</a>
</div>
<div class="alert alert-info mb-0 mt-3">
Make sure your settings are properly configured:
<ul class="mb-0">
<li>
Enable the right google map APIs in your google account
<ul>
<li>Maps Static API</li>
<li>Maps JavaScript API</li>
<li>Places API</li>
</ul>
</li>
<li>
Make sure billing is enabled
</li>
<li>
Make sure to wait if errors keep being shown: sometimes enabling an API allows to use it immediately but Google keeps triggering errors for a while
</li>
</ul>
</div>
</div>
</div>
</div>