Compare commits

..

22 Commits

Author SHA1 Message Date
Kalista Payne
4ecd13fbdc chore(github): split responsiveness to #15514 2025-09-17 16:25:19 -05:00
Hafiz
8ac414a184 move !error.response to correct level
!error.response before any attempt to access error.response.status
2025-09-15 12:03:22 -05:00
Kalista Payne
c4c0cca369 fix(admin): revert accidental change from rebase 2025-09-12 18:21:52 -05:00
Kalista Payne
326843fdc5 fix(blockers): duplicated code from rebase 2025-09-12 18:05:30 -05:00
Hafiz
0df9ea32fb Revert "Merge branch 'fiz/item-container-scaling' into qa/bat"
This reverts commit 4f28bfaad4, reversing
changes made to 477dd6328a.
2025-09-12 18:00:53 -05:00
Hafiz
bd3625aa4b remove redundant disabled styles in task modals
The .disabled class conflicting with existing disabled state implementations
2025-09-12 18:00:53 -05:00
Hafiz
0654d59752 Responsive Layout for Equipment Containers
- Added responsive CSS for mobile (<768px) and tablet (769px-1024px)
- Implemented flex-wrap layout that automatically stacks items in rows of 4 on smaller
2025-09-12 18:00:49 -05:00
Hafiz
0f901c9007 lint fix 2025-09-12 18:00:05 -05:00
Hafiz
30b6584a47 Update ToS error message
- Updated account suspension message from "This account, User ID..." to "Your account @[username] has been
  blocked..."
- Modified server auth middleware to pass username parameter when throwing account suspended error
-Modified auth utils loginRes function to include username in suspended account error
- Updated client bannedAccountModal component to pass username (empty string if unavailable)
- Updated login test to expect username in account suspended message
2025-09-12 18:00:05 -05:00
Hafiz
846250e4b8 Fix shop tabs overflow off screen at certain zoom levels
Fix quest cards get cut off on small screens
Fix pop-up windows extend past screen edges on mobile
2025-09-12 18:00:05 -05:00
Hafiz
0d1a5b6a7c Await genericPurchase completion before page reload to prevent request cancellation.
Also adds defensive check for undefined error.response in axios interceptor to prevent "t.response undefined" errors.
2025-09-12 18:00:05 -05:00
Phillip Thelen
c97845329a lint fixes 2025-09-12 17:56:34 -05:00
Phillip Thelen
5adbf536f5 add blocker to block emails from registration 2025-09-12 17:51:49 -05:00
Phillip Thelen
d9e76fcb3f restructure admin pages 2025-09-12 17:46:26 -05:00
Phillip Thelen
1efe30b7a7 Add UI for managing blockers 2025-09-12 17:36:29 -05:00
Phillip Thelen
4e2a8eb550 Tweak wording 2025-09-12 17:36:29 -05:00
Phillip Thelen
c3fd1fdd66 correctly reset local data after creating blocker 2025-09-12 17:36:29 -05:00
Phillip Thelen
0ec74582f0 Add UI for managing blockers 2025-09-12 17:36:27 -05:00
Phillip Thelen
69bf75322f add new frontend files 2025-09-12 17:33:03 -05:00
dependabot[bot]
447eb6a0c4 chore(deps): bump brace-expansion from 1.1.11 to 1.1.12 (#15498)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 14:58:24 -05:00
Kalista Payne
3dec49b72c GPC Message (#15508)
* feat(gpc): warn user about enabling analytics

* fix(gpc): style tweaks

* fix(privacy): local storage doesn't understand Boolean

* fix(gpc): do record if user has opted in

* fix(privacy): don't flip flop if no value changed
2025-09-11 14:58:10 -05:00
dependabot[bot]
472d03f276 chore(deps): bump vite from 6.3.5 to 6.3.6 in /website/client (#15507)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.5 to 6.3.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.3.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-11 14:53:43 -05:00
18 changed files with 146 additions and 35 deletions

14
package-lock.json generated
View File

@@ -6070,9 +6070,10 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -11431,9 +11432,10 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}

View File

@@ -44,7 +44,7 @@ describe('POST /user/auth/local/login', () => {
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id }),
message: t('accountSuspended', { communityManagerEmail: nconf.get('EMAILS_COMMUNITY_MANAGER_EMAIL'), userId: user._id, username: user.auth.local.username }),
});
});

View File

@@ -38,7 +38,7 @@
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",
@@ -5123,6 +5123,20 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -7126,6 +7140,21 @@
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/popper.js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
@@ -8528,9 +8557,10 @@
}
},
"node_modules/vite": {
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"version": "6.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",

View File

@@ -42,7 +42,7 @@
"timers-browserify": "^2.0.12",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vite": "^6.0.0",
"vite": "^6.3.6",
"vite-plugin-compression2": "^1.3.3",
"vue": "^2.7.10",
"vue-fragment": "^1.6.0",

View File

@@ -203,6 +203,9 @@ export default {
return response;
}, error => { // Set up Error interceptors
if (!error.response) {
return Promise.reject(error);
}
if (error.response.status >= 400) {
const isBanned = this.checkForBannedUser(error);
if (isBanned === true) return null; // eslint-disable-line consistent-return

View File

@@ -123,6 +123,10 @@ h4 {
background-color: $purple-300 !important;
}
.bg-yellow-50 {
background-color: $yellow-50 !important;
}
.bg-white {
background-color: $white !important;
}

View File

@@ -43,9 +43,11 @@ export default {
const AUTH_SETTINGS = localStorage.getItem(LOCALSTORAGE_AUTH_KEY);
const parseSettings = JSON.parse(AUTH_SETTINGS);
const userId = parseSettings ? parseSettings.auth.apiId : '';
const username = this.$store?.state?.user?.data?.auth?.local?.username || '';
return this.$t('accountSuspended', {
userId,
username,
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
});
},

View File

@@ -851,7 +851,7 @@ export default {
return;
}
if (this.genericPurchase) {
this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
await this.makeGenericPurchase(this.item, 'buyModal', this.selectedAmountToBuy);
await this.purchased(this.item.text);
}
}

View File

@@ -35,7 +35,7 @@
</button>
<button
class="btn btn-secondary d-flex align-items-center justify-content-center"
:class="{disabled: !canSave}"
:class="{'btn-disabled': !canSave}"
type="button"
@click="submit()"
>
@@ -162,13 +162,13 @@
>
<div
class="habit-option-icon svg-icon no-transition"
:class="task.up ? '' : 'disabled'"
:class="task.up ? '' : 'icon-disabled'"
v-html="icons.positive"
></div>
</div>
<div
class="habit-option-label no-transition"
:class="task.up ? cssClass('icon') : 'disabled'"
:class="task.up ? cssClass('icon') : 'label-disabled'"
>
{{ $t('positive') }}
</div>
@@ -188,13 +188,13 @@
>
<div
class="habit-option-icon no-transition svg-icon negative mx-auto"
:class="task.down ? '' : 'disabled'"
:class="task.down ? '' : 'icon-disabled'"
v-html="icons.negative"
></div>
</div>
<div
class="habit-option-label no-transition"
:class="task.down ? cssClass('icon') : 'disabled'"
:class="task.down ? cssClass('icon') : 'label-disabled'"
>
{{ $t('negative') }}
</div>
@@ -592,7 +592,7 @@
<button
class="btn btn-primary btn-footer
d-flex align-items-center justify-content-center"
:class="{disabled: !canSave}"
:class="{'btn-disabled': !canSave}"
type="button"
@click="submit()"
>
@@ -881,12 +881,14 @@
}
}
.disabled {
.btn-disabled {
background-color: $white;
border: 2px solid transparent;
color: $gray-200;
line-height: 1.714;
box-shadow: 0px 1px 3px 0px rgba(26, 24, 29, 0.12), 0px 1px 2px 0px rgba(26, 24, 29, 0.24);
cursor: not-allowed;
opacity: 0.6;
&:focus {
background-color: $white;
@@ -948,7 +950,7 @@
height: 10px;
color: $white;
&.disabled {
&.icon-disabled {
color: $gray-200;
}
@@ -962,7 +964,7 @@
font-weight: bold;
text-align: center;
&.disabled {
&.label-disabled {
color: $gray-100;
font-weight: normal;
}
@@ -1018,7 +1020,7 @@
border: 0;
}
.disabled .input-group-text {
.input-group-outer.disabled .input-group-text {
color: $gray-200;
}

View File

@@ -116,7 +116,7 @@
.toggle-switch-inner:before {
content: "";
padding-left: 10px;
background-color: $green-10;
background-color: $green-50;
}
.toggle-switch-inner:after {

View File

@@ -19,7 +19,7 @@ let analyticsReady = false;
function _getConsentedUser () {
const store = getStore();
const user = store.state.user.data;
if (!user?.preferences?.analyticsConsent || navigator.globalPrivacyControl) {
if (!user?.preferences?.analyticsConsent) {
return false;
}
return user;

View File

@@ -98,7 +98,7 @@
}
.settings-content {
flex: 0 0 732px;
flex: 0 0 751px;
max-width: unset;
::v-deep {

View File

@@ -33,6 +33,21 @@
v-html="$t('privacySettingsOverview') + ' ' + $t('learnMorePrivacy')"
>
</p>
<div
v-if="gpcEnabled"
class="mx-4 px-3 py-2 mb-4 gpc-alert d-flex align-items-center black bg-yellow-50"
>
<div
class="svg svg-icon mr-2"
v-html="icons.alert"
>
</div>
<div
class="gpc-message"
v-html="gpcInfo"
>
</div>
</div>
<div
class="d-flex justify-content-center"
>
@@ -91,6 +106,29 @@
line-height: 1.33;
}
.gpc-alert {
border-radius: 4px;
line-height: 1.714;
.gpc-message {
opacity: 0.9;
}
::v-deep a {
color: $black;
text-decoration: underline;
}
.svg-icon {
width: 16px;
opacity: 0.75;
::v-deep svg path {
fill: $black;
}
}
}
.mb-28p {
margin-bottom: 28px;
}
@@ -110,6 +148,7 @@ import ToggleSwitch from '@/components/ui/toggleSwitch.vue';
import { GenericUserPreferencesMixin } from '@/pages/settings/components/genericUserPreferencesMixin';
import { InlineSettingMixin } from '../components/inlineSettingMixin';
import { mapState } from '@/libs/store';
import alert from '@/assets/svg/for-css/alert.svg?raw';
export default {
mixins: [
@@ -120,14 +159,32 @@ export default {
SaveCancelButtons,
ToggleSwitch,
},
data () {
return {
icons: Object.freeze({
alert,
}),
};
},
computed: {
...mapState({
user: 'user.data',
}),
gpcEnabled () {
return navigator.globalPrivacyControl;
},
gpcInfo () {
const gpcUrl = 'https://globalprivacycontrol.org/';
if (this.user.preferences.analyticsConsent) {
return this.$t('gpcPlusAnalytics', { url: gpcUrl });
}
return this.$t('gpcWarning', { url: gpcUrl });
},
},
methods: {
finalize () {
this.setUserPreference('analyticsConsent');
localStorage.setItem('analyticsConsent', this.user.preferences.analyticsConsent);
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = false;
},
prefToggled () {
@@ -135,7 +192,10 @@ export default {
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = newVal;
},
resetControls () {
if (this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues) {
this.user.preferences.analyticsConsent = !this.user.preferences.analyticsConsent;
this.mixinData.inlineSettingMixin.sharedState.inlineSettingUnsavedValues = false;
}
},
},
};

View File

@@ -263,12 +263,13 @@ export default {
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
const analyticsConsent = localStorage.getItem('analyticsConsent');
if (analyticsConsent !== null
&& analyticsConsent !== this.user.preferences.analyticsConsent
) {
let analyticsConsent = localStorage.getItem('analyticsConsent');
if (analyticsConsent !== null) {
analyticsConsent = analyticsConsent === 'true';
if (analyticsConsent !== this.user.preferences.analyticsConsent) {
this.$store.dispatch('user:set', { 'preferences.analyticsConsent': analyticsConsent });
}
}
if (window && window['habitica-i18n']) {
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
return null;

View File

@@ -133,7 +133,7 @@
"passwordReset": "If we have your email or username on file, instructions for setting a new password have been sent to your email.",
"invalidLoginCredentialsLong": "Your email, username, or password are incorrect. Please try again or use \"Forgot Password.\"",
"invalidCredentials": "There is no account that uses those credentials.",
"accountSuspended": "This account, User ID \"<%= userId %>\", has been blocked for breaking the Community Guidelines (https://habitica.com/static/community-guidelines) or Terms of Service (https://habitica.com/static/terms). For details or to ask to be unblocked, please email our Community Manager at <%= communityManagerEmail %> or ask your parent or guardian to email them. Please include your @Username in the email.",
"accountSuspended": "Your account @<%= username %> has been blocked. For additional information, or to request an appeal, email admin@habitica.com with your Habitica username or User ID.",
"accountSuspendedTitle": "Account has been suspended",
"unsupportedNetwork": "This network is not currently supported.",
"cantDetachSocial": "Account lacks another authentication method; can't detach this authentication method.",

View File

@@ -271,5 +271,7 @@
"performanceAnalytics": "Performance and Analytics",
"usedForSupport": "These are used to improve the user experience, performance, and services of our website and apps. This data is used by our support team when handling requests and bug reports.",
"savePreferences": "Save Preferences",
"habiticaPrivacyPolicy": "Habitica's Privacy Policy"
"habiticaPrivacyPolicy": "Habitica's Privacy Policy",
"gpcWarning": "<a href='<%= url %>' target='_blank'>GPC</a> is on. Turning on tracking below will override this and send data to our analytics partners.",
"gpcPlusAnalytics": "<a href='<%= url %>' target='_blank'>GPC</a> is on. You have opted in to tracking and sending data to our analytics partners."
}

View File

@@ -17,7 +17,11 @@ export function loginRes (user, req, res) {
if (user.auth.blocked) {
throw new NotAuthorized(res.t(
'accountSuspended',
{ communityManagerEmail: COMMUNITY_MANAGER_EMAIL, userId: user._id },
{
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
userId: user._id,
username: user.auth.local.username,
},
));
}
const urlPath = url.parse(req.url).pathname;

View File

@@ -100,6 +100,7 @@ export function authWithHeaders (options = {}) {
throw new NotAuthorized(common.i18n.t('accountSuspended', {
communityManagerEmail: COMMUNITY_MANAGER_EMAIL,
userId: user._id,
username: user.auth.local.username,
}, language));
}