@sphoro/bugcatch-web-sdk 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -5
- package/dist/bugcatch-call.es.js +586 -0
- package/dist/bugcatch-call.es.js.map +1 -0
- package/dist/bugcatch-call.umd.js +2 -0
- package/dist/bugcatch-call.umd.js.map +1 -0
- package/dist/bugcatch-core.es.js +3214 -0
- package/dist/bugcatch-core.es.js.map +1 -0
- package/dist/bugcatch-panel.es.js +2402 -0
- package/dist/bugcatch-panel.es.js.map +1 -0
- package/dist/bugcatch-panel.umd.js +1240 -0
- package/dist/bugcatch-panel.umd.js.map +1 -0
- package/dist/bugcatch.es.js +2 -5522
- package/dist/bugcatch.es.js.map +1 -1
- package/dist/bugcatch.standalone.umd.js +1536 -0
- package/dist/bugcatch.standalone.umd.js.map +1 -0
- package/dist/bugcatch.umd.js +89 -1183
- package/dist/bugcatch.umd.js.map +1 -1
- package/package.json +6 -6
- package/src/agent-issues.js +30 -0
- package/src/announcements.js +147 -0
- package/src/busy.js +18 -0
- package/src/call.js +7 -0
- package/src/capture.js +9 -0
- package/src/chunks.js +56 -0
- package/src/gesture.js +33 -0
- package/src/icons.js +18 -0
- package/src/identity.js +14 -9
- package/src/incoming-call.js +8 -30
- package/src/index.d.ts +50 -0
- package/src/index.js +170 -37
- package/src/lazy.js +23 -0
- package/src/notice.js +68 -0
- package/src/panel/call.js +242 -0
- package/src/panel/compose.js +416 -0
- package/src/panel/composer.js +235 -0
- package/src/panel/icons.js +34 -0
- package/src/panel/index.js +251 -0
- package/src/panel/known.js +130 -0
- package/src/panel/messages.js +176 -0
- package/src/panel/notices.js +34 -0
- package/src/panel/picks.js +61 -0
- package/src/panel/refs.js +321 -0
- package/src/panel/render.js +124 -0
- package/src/panel/signin.js +228 -0
- package/src/panel/styles.js +960 -0
- package/src/panel/thread.js +384 -0
- package/src/session.js +8 -3
- package/src/umd/call.js +6 -0
- package/src/umd/lazy.js +98 -0
- package/src/umd/panel.js +6 -0
- package/src/updates.js +20 -7
- package/src/widget.js +243 -3164
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Who is reporting.
|
|
2
|
+
//
|
|
3
|
+
// Three states, and the project picks which: 'off' is every project that has
|
|
4
|
+
// not asked for this and draws nothing at all; 'optional' offers a sign-in
|
|
5
|
+
// above the form that the reporter may ignore; 'required' replaces the form
|
|
6
|
+
// with it until they have.
|
|
7
|
+
//
|
|
8
|
+
// Asked on every open rather than remembered for the page, because the answer
|
|
9
|
+
// includes whether this browser's session is still live — and it can end
|
|
10
|
+
// somewhere else, when the same address signs in on another device. A widget
|
|
11
|
+
// holding a cached "signed in as you" would find out at the moment the report
|
|
12
|
+
// failed, which is the worst moment to discover it.
|
|
13
|
+
|
|
14
|
+
import { escapeHtml } from './render.js';
|
|
15
|
+
|
|
16
|
+
export function mountSignIn(ctx) {
|
|
17
|
+
const {
|
|
18
|
+
client, say, compose, sendBtn, historyBtn, callBtn, whoami,
|
|
19
|
+
} = ctx;
|
|
20
|
+
|
|
21
|
+
let login = { mode: client.lastLoginMode || 'off', signedIn: null };
|
|
22
|
+
// 'idle' — showing who they are; 'email' / 'code' — the two steps of getting there.
|
|
23
|
+
let step = 'idle';
|
|
24
|
+
let pendingEmail = client.config.user?.email || '';
|
|
25
|
+
let pendingCode = '';
|
|
26
|
+
let signInBusy = false;
|
|
27
|
+
let signInError = '';
|
|
28
|
+
|
|
29
|
+
const mustSignIn = () => login.mode === 'required' && !login.signedIn;
|
|
30
|
+
|
|
31
|
+
/** Show or hide everything that only makes sense once we know who is filing. */
|
|
32
|
+
function applyGate() {
|
|
33
|
+
const shut = mustSignIn();
|
|
34
|
+
compose.hidden = shut;
|
|
35
|
+
sendBtn.hidden = shut;
|
|
36
|
+
if (historyBtn) historyBtn.hidden = shut;
|
|
37
|
+
// The call offer is the same errand as the form, so it goes with it — and
|
|
38
|
+
// comes back only if the API had already said this project can take one.
|
|
39
|
+
if (shut) callBtn.hidden = true;
|
|
40
|
+
else if (ctx.callOffered()) callBtn.hidden = false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function drawWhoami() {
|
|
44
|
+
if (login.mode === 'off') {
|
|
45
|
+
whoami.hidden = true;
|
|
46
|
+
whoami.innerHTML = '';
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
whoami.hidden = false;
|
|
50
|
+
whoami.classList.toggle('asking', step !== 'idle');
|
|
51
|
+
|
|
52
|
+
const problem = signInError ? `<div class="after err">${escapeHtml(signInError)}</div>` : '';
|
|
53
|
+
|
|
54
|
+
if (step === 'idle') {
|
|
55
|
+
if (login.signedIn) {
|
|
56
|
+
whoami.innerHTML = `<span class="who">Signed in as <strong>${escapeHtml(login.signedIn.email)}</strong></span>`
|
|
57
|
+
+ '<button type="button" class="link" data-act="signout">Sign out</button>';
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// An address the host application passed for its own signed-in user is
|
|
61
|
+
// not nothing — the report will carry it — so saying "anonymously" over
|
|
62
|
+
// the top of one would be untrue. It is simply not proved, which is what
|
|
63
|
+
// the offer beside it is for.
|
|
64
|
+
const claimed = client.config.user?.email;
|
|
65
|
+
whoami.innerHTML = `<span class="who">${login.mode === 'required'
|
|
66
|
+
? 'Sign in to report a bug here.'
|
|
67
|
+
: claimed
|
|
68
|
+
? `Reporting as <strong>${escapeHtml(claimed)}</strong>.`
|
|
69
|
+
: 'Reporting anonymously — sign in and we can tell you what happens next.'}</span>`
|
|
70
|
+
+ '<button type="button" class="link" data-act="signin">Sign in</button>';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (step === 'email') {
|
|
75
|
+
whoami.innerHTML = `
|
|
76
|
+
<p class="why">Sign in with any email address — we'll send you a code.
|
|
77
|
+
It's how you hear back about this report${login.mode === 'required' ? ', and this project asks for it' : ''}.</p>
|
|
78
|
+
<div class="row">
|
|
79
|
+
<input type="email" data-fld="email" value="${escapeHtml(pendingEmail)}"
|
|
80
|
+
placeholder="you@example.com" autocomplete="email" spellcheck="false"
|
|
81
|
+
aria-label="Your email address" ${signInBusy ? 'disabled' : ''} />
|
|
82
|
+
<button type="button" class="primary" data-act="code" ${signInBusy ? 'disabled' : ''}>
|
|
83
|
+
${signInBusy ? 'Sending…' : 'Send code'}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
${problem}
|
|
87
|
+
${login.mode === 'optional'
|
|
88
|
+
? '<div class="after"><button type="button" class="link" data-act="skip">Not now</button></div>'
|
|
89
|
+
: ''}`;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
whoami.innerHTML = `
|
|
94
|
+
<p class="why">We sent a code to <strong>${escapeHtml(pendingEmail)}</strong>. It expires in ten minutes.</p>
|
|
95
|
+
<div class="row">
|
|
96
|
+
<input type="text" class="code" data-fld="code" value="${escapeHtml(pendingCode)}"
|
|
97
|
+
placeholder="123456" inputmode="numeric" autocomplete="one-time-code"
|
|
98
|
+
maxlength="8" aria-label="The code we emailed you" ${signInBusy ? 'disabled' : ''} />
|
|
99
|
+
<button type="button" class="primary" data-act="confirm" ${signInBusy ? 'disabled' : ''}>
|
|
100
|
+
${signInBusy ? 'Checking…' : 'Sign in'}
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
${problem}
|
|
104
|
+
<div class="after">
|
|
105
|
+
<button type="button" class="link" data-act="resend" ${signInBusy ? 'disabled' : ''}>Send it again</button>
|
|
106
|
+
<button type="button" class="link" data-act="signin-back" ${signInBusy ? 'disabled' : ''}>Use another address</button>
|
|
107
|
+
</div>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Redraw, and put the cursor where the reporter is being asked to type. */
|
|
111
|
+
function drawSignIn({ focus = false } = {}) {
|
|
112
|
+
drawWhoami();
|
|
113
|
+
applyGate();
|
|
114
|
+
if (!focus || signInBusy) return;
|
|
115
|
+
const field = whoami.querySelector('[data-fld]');
|
|
116
|
+
if (field) { field.focus(); field.setSelectionRange?.(field.value.length, field.value.length); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Typed values survive a redraw because they are held here, not read back off
|
|
120
|
+
// a node that the next render replaces.
|
|
121
|
+
whoami.addEventListener('input', (e) => {
|
|
122
|
+
const field = e.target.closest('[data-fld]');
|
|
123
|
+
if (!field) return;
|
|
124
|
+
if (field.dataset.fld === 'email') pendingEmail = field.value;
|
|
125
|
+
else pendingCode = field.value;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
whoami.addEventListener('keydown', (e) => {
|
|
129
|
+
if (e.key !== 'Enter' || !e.target.closest('[data-fld]')) return;
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
if (step === 'email') requestCode();
|
|
132
|
+
else confirmCode();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
async function requestCode({ resent = false } = {}) {
|
|
136
|
+
const email = pendingEmail.trim();
|
|
137
|
+
if (!email) { signInError = 'Enter the address to send the code to.'; drawSignIn({ focus: true }); return; }
|
|
138
|
+
|
|
139
|
+
signInBusy = true; signInError = ''; drawSignIn();
|
|
140
|
+
try {
|
|
141
|
+
await client.signIn(email);
|
|
142
|
+
pendingEmail = email;
|
|
143
|
+
if (!resent) pendingCode = '';
|
|
144
|
+
step = 'code';
|
|
145
|
+
} catch (err) {
|
|
146
|
+
signInError = err.message;
|
|
147
|
+
} finally {
|
|
148
|
+
signInBusy = false;
|
|
149
|
+
drawSignIn({ focus: true });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function confirmCode() {
|
|
154
|
+
const code = pendingCode.trim();
|
|
155
|
+
if (!code) { signInError = 'Enter the code we emailed you.'; drawSignIn({ focus: true }); return; }
|
|
156
|
+
|
|
157
|
+
signInBusy = true; signInError = ''; drawSignIn();
|
|
158
|
+
try {
|
|
159
|
+
const who = await client.confirmSignIn(pendingEmail.trim(), code);
|
|
160
|
+
login.signedIn = who;
|
|
161
|
+
step = 'idle';
|
|
162
|
+
pendingCode = '';
|
|
163
|
+
say('');
|
|
164
|
+
// Signing in is what makes a call possible on most projects — there is
|
|
165
|
+
// now somebody to attribute it to — so the offer is asked for again
|
|
166
|
+
// rather than staying hidden until the panel is reopened.
|
|
167
|
+
ctx.offerCall();
|
|
168
|
+
} catch (err) {
|
|
169
|
+
signInError = err.message;
|
|
170
|
+
} finally {
|
|
171
|
+
signInBusy = false;
|
|
172
|
+
drawSignIn({ focus: step !== 'idle' });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
whoami.addEventListener('click', async (e) => {
|
|
177
|
+
const btn = e.target.closest('button[data-act]');
|
|
178
|
+
if (!btn) return;
|
|
179
|
+
const act = btn.dataset.act;
|
|
180
|
+
|
|
181
|
+
if (act === 'signin') { step = 'email'; signInError = ''; drawSignIn({ focus: true }); return; }
|
|
182
|
+
if (act === 'skip') { step = 'idle'; signInError = ''; drawSignIn(); return; }
|
|
183
|
+
// Namespaced: the report screen has a back button of its own, and the two
|
|
184
|
+
// must not be one selector away from each other.
|
|
185
|
+
if (act === 'signin-back') { step = 'email'; signInError = ''; pendingCode = ''; drawSignIn({ focus: true }); return; }
|
|
186
|
+
if (act === 'code') { requestCode(); return; }
|
|
187
|
+
if (act === 'resend') { requestCode({ resent: true }); return; }
|
|
188
|
+
if (act === 'confirm') { confirmCode(); return; }
|
|
189
|
+
if (act === 'signout') {
|
|
190
|
+
signInBusy = true; drawSignIn();
|
|
191
|
+
try {
|
|
192
|
+
await client.signOut();
|
|
193
|
+
} finally {
|
|
194
|
+
login.signedIn = null;
|
|
195
|
+
signInBusy = false;
|
|
196
|
+
// On a project that requires it, signing out puts the form away again
|
|
197
|
+
// and asks — which is what applyGate does off the back of this.
|
|
198
|
+
step = login.mode === 'required' ? 'email' : 'idle';
|
|
199
|
+
drawSignIn({ focus: step !== 'idle' });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
/** Re-read the project's setting and this browser's session, and redraw. */
|
|
205
|
+
async function refreshLogin({ announce = false } = {}) {
|
|
206
|
+
const state = await client.loginState();
|
|
207
|
+
login = { mode: state.mode, signedIn: state.signedIn };
|
|
208
|
+
client.lastLoginMode = state.mode;
|
|
209
|
+
// Nothing to type on a project that requires sign-in and has nobody in it.
|
|
210
|
+
if (mustSignIn() && step === 'idle') step = 'email';
|
|
211
|
+
if (login.signedIn) step = 'idle';
|
|
212
|
+
if (announce && state.signedOutReason) say(state.signedOutReason, 'info');
|
|
213
|
+
drawSignIn();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
refreshLogin({ announce: true }).catch(() => { /* a widget that can't ask stays open */ });
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Put the panel back in step after a send or a call was refused: the reason
|
|
220
|
+
* may be that this browser was signed out while the panel sat open.
|
|
221
|
+
*/
|
|
222
|
+
function recheckSignIn() {
|
|
223
|
+
if (login.mode !== 'off') refreshLogin().catch(() => {});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
ctx.mustSignIn = mustSignIn;
|
|
227
|
+
ctx.recheckSignIn = recheckSignIn;
|
|
228
|
+
}
|