diff --git a/src/UI/Components/QRCode.js b/src/UI/Components/QRCode.js new file mode 100644 index 00000000..41f19233 --- /dev/null +++ b/src/UI/Components/QRCode.js @@ -0,0 +1,47 @@ +import { Component } from "../../util/Component.js"; + +export default class QRCodeView extends Component { + static PROPERTIES = { + value: { + description: 'The text to encode in the QR code', + } + } + + static CSS = /*css*/` + .qr-code { + width: 100%; + display: flex; + justify-content: center; + flex-direction: column; + align-items: center; + } + ` + + create_template ({ template }) { + // TODO: The way we handle loading assets doesn't work well + // with web components, so for now it goes in the template. + $(template).html(` +
+
+ `); + } + + on_ready ({ listen }) { + listen('value', value => { + console.log('got value', value); + // $(this.dom_).find('.qr-code').empty(); + new QRCode($(this.dom_).find('.qr-code').get(0), { + text: value, + currectLevel: QRCode.CorrectLevel.H, + }); + }); + } +} + +// TODO: This is necessary because files can be loaded from +// both `/src/UI` and `/UI` in the URL; we need to fix that +if ( ! window.__component_qr_code ) { + window.__component_qr_code = true; + + customElements.define('qr-code', QRCodeView); +} diff --git a/src/UI/Settings/UITabSecurity.js b/src/UI/Settings/UITabSecurity.js index 17ba0e11..b01c3bf0 100644 --- a/src/UI/Settings/UITabSecurity.js +++ b/src/UI/Settings/UITabSecurity.js @@ -59,7 +59,7 @@ export default { const confirmation = await UIWindowQR({ message_i18n_key: 'scan_qr_2fa', text: data.url, - text_below: data.secret, + text_alternative: data.secret, confirmations: [ i18n('confirm_2fa_setup'), i18n('confirm_2fa_recovery'), diff --git a/src/UI/UIWindowQR.js b/src/UI/UIWindowQR.js index 04eb1f88..a6a4efa7 100644 --- a/src/UI/UIWindowQR.js +++ b/src/UI/UIWindowQR.js @@ -17,7 +17,9 @@ * along with this program. If not, see . */ +import Placeholder from '../util/Placeholder.js'; import TeePromise from '../util/TeePromise.js'; +import QRCodeView from './Components/QRCode.js'; import UIWindow from './UIWindow.js' let checkbox_id_ = 0; @@ -29,6 +31,8 @@ async function UIWindowQR(options){ options = options ?? {}; + const placeholder_qr = Placeholder(); + let h = ''; // close button containing the multiplication sign // h += `
×
`; @@ -38,6 +42,16 @@ async function UIWindowQR(options){ }`; h += ``; + h += placeholder_qr.html; + + if ( options.text_alternative ) { + h += `
`; + h += `

${ + html_encode(options.text_alternative) + }

`; + h += `
`; + } + if ( options.recovery_codes ) { h += `
`; h += `

${ @@ -112,6 +126,14 @@ async function UIWindowQR(options){ }, }) + const component_qr = new QRCodeView({ + value: options.text + }); + console.log('test', component_qr); + component_qr.attach(placeholder_qr); + // placeholder_qr.replaceWith($(`

test

`).get(0)); + + if ( false ) { // generate auth token QR code new QRCode($(el_window).find('.otp-qr-code').get(0), { text: options.text, @@ -121,6 +143,7 @@ async function UIWindowQR(options){ colorLight : "#ffffff", correctLevel : QRCode.CorrectLevel.H }); +} if ( confirmations.length > 0 ) { $(el_window).find('.code-confirm-btn').prop('disabled', true); diff --git a/src/css/style.css b/src/css/style.css index 24a7572c..a3773502 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -2613,6 +2613,10 @@ label { margin-bottom: 20px; } +.otp-as-text { + margin: 20px 0; +} + .recovery-codes { border: 1px solid #ccc; padding: 20px; diff --git a/src/util/Component.js b/src/util/Component.js new file mode 100644 index 00000000..1792cc46 --- /dev/null +++ b/src/util/Component.js @@ -0,0 +1,63 @@ +import ValueHolder from "./ValueHolder.js"; + +export class Component extends HTMLElement { + constructor (property_values) { + super(); + + this.dom_ = this.attachShadow({ mode: 'open' }); + + this.values_ = {}; + + if ( this.constructor.template ) { + const template = document.querySelector(this.constructor.template); + this.dom_.appendChild(template.content.cloneNode(true)); + } + + for ( const key in this.constructor.PROPERTIES ) { + let initial_value; + if ( property_values && key in property_values ) { + initial_value = property_values[key]; + } + this.values_[key] = ValueHolder.adapt(initial_value); + } + } + + connectedCallback () { + console.log('connectedCallback called') + this.on_ready && this.on_ready(this.get_api_()); + } + + attach (placeholder) { + const el = this.create_element_(); + this.dom_.appendChild(el); + placeholder.replaceWith(this); + } + + place (slot_name, child_node) { + child_node.setAttribute('slot', slot_name); + this.appendChild(child_node); + } + + create_element_ () { + const template = document.createElement('template'); + if ( this.constructor.CSS ) { + const style = document.createElement('style'); + style.textContent = this.constructor.CSS; + this.dom_.appendChild(style); + } + if ( this.create_template ) { + this.create_template({ template }); + } + const el = template.content.cloneNode(true); + return el; + } + + get_api_ () { + return { + listen: (name, callback) => { + this.values_[name].sub(callback); + callback(this.values_[name].get()); + } + }; + } +} diff --git a/src/util/Placeholder.js b/src/util/Placeholder.js new file mode 100644 index 00000000..4bc66201 --- /dev/null +++ b/src/util/Placeholder.js @@ -0,0 +1,37 @@ +/** + * @typedef {Object} PlaceholderReturn + * @property {String} html: An html string that represents the placeholder + * @property {String} id: The unique ID of the placeholder + * @property {Function} replaceWith: A function that takes a DOM element + * as an argument and replaces the placeholder with it + */ + +/** + * Placeholder creates a simple element with a unique ID + * as an HTML string. + * + * This can be useful where string concatenation is used + * to build element trees. + * + * The `replaceWith` method can be used to replace the + * placeholder with a real element. + * + * @returns {PlaceholderReturn} + */ +const Placeholder = () => { + const id = Placeholder.get_next_id_(); + return { + html: `
`, + id, + replaceWith: (el) => { + const place = document.getElementById(id); + place.replaceWith(el); + } + }; +}; + +const anti_collision = `94d2cb6b85a1`; // Arbitrary random string +Placeholder.next_id_ = 0; +Placeholder.get_next_id_ = () => `${anti_collision}_${Placeholder.next_id_++}`; + +export default Placeholder; diff --git a/src/util/ValueHolder.js b/src/util/ValueHolder.js new file mode 100644 index 00000000..172b76c8 --- /dev/null +++ b/src/util/ValueHolder.js @@ -0,0 +1,58 @@ +/** + * Holds an observable value. + */ +export default class ValueHolder { + constructor (initial_value) { + this.value_ = null; + this.listeners_ = []; + + Object.defineProperty(this, 'value', { + set: this.set_.bind(this), + get: this.get_.bind(this), + }); + + if (initial_value !== undefined) { + this.set(initial_value); + } + } + + static adapt (value) { + if (value instanceof ValueHolder) { + return value; + } else { + return new ValueHolder(value); + } + } + + set (value) { + this.value = value; + } + + get () { + return this.value; + } + + sub (listener) { + this.listeners_.push(listener); + } + + set_ (value) { + const old_value = this.value_; + this.value_ = value; + const more = { + holder: this, + old_value, + }; + this.listeners_.forEach(listener => listener(value, more)); + } + + get_ () { + return this.value_; + } + + map (fn) { + const holder = new ValueHolder(); + this.sub((value, more) => holder.set(fn(value, more))); + return holder; + } +}