/** * Google OAuth Widget * * Step 2 of the calendar plugin's setup, between uploading the OAuth client * file and picking calendars. Google will not let a headless device complete * consent on its own, so the flow is necessarily two calls with a human in * between: * * 1. POST /api/v3/plugins/calendar/authenticate with no body * -> { auth_url } to open in a browser * 2. the browser lands on a loopback address that fails to load; its URL * carries the authorization code. POST it back as redirect_url * -> the server exchanges it and writes token.pickle * * The failed page in step 2 is expected and is worth saying out loud, because * it looks exactly like something went wrong. * * @module GoogleOAuthWidget */ (function () { 'use strict'; if (typeof window.LEDMatrixWidgets === 'undefined') { console.error('[GoogleOAuthWidget] LEDMatrixWidgets registry not found. Load registry.js first.'); return; } const ENDPOINT = '/api/v3/plugins/calendar/authenticate'; window.LEDMatrixWidgets.register('google-oauth', { name: 'Google OAuth Widget', version: '1.0.0', /** * @param {HTMLElement} container * @param {Object} config - schema config (unused) * @param {*} value - unused; this widget stores nothing * @param {Object} options - { fieldId, pluginId, name } */ render: function (container, config, value, options) { const fieldId = options.fieldId; // Nothing is stored in config by this step -- the result is // token.pickle on the device -- but the form still expects a field. const hidden = document.createElement('input'); hidden.type = 'hidden'; hidden.id = fieldId + '_hidden'; hidden.name = options.name; hidden.value = value || ''; const startBtn = document.createElement('button'); startBtn.type = 'button'; startBtn.className = 'px-3 py-1.5 text-sm rounded-md bg-blue-600 hover:bg-blue-700 text-white'; startBtn.innerHTML = ' Connect Google Account'; const status = document.createElement('p'); status.className = 'text-xs text-gray-400 mt-2'; const step2 = document.createElement('div'); step2.className = 'mt-3 hidden'; const link = document.createElement('a'); link.target = '_blank'; link.rel = 'noopener noreferrer'; link.className = 'text-blue-400 underline text-sm break-all'; link.textContent = 'Open the Google consent screen'; // Deliberately loud. After consent the browser is redirected to a // loopback address nothing is listening on, so it lands on a // browser error page -- which reads as a failure at exactly the // moment the user has to act on it. Said quietly in grey it gets // missed, and the flow looks broken when it is working. const hint = document.createElement('div'); hint.className = 'mt-3 p-3 rounded-md border border-amber-500/60 bg-amber-500/10'; hint.innerHTML = '
' + ' ' + 'The next page will fail to load. That is expected.
' + ''
+ 'After you approve access, Google sends your browser to '
+ '127.0.0.1, where nothing is running \u2014 so you will see '
+ '"This site can\u2019t be reached" or similar. Nothing has gone wrong. '
+ 'Copy the entire address out of the address bar '
+ '(it contains ?code=...) and paste it in the box below.