-
-
Save raxityo/c82abe49b77d5feaeb5cf6b69f0e0d9d to your computer and use it in GitHub Desktop.
| // ==UserScript== | |
| // @name BJ's Coupon Clipper | |
| // @namespace Violentmonkey Scripts | |
| // @match https://www.bjs.com/ | |
| // @grant none | |
| // @version 1.0 | |
| // @author @raxityo | |
| // @description 3/24/2024, 11:01:30 PM | |
| // ==/UserScript== | |
| /** | |
| * Clip all available BJ's coupons | |
| * - Login to BJ's website | |
| * - Open dev console and paste this function | |
| * - Run it! Example `await clipAllOffers()` | |
| * @return {Promise<T>} | |
| */ | |
| (async function clipAllOffers() { | |
| const membershipNumber = localStorage.getItem("x_MembershipNumber"); | |
| const zipcode = JSON.parse( | |
| localStorage.getItem("clubDetailsForClubId") | |
| ).postalCode; | |
| await fetch( | |
| "https://api.bjs.com/digital/live/api/v1.0/member/available/offers", | |
| { | |
| method: "post", | |
| credentials: "include", | |
| body: JSON.stringify({ | |
| membershipNumber, | |
| zipcode, | |
| category: "", | |
| isPrev: false, | |
| isNext: true, | |
| pagesize: 500, | |
| searchString: "", | |
| indexForPagination: 0, | |
| brand: "" | |
| }) | |
| } | |
| ) | |
| .then((r) => r.json()) | |
| .then(([{ availableOffers }]) => { | |
| // Intentionally doing sequential requests to avoid hammering the backend | |
| availableOffers.forEach(async ({ offerId, storeId }) => { | |
| await fetch( | |
| `https://api.bjs.com/digital/live/api/v1.0/store/${storeId}/coupons/activate?zip=07302&offerId=${offerId}`, | |
| { | |
| credentials: "include" | |
| } | |
| ); | |
| }); | |
| }); | |
| })().then(); |
@TheMarf I have just converted this to a UserScript, so you can use it in FireFox or other browsers using plugins such as Tampermonkey or ViolentMonkey and installing the script as a UeserScript. Doing so would automatically run the script whenever you visit bjs.com.
@raxityo Great idea. Thanks for the tip and the code!
I've done some improvements to the script, including adding a button on the page. Thank you for the script:
// ==UserScript==
// @name BJ's Coupon Clipper with Live Count
// @namespace Violentmonkey Scripts
// @match *://*.bjs.com/*
// @grant none
// @version 1.5
// @author @raxityo
// @description Adds a button that shows a live count of coupons being clipped on any BJ's website subdomain.
// ==/UserScript==
(function() {
'use strict';
async function clipAllOffers() {
button.disabled = true;
button.style.backgroundColor = "#ccc";
button.style.cursor = "not-allowed";
try {
const membershipNumber = localStorage.getItem("x_MembershipNumber");
const clubDetails = localStorage.getItem("clubDetailsForClubId");
if (!membershipNumber || !clubDetails) {
console.error("Missing membership or club details in localStorage.");
button.textContent = "Missing login/club info";
return;
}
const zipcode = JSON.parse(clubDetails).postalCode;
const response = await fetch("https://api.bjs.com/digital/live/api/v1.0/member/available/offers", {
method: "POST",
credentials: "include",
body: JSON.stringify({
membershipNumber,
zipcode,
category: "",
isPrev: false,
isNext: true,
pagesize: 500,
searchString: "",
indexForPagination: 0,
brand: ""
})
});
const data = await response.json();
if (!data || !Array.isArray(data) || !data[0]?.availableOffers) {
console.error("Unexpected response structure.", data);
button.textContent = "Unexpected response";
return;
}
const offers = data[0].availableOffers;
const total = offers.length;
if (total === 0) {
button.textContent = "No coupons available";
return;
}
for (let i = 0; i < total; i++) {
button.textContent = `Clipping coupon ${i + 1} of ${total}`;
const { offerId, storeId } = offers[i];
await fetch(
`https://api.bjs.com/digital/live/api/v1.0/store/${storeId}/coupons/activate?zip=07302&offerId=${offerId}`,
{
credentials: "include"
}
);
}
button.textContent = "Coupons clipped successfully!";
} catch (error) {
console.error("Error clipping coupons:", error);
button.textContent = "Error clipping coupons";
}
}
const button = document.createElement("button");
button.textContent = "Clip All Coupons";
button.style.position = "fixed";
button.style.top = "10px";
button.style.right = "10px";
button.style.zIndex = "10000";
button.style.padding = "10px 15px";
button.style.backgroundColor = "#007bff";
button.style.color = "#fff";
button.style.border = "none";
button.style.borderRadius = "5px";
button.style.cursor = "pointer";
button.style.boxShadow = "0 2px 6px rgba(0,0,0,0.3)";
button.addEventListener("click", clipAllOffers);
function addButton() {
if (!document.body.contains(button)) {
document.body.appendChild(button);
}
}
const observer = new MutationObserver(addButton);
observer.observe(document.body, { childList: true, subtree: true });
addButton();
})();
I think they changed their API or introduced throttling or something recently.
I used to just run something like this on their coupons page:
const allClipButtons = document.querySelectorAll('[name="clipToCard"]'); allClipButtons.forEach((button)=> {button.click();});
@trevorknight you can go ahead and try this one: https://greasyfork.org/en/scripts/528560-bj-s-coupon-clipper-with-live-count-fast-batching/code
Made an update to include BATCH_SIZE and BATCH_DELAY. Feel free to modify as needed. It worked for me a couple days ago. Let me know if this doesn't work

Love this code, was able to get it to work like a charm. Not very versed in js, is there any easy way to save this to a bookmark or something to easily run in firefox?
Edit: tried making a bookmark with the following...don't think it worked. Will try again in a couple days when coupons refresh.
javascript:(async function clipAllOffers() { const membershipNumber = localStorage.getItem('x_MembershipNumber') const zipcode = JSON.parse(localStorage.getItem('clubDetailsForClubId')).postalCode await fetch('https://api.bjs.com/digital/live/api/v1.0/member/available/offers', { method: 'post', credentials: 'include', body: JSON.stringify({ membershipNumber, zipcode, 'category': '', 'isPrev': false, 'isNext': true, 'pagesize': 500, 'searchString': '', 'indexForPagination': 0, 'brand': '' }) }) .then(r => r.json()) .then(([{ availableOffers }]) => { // Intentionally doing sequential requests to avoid hammering the backend availableOffers.forEach(async ({ offerId, storeId }) => { await fetch(https://api.bjs.com/digital/live/api/v1.0/store/${storeId}/coupons/activate?zip=07302&offerId=${offerId}`, { credentials: 'include' } ) }) }) })().then()`