options.js (4588 bytes)
1 // The options page: sign in to a sync server, sync now, sign out, and the 2 // plan of the account with its payments. 3 "use strict"; 4 5 const api = globalThis.browser ?? globalThis.chrome; 6 const $ = (id) => document.getElementById(id); 7 const DEFAULT_SERVER = "https://sbmsync.com"; 8 9 // The data that the add-on sends to the server after a sign-in. Firefox 140 10 // and later ask the user for it; other browsers have no such question. 11 const DATA = ["bookmarksInfo", "personallyIdentifyingInfo", "authenticationInfo"]; 12 let askData = false; 13 api.permissions.getAll().then((p) => { 14 askData = !!p.data_collection; 15 }); 16 17 function send(msg) { 18 return api.runtime.sendMessage(msg).then((r) => { 19 if (r && r.error) throw new Error(r.error); 20 return r && r.value; 21 }); 22 } 23 24 function say(text) { 25 $("message").textContent = text; 26 } 27 28 function origin(server) { 29 const s = server.trim().replace(/\/+$/, ""); 30 return new URL(s.includes("://") ? s : "https://" + s).origin; 31 } 32 33 async function show() { 34 const s = await api.storage.local.get(["server", "email", "token", "synced", "error", "text"]); 35 const server = s.server || DEFAULT_SERVER; 36 $("signed-out").hidden = !!s.token; 37 $("signed-in").hidden = !s.token; 38 if (!$("server").value) $("server").value = server; 39 if (!$("email").value) $("email").value = s.email || ""; 40 $("signup").href = origin($("server").value || server) + "/signup"; 41 $("account").href = server + "/account"; 42 const n = Tsv.parse(s.text || "").length; 43 $("who").textContent = "Signed in to " + server + " as " + s.email + ". " + n + " bookmarks" + 44 (s.synced ? ", synced " + new Date(s.synced).toLocaleString() : "") + "."; 45 if (s.error) say("Sync: " + s.error); 46 if (s.token) showPlan(); 47 } 48 49 function button(text, onClick) { 50 const b = document.createElement("button"); 51 b.type = "button"; 52 b.textContent = text; 53 b.addEventListener("click", onClick); 54 return b; 55 } 56 57 /** Show the plan of the account and the payments that the server offers. */ 58 async function showPlan() { 59 let info = null; 60 try { 61 info = await send({ type: "account" }); 62 } catch (err) { 63 // No answer now. The link to the account page stays. 64 } 65 $("plan").hidden = !info; 66 if (!info) return; 67 $("plan-state").textContent = info.state; 68 const buttons = info.plans.map((p) => button("Subscribe: " + p.label, () => pay({ type: "checkout", plan: p.id }))); 69 if (info.portal) buttons.push(button("Manage billing", () => pay({ type: "portal" }))); 70 $("billing").replaceChildren(...buttons); 71 $("billing").hidden = buttons.length === 0; 72 const links = Plan.links(info).map((l) => { 73 const a = document.createElement("a"); 74 a.href = l.href; 75 a.textContent = l.text; 76 a.target = "_blank"; 77 a.rel = "noopener"; 78 return a; 79 }); 80 $("links").replaceChildren(...links.flatMap((a, i) => (i === 0 ? [a] : [" · ", a]))); 81 $("links").hidden = links.length === 0; 82 } 83 84 /** Open a page of Stripe in a new tab. */ 85 async function pay(msg) { 86 say("Opening Stripe…"); 87 try { 88 await send(msg); 89 say(""); 90 } catch (err) { 91 say(err.message); 92 } 93 } 94 95 // Back from a page of Stripe: show the new plan. 96 document.addEventListener("visibilitychange", () => { 97 if (document.visibilityState === "visible") show(); 98 }); 99 100 $("server").addEventListener("input", () => { 101 try { 102 $("signup").href = origin($("server").value) + "/signup"; 103 } catch (e) { 104 // not an address yet 105 } 106 }); 107 108 $("sign-in").addEventListener("submit", async (e) => { 109 e.preventDefault(); 110 let want; 111 try { 112 want = { origins: [origin($("server").value) + "/*"] }; 113 } catch (err) { 114 say("The sync server is not an address."); 115 return; 116 } 117 if (askData) want.data_collection = DATA; 118 // The browser asks the user. The request must come before any await: only 119 // then does the browser see it as part of the click. 120 const granted = api.permissions.request(want); 121 say("Signing in…"); 122 try { 123 if (!(await granted)) { 124 say("Without that permission the add-on cannot sync."); 125 return; 126 } 127 await send({ type: "signIn", server: $("server").value, email: $("email").value, password: $("password").value }); 128 $("password").value = ""; 129 say(""); 130 } catch (err) { 131 say(err.message); 132 } 133 show(); 134 }); 135 136 $("sync").addEventListener("click", async () => { 137 say("Syncing…"); 138 try { 139 await send({ type: "sync" }); 140 say(""); 141 } catch (err) { 142 say(err.message); 143 } 144 show(); 145 }); 146 147 $("sign-out").addEventListener("click", async () => { 148 await send({ type: "signOut" }); 149 say("Signed out. Your bookmarks stay in this browser."); 150 show(); 151 }); 152 153 show();