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