@mushi-mushi/web 1.0.0 → 1.1.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/CONTRIBUTING.md +27 -0
- package/dist/index.cjs +738 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +738 -15
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -935,6 +935,236 @@ function getWidgetStyles(theme) {
|
|
|
935
935
|
letter-spacing: 0.02em;
|
|
936
936
|
}
|
|
937
937
|
|
|
938
|
+
/* \u2500\u2500 Rewards nudge (category step) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
939
|
+
.mushi-rewards-nudge {
|
|
940
|
+
border-top: 1px solid ${rule};
|
|
941
|
+
padding: 10px 0 4px;
|
|
942
|
+
margin-top: 6px;
|
|
943
|
+
}
|
|
944
|
+
.mushi-rewards-row {
|
|
945
|
+
display: flex;
|
|
946
|
+
align-items: center;
|
|
947
|
+
gap: 6px;
|
|
948
|
+
margin-bottom: 8px;
|
|
949
|
+
}
|
|
950
|
+
.mushi-tier-pip {
|
|
951
|
+
width: 7px;
|
|
952
|
+
height: 7px;
|
|
953
|
+
border-radius: 50%;
|
|
954
|
+
flex-shrink: 0;
|
|
955
|
+
}
|
|
956
|
+
.mushi-rewards-tier-name {
|
|
957
|
+
font-family: ${fontMono};
|
|
958
|
+
font-size: 11px;
|
|
959
|
+
letter-spacing: 0.08em;
|
|
960
|
+
text-transform: uppercase;
|
|
961
|
+
color: ${ink};
|
|
962
|
+
}
|
|
963
|
+
.mushi-rewards-pts-count {
|
|
964
|
+
font-family: ${fontMono};
|
|
965
|
+
font-size: 11px;
|
|
966
|
+
color: ${inkMuted};
|
|
967
|
+
margin-right: auto;
|
|
968
|
+
}
|
|
969
|
+
.mushi-rewards-pts-earn {
|
|
970
|
+
font-family: ${fontMono};
|
|
971
|
+
font-size: 10px;
|
|
972
|
+
color: ${vermillion};
|
|
973
|
+
letter-spacing: 0.04em;
|
|
974
|
+
white-space: nowrap;
|
|
975
|
+
}
|
|
976
|
+
.mushi-tier-bar-track {
|
|
977
|
+
height: 3px;
|
|
978
|
+
background: ${ruleStrong};
|
|
979
|
+
border-radius: 2px;
|
|
980
|
+
overflow: hidden;
|
|
981
|
+
margin-bottom: 5px;
|
|
982
|
+
}
|
|
983
|
+
.mushi-tier-bar-fill {
|
|
984
|
+
height: 100%;
|
|
985
|
+
background: ${vermillion};
|
|
986
|
+
border-radius: 2px;
|
|
987
|
+
transition: width 600ms ${easeStamp};
|
|
988
|
+
}
|
|
989
|
+
.mushi-rewards-next-label {
|
|
990
|
+
font-family: ${fontMono};
|
|
991
|
+
font-size: 10px;
|
|
992
|
+
color: ${inkMuted};
|
|
993
|
+
text-align: right;
|
|
994
|
+
letter-spacing: 0.02em;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/* \u2500\u2500 Rewards on success step \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
998
|
+
.mushi-success-rewards {
|
|
999
|
+
margin-top: 14px;
|
|
1000
|
+
padding-top: 12px;
|
|
1001
|
+
border-top: 1px solid ${rule};
|
|
1002
|
+
width: 100%;
|
|
1003
|
+
}
|
|
1004
|
+
.mushi-success-pts-award {
|
|
1005
|
+
font-family: ${fontMono};
|
|
1006
|
+
font-size: 22px;
|
|
1007
|
+
font-weight: 700;
|
|
1008
|
+
color: ${vermillion};
|
|
1009
|
+
text-align: center;
|
|
1010
|
+
letter-spacing: 0.06em;
|
|
1011
|
+
margin-bottom: 10px;
|
|
1012
|
+
opacity: 0;
|
|
1013
|
+
animation: mushi-pts-pop 420ms ${easeStamp} 900ms forwards;
|
|
1014
|
+
}
|
|
1015
|
+
.success-bar { margin: 0 0 5px; }
|
|
1016
|
+
|
|
1017
|
+
@keyframes mushi-pts-pop {
|
|
1018
|
+
from { opacity: 0; transform: scale(0.75) translateY(6px); }
|
|
1019
|
+
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
/* \u2500\u2500\u2500 Beta mode strip (category step) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1023
|
+
|
|
1024
|
+
.mushi-beta-strip {
|
|
1025
|
+
margin: 0 16px 2px;
|
|
1026
|
+
padding: 9px 12px;
|
|
1027
|
+
background: rgba(99, 102, 241, 0.07);
|
|
1028
|
+
border: 1px solid rgba(99, 102, 241, 0.18);
|
|
1029
|
+
border-radius: 8px;
|
|
1030
|
+
display: flex;
|
|
1031
|
+
flex-direction: column;
|
|
1032
|
+
gap: 4px;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.mushi-beta-strip-row {
|
|
1036
|
+
display: flex;
|
|
1037
|
+
align-items: center;
|
|
1038
|
+
gap: 8px;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.mushi-beta-tag {
|
|
1042
|
+
display: inline-flex;
|
|
1043
|
+
align-items: center;
|
|
1044
|
+
padding: 1px 6px;
|
|
1045
|
+
border-radius: 4px;
|
|
1046
|
+
background: rgba(245, 158, 11, 0.15);
|
|
1047
|
+
border: 1px solid rgba(245, 158, 11, 0.35);
|
|
1048
|
+
color: #b45309;
|
|
1049
|
+
font-family: var(--mushi-font-mono);
|
|
1050
|
+
font-size: 9px;
|
|
1051
|
+
font-weight: 700;
|
|
1052
|
+
letter-spacing: 0.08em;
|
|
1053
|
+
line-height: 1.6;
|
|
1054
|
+
white-space: nowrap;
|
|
1055
|
+
flex-shrink: 0;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
@media (prefers-color-scheme: dark) {
|
|
1059
|
+
.mushi-beta-tag {
|
|
1060
|
+
background: rgba(245, 158, 11, 0.12);
|
|
1061
|
+
border-color: rgba(245, 158, 11, 0.28);
|
|
1062
|
+
color: #fbbf24;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.mushi-beta-msg {
|
|
1067
|
+
font-size: 11px;
|
|
1068
|
+
color: var(--mushi-text-dim);
|
|
1069
|
+
line-height: 1.45;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.mushi-beta-contact-hint {
|
|
1073
|
+
font-size: 10px;
|
|
1074
|
+
color: var(--mushi-text-dim);
|
|
1075
|
+
opacity: 0.72;
|
|
1076
|
+
font-family: var(--mushi-font-mono);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.mushi-beta-perks {
|
|
1080
|
+
list-style: none;
|
|
1081
|
+
margin: 2px 0 0;
|
|
1082
|
+
padding: 0;
|
|
1083
|
+
display: flex;
|
|
1084
|
+
flex-direction: column;
|
|
1085
|
+
gap: 2px;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.mushi-beta-perks li {
|
|
1089
|
+
font-size: 10.5px;
|
|
1090
|
+
color: #4f46e5;
|
|
1091
|
+
font-weight: 500;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
@media (prefers-color-scheme: dark) {
|
|
1095
|
+
.mushi-beta-perks li {
|
|
1096
|
+
color: #818cf8;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/* \u2500\u2500\u2500 Beta changelog (collapsible What's new) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1101
|
+
|
|
1102
|
+
.mushi-changelog {
|
|
1103
|
+
margin-top: 5px;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.mushi-changelog-summary {
|
|
1107
|
+
font-size: 10.5px;
|
|
1108
|
+
color: var(--mushi-text-dim);
|
|
1109
|
+
cursor: pointer;
|
|
1110
|
+
list-style: none;
|
|
1111
|
+
display: flex;
|
|
1112
|
+
align-items: center;
|
|
1113
|
+
gap: 4px;
|
|
1114
|
+
user-select: none;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
.mushi-changelog-summary::before {
|
|
1118
|
+
content: '\u25B6';
|
|
1119
|
+
font-size: 7px;
|
|
1120
|
+
opacity: 0.6;
|
|
1121
|
+
transition: transform 0.15s ease;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.mushi-changelog[open] .mushi-changelog-summary::before {
|
|
1125
|
+
transform: rotate(90deg);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
.mushi-changelog-list {
|
|
1129
|
+
margin: 5px 0 0 4px;
|
|
1130
|
+
padding: 0;
|
|
1131
|
+
list-style: none;
|
|
1132
|
+
display: flex;
|
|
1133
|
+
flex-direction: column;
|
|
1134
|
+
gap: 2px;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
.mushi-changelog-list li {
|
|
1138
|
+
font-size: 10.5px;
|
|
1139
|
+
color: var(--mushi-text-dim);
|
|
1140
|
+
line-height: 1.5;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/* \u2500\u2500\u2500 Beta success footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
1144
|
+
|
|
1145
|
+
.mushi-beta-success-footer {
|
|
1146
|
+
margin-top: 14px;
|
|
1147
|
+
padding: 10px 14px;
|
|
1148
|
+
background: rgba(99, 102, 241, 0.06);
|
|
1149
|
+
border: 1px solid rgba(99, 102, 241, 0.14);
|
|
1150
|
+
border-radius: 8px;
|
|
1151
|
+
display: flex;
|
|
1152
|
+
flex-direction: column;
|
|
1153
|
+
gap: 3px;
|
|
1154
|
+
text-align: left;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.mushi-beta-success-line {
|
|
1158
|
+
font-size: 11px;
|
|
1159
|
+
color: var(--mushi-text-dim);
|
|
1160
|
+
line-height: 1.5;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.mushi-beta-success-dim {
|
|
1164
|
+
opacity: 0.65;
|
|
1165
|
+
font-size: 10.5px;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
938
1168
|
@media (prefers-reduced-motion: reduce) {
|
|
939
1169
|
*,
|
|
940
1170
|
*::before,
|
|
@@ -945,6 +1175,7 @@ function getWidgetStyles(theme) {
|
|
|
945
1175
|
}
|
|
946
1176
|
.mushi-success-stamp circle { stroke-dashoffset: 0; }
|
|
947
1177
|
.mushi-success-stamp-label { opacity: 1; }
|
|
1178
|
+
.mushi-success-pts-award { opacity: 1; }
|
|
948
1179
|
}
|
|
949
1180
|
`;
|
|
950
1181
|
}
|
|
@@ -1000,7 +1231,8 @@ var MushiWidget = class {
|
|
|
1000
1231
|
smartHide: config.smartHide ?? false,
|
|
1001
1232
|
draggable: config.draggable ?? false,
|
|
1002
1233
|
brandFooter: config.brandFooter ?? true,
|
|
1003
|
-
outdatedBanner: config.outdatedBanner ?? "auto"
|
|
1234
|
+
outdatedBanner: config.outdatedBanner ?? "auto",
|
|
1235
|
+
betaMode: config.betaMode ?? {}
|
|
1004
1236
|
};
|
|
1005
1237
|
this.callbacks = callbacks;
|
|
1006
1238
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
@@ -1044,6 +1276,7 @@ var MushiWidget = class {
|
|
|
1044
1276
|
* root) for up to ~3.3s after destroy. */
|
|
1045
1277
|
successTimer = null;
|
|
1046
1278
|
autoCloseTimer = null;
|
|
1279
|
+
rewardsState = null;
|
|
1047
1280
|
mount() {
|
|
1048
1281
|
if (this.host.isConnected) return;
|
|
1049
1282
|
document.body.appendChild(this.host);
|
|
@@ -1075,7 +1308,8 @@ var MushiWidget = class {
|
|
|
1075
1308
|
...config.smartHide !== void 0 ? { smartHide: config.smartHide } : {},
|
|
1076
1309
|
...config.draggable !== void 0 ? { draggable: config.draggable } : {},
|
|
1077
1310
|
...config.brandFooter !== void 0 ? { brandFooter: config.brandFooter } : {},
|
|
1078
|
-
...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {}
|
|
1311
|
+
...config.outdatedBanner !== void 0 ? { outdatedBanner: config.outdatedBanner } : {},
|
|
1312
|
+
...config.betaMode !== void 0 ? { betaMode: config.betaMode } : {}
|
|
1079
1313
|
};
|
|
1080
1314
|
this.locale = getLocale(this.config.locale === "auto" ? void 0 : this.config.locale);
|
|
1081
1315
|
this.syncAttachedLaunchers();
|
|
@@ -1150,6 +1384,10 @@ var MushiWidget = class {
|
|
|
1150
1384
|
this.sdkFreshness = info;
|
|
1151
1385
|
if (this.isOpen) this.render();
|
|
1152
1386
|
}
|
|
1387
|
+
setRewardsState(state) {
|
|
1388
|
+
this.rewardsState = state;
|
|
1389
|
+
if (this.isOpen) this.render();
|
|
1390
|
+
}
|
|
1153
1391
|
destroy() {
|
|
1154
1392
|
if (this.successTimer !== null) {
|
|
1155
1393
|
clearTimeout(this.successTimer);
|
|
@@ -1394,6 +1632,7 @@ var MushiWidget = class {
|
|
|
1394
1632
|
`).join("");
|
|
1395
1633
|
return `
|
|
1396
1634
|
${this.renderHeader({ title: t.step1.heading, step: STEP_NUMBER.category })}
|
|
1635
|
+
${this.config.betaMode?.enabled ? this.renderBetaStrip() : ""}
|
|
1397
1636
|
<div class="mushi-body" role="radiogroup" aria-label="${t.step1.heading}">
|
|
1398
1637
|
<button type="button" class="mushi-option-btn mushi-reports-entry" data-action="reports">
|
|
1399
1638
|
<span class="mushi-option-icon" aria-hidden="true">\u{1F4EC}</span>
|
|
@@ -1404,10 +1643,52 @@ var MushiWidget = class {
|
|
|
1404
1643
|
<span class="mushi-option-arrow" aria-hidden="true">\u2192</span>
|
|
1405
1644
|
</button>
|
|
1406
1645
|
${categories}
|
|
1646
|
+
${this.rewardsState ? this.renderRewardsNudge() : ""}
|
|
1407
1647
|
</div>
|
|
1408
1648
|
${this.renderStepIndicator(STEP_NUMBER.category)}
|
|
1409
1649
|
`;
|
|
1410
1650
|
}
|
|
1651
|
+
/** Collapsible "What's new" changelog row. Closes the reporter feedback loop. */
|
|
1652
|
+
renderBetaChangelog() {
|
|
1653
|
+
const entries = this.config.betaMode?.changelogItems;
|
|
1654
|
+
if (!entries?.length) return "";
|
|
1655
|
+
const latest = entries[0];
|
|
1656
|
+
const items = latest.items.map((item) => `<li>\u2022 ${escapeHtml(item)}</li>`).join("");
|
|
1657
|
+
const label = latest.date ? `What\u2019s new in ${escapeHtml(latest.version)} \xB7 ${escapeHtml(latest.date)}` : `What\u2019s new in ${escapeHtml(latest.version)}`;
|
|
1658
|
+
return `
|
|
1659
|
+
<details class="mushi-changelog">
|
|
1660
|
+
<summary class="mushi-changelog-summary">${label}</summary>
|
|
1661
|
+
<ul class="mushi-changelog-list">${items}</ul>
|
|
1662
|
+
</details>
|
|
1663
|
+
`;
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* Discreet beta status strip: communicates "work in progress", invites
|
|
1667
|
+
* feedback, and sets expectations — reducing user frustration while
|
|
1668
|
+
* nudging the reciprocity instinct ("your reports help us build this").
|
|
1669
|
+
*/
|
|
1670
|
+
renderBetaStrip() {
|
|
1671
|
+
const beta = this.config.betaMode;
|
|
1672
|
+
const appName = escapeHtml(beta.appName ?? "This app");
|
|
1673
|
+
const message = beta.message ? escapeHtml(beta.message) : `${appName} is in early development \u2014 updates ship weekly`;
|
|
1674
|
+
const email = beta.contactEmail ? escapeHtml(beta.contactEmail) : null;
|
|
1675
|
+
const perks = beta.perks ?? [];
|
|
1676
|
+
return `
|
|
1677
|
+
<div class="mushi-beta-strip" role="note" aria-label="Beta status">
|
|
1678
|
+
<div class="mushi-beta-strip-row">
|
|
1679
|
+
<span class="mushi-beta-tag" aria-hidden="true">BETA</span>
|
|
1680
|
+
<span class="mushi-beta-msg">${message}</span>
|
|
1681
|
+
</div>
|
|
1682
|
+
${email ? `<div class="mushi-beta-contact-hint">Reports go to ${email} \xB7 reviewed by the team</div>` : ""}
|
|
1683
|
+
${perks.length > 0 ? `
|
|
1684
|
+
<ul class="mushi-beta-perks" aria-label="Beta tester perks">
|
|
1685
|
+
${perks.map((p) => `<li>\u2713 ${escapeHtml(p)}</li>`).join("")}
|
|
1686
|
+
</ul>
|
|
1687
|
+
` : ""}
|
|
1688
|
+
${this.renderBetaChangelog()}
|
|
1689
|
+
</div>
|
|
1690
|
+
`;
|
|
1691
|
+
}
|
|
1411
1692
|
renderReportsStep() {
|
|
1412
1693
|
const reports = this.reporterReports.map((report) => `
|
|
1413
1694
|
<button type="button" class="mushi-report-row" data-report-id="${escapeHtml(report.id)}">
|
|
@@ -1528,7 +1809,92 @@ var MushiWidget = class {
|
|
|
1528
1809
|
</div>
|
|
1529
1810
|
<div class="mushi-success-headline">${t.widget.submitted}</div>
|
|
1530
1811
|
<div class="mushi-success-meta">REPORT \xB7 ${time}</div>
|
|
1812
|
+
${this.rewardsState ? this.renderSuccessRewards() : ""}
|
|
1813
|
+
${this.config.betaMode?.enabled ? this.renderBetaSuccessFooter() : ""}
|
|
1814
|
+
</div>
|
|
1815
|
+
</div>
|
|
1816
|
+
`;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Reciprocity footer on the success step: closes the feedback loop by
|
|
1820
|
+
* attributing where the report goes, sets a response expectation, and
|
|
1821
|
+
* reinforces the "beta tester" identity (Peak-End Rule — the last thing
|
|
1822
|
+
* the user sees shapes their entire impression of the interaction).
|
|
1823
|
+
*/
|
|
1824
|
+
renderBetaSuccessFooter() {
|
|
1825
|
+
const beta = this.config.betaMode;
|
|
1826
|
+
const email = beta.contactEmail ? escapeHtml(beta.contactEmail) : null;
|
|
1827
|
+
const appName = escapeHtml(beta.appName ?? "the team");
|
|
1828
|
+
return `
|
|
1829
|
+
<div class="mushi-beta-success-footer" role="note" aria-label="Beta feedback acknowledgement">
|
|
1830
|
+
${email ? `<div class="mushi-beta-success-line">\u{1F4EC} Sent to ${email}</div>` : `<div class="mushi-beta-success-line">\u{1F4EC} Sent to ${appName}</div>`}
|
|
1831
|
+
<div class="mushi-beta-success-line mushi-beta-success-dim">We aim to review within 48h \xB7 thank you for helping build this</div>
|
|
1832
|
+
</div>
|
|
1833
|
+
`;
|
|
1834
|
+
}
|
|
1835
|
+
tierColor(slug) {
|
|
1836
|
+
const colors = {
|
|
1837
|
+
free: "#6b7280",
|
|
1838
|
+
explorer: "#3b82f6",
|
|
1839
|
+
contributor: "#8b5cf6",
|
|
1840
|
+
champion: "#f59e0b"
|
|
1841
|
+
};
|
|
1842
|
+
return colors[slug] ?? "#6c47ff";
|
|
1843
|
+
}
|
|
1844
|
+
/** Compact rewards nudge rendered at the bottom of the category-step body. */
|
|
1845
|
+
renderRewardsNudge() {
|
|
1846
|
+
const { tier, nextTier, totalPoints, pointsForReport } = this.rewardsState;
|
|
1847
|
+
const tierName = tier?.displayName ?? "Free";
|
|
1848
|
+
const tierSlug = tier?.slug ?? "free";
|
|
1849
|
+
const color = this.tierColor(tierSlug);
|
|
1850
|
+
let pct = 100;
|
|
1851
|
+
let nextLabel = "";
|
|
1852
|
+
if (nextTier) {
|
|
1853
|
+
const base = tier?.pointsThreshold ?? 0;
|
|
1854
|
+
const ceiling = nextTier.pointsThreshold;
|
|
1855
|
+
pct = ceiling > base ? Math.round(Math.min(1, (totalPoints - base) / (ceiling - base)) * 100) : 100;
|
|
1856
|
+
const remaining = Math.max(0, ceiling - totalPoints);
|
|
1857
|
+
nextLabel = `${remaining.toLocaleString()} pts to ${escapeHtml(nextTier.displayName)}`;
|
|
1858
|
+
}
|
|
1859
|
+
return `
|
|
1860
|
+
<div class="mushi-rewards-nudge" aria-label="Rewards progress">
|
|
1861
|
+
<div class="mushi-rewards-row">
|
|
1862
|
+
<span class="mushi-tier-pip" style="background:${color}" aria-hidden="true"></span>
|
|
1863
|
+
<span class="mushi-rewards-tier-name">${escapeHtml(tierName)}</span>
|
|
1864
|
+
<span class="mushi-rewards-pts-count">${totalPoints.toLocaleString()} pts</span>
|
|
1865
|
+
<span class="mushi-rewards-pts-earn">+${pointsForReport} pts for a report</span>
|
|
1531
1866
|
</div>
|
|
1867
|
+
${nextTier ? `
|
|
1868
|
+
<div class="mushi-tier-bar-track" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100" aria-label="Progress to ${escapeHtml(nextTier.displayName)}">
|
|
1869
|
+
<div class="mushi-tier-bar-fill" style="width:${pct}%"></div>
|
|
1870
|
+
</div>
|
|
1871
|
+
<div class="mushi-rewards-next-label">${nextLabel}</div>
|
|
1872
|
+
` : ""}
|
|
1873
|
+
</div>
|
|
1874
|
+
`;
|
|
1875
|
+
}
|
|
1876
|
+
/** Points earned + tier progress shown on the success step. */
|
|
1877
|
+
renderSuccessRewards() {
|
|
1878
|
+
const { tier, nextTier, totalPoints, pointsForReport } = this.rewardsState;
|
|
1879
|
+
const projected = totalPoints + pointsForReport;
|
|
1880
|
+
let pctAfter = 100;
|
|
1881
|
+
let nextLabel = "";
|
|
1882
|
+
if (nextTier) {
|
|
1883
|
+
const base = tier?.pointsThreshold ?? 0;
|
|
1884
|
+
const ceiling = nextTier.pointsThreshold;
|
|
1885
|
+
pctAfter = ceiling > base ? Math.round(Math.min(1, (projected - base) / (ceiling - base)) * 100) : 100;
|
|
1886
|
+
const remaining = Math.max(0, ceiling - projected);
|
|
1887
|
+
nextLabel = remaining > 0 ? `${remaining.toLocaleString()} pts to ${escapeHtml(nextTier.displayName)}` : `\u{1F389} ${escapeHtml(nextTier.displayName)} reached!`;
|
|
1888
|
+
}
|
|
1889
|
+
return `
|
|
1890
|
+
<div class="mushi-success-rewards">
|
|
1891
|
+
<div class="mushi-success-pts-award">+${pointsForReport} pts</div>
|
|
1892
|
+
${nextTier ? `
|
|
1893
|
+
<div class="mushi-tier-bar-track success-bar" role="progressbar" aria-valuenow="${pctAfter}" aria-valuemin="0" aria-valuemax="100" aria-label="Progress to ${escapeHtml(nextTier.displayName)}">
|
|
1894
|
+
<div class="mushi-tier-bar-fill" style="width:${pctAfter}%"></div>
|
|
1895
|
+
</div>
|
|
1896
|
+
<div class="mushi-rewards-next-label">${nextLabel}</div>
|
|
1897
|
+
` : ""}
|
|
1532
1898
|
</div>
|
|
1533
1899
|
`;
|
|
1534
1900
|
}
|
|
@@ -1692,6 +2058,304 @@ var MushiWidget = class {
|
|
|
1692
2058
|
}
|
|
1693
2059
|
};
|
|
1694
2060
|
|
|
2061
|
+
// src/rewards.ts
|
|
2062
|
+
var MIN_FLUSH_INTERVAL = 3e4;
|
|
2063
|
+
var DEFAULT_FLUSH_INTERVAL = 3e5;
|
|
2064
|
+
var DWELL_SAMPLE_INTERVAL = 6e4;
|
|
2065
|
+
var MAX_SESSION_MINUTES_PER_DAY = 60;
|
|
2066
|
+
var DAILY_RESET_KEY_PREFIX = "mushi_session_min_day_";
|
|
2067
|
+
var pendingEvents = [];
|
|
2068
|
+
var flushTimer = null;
|
|
2069
|
+
var dwellTimer = null;
|
|
2070
|
+
var currentUserId = null;
|
|
2071
|
+
var currentUserTraits = null;
|
|
2072
|
+
var reporterTokenHash = null;
|
|
2073
|
+
var apiClient = null;
|
|
2074
|
+
var optedIn = false;
|
|
2075
|
+
var tierCache = null;
|
|
2076
|
+
var tierCacheTime = 0;
|
|
2077
|
+
var TIER_CACHE_TTL = 5 * 60 * 1e3;
|
|
2078
|
+
var seenRoutes = /* @__PURE__ */ new Set();
|
|
2079
|
+
function getConsentKey(projectId) {
|
|
2080
|
+
return `mushi_rewards_consent_${projectId}`;
|
|
2081
|
+
}
|
|
2082
|
+
function isConsentGranted(projectId) {
|
|
2083
|
+
try {
|
|
2084
|
+
return localStorage.getItem(getConsentKey(projectId)) === "1";
|
|
2085
|
+
} catch {
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function setConsentGranted(projectId, granted) {
|
|
2090
|
+
try {
|
|
2091
|
+
if (granted) {
|
|
2092
|
+
localStorage.setItem(getConsentKey(projectId), "1");
|
|
2093
|
+
} else {
|
|
2094
|
+
localStorage.removeItem(getConsentKey(projectId));
|
|
2095
|
+
}
|
|
2096
|
+
optedIn = granted;
|
|
2097
|
+
apiClient?.submitActivity(currentUserId ?? "", [], { optedIn: granted }).catch(() => {
|
|
2098
|
+
});
|
|
2099
|
+
} catch {
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
function getTodayKey() {
|
|
2103
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2104
|
+
}
|
|
2105
|
+
function getSessionMinutesToday(projectId) {
|
|
2106
|
+
try {
|
|
2107
|
+
const val = sessionStorage.getItem(`${DAILY_RESET_KEY_PREFIX}${projectId}_${getTodayKey()}`);
|
|
2108
|
+
return val ? parseInt(val, 10) : 0;
|
|
2109
|
+
} catch {
|
|
2110
|
+
return 0;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
function incrementSessionMinutes(projectId) {
|
|
2114
|
+
const today = getTodayKey();
|
|
2115
|
+
const key = `${DAILY_RESET_KEY_PREFIX}${projectId}_${today}`;
|
|
2116
|
+
try {
|
|
2117
|
+
const next = getSessionMinutesToday(projectId) + 1;
|
|
2118
|
+
sessionStorage.setItem(key, String(next));
|
|
2119
|
+
return next;
|
|
2120
|
+
} catch {
|
|
2121
|
+
return 99;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
function initRewards(ctx) {
|
|
2125
|
+
apiClient = ctx.client;
|
|
2126
|
+
currentUserId = ctx.userId;
|
|
2127
|
+
currentUserTraits = ctx.traits ?? null;
|
|
2128
|
+
reporterTokenHash = ctx.reporterToken ?? null;
|
|
2129
|
+
const { projectId } = ctx;
|
|
2130
|
+
const flushMs = Math.max(
|
|
2131
|
+
MIN_FLUSH_INTERVAL,
|
|
2132
|
+
ctx.config.flushIntervalMs ?? DEFAULT_FLUSH_INTERVAL
|
|
2133
|
+
);
|
|
2134
|
+
if (ctx.config.consentMode === "auto") {
|
|
2135
|
+
optedIn = true;
|
|
2136
|
+
setConsentGranted(projectId, true);
|
|
2137
|
+
} else {
|
|
2138
|
+
optedIn = isConsentGranted(projectId);
|
|
2139
|
+
}
|
|
2140
|
+
if (ctx.config.trackActivity) {
|
|
2141
|
+
installActivityListeners(projectId);
|
|
2142
|
+
}
|
|
2143
|
+
if (flushTimer) clearInterval(flushTimer);
|
|
2144
|
+
flushTimer = setInterval(() => flush(ctx), flushMs);
|
|
2145
|
+
if (dwellTimer) clearInterval(dwellTimer);
|
|
2146
|
+
dwellTimer = setInterval(() => {
|
|
2147
|
+
if (!optedIn || !currentUserId) return;
|
|
2148
|
+
const minutes = getSessionMinutesToday(projectId);
|
|
2149
|
+
if (minutes < MAX_SESSION_MINUTES_PER_DAY) {
|
|
2150
|
+
incrementSessionMinutes(projectId);
|
|
2151
|
+
enqueue({ action: "session_minute", metadata: { minutes_today: minutes + 1 } });
|
|
2152
|
+
}
|
|
2153
|
+
}, DWELL_SAMPLE_INTERVAL);
|
|
2154
|
+
if (ctx.config.showInWidget) {
|
|
2155
|
+
fetchAndCacheTier(currentUserId).then((tier) => {
|
|
2156
|
+
if (tier) renderTierBadge(tier, ctx.config);
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
if (ctx.config.consentMode !== "auto" && !optedIn) {
|
|
2160
|
+
renderConsentBanner(projectId, ctx.config);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function updateRewardsUser(userId, traits) {
|
|
2164
|
+
currentUserId = userId;
|
|
2165
|
+
currentUserTraits = traits ?? null;
|
|
2166
|
+
tierCache = null;
|
|
2167
|
+
tierCacheTime = 0;
|
|
2168
|
+
}
|
|
2169
|
+
function enqueue(event) {
|
|
2170
|
+
if (!optedIn || !currentUserId) return;
|
|
2171
|
+
pendingEvents.push({ ...event, queuedAt: Date.now() });
|
|
2172
|
+
}
|
|
2173
|
+
async function flush(ctx) {
|
|
2174
|
+
if (!optedIn || !currentUserId || pendingEvents.length === 0) return;
|
|
2175
|
+
let hostJwt = null;
|
|
2176
|
+
if (ctx.config.verifyUserToken) {
|
|
2177
|
+
try {
|
|
2178
|
+
hostJwt = await ctx.config.verifyUserToken();
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
const batch = pendingEvents.splice(0, 100);
|
|
2183
|
+
try {
|
|
2184
|
+
await ctx.client.submitActivity(currentUserId, batch, {
|
|
2185
|
+
userTraits: currentUserTraits ?? void 0,
|
|
2186
|
+
reporterTokenHash: reporterTokenHash ?? void 0,
|
|
2187
|
+
optedIn: true,
|
|
2188
|
+
hostJwt: hostJwt ?? void 0
|
|
2189
|
+
});
|
|
2190
|
+
} catch {
|
|
2191
|
+
pendingEvents.unshift(...batch.slice(0, 50));
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
async function getTier(userId) {
|
|
2195
|
+
const now = Date.now();
|
|
2196
|
+
if (tierCache && now - tierCacheTime < TIER_CACHE_TTL) return tierCache;
|
|
2197
|
+
return fetchAndCacheTier(userId);
|
|
2198
|
+
}
|
|
2199
|
+
async function fetchAndCacheTier(userId) {
|
|
2200
|
+
if (!apiClient) return null;
|
|
2201
|
+
const res = await apiClient.getMyTier(userId);
|
|
2202
|
+
if (res.ok && res.data) {
|
|
2203
|
+
tierCache = res.data;
|
|
2204
|
+
tierCacheTime = Date.now();
|
|
2205
|
+
return tierCache;
|
|
2206
|
+
}
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
var routeObserver = null;
|
|
2210
|
+
var clickHandler = null;
|
|
2211
|
+
var origPushState = null;
|
|
2212
|
+
var lastRoute = "";
|
|
2213
|
+
function installActivityListeners(projectId) {
|
|
2214
|
+
const emitRoute = () => {
|
|
2215
|
+
const route = location.pathname;
|
|
2216
|
+
if (route === lastRoute) return;
|
|
2217
|
+
lastRoute = route;
|
|
2218
|
+
const isNewToday = !seenRoutes.has(`${projectId}:${route}`);
|
|
2219
|
+
if (isNewToday) {
|
|
2220
|
+
seenRoutes.add(`${projectId}:${route}`);
|
|
2221
|
+
enqueue({ action: "screen_view_unique_per_day", metadata: { route } });
|
|
2222
|
+
}
|
|
2223
|
+
};
|
|
2224
|
+
origPushState = history.pushState.bind(history);
|
|
2225
|
+
history.pushState = function(...args) {
|
|
2226
|
+
origPushState(...args);
|
|
2227
|
+
emitRoute();
|
|
2228
|
+
};
|
|
2229
|
+
window.addEventListener("popstate", emitRoute);
|
|
2230
|
+
emitRoute();
|
|
2231
|
+
routeObserver = new MutationObserver(() => emitRoute());
|
|
2232
|
+
const main = document.querySelector("main") ?? document.body;
|
|
2233
|
+
routeObserver.observe(main, { childList: true, subtree: false });
|
|
2234
|
+
clickHandler = (e) => {
|
|
2235
|
+
const target = e.target.closest("[data-testid]");
|
|
2236
|
+
if (!target) return;
|
|
2237
|
+
const testid = target.dataset.testid;
|
|
2238
|
+
if (!testid) return;
|
|
2239
|
+
enqueue({ action: "element_selected", metadata: { testid, route: location.pathname } });
|
|
2240
|
+
};
|
|
2241
|
+
document.addEventListener("click", clickHandler, { capture: true, passive: true });
|
|
2242
|
+
}
|
|
2243
|
+
var badgeHost = null;
|
|
2244
|
+
function renderTierBadge(tier, config) {
|
|
2245
|
+
if (!config.showInWidget) return;
|
|
2246
|
+
if (badgeHost) badgeHost.remove();
|
|
2247
|
+
badgeHost = document.createElement("div");
|
|
2248
|
+
badgeHost.id = "mushi-tier-badge";
|
|
2249
|
+
Object.assign(badgeHost.style, {
|
|
2250
|
+
position: "fixed",
|
|
2251
|
+
bottom: "56px",
|
|
2252
|
+
// above the widget button
|
|
2253
|
+
right: "16px",
|
|
2254
|
+
zIndex: "2147483645",
|
|
2255
|
+
fontFamily: "system-ui, sans-serif"
|
|
2256
|
+
});
|
|
2257
|
+
const shadow = badgeHost.attachShadow({ mode: "closed" });
|
|
2258
|
+
shadow.innerHTML = `
|
|
2259
|
+
<style>
|
|
2260
|
+
:host { display: block; }
|
|
2261
|
+
.badge {
|
|
2262
|
+
display: inline-flex;
|
|
2263
|
+
align-items: center;
|
|
2264
|
+
gap: 6px;
|
|
2265
|
+
padding: 4px 10px;
|
|
2266
|
+
border-radius: 999px;
|
|
2267
|
+
background: rgba(0,0,0,0.75);
|
|
2268
|
+
color: #fff;
|
|
2269
|
+
font-size: 11px;
|
|
2270
|
+
font-weight: 600;
|
|
2271
|
+
letter-spacing: 0.02em;
|
|
2272
|
+
backdrop-filter: blur(6px);
|
|
2273
|
+
cursor: default;
|
|
2274
|
+
user-select: none;
|
|
2275
|
+
}
|
|
2276
|
+
.dot {
|
|
2277
|
+
width: 6px;
|
|
2278
|
+
height: 6px;
|
|
2279
|
+
border-radius: 50%;
|
|
2280
|
+
background: #6c47ff;
|
|
2281
|
+
flex-shrink: 0;
|
|
2282
|
+
}
|
|
2283
|
+
</style>
|
|
2284
|
+
<div class="badge">
|
|
2285
|
+
<span class="dot"></span>
|
|
2286
|
+
<span>${tier.displayName}</span>
|
|
2287
|
+
</div>
|
|
2288
|
+
`;
|
|
2289
|
+
document.body.appendChild(badgeHost);
|
|
2290
|
+
}
|
|
2291
|
+
var consentHost = null;
|
|
2292
|
+
function renderConsentBanner(projectId, config) {
|
|
2293
|
+
if (consentHost) return;
|
|
2294
|
+
consentHost = document.createElement("div");
|
|
2295
|
+
consentHost.id = "mushi-consent-banner";
|
|
2296
|
+
Object.assign(consentHost.style, {
|
|
2297
|
+
position: "fixed",
|
|
2298
|
+
bottom: "80px",
|
|
2299
|
+
right: "16px",
|
|
2300
|
+
zIndex: "2147483646",
|
|
2301
|
+
maxWidth: "280px",
|
|
2302
|
+
fontFamily: "system-ui, sans-serif"
|
|
2303
|
+
});
|
|
2304
|
+
const shadow = consentHost.attachShadow({ mode: "closed" });
|
|
2305
|
+
shadow.innerHTML = `
|
|
2306
|
+
<style>
|
|
2307
|
+
:host { display: block; }
|
|
2308
|
+
.banner {
|
|
2309
|
+
background: #fff;
|
|
2310
|
+
border: 1px solid #e5e7eb;
|
|
2311
|
+
border-radius: 12px;
|
|
2312
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
|
|
2313
|
+
padding: 14px 16px;
|
|
2314
|
+
font-size: 13px;
|
|
2315
|
+
line-height: 1.5;
|
|
2316
|
+
color: #374151;
|
|
2317
|
+
}
|
|
2318
|
+
.title { font-weight: 700; margin-bottom: 6px; color: #111827; }
|
|
2319
|
+
.actions { display: flex; gap: 8px; margin-top: 10px; }
|
|
2320
|
+
button {
|
|
2321
|
+
flex: 1;
|
|
2322
|
+
padding: 6px 10px;
|
|
2323
|
+
border-radius: 6px;
|
|
2324
|
+
border: none;
|
|
2325
|
+
cursor: pointer;
|
|
2326
|
+
font-size: 12px;
|
|
2327
|
+
font-weight: 600;
|
|
2328
|
+
}
|
|
2329
|
+
.accept { background: #6c47ff; color: #fff; }
|
|
2330
|
+
.decline { background: #f3f4f6; color: #374151; }
|
|
2331
|
+
</style>
|
|
2332
|
+
<div class="banner">
|
|
2333
|
+
<div class="title">\u{1F3AF} Earn rewards</div>
|
|
2334
|
+
<div>Help improve this app and earn points, badges, and perks for your contributions.</div>
|
|
2335
|
+
<div class="actions">
|
|
2336
|
+
<button class="accept" id="accept">Enable</button>
|
|
2337
|
+
<button class="decline" id="decline">No thanks</button>
|
|
2338
|
+
</div>
|
|
2339
|
+
</div>
|
|
2340
|
+
`;
|
|
2341
|
+
shadow.getElementById("accept")?.addEventListener("click", () => {
|
|
2342
|
+
setConsentGranted(projectId, true);
|
|
2343
|
+
consentHost?.remove();
|
|
2344
|
+
consentHost = null;
|
|
2345
|
+
if (config.showInWidget && currentUserId) {
|
|
2346
|
+
fetchAndCacheTier(currentUserId).then((tier) => {
|
|
2347
|
+
if (tier) renderTierBadge(tier, config);
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
shadow.getElementById("decline")?.addEventListener("click", () => {
|
|
2352
|
+
setConsentGranted(projectId, false);
|
|
2353
|
+
consentHost?.remove();
|
|
2354
|
+
consentHost = null;
|
|
2355
|
+
});
|
|
2356
|
+
document.body.appendChild(consentHost);
|
|
2357
|
+
}
|
|
2358
|
+
|
|
1695
2359
|
// src/capture/console.ts
|
|
1696
2360
|
var MAX_ENTRIES = 50;
|
|
1697
2361
|
var MAX_MESSAGE_LENGTH = 500;
|
|
@@ -2849,7 +3513,7 @@ function createProactiveManager(config = {}) {
|
|
|
2849
3513
|
|
|
2850
3514
|
// src/version.ts
|
|
2851
3515
|
var MUSHI_SDK_PACKAGE = "@mushi-mushi/web";
|
|
2852
|
-
var MUSHI_SDK_VERSION = "1.
|
|
3516
|
+
var MUSHI_SDK_VERSION = "1.1.0" ;
|
|
2853
3517
|
|
|
2854
3518
|
// src/mushi.ts
|
|
2855
3519
|
var instance = null;
|
|
@@ -2888,7 +3552,7 @@ function createInstance(config) {
|
|
|
2888
3552
|
const bootstrapConfig = applyPresetConfig(config);
|
|
2889
3553
|
let activeConfig = bootstrapConfig;
|
|
2890
3554
|
const log = config.debug ?? false ? core.createLogger({ scope: "mushi", level: "debug", format: "pretty" }) : core.noopLogger;
|
|
2891
|
-
const
|
|
3555
|
+
const apiClient2 = core.createApiClient({
|
|
2892
3556
|
projectId: bootstrapConfig.projectId,
|
|
2893
3557
|
apiKey: bootstrapConfig.apiKey,
|
|
2894
3558
|
...bootstrapConfig.apiEndpoint ? { apiEndpoint: bootstrapConfig.apiEndpoint } : {}
|
|
@@ -2997,7 +3661,7 @@ function createInstance(config) {
|
|
|
2997
3661
|
getUserId: () => userInfo?.id ?? null,
|
|
2998
3662
|
getSessionId: core.getSessionId,
|
|
2999
3663
|
onEvent: (event) => {
|
|
3000
|
-
void
|
|
3664
|
+
void apiClient2.postDiscoveryEvent({
|
|
3001
3665
|
...event,
|
|
3002
3666
|
sdk_version: MUSHI_SDK_VERSION
|
|
3003
3667
|
}).catch((err) => {
|
|
@@ -3073,17 +3737,17 @@ function createInstance(config) {
|
|
|
3073
3737
|
}
|
|
3074
3738
|
},
|
|
3075
3739
|
async onReporterReportsRequest() {
|
|
3076
|
-
const result = await
|
|
3740
|
+
const result = await apiClient2.listReporterReports(core.getReporterToken());
|
|
3077
3741
|
if (!result.ok) throw new Error(result.error?.message ?? "Could not load reports");
|
|
3078
3742
|
return result.data?.reports ?? [];
|
|
3079
3743
|
},
|
|
3080
3744
|
async onReporterCommentsRequest(reportId) {
|
|
3081
|
-
const result = await
|
|
3745
|
+
const result = await apiClient2.listReporterComments(reportId, core.getReporterToken());
|
|
3082
3746
|
if (!result.ok) throw new Error(result.error?.message ?? "Could not load thread");
|
|
3083
3747
|
return result.data?.comments ?? [];
|
|
3084
3748
|
},
|
|
3085
3749
|
async onReporterReply(reportId, body) {
|
|
3086
|
-
const result = await
|
|
3750
|
+
const result = await apiClient2.replyToReporterReport(reportId, core.getReporterToken(), body);
|
|
3087
3751
|
if (!result.ok) throw new Error(result.error?.message ?? "Could not send reply");
|
|
3088
3752
|
}
|
|
3089
3753
|
}, MUSHI_SDK_VERSION);
|
|
@@ -3129,8 +3793,8 @@ function createInstance(config) {
|
|
|
3129
3793
|
errorBoundary: proactiveCfg?.errorBoundary === true
|
|
3130
3794
|
});
|
|
3131
3795
|
}
|
|
3132
|
-
offlineQueue.startAutoSync(
|
|
3133
|
-
offlineQueue.flush(
|
|
3796
|
+
offlineQueue.startAutoSync(apiClient2);
|
|
3797
|
+
offlineQueue.flush(apiClient2).then((result) => {
|
|
3134
3798
|
if (result.sent > 0) log.info("Synced offline reports", { sent: result.sent });
|
|
3135
3799
|
});
|
|
3136
3800
|
function applyRuntimeConfig(runtime) {
|
|
@@ -3151,7 +3815,7 @@ function createInstance(config) {
|
|
|
3151
3815
|
if (shouldUseRuntimeConfig(config)) {
|
|
3152
3816
|
const cached = readCachedRuntimeConfig(config.projectId);
|
|
3153
3817
|
if (cached) applyRuntimeConfig(cached);
|
|
3154
|
-
|
|
3818
|
+
apiClient2.getSdkConfig().then((result) => {
|
|
3155
3819
|
if (result.ok && result.data) {
|
|
3156
3820
|
cacheRuntimeConfig(config.projectId, result.data);
|
|
3157
3821
|
applyRuntimeConfig(result.data);
|
|
@@ -3170,7 +3834,7 @@ function createInstance(config) {
|
|
|
3170
3834
|
if (activeConfig.widget?.outdatedBanner === "off") return;
|
|
3171
3835
|
const cached = readCachedSdkVersion(MUSHI_SDK_PACKAGE);
|
|
3172
3836
|
if (cached) applySdkFreshness(cached);
|
|
3173
|
-
const result = await
|
|
3837
|
+
const result = await apiClient2.getLatestSdkVersion(MUSHI_SDK_PACKAGE);
|
|
3174
3838
|
if (!result.ok || !result.data) return;
|
|
3175
3839
|
cacheSdkVersion(MUSHI_SDK_PACKAGE, result.data);
|
|
3176
3840
|
applySdkFreshness(result.data);
|
|
@@ -3312,7 +3976,7 @@ function createInstance(config) {
|
|
|
3312
3976
|
emit("report:queued", { reportId: report.id });
|
|
3313
3977
|
return;
|
|
3314
3978
|
}
|
|
3315
|
-
const result = await
|
|
3979
|
+
const result = await apiClient2.submitReport(report);
|
|
3316
3980
|
if (result.ok) {
|
|
3317
3981
|
log.info("Report sent", { reportId: result.data?.reportId });
|
|
3318
3982
|
emit("report:sent", { reportId: result.data?.reportId });
|
|
@@ -3321,6 +3985,10 @@ function createInstance(config) {
|
|
|
3321
3985
|
level: "info",
|
|
3322
3986
|
message: `Mushi report sent (${result.data?.reportId ?? report.id})`
|
|
3323
3987
|
});
|
|
3988
|
+
enqueue({
|
|
3989
|
+
action: "report_submit",
|
|
3990
|
+
metadata: { category, reportId: result.data?.reportId ?? report.id }
|
|
3991
|
+
});
|
|
3324
3992
|
try {
|
|
3325
3993
|
if (config.sentry && result.data?.reportId) {
|
|
3326
3994
|
tagSentryScope(result.data.reportId);
|
|
@@ -3466,7 +4134,7 @@ function createInstance(config) {
|
|
|
3466
4134
|
emit("report:queued", { reportId: report.id });
|
|
3467
4135
|
return null;
|
|
3468
4136
|
}
|
|
3469
|
-
const res = await
|
|
4137
|
+
const res = await apiClient2.submitReport(report);
|
|
3470
4138
|
if (res.ok) {
|
|
3471
4139
|
emit("report:sent", { reportId: res.data?.reportId });
|
|
3472
4140
|
try {
|
|
@@ -3518,6 +4186,33 @@ function createInstance(config) {
|
|
|
3518
4186
|
level: "info",
|
|
3519
4187
|
message: `Mushi.identify(${userId})`
|
|
3520
4188
|
});
|
|
4189
|
+
if (activeConfig.rewards?.enabled) {
|
|
4190
|
+
const rewardsCtx = {
|
|
4191
|
+
client: apiClient2,
|
|
4192
|
+
config: activeConfig.rewards,
|
|
4193
|
+
projectId: bootstrapConfig.projectId,
|
|
4194
|
+
userId,
|
|
4195
|
+
traits: traits ? { email: traits.email, name: traits.name, provider: traits.provider } : void 0
|
|
4196
|
+
};
|
|
4197
|
+
if (userInfo.id === userId) {
|
|
4198
|
+
initRewards(rewardsCtx);
|
|
4199
|
+
} else {
|
|
4200
|
+
updateRewardsUser(userId, rewardsCtx.traits);
|
|
4201
|
+
}
|
|
4202
|
+
if (activeConfig.rewards.showInWidget !== false) {
|
|
4203
|
+
void apiClient2.getMyPoints(userId).then((res) => {
|
|
4204
|
+
if (!res.ok) return;
|
|
4205
|
+
const d = res.data;
|
|
4206
|
+
widget.setRewardsState({
|
|
4207
|
+
tier: d.tier ? { slug: d.tier.slug ?? "free", displayName: d.tier.display_name ?? "Free", pointsThreshold: d.tier.points_threshold ?? 0 } : null,
|
|
4208
|
+
nextTier: d.next_tier ? { displayName: d.next_tier.display_name ?? "", pointsThreshold: d.next_tier.points_threshold ?? 0 } : null,
|
|
4209
|
+
totalPoints: d.total_points ?? 0,
|
|
4210
|
+
pointsForReport: d.report_submit_pts ?? 50
|
|
4211
|
+
});
|
|
4212
|
+
}).catch(() => {
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
3521
4216
|
},
|
|
3522
4217
|
addBreadcrumb(crumb) {
|
|
3523
4218
|
breadcrumbs.add(crumb);
|
|
@@ -3543,6 +4238,27 @@ function createInstance(config) {
|
|
|
3543
4238
|
return;
|
|
3544
4239
|
}
|
|
3545
4240
|
for (const k of Object.keys(stickyTags)) delete stickyTags[k];
|
|
4241
|
+
},
|
|
4242
|
+
// ─── Rewards program (P1) ──────────────────────────────────
|
|
4243
|
+
async getReputation() {
|
|
4244
|
+
if (!userInfo?.id) return null;
|
|
4245
|
+
const res = await apiClient2.getMyPoints(userInfo.id);
|
|
4246
|
+
if (!res.ok) return null;
|
|
4247
|
+
return {
|
|
4248
|
+
totalPoints: res.data.total_points ?? 0,
|
|
4249
|
+
points30d: res.data.points_30d ?? 0,
|
|
4250
|
+
reputation: 1,
|
|
4251
|
+
confirmedBugs: 0,
|
|
4252
|
+
totalReports: 0
|
|
4253
|
+
};
|
|
4254
|
+
},
|
|
4255
|
+
async getTier() {
|
|
4256
|
+
if (!userInfo?.id) return null;
|
|
4257
|
+
return getTier(userInfo.id);
|
|
4258
|
+
},
|
|
4259
|
+
recordActivity(action, metadata) {
|
|
4260
|
+
if (!activeConfig.rewards?.enabled) return;
|
|
4261
|
+
enqueue({ action, metadata });
|
|
3546
4262
|
}
|
|
3547
4263
|
};
|
|
3548
4264
|
return sdk;
|
|
@@ -3555,7 +4271,10 @@ function mergeRuntimeConfig(config, runtime) {
|
|
|
3555
4271
|
widget: {
|
|
3556
4272
|
...config.widget,
|
|
3557
4273
|
...runtime.widget,
|
|
3558
|
-
...widgetTrigger ? { trigger: widgetTrigger } : {}
|
|
4274
|
+
...widgetTrigger ? { trigger: widgetTrigger } : {},
|
|
4275
|
+
// betaMode is local-only: set by the host app, not the dashboard.
|
|
4276
|
+
// Restore it after the runtime spread so it is never silently cleared.
|
|
4277
|
+
...config.widget?.betaMode ? { betaMode: config.widget.betaMode } : {}
|
|
3559
4278
|
},
|
|
3560
4279
|
capture: {
|
|
3561
4280
|
...config.capture,
|
|
@@ -3791,6 +4510,10 @@ function createNoopInstance() {
|
|
|
3791
4510
|
setTags: () => {
|
|
3792
4511
|
},
|
|
3793
4512
|
clearTag: () => {
|
|
4513
|
+
},
|
|
4514
|
+
getReputation: async () => null,
|
|
4515
|
+
getTier: async () => null,
|
|
4516
|
+
recordActivity: () => {
|
|
3794
4517
|
}
|
|
3795
4518
|
};
|
|
3796
4519
|
}
|