@usero/sdk 1.1.3 → 1.1.5
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.
|
@@ -24,6 +24,19 @@ function readTesterName(override) {
|
|
|
24
24
|
}
|
|
25
25
|
return void 0;
|
|
26
26
|
}
|
|
27
|
+
function getAdoptSessionId() {
|
|
28
|
+
if (typeof window === "undefined" || typeof window.location === "undefined") return null;
|
|
29
|
+
try {
|
|
30
|
+
const params = new URLSearchParams(window.location.search);
|
|
31
|
+
const raw = params.get("uts");
|
|
32
|
+
if (!raw) return null;
|
|
33
|
+
const cleaned = raw.trim().slice(0, 64);
|
|
34
|
+
if (!/^[a-z0-9]+$/i.test(cleaned)) return null;
|
|
35
|
+
return cleaned;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
27
40
|
function getTestSlug(queryParam) {
|
|
28
41
|
if (typeof window === "undefined" || typeof window.location === "undefined") return null;
|
|
29
42
|
try {
|
|
@@ -334,78 +347,225 @@ function buildIndicator(host, store, callbacks) {
|
|
|
334
347
|
.btn-ghost { background: transparent; color: rgba(255,255,255,0.7); }
|
|
335
348
|
.btn-ghost:hover { background: rgba(255,255,255,0.10); color: #fff; }
|
|
336
349
|
|
|
337
|
-
/*
|
|
350
|
+
/* ---- Finished screen (complete + ended-early). Usero warm-stone palette,
|
|
351
|
+
shadow-DOM scoped so host CSS can't leak in. Scrollable so the primary
|
|
352
|
+
action stays reachable on a short phone with the keyboard open. ---- */
|
|
338
353
|
.thanks {
|
|
339
354
|
position: fixed; inset: 0;
|
|
340
|
-
display:
|
|
341
|
-
background: rgba(
|
|
342
|
-
backdrop-filter: blur(
|
|
343
|
-
-webkit-backdrop-filter: blur(
|
|
344
|
-
color: #
|
|
345
|
-
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
355
|
+
display: flex; align-items: flex-start; justify-content: center;
|
|
356
|
+
background: rgba(28, 25, 23, 0.62);
|
|
357
|
+
backdrop-filter: blur(8px);
|
|
358
|
+
-webkit-backdrop-filter: blur(8px);
|
|
359
|
+
color: #1c1917;
|
|
360
|
+
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
346
361
|
z-index: 2147483647;
|
|
347
|
-
padding: 24px;
|
|
348
|
-
|
|
362
|
+
padding: 24px 16px calc(env(safe-area-inset-bottom, 0px) + 24px);
|
|
363
|
+
overflow-y: auto;
|
|
364
|
+
-webkit-overflow-scrolling: touch;
|
|
349
365
|
}
|
|
350
366
|
.thanks-card {
|
|
351
|
-
background: #fff; color: #
|
|
352
|
-
border-radius:
|
|
353
|
-
max-width:
|
|
354
|
-
|
|
367
|
+
background: #fff; color: #1c1917;
|
|
368
|
+
border-radius: 22px; padding: 30px 24px 24px;
|
|
369
|
+
max-width: 400px; width: 100%;
|
|
370
|
+
margin: auto 0;
|
|
371
|
+
box-shadow: 0 24px 60px rgba(28, 25, 23, 0.28), 0 2px 8px rgba(28, 25, 23, 0.12);
|
|
355
372
|
text-align: left;
|
|
373
|
+
animation: thanks-in 0.34s cubic-bezier(0.16, 1, 0.3, 1);
|
|
374
|
+
}
|
|
375
|
+
@keyframes thanks-in {
|
|
376
|
+
from { opacity: 0; transform: translateY(14px) scale(0.985); }
|
|
377
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
356
378
|
}
|
|
357
379
|
.thanks-card .head { text-align: center; }
|
|
358
|
-
.thanks h2 {
|
|
359
|
-
|
|
380
|
+
.thanks h2 {
|
|
381
|
+
margin: 0 0 7px; font-size: 22px; line-height: 1.2;
|
|
382
|
+
font-weight: 600; letter-spacing: -0.018em; color: #1c1917;
|
|
383
|
+
}
|
|
384
|
+
.thanks .lede {
|
|
385
|
+
margin: 0 auto 22px; font-size: 14.5px; line-height: 1.5;
|
|
386
|
+
color: #57534e; text-align: center; max-width: 30ch;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/* Status medallion: green tick when complete, warm ring when ended early */
|
|
360
390
|
.thanks .check {
|
|
361
|
-
width:
|
|
362
|
-
background: #10b981; color: #fff;
|
|
391
|
+
width: 56px; height: 56px; border-radius: 50%;
|
|
363
392
|
display: grid; place-items: center;
|
|
364
|
-
margin: 0 auto
|
|
365
|
-
|
|
393
|
+
margin: 0 auto 16px;
|
|
394
|
+
}
|
|
395
|
+
.thanks .check.ok {
|
|
396
|
+
background: #ecfdf5;
|
|
397
|
+
box-shadow: inset 0 0 0 1px rgba(16,185,129,0.22);
|
|
398
|
+
color: #059669;
|
|
399
|
+
}
|
|
400
|
+
.thanks .check.ok svg { width: 26px; height: 26px; }
|
|
401
|
+
.thanks .check.early {
|
|
402
|
+
background: #fff7ed;
|
|
403
|
+
box-shadow: inset 0 0 0 1px rgba(234,88,12,0.20);
|
|
404
|
+
color: #ea580c;
|
|
405
|
+
}
|
|
406
|
+
.thanks .check.early svg { width: 24px; height: 24px; }
|
|
407
|
+
|
|
408
|
+
/* Verified-checks list (complete) / progress list (ended early) */
|
|
409
|
+
.thanks .checks {
|
|
410
|
+
list-style: none; margin: 0 0 4px; padding: 0;
|
|
411
|
+
border: 1px solid #f0eeec; border-radius: 14px;
|
|
412
|
+
background: #fafaf9; overflow: hidden;
|
|
413
|
+
}
|
|
414
|
+
.thanks .checks li {
|
|
415
|
+
display: flex; align-items: center; gap: 11px;
|
|
416
|
+
padding: 12px 14px; font-size: 14px; color: #292524;
|
|
417
|
+
border-top: 1px solid #f0eeec;
|
|
418
|
+
}
|
|
419
|
+
.thanks .checks li:first-child { border-top: 0; }
|
|
420
|
+
.thanks .checks .ic {
|
|
421
|
+
width: 20px; height: 20px; border-radius: 50%;
|
|
422
|
+
display: grid; place-items: center; flex-shrink: 0;
|
|
423
|
+
}
|
|
424
|
+
.thanks .checks .ic.done { background: #d1fae5; color: #059669; }
|
|
425
|
+
.thanks .checks .ic.todo { background: #f5f5f4; color: #a8a29e; box-shadow: inset 0 0 0 1px #e7e5e4; }
|
|
426
|
+
.thanks .checks .ic svg { width: 12px; height: 12px; }
|
|
427
|
+
.thanks .checks li.muted-row { color: #78716c; }
|
|
428
|
+
|
|
429
|
+
/* Payout block (complete) */
|
|
430
|
+
.thanks .payout { margin-top: 20px; }
|
|
431
|
+
.thanks .payout-q {
|
|
432
|
+
font-size: 12px; font-weight: 600; letter-spacing: 0.04em;
|
|
433
|
+
text-transform: uppercase; color: #a8a29e;
|
|
434
|
+
margin: 0 0 10px;
|
|
435
|
+
}
|
|
436
|
+
.thanks .pay-primary {
|
|
437
|
+
width: 100%; box-sizing: border-box;
|
|
438
|
+
appearance: none; border: 0; cursor: pointer;
|
|
439
|
+
background: #ea580c; color: #fff;
|
|
440
|
+
padding: 15px 18px; border-radius: 14px;
|
|
441
|
+
font: inherit; font-weight: 600; font-size: 15.5px;
|
|
442
|
+
line-height: 1.3; text-align: center;
|
|
443
|
+
box-shadow: 0 6px 16px rgba(234, 88, 12, 0.28);
|
|
444
|
+
transition: background 0.15s ease, transform 0.07s ease, box-shadow 0.15s ease;
|
|
445
|
+
}
|
|
446
|
+
.thanks .pay-primary:hover { background: #c2410c; }
|
|
447
|
+
.thanks .pay-primary:active { transform: scale(0.985); }
|
|
448
|
+
.thanks .pay-primary:focus-visible { outline: 2px solid #ea580c; outline-offset: 2px; }
|
|
449
|
+
.thanks .pay-primary[disabled] { opacity: 0.6; cursor: progress; box-shadow: none; }
|
|
450
|
+
.thanks .pay-primary .amt { font-variant-numeric: tabular-nums; }
|
|
451
|
+
.thanks .pay-alt {
|
|
452
|
+
display: block; width: 100%;
|
|
453
|
+
margin-top: 12px; padding: 4px;
|
|
454
|
+
background: none; border: 0; cursor: pointer;
|
|
455
|
+
font: inherit; font-size: 13px; font-weight: 500;
|
|
456
|
+
color: #78716c; text-align: center;
|
|
457
|
+
text-decoration: underline; text-underline-offset: 2px;
|
|
458
|
+
transition: color 0.15s ease;
|
|
459
|
+
}
|
|
460
|
+
.thanks .pay-alt:hover { color: #44403c; }
|
|
461
|
+
.thanks .pay-alt:focus-visible { outline: 2px solid #ea580c; outline-offset: 2px; border-radius: 6px; }
|
|
462
|
+
.thanks [hidden] { display: none !important; }
|
|
463
|
+
|
|
464
|
+
/* Alternate-email expander */
|
|
465
|
+
.thanks .pay-edit { margin-top: 14px; animation: pop-in 0.2s cubic-bezier(0.2,0.8,0.2,1); }
|
|
466
|
+
.thanks .pay-edit[hidden] { display: none; }
|
|
467
|
+
.thanks .pay-label {
|
|
468
|
+
display: block; margin: 0 0 7px;
|
|
469
|
+
font-size: 13px; font-weight: 500; color: #44403c;
|
|
470
|
+
}
|
|
471
|
+
.thanks .pay-input {
|
|
472
|
+
width: 100%; box-sizing: border-box;
|
|
473
|
+
padding: 12px 13px;
|
|
474
|
+
background: #fff; border: 1px solid #e7e5e4; border-radius: 11px;
|
|
475
|
+
font: inherit; font-size: 15px; color: #1c1917;
|
|
476
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
477
|
+
}
|
|
478
|
+
.thanks .pay-input:focus {
|
|
479
|
+
outline: none; border-color: #ea580c;
|
|
480
|
+
box-shadow: 0 0 0 3px rgba(234, 88, 12, 0.16);
|
|
481
|
+
}
|
|
482
|
+
.thanks .pay-input::placeholder { color: #a8a29e; }
|
|
483
|
+
.thanks .pay-eta {
|
|
484
|
+
margin: 14px 0 0; font-size: 12px; line-height: 1.45;
|
|
485
|
+
color: #a8a29e; text-align: center;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* Ended-early "what unlocks the reward" note */
|
|
489
|
+
.thanks .early-note {
|
|
490
|
+
display: flex; align-items: flex-start; gap: 10px;
|
|
491
|
+
margin-top: 18px; padding: 13px 14px;
|
|
492
|
+
background: #fff7ed; border: 1px solid #fed7aa; border-radius: 13px;
|
|
493
|
+
font-size: 13.5px; line-height: 1.45; color: #9a3412;
|
|
494
|
+
}
|
|
495
|
+
.thanks .early-note svg { width: 17px; height: 17px; flex-shrink: 0; margin-top: 1px; color: #ea580c; }
|
|
496
|
+
.thanks .early-actions { margin-top: 18px; display: flex; flex-direction: column; gap: 10px; }
|
|
497
|
+
.thanks .resume-btn {
|
|
498
|
+
width: 100%; box-sizing: border-box;
|
|
499
|
+
appearance: none; border: 0; cursor: pointer;
|
|
500
|
+
background: #ea580c; color: #fff;
|
|
501
|
+
padding: 15px 18px; border-radius: 14px;
|
|
502
|
+
font: inherit; font-weight: 600; font-size: 15.5px;
|
|
503
|
+
box-shadow: 0 6px 16px rgba(234, 88, 12, 0.28);
|
|
504
|
+
transition: background 0.15s ease, transform 0.07s ease;
|
|
505
|
+
}
|
|
506
|
+
.thanks .resume-btn:hover { background: #c2410c; }
|
|
507
|
+
.thanks .resume-btn:active { transform: scale(0.985); }
|
|
508
|
+
.thanks .resume-btn:focus-visible { outline: 2px solid #ea580c; outline-offset: 2px; }
|
|
509
|
+
.thanks .exit-btn {
|
|
510
|
+
width: 100%; box-sizing: border-box;
|
|
511
|
+
appearance: none; border: 0; background: none; cursor: pointer;
|
|
512
|
+
padding: 4px; font: inherit; font-size: 13px; line-height: 1.45;
|
|
513
|
+
color: #78716c; text-align: center;
|
|
514
|
+
}
|
|
515
|
+
.thanks .exit-btn:hover { color: #44403c; }
|
|
516
|
+
.thanks .exit-btn:focus-visible { outline: 2px solid #ea580c; outline-offset: 2px; border-radius: 6px; }
|
|
517
|
+
|
|
518
|
+
/* End-of-test note (shown after payout is set, complete path only) */
|
|
519
|
+
.thanks .note-section {
|
|
520
|
+
margin-top: 22px; padding-top: 20px;
|
|
521
|
+
border-top: 1px solid #f0eeec;
|
|
366
522
|
}
|
|
367
523
|
.thanks .end-label {
|
|
368
524
|
display: block; margin: 0 0 8px;
|
|
369
|
-
font-size: 13px; font-weight: 500; color: #
|
|
525
|
+
font-size: 13px; font-weight: 500; color: #44403c;
|
|
370
526
|
}
|
|
371
527
|
.thanks .end-textarea {
|
|
372
528
|
width: 100%; box-sizing: border-box;
|
|
373
|
-
min-height:
|
|
374
|
-
padding:
|
|
375
|
-
background: #
|
|
376
|
-
border: 1px solid #
|
|
377
|
-
border-radius:
|
|
378
|
-
font: inherit; font-size:
|
|
379
|
-
color: #
|
|
380
|
-
transition: border-color 0.15s ease, background 0.15s ease;
|
|
529
|
+
min-height: 84px; resize: vertical;
|
|
530
|
+
padding: 12px 13px;
|
|
531
|
+
background: #fafaf9;
|
|
532
|
+
border: 1px solid #e7e5e4;
|
|
533
|
+
border-radius: 12px;
|
|
534
|
+
font: inherit; font-size: 14.5px; line-height: 1.5;
|
|
535
|
+
color: #1c1917;
|
|
536
|
+
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
|
|
381
537
|
}
|
|
382
538
|
.thanks .end-textarea:focus {
|
|
383
|
-
outline: none; border-color: #
|
|
539
|
+
outline: none; border-color: #ea580c; background: #fff;
|
|
540
|
+
box-shadow: 0 0 0 3px rgba(234, 88, 12, 0.14);
|
|
384
541
|
}
|
|
385
|
-
.thanks .end-textarea::placeholder { color: #
|
|
542
|
+
.thanks .end-textarea::placeholder { color: #a8a29e; }
|
|
386
543
|
.thanks .end-actions {
|
|
387
544
|
display: flex; gap: 10px; margin-top: 14px;
|
|
388
545
|
}
|
|
389
546
|
.thanks .end-actions button {
|
|
390
547
|
flex: 1;
|
|
391
|
-
appearance: none; border: 1px solid #
|
|
392
|
-
background: #fff; color: #
|
|
393
|
-
padding:
|
|
548
|
+
appearance: none; border: 1px solid #e7e5e4;
|
|
549
|
+
background: #fff; color: #44403c;
|
|
550
|
+
padding: 12px 14px; border-radius: 12px;
|
|
394
551
|
font: inherit; font-weight: 600; font-size: 14px;
|
|
395
552
|
cursor: pointer;
|
|
396
553
|
transition: background 0.15s ease, border-color 0.15s ease;
|
|
397
554
|
}
|
|
398
|
-
.thanks .end-actions button:hover { background: #
|
|
555
|
+
.thanks .end-actions button:hover { background: #fafaf9; border-color: #d6d3d1; }
|
|
399
556
|
.thanks .end-actions button.primary {
|
|
400
|
-
background: #
|
|
557
|
+
background: #1c1917; color: #fff; border-color: #1c1917; flex: 1.4;
|
|
401
558
|
}
|
|
402
|
-
.thanks .end-actions button.primary:hover { background: #
|
|
403
|
-
.thanks .end-actions button:focus-visible { outline: 2px solid #
|
|
559
|
+
.thanks .end-actions button.primary:hover { background: #292524; border-color: #292524; }
|
|
560
|
+
.thanks .end-actions button:focus-visible { outline: 2px solid #ea580c; outline-offset: 2px; }
|
|
404
561
|
.thanks .end-hint {
|
|
405
|
-
margin:
|
|
562
|
+
margin: 11px 0 0; font-size: 11.5px; color: #a8a29e; text-align: center;
|
|
406
563
|
}
|
|
407
564
|
.thanks .end-sent {
|
|
408
|
-
margin-top:
|
|
565
|
+
margin-top: 16px; text-align: center; color: #57534e; font-size: 13.5px; line-height: 1.45;
|
|
566
|
+
}
|
|
567
|
+
@media (prefers-reduced-motion: reduce) {
|
|
568
|
+
.thanks-card, .thanks .pay-edit { animation: none; }
|
|
409
569
|
}
|
|
410
570
|
|
|
411
571
|
@keyframes pulse {
|
|
@@ -483,6 +643,10 @@ function buildIndicator(host, store, callbacks) {
|
|
|
483
643
|
var MIC_ICON_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" width="13" height="13"><path d="M8 1.5a2 2 0 0 0-2 2v4a2 2 0 1 0 4 0v-4a2 2 0 0 0-2-2Z" fill="currentColor"/><path d="M4 7.5a4 4 0 0 0 8 0M8 11.5v3M5.5 14.5h5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
|
|
484
644
|
var MIC_MUTED_ICON_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" width="13" height="13"><path d="M8 1.5a2 2 0 0 0-2 2v3.2L10 11V3.5a2 2 0 0 0-2-2Z" fill="currentColor"/><path d="M4 7.5a4 4 0 0 0 6.5 3.12M12 7.5a4 4 0 0 1-.3 1.5M8 11.5v3M5.5 14.5h5M2 2l12 12" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>`;
|
|
485
645
|
var NOTE_ICON_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path d="M3 3.5A1.5 1.5 0 0 1 4.5 2h7A1.5 1.5 0 0 1 13 3.5V10a1.5 1.5 0 0 1-1.5 1.5H7L4 14v-2.5h-.5A1.5 1.5 0 0 1 2 10V3.5A1.5 1.5 0 0 1 3.5 3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
646
|
+
var TICK_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M5 12.5 10 17.5 19 7" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
647
|
+
var TICK_SM_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M3.5 8.5 6.5 11.5 12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
648
|
+
var CLOCK_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="12" r="8.4" stroke="currentColor" stroke-width="2"/><path d="M12 7.5V12l3 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
649
|
+
var SPARK_ICON_SVG = `<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 1.5 9.5 6.5 14.5 8 9.5 9.5 8 14.5 6.5 9.5 1.5 8 6.5 6.5Z" fill="currentColor"/></svg>`;
|
|
486
650
|
function installTasksToggle(bar, finishBtn, store, onToggleTasks) {
|
|
487
651
|
const tasksBtn = document.createElement("button");
|
|
488
652
|
tasksBtn.type = "button";
|
|
@@ -719,43 +883,216 @@ function closeNotePopover(store) {
|
|
|
719
883
|
store.notesPopoverOpen = false;
|
|
720
884
|
store.notePopoverAtMs = null;
|
|
721
885
|
}
|
|
886
|
+
function escapeHtml(value) {
|
|
887
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
888
|
+
}
|
|
889
|
+
function isValidEmail(value) {
|
|
890
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
|
|
891
|
+
}
|
|
722
892
|
function showThanksScreen(root, opts) {
|
|
723
893
|
const overlay = document.createElement("div");
|
|
724
894
|
overlay.className = "thanks";
|
|
895
|
+
overlay.setAttribute("role", "dialog");
|
|
896
|
+
overlay.setAttribute("aria-modal", "true");
|
|
725
897
|
const card = document.createElement("div");
|
|
726
898
|
card.className = "thanks-card";
|
|
899
|
+
overlay.appendChild(card);
|
|
900
|
+
root.appendChild(overlay);
|
|
901
|
+
if (opts.payment && !opts.payment.qualified) {
|
|
902
|
+
renderEndedEarly(card, opts);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
renderComplete(card, opts);
|
|
906
|
+
}
|
|
907
|
+
function checksList(rows) {
|
|
908
|
+
const items = rows.map((r) => {
|
|
909
|
+
const icClass = r.done ? "ic done" : "ic todo";
|
|
910
|
+
const icon = r.done ? TICK_SM_SVG : "";
|
|
911
|
+
const liClass = r.muted ? ' class="muted-row"' : "";
|
|
912
|
+
return `<li${liClass}><span class="${icClass}" aria-hidden="true">${icon}</span><span>${escapeHtml(r.label)}</span></li>`;
|
|
913
|
+
}).join("");
|
|
914
|
+
return `<ul class="checks">${items}</ul>`;
|
|
915
|
+
}
|
|
916
|
+
function renderComplete(card, opts) {
|
|
917
|
+
const payment = opts.payment;
|
|
918
|
+
const reward = payment?.reward ?? null;
|
|
919
|
+
const defaultEmail = payment?.payoutEmail ?? null;
|
|
920
|
+
const tasksTotal = payment?.tasksTotal ?? 0;
|
|
921
|
+
const head = document.createElement("div");
|
|
922
|
+
head.className = "head";
|
|
923
|
+
const lede = reward ? `We have your recording. Confirm where to send your ${escapeHtml(reward)} and the team will review it shortly.` : "We have your recording. Thanks for taking the time to walk us through it.";
|
|
924
|
+
head.innerHTML = `
|
|
925
|
+
<div class="check ok" aria-hidden="true">${TICK_ICON_SVG}</div>
|
|
926
|
+
<h2>You're done.</h2>
|
|
927
|
+
<p class="lede">${lede}</p>
|
|
928
|
+
${tasksTotal > 0 ? checksList([
|
|
929
|
+
{ label: tasksTotal === 1 ? "1 task completed" : `All ${tasksTotal} tasks completed`, done: true },
|
|
930
|
+
{ label: "Voice recording captured", done: true },
|
|
931
|
+
{ label: "Screen replay uploaded", done: true }
|
|
932
|
+
]) : checksList([
|
|
933
|
+
{ label: "Voice recording captured", done: true },
|
|
934
|
+
{ label: "Screen replay uploaded", done: true }
|
|
935
|
+
])}
|
|
936
|
+
`;
|
|
937
|
+
card.appendChild(head);
|
|
938
|
+
if (!payment) {
|
|
939
|
+
appendNoteSection(card, opts, "Your session was saved. Anything you would add?");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
renderPayout(card, opts, reward, defaultEmail);
|
|
943
|
+
}
|
|
944
|
+
function renderPayout(card, opts, reward, defaultEmail) {
|
|
945
|
+
const wrap = document.createElement("div");
|
|
946
|
+
wrap.className = "payout";
|
|
947
|
+
const rewardLabel = reward ?? "my reward";
|
|
948
|
+
const haveDefault = !!defaultEmail && isValidEmail(defaultEmail);
|
|
949
|
+
wrap.innerHTML = `
|
|
950
|
+
<p class="payout-q">Where should we send ${escapeHtml(reward ?? "your reward")}?</p>
|
|
951
|
+
<button type="button" class="pay-primary" ${haveDefault ? "" : "hidden"}>
|
|
952
|
+
Send <span class="amt">${escapeHtml(rewardLabel)}</span>${haveDefault ? ` to ${escapeHtml(defaultEmail)}` : ""}
|
|
953
|
+
</button>
|
|
954
|
+
<button type="button" class="pay-alt">${haveDefault ? "Use a different email" : "Add your payout email"}</button>
|
|
955
|
+
<div class="pay-edit" ${haveDefault ? "hidden" : ""}>
|
|
956
|
+
<label class="pay-label" for="usero-payout-email">Payout email</label>
|
|
957
|
+
<input id="usero-payout-email" class="pay-input" type="email" inputmode="email"
|
|
958
|
+
autocomplete="email" placeholder="you@example.com" value="${haveDefault ? "" : escapeHtml(defaultEmail ?? "")}" />
|
|
959
|
+
</div>
|
|
960
|
+
<p class="pay-eta">Reward arrives within about 2 days of the team reviewing it.</p>
|
|
961
|
+
`;
|
|
962
|
+
card.appendChild(wrap);
|
|
963
|
+
const primary = wrap.querySelector(".pay-primary");
|
|
964
|
+
const altLink = wrap.querySelector(".pay-alt");
|
|
965
|
+
const editBox = wrap.querySelector(".pay-edit");
|
|
966
|
+
const emailInput = wrap.querySelector(".pay-input");
|
|
967
|
+
if (!primary || !altLink || !editBox || !emailInput) return;
|
|
968
|
+
const confirm = async (destination) => {
|
|
969
|
+
primary.disabled = true;
|
|
970
|
+
altLink.style.pointerEvents = "none";
|
|
971
|
+
const ok = await opts.onPayout(destination);
|
|
972
|
+
wrap.remove();
|
|
973
|
+
const confirmedTo = destination ?? defaultEmail;
|
|
974
|
+
const sentMsg = confirmedTo ? `${reward ? `${reward} is` : "Your reward is"} set to go to ${confirmedTo}.` : "Your reward is on its way.";
|
|
975
|
+
const note = ok ? sentMsg : `${sentMsg} (We will retry sending the details.)`;
|
|
976
|
+
appendNoteSection(card, opts, `${note} Anything you would add before you go?`);
|
|
977
|
+
};
|
|
978
|
+
primary.addEventListener("click", () => {
|
|
979
|
+
void confirm(null);
|
|
980
|
+
});
|
|
981
|
+
const openEditor = () => {
|
|
982
|
+
primary.hidden = true;
|
|
983
|
+
altLink.hidden = true;
|
|
984
|
+
editBox.hidden = false;
|
|
985
|
+
if (!editBox.querySelector(".pay-confirm")) {
|
|
986
|
+
const btn = document.createElement("button");
|
|
987
|
+
btn.type = "button";
|
|
988
|
+
btn.className = "pay-primary pay-confirm";
|
|
989
|
+
btn.style.marginTop = "12px";
|
|
990
|
+
btn.textContent = reward ? `Send ${reward} here` : "Use this email";
|
|
991
|
+
editBox.appendChild(btn);
|
|
992
|
+
btn.addEventListener("click", () => void submitEmail());
|
|
993
|
+
}
|
|
994
|
+
window.requestAnimationFrame(() => emailInput.focus({ preventScroll: true }));
|
|
995
|
+
};
|
|
996
|
+
const submitEmail = async () => {
|
|
997
|
+
const value = emailInput.value.trim().toLowerCase();
|
|
998
|
+
if (!isValidEmail(value)) {
|
|
999
|
+
emailInput.focus();
|
|
1000
|
+
emailInput.style.borderColor = "#dc2626";
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
await confirm(value);
|
|
1004
|
+
};
|
|
1005
|
+
altLink.addEventListener("click", openEditor);
|
|
1006
|
+
emailInput.addEventListener("input", () => {
|
|
1007
|
+
emailInput.style.borderColor = "";
|
|
1008
|
+
});
|
|
1009
|
+
emailInput.addEventListener("keydown", (e) => {
|
|
1010
|
+
if (e.key === "Enter") {
|
|
1011
|
+
e.preventDefault();
|
|
1012
|
+
void submitEmail();
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
function renderEndedEarly(card, opts) {
|
|
1017
|
+
const payment = opts.payment;
|
|
1018
|
+
const done = payment?.tasksDone ?? 0;
|
|
1019
|
+
const total = payment?.tasksTotal ?? 0;
|
|
1020
|
+
const reward = payment?.reward ?? null;
|
|
727
1021
|
const head = document.createElement("div");
|
|
728
1022
|
head.className = "head";
|
|
1023
|
+
const lede = total > 0 ? `We saw ${done} of ${total} ${total === 1 ? "task" : "tasks"} finished. No worries, you can pick up right where you left off.` : "It looks like the session ended before you finished. No worries, you can pick up where you left off.";
|
|
729
1024
|
head.innerHTML = `
|
|
730
|
-
<div class="check" aria-hidden="true"
|
|
731
|
-
<h2>
|
|
732
|
-
<p class="lede"
|
|
1025
|
+
<div class="check early" aria-hidden="true">${CLOCK_ICON_SVG}</div>
|
|
1026
|
+
<h2>Looks like you stopped early</h2>
|
|
1027
|
+
<p class="lede">${lede}</p>
|
|
733
1028
|
`;
|
|
1029
|
+
card.appendChild(head);
|
|
1030
|
+
if (total > 0) {
|
|
1031
|
+
const rows = [];
|
|
1032
|
+
for (let i = 0; i < total; i += 1) {
|
|
1033
|
+
rows.push({ label: `Task ${i + 1}`, done: i < done });
|
|
1034
|
+
}
|
|
1035
|
+
const list = document.createElement("div");
|
|
1036
|
+
list.innerHTML = checksList(rows);
|
|
1037
|
+
const ul = list.firstElementChild;
|
|
1038
|
+
if (ul) card.appendChild(ul);
|
|
1039
|
+
}
|
|
1040
|
+
const note = document.createElement("div");
|
|
1041
|
+
note.className = "early-note";
|
|
1042
|
+
note.innerHTML = `${SPARK_ICON_SVG}<span><strong style="font-weight:600">Resume the test.</strong> ${reward ? `Your ${escapeHtml(reward)} reward unlocks` : "The reward unlocks"} once all ${total > 0 ? total : "the"} ${total === 1 ? "task is" : "tasks are"} done.</span>`;
|
|
1043
|
+
card.appendChild(note);
|
|
1044
|
+
const actions = document.createElement("div");
|
|
1045
|
+
actions.className = "early-actions";
|
|
1046
|
+
const resume = document.createElement("button");
|
|
1047
|
+
resume.type = "button";
|
|
1048
|
+
resume.className = "resume-btn";
|
|
1049
|
+
resume.textContent = "Resume where I left off";
|
|
1050
|
+
const exit = document.createElement("button");
|
|
1051
|
+
exit.type = "button";
|
|
1052
|
+
exit.className = "exit-btn";
|
|
1053
|
+
exit.textContent = "Thanks for trying. No reward this time since the tasks weren't finished.";
|
|
1054
|
+
actions.appendChild(resume);
|
|
1055
|
+
actions.appendChild(exit);
|
|
1056
|
+
card.appendChild(actions);
|
|
1057
|
+
resume.addEventListener("click", () => {
|
|
1058
|
+
const overlay = card.closest(".thanks");
|
|
1059
|
+
if (overlay instanceof HTMLElement) overlay.remove();
|
|
1060
|
+
opts.onResume();
|
|
1061
|
+
});
|
|
1062
|
+
exit.addEventListener("click", () => {
|
|
1063
|
+
card.innerHTML = "";
|
|
1064
|
+
const sent = document.createElement("p");
|
|
1065
|
+
sent.className = "end-sent";
|
|
1066
|
+
sent.textContent = "Thanks for giving it a go. You can close this tab now.";
|
|
1067
|
+
card.appendChild(sent);
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
function appendNoteSection(card, opts, prompt) {
|
|
1071
|
+
const section = document.createElement("div");
|
|
1072
|
+
section.className = "note-section";
|
|
734
1073
|
const form = document.createElement("form");
|
|
735
1074
|
form.noValidate = true;
|
|
736
1075
|
form.innerHTML = `
|
|
737
|
-
<label class="end-label" for="usero-end-note"
|
|
1076
|
+
<label class="end-label" for="usero-end-note">${escapeHtml(prompt)}</label>
|
|
738
1077
|
<textarea
|
|
739
1078
|
id="usero-end-note"
|
|
740
1079
|
class="end-textarea"
|
|
741
|
-
rows="
|
|
1080
|
+
rows="3"
|
|
742
1081
|
placeholder="Confusing bits, things you liked, what you'd change..."
|
|
743
1082
|
></textarea>
|
|
744
1083
|
<div class="end-actions">
|
|
745
1084
|
<button type="button" class="skip">Skip</button>
|
|
746
|
-
<button type="submit" class="primary">Send</button>
|
|
1085
|
+
<button type="submit" class="primary">Send feedback</button>
|
|
747
1086
|
</div>
|
|
748
1087
|
<p class="end-hint">Cmd or Ctrl plus Enter to send. Either button is fine.</p>
|
|
749
1088
|
`;
|
|
750
|
-
|
|
751
|
-
card.appendChild(
|
|
752
|
-
overlay.appendChild(card);
|
|
753
|
-
root.appendChild(overlay);
|
|
1089
|
+
section.appendChild(form);
|
|
1090
|
+
card.appendChild(section);
|
|
754
1091
|
const ta = form.querySelector("#usero-end-note");
|
|
755
1092
|
const skipBtn = form.querySelector("button.skip");
|
|
756
1093
|
if (!ta || !skipBtn) return;
|
|
757
1094
|
const swapToSent = (message) => {
|
|
758
|
-
|
|
1095
|
+
section.remove();
|
|
759
1096
|
const sent = document.createElement("p");
|
|
760
1097
|
sent.className = "end-sent";
|
|
761
1098
|
sent.textContent = message;
|
|
@@ -794,7 +1131,8 @@ function showThanksScreen(root, opts) {
|
|
|
794
1131
|
showError("Couldn't save your note. Try again?");
|
|
795
1132
|
}
|
|
796
1133
|
} else {
|
|
797
|
-
|
|
1134
|
+
opts.onSkip();
|
|
1135
|
+
swapToSent("All set. You can close this tab.");
|
|
798
1136
|
}
|
|
799
1137
|
};
|
|
800
1138
|
form.addEventListener("submit", (e) => {
|
|
@@ -840,6 +1178,31 @@ async function createSession(apiUrl, slug, testerName) {
|
|
|
840
1178
|
return null;
|
|
841
1179
|
}
|
|
842
1180
|
}
|
|
1181
|
+
async function adoptSession(apiUrl, sessionId) {
|
|
1182
|
+
try {
|
|
1183
|
+
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/adopt`, {
|
|
1184
|
+
method: "GET"
|
|
1185
|
+
});
|
|
1186
|
+
if (!res.ok) return null;
|
|
1187
|
+
const json = await res.json();
|
|
1188
|
+
if (typeof json.sessionId !== "string" || typeof json.clientId !== "string") return null;
|
|
1189
|
+
return { sessionId: json.sessionId, clientId: json.clientId, tasks: parseTasks(json.tasks) };
|
|
1190
|
+
} catch {
|
|
1191
|
+
return null;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function parsePaymentSummary(raw) {
|
|
1195
|
+
if (typeof raw !== "object" || raw === null) return null;
|
|
1196
|
+
const p = raw;
|
|
1197
|
+
if (typeof p.qualified !== "boolean") return null;
|
|
1198
|
+
return {
|
|
1199
|
+
qualified: p.qualified,
|
|
1200
|
+
reward: typeof p.reward === "string" ? p.reward : null,
|
|
1201
|
+
payoutEmail: typeof p.payoutEmail === "string" ? p.payoutEmail : null,
|
|
1202
|
+
tasksDone: typeof p.tasksDone === "number" ? p.tasksDone : 0,
|
|
1203
|
+
tasksTotal: typeof p.tasksTotal === "number" ? p.tasksTotal : 0
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
843
1206
|
async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {}) {
|
|
844
1207
|
try {
|
|
845
1208
|
const body = {
|
|
@@ -866,11 +1229,42 @@ async function finaliseSession(apiUrl, sessionId, durationSeconds, extras = {})
|
|
|
866
1229
|
body: JSON.stringify(body),
|
|
867
1230
|
keepalive: true
|
|
868
1231
|
});
|
|
869
|
-
|
|
1232
|
+
if (!res.ok) return { ok: false, payment: null };
|
|
1233
|
+
let payment = null;
|
|
1234
|
+
try {
|
|
1235
|
+
const json = await res.json();
|
|
1236
|
+
payment = parsePaymentSummary(json.payment);
|
|
1237
|
+
} catch {
|
|
1238
|
+
}
|
|
1239
|
+
return { ok: true, payment };
|
|
870
1240
|
} catch {
|
|
871
|
-
return false;
|
|
1241
|
+
return { ok: false, payment: null };
|
|
872
1242
|
}
|
|
873
1243
|
}
|
|
1244
|
+
async function postPayout(apiUrl, sessionId, destination, logger) {
|
|
1245
|
+
const url = `${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/payout`;
|
|
1246
|
+
const body = { method: "email" };
|
|
1247
|
+
if (destination) body.destination = destination;
|
|
1248
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1249
|
+
try {
|
|
1250
|
+
const res = await fetch(url, {
|
|
1251
|
+
method: "POST",
|
|
1252
|
+
headers: { "Content-Type": "application/json" },
|
|
1253
|
+
body: JSON.stringify(body),
|
|
1254
|
+
keepalive: true
|
|
1255
|
+
});
|
|
1256
|
+
if (res.ok) return true;
|
|
1257
|
+
if (res.status >= 400 && res.status < 500) {
|
|
1258
|
+
logger.warn(`payout rejected with ${res.status}`);
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
} catch (err) {
|
|
1262
|
+
logger.warn(`payout attempt ${attempt + 1} failed`, err);
|
|
1263
|
+
}
|
|
1264
|
+
await new Promise((resolve) => setTimeout(resolve, 400 + Math.floor(Math.random() * 200)));
|
|
1265
|
+
}
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
874
1268
|
async function postNoteOnce(apiUrl, sessionId, atMs, text, logger) {
|
|
875
1269
|
try {
|
|
876
1270
|
const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/user-test-sessions/${encodeURIComponent(sessionId)}/notes`, {
|
|
@@ -1037,35 +1431,51 @@ async function finishFlow(store, ctx, opts) {
|
|
|
1037
1431
|
if (store.replayOffsetAtStartMs !== null) {
|
|
1038
1432
|
replayLinkage.replayOffsetMs = store.replayOffsetAtStartMs;
|
|
1039
1433
|
}
|
|
1434
|
+
let payment = null;
|
|
1040
1435
|
if (store.sessionId) {
|
|
1041
1436
|
const unackedNotes = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
1042
|
-
const
|
|
1437
|
+
const result = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
1043
1438
|
mutedSegments: store.mutedSegments,
|
|
1044
1439
|
notes: unackedNotes,
|
|
1045
1440
|
...replayLinkage
|
|
1046
1441
|
});
|
|
1047
|
-
if (ok) {
|
|
1442
|
+
if (result.ok) {
|
|
1443
|
+
payment = result.payment;
|
|
1048
1444
|
for (const n of store.notes) {
|
|
1049
1445
|
if (!n.acked) n.acked = true;
|
|
1050
1446
|
}
|
|
1051
1447
|
}
|
|
1052
|
-
store.indicatorState = ok ? "done" : "error";
|
|
1448
|
+
store.indicatorState = result.ok ? "done" : "error";
|
|
1053
1449
|
} else {
|
|
1054
1450
|
store.indicatorState = "error";
|
|
1055
1451
|
}
|
|
1056
1452
|
renderIndicatorState(store);
|
|
1057
1453
|
if (opts.showThanks && store.indicatorRoot && store.indicatorState === "done") {
|
|
1058
1454
|
showThanksScreen(store.indicatorRoot, {
|
|
1455
|
+
payment,
|
|
1456
|
+
onPayout: async (destination) => {
|
|
1457
|
+
if (!store.sessionId) return false;
|
|
1458
|
+
return postPayout(store.options.apiUrl, store.sessionId, destination, ctx.logger);
|
|
1459
|
+
},
|
|
1460
|
+
onResume: () => {
|
|
1461
|
+
store.finishFlowRan = false;
|
|
1462
|
+
store.indicatorState = "recording";
|
|
1463
|
+
store.startedAt = Date.now();
|
|
1464
|
+
store.muted = false;
|
|
1465
|
+
store.mutedSinceMs = null;
|
|
1466
|
+
renderIndicatorState(store);
|
|
1467
|
+
void startRecording(store, ctx);
|
|
1468
|
+
},
|
|
1059
1469
|
onSubmitNote: async (text) => {
|
|
1060
1470
|
if (!store.sessionId) return;
|
|
1061
1471
|
store.endNote = text;
|
|
1062
1472
|
const stillUnacked = store.notes.filter((n) => !n.acked).map((n) => ({ atMs: n.atMs, text: n.text }));
|
|
1063
|
-
const
|
|
1473
|
+
const result = await finaliseSession(store.options.apiUrl, store.sessionId, durationSeconds, {
|
|
1064
1474
|
endNote: text,
|
|
1065
1475
|
notes: stillUnacked,
|
|
1066
1476
|
...replayLinkage
|
|
1067
1477
|
});
|
|
1068
|
-
if (!ok) throw new Error("finalise failed");
|
|
1478
|
+
if (!result.ok) throw new Error("finalise failed");
|
|
1069
1479
|
for (const n of store.notes) {
|
|
1070
1480
|
if (!n.acked) n.acked = true;
|
|
1071
1481
|
}
|
|
@@ -1206,10 +1616,11 @@ function userTest(options = {}) {
|
|
|
1206
1616
|
store.pageHideHandler = pageHide;
|
|
1207
1617
|
window.addEventListener("pagehide", pageHide);
|
|
1208
1618
|
void (async () => {
|
|
1209
|
-
const
|
|
1619
|
+
const adoptId = getAdoptSessionId();
|
|
1620
|
+
const created = adoptId ? await adoptSession(apiUrl, adoptId) : await createSession(apiUrl, slug, readTesterName(merged.testerName));
|
|
1210
1621
|
if (store.cancelled) return;
|
|
1211
1622
|
if (!created) {
|
|
1212
|
-
ctx.logger.error("failed to create user-test session");
|
|
1623
|
+
ctx.logger.error(adoptId ? "failed to adopt user-test session" : "failed to create user-test session");
|
|
1213
1624
|
store.indicatorState = "error";
|
|
1214
1625
|
renderIndicatorState(store);
|
|
1215
1626
|
return;
|