Recently Written · git

sbm-extension

sbm bookmarks for Firefox and Chrome: fuzzy search, add the page, sync with bm and the sbm app

git clone https://github.com/equwal/sbm-extension

Log | Files | Refs


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();