Adds a sleek "Hide/Unhide Robux" toggle button into the Roblox navigation header menu, allowing you to instantly mask your currency balance for privacy or content creation. Saves preferences across your session.
// ==UserScript==
// @name 🔒 Robux Privacy
// @namespace https://github.com/yourusername/roblox-robux-privacy
// @version 0.2
// @description Adds a sleek "Hide/Unhide Robux" toggle button into the Roblox navigation header menu, allowing you to instantly mask your currency balance for privacy or content creation. Saves preferences across your session.
// @author not-oops
// @match https://*.roblox.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=roblox.com
// @grant none
// @run-at document-idle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
/**
* Shared application state managing persistence.
*/
class PrivacyState {
static STORAGE_KEY = 'robux_privacy';
/**
* Retrieves the current privacy state.
* @returns {boolean} True if Robux should be hidden.
*/
static isHidden() {
const savedState = sessionStorage.getItem(this.STORAGE_KEY);
return savedState !== null ? savedState === 'true' : true;
}
/**
* Toggles and saves the privacy state.
* @returns {boolean} The new privacy state.
*/
static toggle() {
const newState = !this.isHidden();
sessionStorage.setItem(this.STORAGE_KEY, String(newState));
return newState;
}
}
/**
* Component responsible for creating standardized elements.
*/
class ElementFactory {
/**
* Creates a uniform span element styled to show "hidden".
* @returns {HTMLSpanElement} The styled hidden label element.
*/
static createHiddenSpan() {
const hiddenSpan = document.createElement('span');
hiddenSpan.textContent = 'hidden';
hiddenSpan.style.opacity = '0.5';
hiddenSpan.style.fontSize = 'small';
hiddenSpan.setAttribute('data-hidden-marker', 'true');
return hiddenSpan;
}
/**
* Creates the toggle privacy button for the dropdown list.
* @param {Function} onClickCallback - Action to trigger on click.
* @returns {HTMLLIElement} The structured menu item container.
*/
static createToggleButton(onClickCallback) {
const listItem = document.createElement('li');
listItem.className = 'rbx-menu-item-container';
listItem.setAttribute('data-privacy-toggle', 'true');
const anchor = document.createElement('a');
anchor.className = 'rbx-menu-item';
anchor.href = '#';
anchor.textContent = PrivacyState.isHidden() ? 'Unhide Robux' : 'Hide Robux';
anchor.style.cursor = 'pointer';
anchor.addEventListener('click', (event) => {
event.preventDefault();
onClickCallback(anchor);
});
listItem.appendChild(anchor);
return listItem;
}
}
/**
* Base interface/class for elements that need Robux data masked or restored.
*/
class RobuxElementMasker {
/**
* Process target elements based on visibility state.
*/
mask() {
throw new Error('Method "mask()" must be implemented.');
}
/**
* Replaces an element's target content with the standardized hidden element.
* @param {HTMLElement} element - The DOM element to transform.
*/
applyHiddenLabel(element) {
if (!element.querySelector('[data-hidden-marker]')) {
element.setAttribute('data-original-amount', element.textContent.trim());
element.textContent = '';
element.appendChild(ElementFactory.createHiddenSpan());
}
}
/**
* Restores the original currency text value back to the element.
* @param {HTMLElement} element - The DOM element to restore.
*/
restoreOriginalText(element) {
const original = element.getAttribute('data-original-amount');
if (original !== null) {
element.textContent = original;
element.removeAttribute('data-original-amount');
}
const marker = element.querySelector('[data-hidden-marker]');
if (marker) {
marker.remove();
}
}
}
/**
* Handles the navigation bar Robux counter element.
*/
class NavRobuxMasker extends RobuxElementMasker {
mask() {
const navRobuxAmount = document.getElementById('nav-robux-amount');
if (!navRobuxAmount) return;
if (PrivacyState.isHidden()) {
this.applyHiddenLabel(navRobuxAmount);
} else {
this.restoreOriginalText(navRobuxAmount);
}
}
}
/**
* Handles the text-based balance details container elements.
*/
class BalanceContainerMasker extends RobuxElementMasker {
mask() {
const balanceContainers = document.querySelectorAll('.balance-label.icon-robux-container');
balanceContainers.forEach(container => {
const innerSpan = container.querySelector('span');
if (!innerSpan) return;
if (PrivacyState.isHidden()) {
if (!innerSpan.querySelector('[data-hidden-marker]')) {
this.saveAndClearTextNodes(innerSpan);
innerSpan.appendChild(ElementFactory.createHiddenSpan());
}
} else {
this.restoreTextNodes(innerSpan);
}
});
}
saveAndClearTextNodes(innerSpan) {
const textNodes = Array.from(innerSpan.childNodes);
textNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== 'My Balance:' && node.textContent.trim() !== '') {
innerSpan.setAttribute('data-original-balance', node.textContent.trim());
node.textContent = '';
}
});
}
restoreTextNodes(innerSpan) {
const original = innerSpan.getAttribute('data-original-balance');
const marker = innerSpan.querySelector('[data-hidden-marker]');
if (marker) marker.remove();
if (original !== null) {
const textNodes = Array.from(innerSpan.childNodes);
let filled = false;
textNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === '') {
node.textContent = ' ' + original;
filled = true;
}
});
if (!filled) {
innerSpan.appendChild(document.createTextNode(' ' + original));
}
innerSpan.removeAttribute('data-original-balance');
}
}
}
/**
* Handles large extended font display elements containing Robux text.
*/
class LargeTitleMasker extends RobuxElementMasker {
mask() {
const largeTitleAmounts = document.querySelectorAll('.font-builder-extended.content-action-standard.text-title-large');
largeTitleAmounts.forEach(element => {
if (PrivacyState.isHidden()) {
if (element.textContent.trim() !== '') {
this.applyHiddenLabel(element);
}
} else {
this.restoreOriginalText(element);
}
});
}
}
/**
* Orchestrates mutation observation, manages masking components, and injects toggle UI controls.
*/
class RobuxPrivacyManager {
constructor() {
this.maskers = [
new NavRobuxMasker(),
new BalanceContainerMasker(),
new LargeTitleMasker()
];
this.observer = null;
}
/**
* Runs all registered element maskers safely.
*/
runMaskers() {
this.maskers.forEach(masker => masker.mask());
}
/**
* Handles injection of the Privacy Toggle button inside the opened dropdown menu interface.
*/
injectToggleButton() {
const menu = document.getElementById('buy-robux-popover-menu');
if (menu && !menu.querySelector('[data-privacy-toggle]')) {
const buyButtonContainer = menu.querySelector('.rbx-menu-item-container');
const toggleButton = ElementFactory.createToggleButton((buttonAnchor) => {
const isHidden = PrivacyState.toggle();
buttonAnchor.textContent = isHidden ? 'Unhide Robux' : 'Hide Robux';
this.runMaskers();
});
if (buyButtonContainer && buyButtonContainer.nextSibling) {
menu.insertBefore(toggleButton, buyButtonContainer.nextSibling);
} else {
menu.appendChild(toggleButton);
}
}
}
/**
* Starts DOM observation loop without creating recursive processing lag.
*/
init() {
this.observer = new MutationObserver(() => {
this.observer.disconnect();
this.injectToggleButton();
this.runMaskers();
this.observer.observe(document.body, { childList: true, subtree: true });
});
this.observer.observe(document.body, { childList: true, subtree: true });
this.injectToggleButton();
this.runMaskers();
}
}
const privacyManager = new RobuxPrivacyManager();
privacyManager.init();
})();