diff options
-rw-r--r-- | web/index.html | 513 | ||||
-rw-r--r-- | web/js/main.js | 949 |
2 files changed, 782 insertions, 680 deletions
diff --git a/web/index.html b/web/index.html index f36654f..dd60e7d 100644 --- a/web/index.html +++ b/web/index.html @@ -2,7 +2,7 @@ <html> <head> <script src="/js/main" type="module"></script> - <style> + <style id="styles"> @keyframes fade-blink { 0% { opacity: 100%; } 100% { opacity: 0%; } @@ -25,15 +25,6 @@ animation: fade-blink 1s ease-in-out infinite alternate; } - #settings-avatar-img { - max-width: 200px; - max-height: 200px; - } - - [hidden] { - display: none !important; - } - input[type=email]:invalid { background: #ff8080; } @@ -109,284 +100,302 @@ font-style: italic; } </style> + <template id="tpl-fenix-style"> <style> body { font-size: 300%; } input { font-size: 100%; } </style> </template> - </head> - <body> - <noscript>this thing requires javascript!</noscript> - <dialog id="message-modal"> - <div id="message-modal-content"> - <p id="message"></p> - <a href="#" id="message-modal-close" hidden>close</a> + <template id="tpl-desktop-signedin"> + <div class="container dialog"> + successfully signed in! </div> - </dialog> + </template> - <div id="desktop-signedin" class="container dialog" hidden> - successfully signed in! - </div> - - <div id="desktop-deleted" class="container dialog" hidden> - account has been deleted. - </div> + <template id="tpl-desktop-deleted"> + <div class="container dialog"> + account has been deleted. + </div> + </template> - <div id="desktop-signup" class="container dialog" hidden> - <form id="frm-signup"> - <label for="frm-signup-email">email</label> - <input id="frm-signup-email" type="email" name="email" maxlength="256" value=""> - <label for="frm-signup-password">password</label> - <input id="frm-signup-password" type="password" name="password"> - <input type="submit" name="submit" value="sign up"> + <template id="btpl-credentials"> + <form> + <label for="email">email</label> + <input id="email" type="email" name="email" maxlength="256"> + <label for="password">password</label> + <input id="password" type="password" name="password" minlength="8" maxlength="256"> + <input type="submit" name="submit"> </form> - </div> + </template> - <div id="desktop-signup-unverified" class="container dialog" hidden> - <p>signup completed! please verify your account by clicking the - link in the email you've just received.</p> - <p>if you haven't received anything, go to Settings/Sync and resend - the verification code.</p> - </div> + <template id="tpl-desktop-signup"> + <div class="container dialog"> + <credentials-form submit-text="sign up"></credentials-form> + </div> + </template> + + <template id="tpl-desktop-signup-unverified"> + <div class="container dialog"> + <p>signup completed! please verify your account by clicking the + link in the email you've just received.</p> + <p>if you haven't received anything, go to Settings/Sync and resend + the verification code.</p> + </div> + </template> - <div id="desktop-signin" class="container dialog" hidden> - <form id="frm-signin"> - <label for="frm-signin-email">email</label> - <input id="frm-signin-email" type="email" name="email" maxlength="256" value=""> - <label for="frm-signin-password">password</label> - <input id="frm-signin-password" type="password" name="password"> - <input type="submit" name="submit" value="sign in"> - <hr> + <template id="tpl-desktop-signin"> + <div class="container dialog"> + <credentials-form submit-text="sign in"></credentials-form> <div> - <a href="#" class="_signup">sign up</a> | - <a href="#" class="_reset">reset password</a> + <a href="#/signup" class="_signup">sign up</a> | + <a href="#/reset-password" class="_reset">reset password</a> </div> - </form> - </div> + </div> + </template> + + <template id="tpl-desktop-signin-confirm"> + <div class="container dialog"> + <p>signin completed! please verify your session by copying the code + you've just received as an email into this box.</p> + <form> + <label for="code">code</label> + <input id="code" type="text" name="code" minlength="6" maxlength="6"> + <input type="submit" name="submit" value="confirm signin"> + <hr> + <a href="#" class="_resend">no email appeared? resend code</a> + </form> + </div> + </template> + + <template id="tpl-desktop-resetpw"> + <div class="container dialog"> + <form> + <label for="email">email</label> + <input id="email" type="email" name="email" maxlength="256"> + <input type="submit" name="submit" value="send reset code"> + </form> + </div> + </template> - <div id="desktop-signin-confirm" class="container dialog" hidden> - <p>signin completed! please verify your session by copying the code - you've just received as an email into this box.</p> - <form id="frm-signin-confirm"> - <label for="frm-signin-confirm-code">code</label> - <input id="frm-signin-confirm-code" type="text" name="code" maxlength="6"> - <input type="submit" name="submit" value="confirm signin"> - <hr> - <a href="#" class="_resend">no email appeared? resend code</a> + <template id="tpl-desktop-resetpw-newpw"> + <div class="container dialog"> + <form> + <table> + <tr> + <td><label for="new">new password</label></td> + <td><input id="new" type="password" name="new" minlength="8" maxlength="256"></td> + </tr> + <tr> + <td><label for="confirm">new password (confirm)</label></td> + <td><input id="confirm" type="password" name="new-confirm" + minlength="8" maxlength="256"></td> + </tr> + </table> + <div> + <input type="submit" name="submit"> + </div> + </form> + </div> + </template> + + <template id="tpl-desktop-cwts"> + <div class="container dialog"> + <form id="frm"> + <p>choose what to sync:</p> + <div class="cwts-container"> + <div> + <input type="checkbox" id="addons" name="addons"> + <label for="addons">add-ons</label> + </div> + <div> + <input type="checkbox" id="addresses" name="addresses"> + <label for="addresses">addresses</label> + </div> + <div> + <input type="checkbox" id="bookmarks" name="bookmarks"> + <label for="bookmarks">bookmarks</label> + </div> + <div> + <input type="checkbox" id="creditcards" name="creditcards"> + <label for="creditcards">credit cards</label> + </div> + <div> + <input type="checkbox" id="history" name="history"> + <label for="history">history</label> + </div> + <div> + <input type="checkbox" id="passwords" name="passwords"> + <label for="passwords">passwords</label> + </div> + <!-- NOTE the spec says this key is named `preferences` --> + <div> + <input type="checkbox" id="prefs" name="prefs"> + <label for="prefs">preferences</label> + </div> + <div> + <input type="checkbox" id="tabs" name="tabs"> + <label for="tabs">open tabs</label> + </div> + </div> + <input type="submit" name="submit" value="start syncing"> + </form> + </div> + </template> + + <template id="tpl-desktop-settings"> + <div class="container"> + <nav> + <a href="#/settings">settings</a> | + <a href="#/settings/change-password">change password</a> | + <a href="#/settings/destroy">delete account</a> + </nav> + <div class="settings-container tab"></div> + </div> + </template> + + <template id="tpl-desktop-settings-main"> + <style> + #avatar-img { + max-width: 200px; + max-height: 200px; + } + </style> + <form id="frm-avatar"> + <img id="avatar-img"> + <input type="file" accept="image/*" name="avatar"> + <input type="submit" value="save"> </form> - </div> - - <div id="desktop-resetpw" class="container dialog" hidden> - <form id="frm-resetpw"> - <label for="frm-resetpw-email">email</label> - <input id="frm-resetpw-email" type="email" name="email" maxlength="256" value=""> - <input type="submit" name="submit" value="send reset code"> + <form id="frm-name"> + <label for="name">user name:</label> + <input type="text" id="name" name="name" maxlength="256"> + <input type="submit" value="save"> </form> - </div> - - <div id="desktop-resetpw-newpw" class="container dialog" hidden> - <form id="frm-resetpw-newpw"> + <table id="clients"> + <thead> + <tr> + <td>name</td> + <td>deviceType</td> + <td>createdTime</td> + <td>lastAccessTime</td> + <td>oauth?</td> + </tr> + </thead> + <tbody> + </tbody> + </table> + </template> + + <template id="tpl-desktop-settings-destroy"> + <p>deleting your account requires confirmation.</p> + <credentials-form submit-text="really delete accout"></credentials-form> + </template> + + <template id="tpl-desktop-settings-chpw"> + <form> <table> <tr> - <td><label for="frm-resetpw-newpw-old">old password</label></td> - <td><input id="frm-resetpw-newpw-old" type="password" name="old"></td> + <td><label for="old">old password</label></td> + <td><input id="old" type="password" name="old" minlength="8" maxlength="256"></td> </tr> <tr> - <td><label for="frm-resetpw-newpw-new">new password</label></td> - <td><input id="frm-resetpw-newpw-new" type="password" name="new"></td> + <td><label for="new">new password</label></td> + <td><input id="new" type="password" name="new" minlength="8" maxlength="256"></td> </tr> <tr> - <td><label for="frm-resetpw-newpw-new-confirm">new password (confirm)</label></td> - <td> - <input id="frm-resetpw-newpw-new-confirm" type="password" name="new-confirm"> - </td> + <td><label for="confirm">new password (confirm)</label></td> + <td><input id="confirm" type="password" name="new-confirm" + minlength="8" maxlength="256"></td> </tr> </table> <div> - <input type="submit" name="submit" value="change password"> - </div> - </form> - </div> - - <div id="desktop-cwts" class="container dialog" hidden> - <form id="frm-cwts"> - <p>choose what to sync:</p> - <div class="cwts-container"> - <div> - <input type="checkbox" id="frm-cwts-addons" name="addons"> - <label for="frm-cwts-addons">add-ons</label> - </div> - <div> - <input type="checkbox" id="frm-cwts-addresses" name="addresses"> - <label for="frm-cwts-addresses">addresses</label> - </div> - <div> - <input type="checkbox" id="frm-cwts-bookmarks" name="bookmarks"> - <label for="frm-cwts-bookmarks">bookmarks</label> - </div> - <div> - <input type="checkbox" id="frm-cwts-creditcards" name="creditcards"> - <label for="frm-cwts-creditcards">credit cards</label> - </div> - <div> - <input type="checkbox" id="frm-cwts-history" name="history"> - <label for="frm-cwts-history">history</label> - </div> - <div> - <input type="checkbox" id="frm-cwts-passwords" name="passwords"> - <label for="frm-cwts-passwords">passwords</label> - </div> - <!-- NOTE the spec says this key is named `preferences` --> - <div> - <input type="checkbox" id="frm-cwts-prefs" name="prefs"> - <label for="frm-cwts-prefs">preferences</label> - </div> - <div> - <input type="checkbox" id="frm-cwts-tabs" name="tabs"> - <label for="frm-cwts-tabs">open tabs</label> - </div> + <input type="submit" name="submit"> </div> - <input type="submit" name="submit" value="start syncing"> </form> - </div> - - <div id="desktop-settings" class="container" hidden> - <nav> - <a href="#/settings">settings</a> | - <a href="#/settings/change-password">change password</a> | - <a href="#/settings/destroy">delete account</a> - </nav> - <div class="settings-container tab" id="desktop-settings-main" hidden> - <form id="frm-settings-avatar"> - <img id="settings-avatar-img"> - <input type="file" accept="image/*" id="settings-avatar"> - <input type="submit" id="settings-avatar-save" value="save"> - </form> - <form id="frm-settings-name"> - <label for="settings-name">user name:</label> - <input type="text" id="settings-name" maxlength="256"> - <input type="submit" id="settings-name-save" value="save"> + </template> + + <template id="tpl-desktop-generate-invite"> + <div class="container dialog"> + <form> + <label for="ttl">invite valid for</label> + <select id="ttl" name="ttl" maxlength="256"> + <option value="1">one hour</option> + <option value="24">one day</option> + <option value="168">one week</option> + </select> + <input type="submit" name="submit" value="generate invite"> </form> - <table id="settings-clients"> - <thead> - <tr> - <td>name</td> - <td>deviceType</td> - <td>createdTime</td> - <td>lastAccessTime</td> - <td>oauth?</td> - </tr> - </thead> - <tbody> - </tbody> - </table> + <div id="result" hidden> + invite link is <a id="link"></a> + </div> </div> - <div class="settings-container tab" id="desktop-settings-destroy" hidden> - <p>deleting your account requires confirmation.</p> - <form id="frm-settings-destroy"> - <label for="frm-settings-destroy-email">email</label> - <input id="frm-settings-destroy-email" type="email" - name="email" maxlength="256" value=""> - <label for="frm-settings-destroy-password">password</label> - <input id="frm-settings-destroy-password" type="password" name="password"> - <input type="submit" name="submit" value="really delete account"> - </form> + </template> + + <template id="tpl-fenix-signin-warning"> + <div class="container"> + <p>it looks like you're trying to sign in to sync from an android firefox instance. + we're very sorry, but this is going to hurt a bit.</p> + <p>to sign in you'll need to follow these steps:</p> + <ol> + <li> + enable USB debugging in the android: + <ol> + <li>go to setting → about phone</li> + <li>scroll to the build number</li> + <li>tap it until a "your are new a developer" message appears</li> + <li>go to settings → system → developer options</li> + <li>scroll down to "USB debugging", enable it</li> + </ol> + </li> + <li>enable USB debugging in the android firefox settings</li> + <li>connect your android device to a PC for USB debugging</li> + <li>on this PC, open firefox and go to <pre>about:debugging</pre></li> + <li>enable USB devices</li> + <li>connect to your android device</li> + <li>open this page in a normal tab, not through the sign-in interface. + this is very important, if there's no normal tab with this URL open + <em>signin will not work</em></li> + <li>open the login page through the sign-in interface as well</li> + <li>in the PC debugger, inspect the new tab that has just appeared</li> + <li><a href="#" id="signin">actually sign in</a>. + clicking this link will lead away from this page, finishing sign-in + will bring you back.</li> + <li>go to the javascript debug console of the tab we inspected previously, + copy the long block of code</li> + <li>locate the <pre>Firefox Accounts WebChannel</pre> extension in the + debugger and inspect it. this may fail, if it does go to the setup + tab of the debugger and disable USB devices, enable then, reconnect + to your devices, and repeat this step + <p><b>WARNING:</b> this is known to not work on firefox 102. if you get + a blank tab instead of a tab with debug tools as seen earlier you + may have to downgrade your android firefox to 101 to log in. + unfortunately this seems to require uninstalling and reinstalling + firefox, which wipes your data!</p> + </li> + <li>go to the javascript debug console, paste the block of code, and run it</li> + <li>enjoy sync!</li> + </ol> </div> - <div class="settings-container tab" id="desktop-settings-chpw" hidden> - <form id="frm-settings-chpw"> - <table> - <tr> - <td><label for="frm-settings-chpw-old">old password</label></td> - <td><input id="frm-settings-chpw-old" type="password" name="old"></td> - </tr> - <tr> - <td><label for="frm-settings-chpw-new">new password</label></td> - <td><input id="frm-settings-chpw-new" type="password" name="new"></td> - </tr> - <tr> - <td><label for="frm-settings-chpw-new-confirm">new password (confirm)</label></td> - <td> - <input id="frm-settings-chpw-new-confirm" type="password" name="new-confirm"> - </td> - </tr> - </table> - <div> - <input type="submit" name="submit" value="change password"> - </div> - </form> - </div> - </div> + </template> - <div id="desktop-generate-invite" class="container dialog" hidden> - <form id="frm-generate-invite"> - <label for="frm-generate-invite-email">invite valid for</label> - <select id="frm-generate-invite-email" name="ttl" maxlength="256"> - <option value="1">one hour</option> - <option value="24">one day</option> - <option value="168">one week</option> - </select> - <input type="submit" name="submit" value="generate invite"> - </form> - <div id="desktop-generate-invite-result" hidden> - invite link is <a id="desktop-generate-invite-result-link"></a> + <template id="tpl-fenix-signin"> + <div class="container dialog"> + <credentials-form submit-text="do the fenix dance"></credentials-form> </div> - </div> + </template> + </head> + <body> + <noscript>this thing requires javascript!</noscript> - <div id="fenix-signin-warning" class="container" hidden> - <p>it looks like you're trying to sign in to sync from an android firefox instance. - we're very sorry, but this is going to hurt a bit.</p> - <p>to sign in you'll need to follow these steps:</p> - <ol> - <li> - enable USB debugging in the android: - <ol> - <li>go to setting → about phone</li> - <li>scroll to the build number</li> - <li>tap it until a "your are new a developer" message appears</li> - <li>go to settings → system → developer options</li> - <li>scroll down to "USB debugging", enable it</li> - </ol> - </li> - <li>enable USB debugging in the android firefox settings</li> - <li>connect your android device to a PC for USB debugging</li> - <li>on this PC, open firefox and go to <pre>about:debugging</pre></li> - <li>enable USB devices</li> - <li>connect to your android device</li> - <li>open this page in a normal tab, not through the sign-in interface. - this is very important, if there's no normal tab with this URL open - <em>signin will not work</em></li> - <li>open the login page through the sign-in interface as well</li> - <li>in the PC debugger, inspect the new tab that has just appeared</li> - <li><a href="#" id="fenix-signin-dialog-show">actually sign in</a>. - clicking this link will lead away from this page, finishing sign-in - will bring you back.</li> - <li>go to the javascript debug console of the tab we inspected previously, - copy the long block of code</li> - <li>locate the <pre>Firefox Accounts WebChannel</pre> extension in the - debugger and inspect it. this may fail, if it does go to the setup - tab of the debugger and disable USB devices, enable then, reconnect - to your devices, and repeat this step - <p><b>WARNING:</b> this is known to not work on firefox 102. if you get - a blank tab instead of a tab with debug tools as seen earlier you - may have to downgrade your android firefox to 101 to log in. - unfortunately this seems to require uninstalling and reinstalling - firefox, which wipes your data!</p> - </li> - <li>go to the javascript debug console, paste the block of code, and run it</li> - <li>enjoy sync!</li> - </ol> - </div> + <dialog id="message-modal"> + <div id="message-modal-content"> + <p id="message"></p> + <a href="#" id="message-modal-close" hidden>close</a> + </div> + </dialog> - <div id="fenix-signin" class="container dialog" hidden> - <form id="frm-fenix-signin"> - <label for="email">email</label> - <input type="text" name="email" value=""> - <label for="password">password</label> - <input type="password" name="password"> - <input type="submit" name="submit" value="do the fenix dance"> - </form> + <div id="content"> </div> </body> </html> diff --git a/web/js/main.js b/web/js/main.js index 23610b6..cefb003 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -206,18 +206,8 @@ function wrapHandler(fn) { }; } -function switchTo(id, tabID) { - for (const screen of document.getElementsByClassName("container")) { - screen.hidden = true; - } - $(id).hidden = false; - - if (tabID) { - for (const tab of $(id).getElementsByClassName("tab")) { - tab.hidden = true; - } - $(tabID).hidden = false; - } +function switchTo(v) { + $("content").replaceChildren(v); } function dateDiffText(when) { @@ -243,520 +233,623 @@ function dateDiffText(when) { } ////////////////////////////////////////// -// initialization +// credentials form ////////////////////////////////////////// -let client_config; -var channel = new Channel(); -const isAndroid = /Android/.test(navigator.userAgent); - -document.body.onload = () => { - showMessage("Loading ...", "animate"); - - if (isAndroid) { - document.head.appendChild($("tpl-fenix-style").content); - } - - fetch("/.well-known/fxa-client-configuration") - .then(resp => { - if (!resp.ok) throw new Error(resp.statusText); - return resp.json(); - }) - .then(data => { - client_config = data; - hideMessage(); - return initUI(); - }) - .catch(e => { - showError("initialization failed: ", e); - }); -}; - -async function initUI() { - return isAndroid - ? await initUIAndroid() - : await initUIDesktop(); -} - -async function initUIDesktop() { - let present = wrapHandler(async () => { - if (window.location.hash.startsWith("#/verify/")) { - verify_init(JSON.parse(urlatob(window.location.hash.substr(9)))); - } else if (window.location.hash.startsWith("#/register/")) { - signup_init(window.location.hash.substr(11)); - } else { - let data = await channel.getStatus(); - if (!data.signedInUser) { - signin_init(); - } else if (window.location.hash == "#/settings") { - settings_init(data); - } else if (window.location.hash == "#/settings/change-password") { - settings_chpw_init(data); - } else if (window.location.hash == "#/settings/destroy") { - settings_destroy_init(data); - } else if (window.location.hash == "#/force_auth") { - signin_init(); - } else if (window.location.hash == "#/generate-invite") { - generate_invite_init(data); - } else { - switchTo("desktop-signedin"); - } - } - hideMessage(); - }); - - window.onpopstate = () => window.setTimeout(present, 0); - - for (let a of $("desktop-settings").querySelectorAll("nav a")) { - a.onclick = async ev => { +class CredentialsForm extends HTMLElement { + constructor() { + super(); + this.replaceChildren($("btpl-credentials").content.cloneNode(true)); + let frm = this.querySelector("form"); + frm['submit'].value = this.getAttribute('submit-text'); + frm.onsubmit = wrapHandler(async ev => { ev.preventDefault(); - window.location = ev.target.href; - await present(); - }; + this.dispatchEvent(new CustomEvent('confirm', { + detail: { + email: ev.target['email'].value, + password: ev.target['password'].value, + }, + })); + }); } - await present(); -} - -async function initUIAndroid() { - fenix_signin_init(); -} +}; +customElements.define('credentials-form', CredentialsForm); ////////////////////////////////////////// // signin form ////////////////////////////////////////// -function signin_init() { - switchTo("desktop-signin"); - let frm = $("frm-signin"); - frm.onsubmit = wrapHandler(signin_run); - frm.getElementsByClassName("_signup")[0].onclick = wrapHandler(signin_signup_instead); - frm.getElementsByClassName("_reset")[0].onclick = wrapHandler(ev => { - ev.preventDefault(); - password_reset_init(); - }); -} - -function signin_signup_instead(ev) { - ev.preventDefault(); - signup_init(); -} - -// NOTE it looks like firefox discards its session token before asking for reauth -// (eg if oauth verification of a sync token fails). we can't use the reauth -// endpoint for that, so we'll just always log in. -async function signin_run(ev) { - ev.preventDefault(); - let frm = ev.target; - - if (frm[0].value == "" || !frm[0].validity.valid) { - return showRecoverableError("email is not valid"); - } - if (frm[1].value == "" || !frm[1].validity.valid) { - return showRecoverableError("password is not valid"); - } - - let c = new AuthClient(client_config.auth_server_base_url); - try { - let session = await c.signIn(frm["email"].value, frm["password"].value, { - keys: true, +class Signin extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-signin").content.cloneNode(true)); + let frm = shadow.querySelector("credentials-form"); + frm.addEventListener('confirm', wrapHandler(async ev => { + await this._signin(ev.detail.email, ev.detail.password); + })); + shadow.querySelector("a._signup").onclick = wrapHandler(async ev => { + ev.preventDefault(); + window.location = ev.target.href; + }); + shadow.querySelector("a._reset").onclick = wrapHandler(async ev => { + ev.preventDefault(); + window.location = ev.target.href; }); + } - signin_confirm_init(frm["email"].value, session); - } catch (e) { - if (e.errno == 104) { - showRecoverableError(` - account is not verified. please verify or wait a few minutes and register again. - `.trim()); - } else { - throw e; + // NOTE it looks like firefox discards its session token before asking for reauth + // (eg if oauth verification of a sync token fails). we can't use the reauth + // endpoint for that, so we'll just always log in. + async _signin(email, password) { + let c = new AuthClient(client_config.auth_server_base_url); + try { + let session = await c.signIn(email, password, { keys: true }); + switchTo(new SigninConfirm(email, session)); + } catch (e) { + if (e.errno == 104) { + showRecoverableError(` + account is not verified. please verify or wait a few minutes and register again. + `.trim()); + } else { + throw e; + } } } -} +}; +customElements.define('do-signin', Signin); ////////////////////////////////////////// // sign-in confirmation ////////////////////////////////////////// -function signin_confirm_init(email, session) { - let frm = $("frm-signin-confirm"); - frm.getElementsByClassName("_resend")[0].onclick = wrapHandler(async ev => { - ev.preventDefault(); - - let c = new AuthClient(client_config.auth_server_base_url); - await c.sessionResendVerifyCode(session.sessionToken); - alert("code resent!"); - }); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - - let c = new AuthClient(client_config.auth_server_base_url); - c.sessionVerifyCode(session.sessionToken, frm["code"].value) - .then(resp => { +class SigninConfirm extends HTMLElement { + constructor(email, session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-signin-confirm").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.querySelector("a._resend").onclick = wrapHandler(async ev => { + ev.preventDefault(); + let c = new AuthClient(client_config.auth_server_base_url); + await c.sessionResendVerifyCode(session.sessionToken); + alert("code resent!"); + }); + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); + let c = new AuthClient(client_config.auth_server_base_url); + try { + await c.sessionVerifyCode(session.sessionToken, frm["code"].value); channel.loadCredentials(email, session, { offered: [], declined: [] }); - switchTo("desktop-signedin"); - }) - .catch(e => { + switchTo(new SignedIn()); + } catch (e) { showError("verification failed: ", e); - }); - }); - switchTo("desktop-signin-confirm"); -} + } + }); + } +}; +customElements.define('do-signin-confirm', SigninConfirm); ////////////////////////////////////////// // password reset ////////////////////////////////////////// -function password_reset_init() { - let frm = $("frm-resetpw"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - - let c = new AuthClient(client_config.auth_server_base_url); - let token = await c.passwordForgotSendCode(frm["email"].value); - let code = prompt(` - please enter your password reset code from the email you've just received - `.trim()); - if (code === null) { - return showRecoverableError("password reset aborted"); - } - - let reset_token = await c.passwordForgotVerifyCode(code, token.passwordForgotToken); - password_reset_newpw(frm['email'].value, reset_token.accountResetToken); - }); - switchTo("desktop-resetpw"); -} - -function password_reset_newpw(email, reset_token) { - let frm = $("frm-resetpw-newpw"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); +class ResetPassword extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-resetpw").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); - let c = new AuthClient(client_config.auth_server_base_url); - if (frm['new'].value != frm['new-confirm'].value) { - return showRecoverableError("passwords don't match!"); - } + let c = new AuthClient(client_config.auth_server_base_url); + let token = await c.passwordForgotSendCode(frm["email"].value); + let code = prompt("please enter your password reset code from the email you've just received"); + if (code === null) { + return showRecoverableError("password reset aborted"); + } - await c.accountReset(email, frm['new'].value, reset_token); - switchTo("desktop-signin"); - }); - switchTo("desktop-resetpw-newpw"); -} + let reset_token = await c.passwordForgotVerifyCode(code, token.passwordForgotToken); + switchTo(new ResetPasswordCreate(frm['email'].value, reset_token.accountResetToken)); + }); + } +}; +customElements.define('do-resetpw', ResetPassword); + +class ResetPasswordCreate extends HTMLElement { + constructor(email, reset_token) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-resetpw-newpw").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.onsubmit = async ev => { + ev.preventDefault(); + if (frm['new'].value != frm['new-confirm'].value) { + return showRecoverableError("passwords don't match!"); + } + let c = new AuthClient(client_config.auth_server_base_url); + await c.accountReset(email, frm['new'].value, reset_token); + switchTo(new Signin()); + }; + } +}; +customElements.define('do-resetpw-create', ResetPasswordCreate); ////////////////////////////////////////// // signup form ////////////////////////////////////////// -function signup_init(code) { - let frm = $("frm-signup"); - switchTo("desktop-signup"); - frm.onsubmit = wrapHandler(async ev => signup_run(ev, code)); -} - -async function signup_run(ev, code) { - ev.preventDefault(); - let frm = ev.target; - - if (frm[0].value == "" || !frm[0].validity.valid) { - return showRecoverableError("email is not valid"); - } - if (frm[1].value == "" || !frm[1].validity.valid) { - return showRecoverableError("password is not valid"); +class Signup extends HTMLElement { + constructor(code) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-signup").content.cloneNode(true)); + let frm = shadow.querySelector("credentials-form"); + frm.addEventListener('confirm', async ev => { + await this._signup(ev.detail.email, ev.detail.password, code); + }) } - let c = new AuthClient(client_config.auth_server_base_url); - let session = await c.signUp(frm["email"].value, frm["password"].value, { - keys: true, - style: code, - }); - - cwts_continue(frm["email"].value, session); -} + async _signup(email, password, code) { + let c = new AuthClient(client_config.auth_server_base_url); + let session = await c.signUp(email, password, { + keys: true, + style: code, + }); -////////////////////////////////////////// -// choose-what-to-sync form -////////////////////////////////////////// + switchTo(new CWTS(email, session)); + } +}; +customElements.define('do-signup', Signup); + +class CWTS extends HTMLElement { + constructor(email, session) { + super(); + // TODO we don't query browser capabilities, but we probably should + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-cwts").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.onsubmit = wrapHandler((ev) => { + ev.preventDefault(); -function cwts_continue(email, session) { - // TODO we don't query browser capabilities, but we probably should - let frm = $("frm-cwts"); - frm.onsubmit = wrapHandler((ev) => { - ev.preventDefault(); + let offered = [], declined = []; + for (const engine of frm.querySelectorAll("input[type=checkbox]")) { + if (!engine.checked) declined.push(engine.name); + else offered.push(engine.name); + } + channel.loadCredentials(email, session, { offered, declined }); - let offered = [], declined = []; - for (const engine of frm.querySelectorAll("input[type=checkbox]")) { - if (!engine.checked) declined.push(engine.name); - else offered.push(engine.name); - } - channel.loadCredentials(email, session, { offered, declined }); + switchTo(new SignupUnverified()); + }); + } +}; +customElements.define('do-cwts', CWTS); - switchTo("desktop-signup-unverified"); - }); - switchTo("desktop-cwts"); +class SignupUnverified extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-signup-unverified").content.cloneNode(true)); + } } +customElements.define('do-signup-unverified', SignupUnverified); ////////////////////////////////////////// // verification ////////////////////////////////////////// -function verify_init(context) { +class SignedIn extends HTMLElement { + constructor() { + super(); + this.replaceChildren($("tpl-desktop-signedin").content.cloneNode(true)); + } +}; +customElements.define('signed-in', SignedIn); + +async function verify(context) { let c = new AuthClient(client_config.auth_server_base_url); - c.verifyCode(context.uid, context.code) - .then(resp => { - switchTo("desktop-signedin"); - }) - .catch(e => { - showError("verification failed: ", e); - }); + try { + await c.verifyCode(context.uid, context.code) + switchTo(new SignedIn()); + } catch(e) { + showError("verification failed: ", e); + } } ////////////////////////////////////////// -// settings root +// settings ////////////////////////////////////////// -function settings_init(session) { - switchTo("desktop-settings", "desktop-settings-main"); +class Settings extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + this.session = session; + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-settings").content.cloneNode(true)); + + for (let a of shadow.querySelectorAll("nav a")) { + a.onclick = wrapHandler(async ev => { + ev.preventDefault(); + window.location = ev.target.href; + await this._display(); + }); + } + + this._display(); + } + + async _display() { + let tab = SettingsMain; + if (window.location.hash == "#/settings/change-password") { + tab = SettingsChangePassword; + } else if (window.location.hash == "#/settings/destroy") { + tab = SettingsDestroy; + } + + try { + this.shadowRoot.querySelector(".tab").replaceChildren(new tab(this.session)); + } catch(e) { + showError("initialization failed: ", e); + } + } +}; +customElements.define('do-settings', Settings); + +class SettingsMain extends HTMLElement { + constructor(session) { + super(); + this.session = session; + this.replaceChildren($("tpl-desktop-settings-main").content.cloneNode(true)); + this._display(); + } - let inner = async () => { + async _display() { showMessage("Loading ...", "animate"); let ac = new AuthClient(client_config.auth_server_base_url) - let pc = new ProfileClient(ac, session); + let pc = new ProfileClient(ac, this.session); let initProfile = async () => { let profile = await pc.getProfile(); - $("settings-name").value = profile.displayName || ""; - $("frm-settings-name").onsubmit = wrapHandler(async ev => { - showMessage("Applying ...") + this.querySelector("#name").value = profile.displayName || ""; + this.querySelector("#frm-name").onsubmit = wrapHandler(async ev => { ev.preventDefault(); - await pc.setDisplayName($("settings-name").value); + showMessage("Applying ...") + await pc.setDisplayName(ev.target['name'].value); hideMessage(); }); - $("settings-avatar-img").src = profile.avatar; - $("frm-settings-avatar").onsubmit = wrapHandler(async ev => { - showMessage("Saving ...") + this.querySelector("#avatar-img").src = profile.avatar; + this.querySelector("#frm-avatar").onsubmit = wrapHandler(async ev => { ev.preventDefault(); - await pc.setAvatar($("settings-avatar").files[0]); - settings_init(session); + showMessage("Saving ...") + await pc.setAvatar(ev.target['avatar'].files[0]); + await this._display(); }); }; await Promise.all([ initProfile(), - settings_populateClients(ac, session), + this._populateClients(ac), ]); hideMessage(); - }; - - inner().catch(e => { - showError("initialization failed: ", e); - }); -} + } -async function settings_populateClients(authClient, session) { - let clients = await authClient.attachedClients(session.signedInUser.sessionToken); - - let body = $("settings-clients").getElementsByTagName("tbody")[0]; - body.innerHTML = ""; - for (const c of clients) { - let row = document.createElement("tr"); - let add = (val, tip) => { - let cell = document.createElement("td"); - cell.innerText = val || ""; - if (tip) cell.title = tip; - row.appendChild(cell); - }; - let addDateDiff = val => { - let text = dateDiffText(new Date(val)); - add(text, new Date(val)); - }; - add(c.name); - add(c.deviceType); - addDateDiff(c.createdTime * 1000); - addDateDiff(c.lastAccessTime * 1000); - add(c.scope ? "yes" : "", (c.scope ? c.scope : "").replace(/ +/g, "\n")); - if (c.isCurrentSession) { - let cell = document.createElement("td"); - cell.innerHTML = `<span class="disabled">current session</span>`; - row.appendChild(cell); - } else if (c.deviceId || c.sessionTokenId || c.refreshTokenId) { - let remove = document.createElement("button"); - remove.innerText = 'remove'; - remove.onclick = wrapHandler(async ev => { - ev.preventDefault(); - let info = { clientId: c.clientId }; - if (c.deviceId) - info.deviceId = c.deviceId; - else if (c.sessionTokenId) - info.sessionTokenId = c.sessionTokenId; - else if (c.refreshTokenId) - info.refreshTokenId = c.refreshTokenId; - showMessage("Processing ...", "animate"); - await authClient.attachedClientDestroy(session.signedInUser.sessionToken, info); - await settings_populateClients(authClient, session); - hideMessage(); - }); - row.appendChild(remove); + async _populateClients(authClient) { + let clients = await authClient.attachedClients(this.session.signedInUser.sessionToken); + + let body = this.querySelector("#clients tbody"); + body.innerHTML = ""; + for (const c of clients) { + let row = document.createElement("tr"); + let add = (val, tip) => { + let cell = document.createElement("td"); + cell.innerText = val || ""; + if (tip) cell.title = tip; + row.appendChild(cell); + }; + let addDateDiff = val => { + let text = dateDiffText(new Date(val)); + add(text, new Date(val)); + }; + add(c.name); + add(c.deviceType); + addDateDiff(c.createdTime * 1000); + addDateDiff(c.lastAccessTime * 1000); + add(c.scope ? "yes" : "", (c.scope ? c.scope : "").replace(/ +/g, "\n")); + if (c.isCurrentSession) { + let cell = document.createElement("td"); + cell.innerHTML = `<span class="disabled">current session</span>`; + row.appendChild(cell); + } else if (c.deviceId || c.sessionTokenId || c.refreshTokenId) { + let remove = document.createElement("button"); + remove.innerText = 'remove'; + remove.onclick = wrapHandler(async ev => { + ev.preventDefault(); + let info = { clientId: c.clientId }; + if (c.deviceId) + info.deviceId = c.deviceId; + else if (c.sessionTokenId) + info.sessionTokenId = c.sessionTokenId; + else if (c.refreshTokenId) + info.refreshTokenId = c.refreshTokenId; + showMessage("Processing ...", "animate"); + await authClient.attachedClientDestroy(this.session.signedInUser.sessionToken, info); + await this._populateClients(authClient, this.session); + hideMessage(); + }); + row.appendChild(remove); + } + body.appendChild(row); } - body.appendChild(row); } -} +}; +customElements.define('do-settings-main', SettingsMain); -////////////////////////////////////////// -// settings change password -////////////////////////////////////////// +class SettingsChangePassword extends HTMLElement { + constructor(session) { + super(); + this.replaceChildren($("tpl-desktop-settings-chpw").content.cloneNode(true)); -function settings_chpw_init(session) { - switchTo("desktop-settings", "desktop-settings-chpw"); - let frm = $("frm-settings-chpw"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - let frm = ev.target; + let frm = this.querySelector("form"); + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); + if (frm['new'].value != frm['new-confirm'].value) { + return showRecoverableError("passwords don't match!"); + } - if (frm["new"].value != frm["new-confirm"].value) { - showRecoverableError("passwords don't match"); - return; - } + let c = new AuthClient(client_config.auth_server_base_url); + await c.passwordChange(session.signedInUser.email, frm['old'].value, frm['new'].value, {}); - let c = new AuthClient(client_config.auth_server_base_url); - let resp = await c.passwordChange(session.signedInUser.email, frm['old'].value, frm['new'].value, {}); + channel.passwordChanged(session.signedInUser.email, session.signedInUser.uid); + alert("password changed"); + }); + } +}; +customElements.define('do-settings-chpw', SettingsChangePassword); - channel.passwordChanged(session.signedInUser.email, session.signedInUser.uid); - alert("password changed"); - }); -} +class SettingsDestroy extends HTMLElement { + constructor(session) { + super(); + this.replaceChildren($("tpl-desktop-settings-destroy").content.cloneNode(true)); -////////////////////////////////////////// -// settings destroy -////////////////////////////////////////// + this.querySelector("credentials-form").addEventListener('confirm', wrapHandler(async ev => { + let c = new AuthClient(client_config.auth_server_base_url); + await c.accountDestroy(ev.detail.email, ev.detail.password); -function settings_destroy_init(session) { - switchTo("desktop-settings", "desktop-settings-destroy"); - let frm = $("frm-settings-destroy"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - let frm = ev.target; + channel.accountDestroyed(ev.detail.email, session.signedInUser.uid); + switchTo(new AccountDestroyed()); + })); + } +}; +customElements.define('do-settings-destroy', SettingsDestroy); - if (frm[0].value == "" || !frm[0].validity.valid) { - return showRecoverableError("email is not valid"); - } - if (frm[1].value == "" || !frm[1].validity.valid) { - return showRecoverableError("password is not valid"); - } +class AccountDestroyed extends HTMLElement { + constructor() { + super(); + this.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-deleted").content.cloneNode(true)); + } +}; +customElements.define('account-destroyed', AccountDestroyed); - let c = new AuthClient(client_config.auth_server_base_url); - await c.accountDestroy(frm["email"].value, frm["password"].value); +////////////////////////////////////////// +// generate invite +////////////////////////////////////////// - channel.accountDestroyed(frm["email"], session.signedInUser.uid); - switchTo("desktop-deleted"); - }); -} +class GenerateInvite extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-generate-invite").content.cloneNode(true)); + + let frm = shadow.querySelector("form"); + shadow.querySelector("#result").hidden = true; + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); + + let server_url = new URL(client_config.auth_server_base_url); + server_url.pathname = server_url.pathname.split("/").slice(0, -1).join("/") + "/_invite"; + let c = new AuthClient(server_url.toString()); + let resp = await c.sessionPost( + "/generate", + session.signedInUser.sessionToken, + { 'ttl_hours': parseInt(frm["ttl"].value) }); + + shadow.querySelector("#link").href = resp.url; + shadow.querySelector("#link").innerText = resp.url; + shadow.querySelector("#result").hidden = false; + }); + } +}; +customElements.define('do-generate-invite', GenerateInvite); ////////////////////////////////////////// -// generate invite +// fenix signin ////////////////////////////////////////// -function generate_invite_init(session) { - let frm = $("frm-generate-invite"); - $("desktop-generate-invite-result").hidden = true; - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); +class FenixSignin extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-fenix-signin-warning").content.cloneNode(true)); + shadow.querySelector("#signin").onclick = wrapHandler(async ev => { + ev.preventDefault(); + switchTo(new FenixSigninEnter()); + }); + } +} +customElements.define('do-fenix-signin', FenixSignin); + +class FenixSigninEnter extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-fenix-signin").content.cloneNode(true)); + shadow.querySelector("credentials-form").addEventListener('confirm', wrapHandler(async ev => { + ev.preventDefault(); + await this._step2(ev.detail.email, ev.detail.password); + })); + } - let server_url = new URL(client_config.auth_server_base_url); - server_url.pathname = server_url.pathname.split("/").slice(0, -1).join("/") + "/_invite"; - let c = new AuthClient(server_url.toString()); - let resp = await c.sessionPost( - "/generate", - session.signedInUser.sessionToken, - { 'ttl_hours': parseInt(frm["ttl"].value) }); + async _step2(email, password) { + let url = new URL(window.location); + let params = new URLSearchParams(url.search); + let param = (p) => { + let val = params.get(p); + if (val === undefined) throw `missing parameter ${p}`; + return val; + }; - $("desktop-generate-invite-result-link").href = resp.url; - $("desktop-generate-invite-result-link").innerText = resp.url; - $("desktop-generate-invite-result").hidden = false; + let c = new AuthClient(client_config.auth_server_base_url); + let session = await c.signIn(email, password, { keys: true }); + let verifyCode = prompt("enter verify code"); + await c.sessionVerifyCode(session.sessionToken, verifyCode); + try { + let keys = await c.accountKeys(session.keyFetchToken, session.unwrapBKey); + let scoped_keys = await c.getOAuthScopedKeyData( + session.sessionToken, + param("client_id"), + param("scope")); + for (var scope in scoped_keys) { + scoped_keys[scope] = await deriveScopedKey( + keys.kB, + session.uid, + scope, + scoped_keys[scope].keyRotationSecret, + scoped_keys[scope].keyRotationTimestamp); + } - }); - switchTo("desktop-generate-invite"); + let keys_jwe = await encryptScopedKeys(scoped_keys, param("keys_jwk")); + + let code = await c.createOAuthCode( + session.sessionToken, + param("client_id"), + param("state"), + { + access_type: param("access_type"), + keys_jwe, + response_type: param("response_type"), + scope: param("scope"), + code_challenge_method: param("code_challenge_method"), + code_challenge: param("code_challenge"), + }); + + console.log(`browser.tabs.executeScript({code: \` + port = browser.runtime.connectNative("mozacWebchannel"); + port.postMessage({ + id: "account_updates", + message: { + command: "fxaccounts:oauth_login", + data: { + "code": "${code.code}", + "state": "${code.state}", + "redirect": "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel", + "action": "signin" + }, + messageId: 1, + }}); + \`});`); + + switchTo(new FenixSignin()); + } finally { + c.sessionDestroy(session.sessionToken); + } + } } +customElements.define('do-fenix-signin-enter', FenixSigninEnter); ////////////////////////////////////////// -// fenix signin +// initialization ////////////////////////////////////////// -function fenix_signin_init() { - switchTo("fenix-signin-warning"); - $("fenix-signin-dialog-show").onclick = wrapHandler(async ev => { - ev.preventDefault(); - switchTo("fenix-signin"); - $("frm-fenix-signin").onsubmit = wrapHandler(fenix_signin_step2); - }); -} +let client_config; +var channel = new Channel(); +const isAndroid = /Android/.test(navigator.userAgent); + +document.body.onload = async () => { + showMessage("Loading ...", "animate"); -async function fenix_signin_step2(ev) { - ev.preventDefault(); + if (isAndroid) { + document.head.appendChild($("tpl-fenix-style").content); + } - let url = new URL(window.location); - let params = new URLSearchParams(url.search); - let param = (p) => { - let val = params.get(p); - if (val === undefined) throw `missing parameter ${p}`; - return val; - }; + try { + let resp = await fetch("/.well-known/fxa-client-configuration"); + if (!resp.ok) throw new Error(resp.statusText); + client_config = await resp.json(); + hideMessage(); + initUI(); + } catch(e) { + showError("initialization failed: ", e); + } +}; - let frm = ev.target; - let c = new AuthClient(client_config.auth_server_base_url); - let session = await c.signIn(frm["email"].value, frm["password"].value, { - keys: true, - }); - let verifyCode = prompt("enter verify code"); - await c.sessionVerifyCode(session.sessionToken, verifyCode); - try { - let keys = await c.accountKeys(session.keyFetchToken, session.unwrapBKey); - let scoped_keys = await c.getOAuthScopedKeyData( - session.sessionToken, - param("client_id"), - param("scope")); - for (var scope in scoped_keys) { - scoped_keys[scope] = await deriveScopedKey( - keys.kB, - session.uid, - scope, - scoped_keys[scope].keyRotationSecret, - scoped_keys[scope].keyRotationTimestamp); +async function initUI() { + if (isAndroid) { + await initUIAndroid(); + } else { + await initUIDesktop(); + } +} + +async function initUIDesktop() { + let present = wrapHandler(async () => { + if (window.location.hash.startsWith("#/verify/")) { + await verify(JSON.parse(urlatob(window.location.hash.substr(9)))); + } else if (window.location.hash.startsWith("#/register/")) { + switchTo(new Signup(window.location.hash.substr(11))); + } else { + let data = await channel.getStatus(); + if (window.location.hash == "#/signup") { + switchTo(new Signup()); + } else if (window.location.hash == "#/reset-password") { + switchTo(new ResetPassword()); + } else if (!data.signedInUser + || window.location.hash == "#/force_auth") { + switchTo(new Signin()); + } else if (window.location.hash == "#/settings" + || window.location.hash == "#/settings/change-password" + || window.location.hash == "#/settings/destroy") { + switchTo(new Settings(data)); + } else if (window.location.hash == "#/generate-invite") { + switchTo(new GenerateInvite(data)); + } else { + switchTo(new SignedIn()); + } } + hideMessage(); + }); - let keys_jwe = await encryptScopedKeys(scoped_keys, param("keys_jwk")); - - let code = await c.createOAuthCode( - session.sessionToken, - param("client_id"), - param("state"), - { - access_type: param("access_type"), - keys_jwe, - response_type: param("response_type"), - scope: param("scope"), - code_challenge_method: param("code_challenge_method"), - code_challenge: param("code_challenge"), - }); + window.onpopstate = () => window.setTimeout(present, 0); + window.onhashchange = () => window.setTimeout(present, 0); + await present(); +} - console.log(`browser.tabs.executeScript({code: \` - port = browser.runtime.connectNative("mozacWebchannel"); - port.postMessage({ - id: "account_updates", - message: { - command: "fxaccounts:oauth_login", - data: { - "code": "${code.code}", - "state": "${code.state}", - "redirect": "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel", - "action": "signin" - }, - messageId: 1, - }}); - \`});`); - - switchTo("fenix-signin-warning"); - } finally { - c.sessionDestroy(session.sessionToken); - } +async function initUIAndroid() { + switchTo(new FenixSignin()); } |