[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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user