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