You're in K rove's space

No I Won’t Pay Just to Fullscreen Your Game

Posted on December 16, 2025

hackingweb

bestcrosswords.com is my go-to online quick crosswords website. It hosts American-style 15x15 free-to-play puzzles at different difficulty levels. According to them, they’re “the largest supplier of free crossword puzzles on the web, publishing 15 grids daily from an archive of more 100,000”. That’s awesome! You know what’s not awesome? Asking for a payment to fullscreen a crossword twice.

Thank you for providing such good puzzles for free, but no thank you to this specific request. This clearly seems like a flag they’d want to store per-device. Because accounts aren’t involved yet, the only two reasonable methods are cookies and localStorage. I happened to check the right one first.

Devtools console showing localStorage containing a single key named "com.bestcrosswords.applet.solvable.SolvableCrosswordPuzzleApplication/settings/setFullScreen".

Hehe. Presumably a timestamp of when the user last used fullscreen. So if the key is no longer there, like the first load, no more blockers?

delete localStorage["com.bestcrosswords.applet.solvable.SolvableCrosswordPuzzleApplication/settings/setFullScreen"];

Yes. No more blockers. Only until the next time you use fullscreen though, of course.

Multiple ways to fix that. Adding a button to delete the timestamp is the simplest and easiest, but we can do better. Possibly made using GWT (pronounced gwit), the page’s event handler code is obfuscated. Luckily though, the fullscreen “button” contains a single click event listener. And it’s returned by the element’s onclick property.

Devtools inspector showing the fullscreen "button" containing a single "click" event handler.

This makes things easy. A simple hack would be to just overwrite onclick with a function that deletes the timestamp and calls the original handler.

const button = document.querySelector("#gwt_casualinteractive div.bc-fullscreen-button");
const originalHandler = button.onclick;
button.onclick = function() {
    delete localStorage["com.bestcrosswords.applet.solvable.SolvableCrosswordPuzzleApplication/settings/setFullScreen"];
    originalHandler.apply(this, arguments);
}

Sweet! We know have infinite fullscreen toggle-ability.

Slightly unfortunately though, this hack cannot be run on window load, as the inner game DOM is loaded separately. Multiple ways to fix that too. Considering the simplicity of the situation, a timed loop to check the existence of the button is good enough; though you may opt for a more sophisticated solution if you want.

After some use, it turns out the fullscreen button selector and localStorage key were too specific to the mode I was testing this in. It’s since been accounted for by changing #gwt_casualinteractive to a more generic div[id^=\"gwt\"], meaning any div whose ID contains “gwt”, and by removing the hardcoded localStorage key altogether, using whatever the first key is (localStorage.key(0)) instead.

Changing the puzzle number from the dropdown still displays the paywall popup and un-maximizes the game. Not sure why. You can still manually re-maximize it though. I tried debugging it but the obfuscated code was sucking up too much time. Removing the localStorage data after calling the original handler doesn’t work either. Even if localStorage is empty when our handler returns, the popup is still displayed. So for now, that’s an inconvenience we’ll have to live with.

With all that, here’s a sample user script that checks for the button every second, for a maximum of ten times:

// ==UserScript==
// @name         BestCrosswords Fullscreen De-limiter
// @version      1.2
// @description  Removes the paywall for more-than-once fullscreen toggle.
// @namespace    https://krove.space/
// @author       Krove
// @match        https://www.bestcrosswords.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bestcrosswords.com
// ==/UserScript==

(function() {
    'use strict';

    const checkIntervalMs = 1000;
    const maxCheckCount = 10;
    let currentCheckCount = 0;

    function getFullscreenButton() {
        return document.querySelector("div[id^=\"gwt\"] div.bc-fullscreen-button");
    }

    function patchFullscreenButton(button) {
        const originalHandler = button.onclick;
        button.onclick = function() {
            delete localStorage[localStorage.key(0)];
            originalHandler.apply(this, arguments);
        }
    }

    function patchOrSchedule() {
        if (++currentCheckCount > maxCheckCount) {
            return;
        }

        const button = getFullscreenButton();
        if (button === null) {
            setTimeout(patchOrSchedule, checkIntervalMs);
        } else {
            patchFullscreenButton(button);
        }
    }

    window.addEventListener('load', patchOrSchedule);
})();