// ==UserScript==
// @name DoorDash fees buster
// @namespace http://tampermonkey.net/
// @version 2025-01-01.2
// @description Display DoorDash fees in a clearer way.
// @author Somebody
// @match https://www.doordash.com/consumer/checkout/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=doordash.com
// @grant none
// @run-at document-end
// @license GNU GPLv3
// ==/UserScript==
(function () {
'use strict';
const D = window.document;
const GLOBAL_TIMEOUT = 10 * 1000;
const KNOWN_MARKUP_BUSINESS_NAMES = [
'Popeyes Louisiana Kitchen',
'Jollibee',
'KFC',
];
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const createElementWithInnerTextAndStyle = (ElementName, innerText, style = {}) => {
const element = D.createElement(ElementName);
if (typeof innerText === 'string') {
element.innerText = innerText;
}
Object.assign(
element.style,
style
);
return element;
};
const createToastWithText = (() => {
const timeoutIdToElementMap = new Map();
const showToast = (text, disappearDelayInMs = 1000) => {
const defaultStyle = {
position: 'fixed',
top: '10vh',
zIndex: 9999,
transform: 'translateX(-50%)',
left: '50%',
transition: 'all 0.25s ease',
borderRadius: '2em',
backgroundColor: 'black',
padding: '0.5em 2em',
color: 'white',
textAlign: 'center',
opacity: '0',
};
const toast = createElementWithInnerTextAndStyle('div', text, defaultStyle);
D.body.append(toast);
const timeoutId = setTimeout(
() => {
timeoutIdToElementMap.delete(timeoutId);
D.body.removeChild(toast);
},
disappearDelayInMs
);
timeoutIdToElementMap.set(timeoutId, toast);
setTimeout(
() => {
Object.assign(
toast.style, {
opacity: '100%'
}
);
},
0
);
setTimeout(
() => {
Object.assign(
toast.style, {
opacity: '0'
}
);
},
disappearDelayInMs - 250
);
};
return showToast;
})();
const getInitialServiceFee = () => {
// This is no longer true if items are modified after page load
const keyword = 'Service Fee: ';
let line = Array.from(D.querySelectorAll('script')).find(e => e.innerText.includes(keyword)).innerText;
line = line.substring(line.indexOf(keyword));
line = line.substring(0, line.indexOf('\"'));
return line;
};
const getInitialEstimatedTax = () => {
// This is no longer true if items are modified after page load
const keyword = 'Estimated Tax: ';
let line = Array.from(D.querySelectorAll('script')).find(e => e.innerText.includes(keyword)).innerText;
line = line.substring(line.indexOf(keyword));
line = line.substring(0, line.indexOf('\"'));
return line;
};
const getInitialBusinessName = () => {
const businessNameElement = Array.from(document.querySelectorAll('span')).find(spenElement => ['Your cart from', 'Order From', 'Checkout'].includes(spenElement.innerText))?.nextSibling;
if (businessNameElement instanceof HTMLElement) {
return Array.from(businessNameElement.querySelectorAll('span')).find(spenElement => typeof spenElement.innerText === 'string' && spenElement.innerText.length > 0)?.innerText;
}
};
const triggerHover = (element) => {
if (element instanceof HTMLElement) {
// Create the mouseenter event
const mouseEnterEvent = new MouseEvent('mouseenter', {
bubbles: true,
cancelable: true,
view: window,
});
// Create the mouseover event
const mouseOverEvent = new MouseEvent('mouseover', {
bubbles: true,
cancelable: true,
view: window,
});
// Create the mouseleave event
const mouseLeaveEvent = new MouseEvent('mouseleave', {
bubbles: true,
cancelable: true,
view: window,
});
// Dispatch mouseenter and mouseover events
element.dispatchEvent(mouseEnterEvent);
element.dispatchEvent(mouseOverEvent);
const triggerHoverEnd = () => {
element.dispatchEvent(mouseLeaveEvent);
};
return triggerHoverEnd;
}
};
const findLineItemsElement = () => D.querySelector('[data-testid="LineItems"]');
// const findStackChildrenElement = () => D.querySelector('[class^="StackChildren"]');
// const findTooltipElements = () => D.querySelectorAll('[role="tooltip"]');
const findFeesAndEstimatedTaxElement = () => D.querySelector('[data-testid="Fees & Estimated Tax"]');
const findFeesAndEstimatedTaxTooltipTriggerElement = () => findFeesAndEstimatedTaxElement()?.querySelector('button');
const findSubtotalElement = () => D.querySelector('[data-testid="Subtotal"]');
const findDeliveryFeeElement = () => D.querySelector('[data-testid="Delivery Fee"]');
const findLongDistanceFeeElement = () => D.querySelector('[data-testid="Long Distance Fee"]');
const findDasherTipElement = () => D.querySelector('[data-testid="Dasher Tip"]');
const findTotalElement = () => D.querySelector('[data-testid="Total"]');
const findCartTotalElement = () => D.querySelector('[data-anchor-id="OrderCartTotal"]');
const findPopupLayerElement = () => D.querySelector('[data-testid="LAYER-MANAGER-POPOVER_CONTENT"]');
const findFeesAndEstimatedTaxTotalElement = () => {
const feesAndEstimatedTaxElement = findFeesAndEstimatedTaxElement();
if (feesAndEstimatedTaxElement instanceof HTMLElement) {
const totalElement = Array.from(feesAndEstimatedTaxElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
return totalElement;
}
return undefined;
};
const findSubtotalTotalElement = () => {
const subtotalElement = findSubtotalElement();
if (subtotalElement instanceof HTMLElement) {
const totalElement = Array.from(subtotalElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
return totalElement;
}
return undefined;
};
const findDoorDashCreditsTotalElement = () => {
const lineItemsElement = D.querySelector('[data-testid="LineItems"]');
if (lineItemsElement instanceof HTMLElement) {
const doorDashCreditsDescriptionElement = Array.from(lineItemsElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('DoorDash Credits'));
const doorDashCreditsTotalElement = doorDashCreditsDescriptionElement?.parentNode?.lastChild;
return doorDashCreditsTotalElement;
}
return undefined;
};
const findDasherTipTotalElement = () => {
const dasherTipElement = findDasherTipElement();
if (dasherTipElement instanceof HTMLElement) {
const doorDashTipTotalElement = Array.from(dasherTipElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$'));
return doorDashTipTotalElement;
}
return undefined;
}
const findDeliveryFeeElementTotalElement = () => {
const deliveryFeeElement = findDeliveryFeeElement();
if (deliveryFeeElement instanceof HTMLElement) {
const deliveryFeeTotalElement = Array.from(deliveryFeeElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
return deliveryFeeTotalElement;
}
return undefined;
};
const findLongDistanceFeeTotalElement = () => {
const longDistanceFeeElement = findLongDistanceFeeElement();
if (longDistanceFeeElement instanceof HTMLElement) {
const longDistanceFeeTotalElement = Array.from(longDistanceFeeElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$') && !window.getComputedStyle(spanElement).textDecoration.includes('line-through'));
return longDistanceFeeTotalElement;
}
return undefined;
};
const findTooltipByKeywordsWithObserver = (keywords) => new Promise((resolve, reject) => {
const popupLayerElement = findPopupLayerElement();
if (popupLayerElement instanceof HTMLElement) {
const timeoutId = setTimeout(() => {
createToastWithText('Unable to find target popup element before timeout');
reject(new Error('unable to find target popup element before timeout'));
observer.disconnect();
}, GLOBAL_TIMEOUT);
const observer = new MutationObserver(async (mutationsList, observer) => {
try {
// Iterate over all mutations
loop:
for (let mutation of mutationsList) {
// Check if nodes were added to the DOM
if (mutation.type === 'childList') {
// Look for the element with the specific keyword
for (const addedNode of mutation.addedNodes) {
if (keywords.every(keyword => addedNode.innerText?.includes(keyword))) {
// disconnect the observer once the element is found
observer.disconnect();
clearTimeout(timeoutId);
resolve(addedNode);
break loop;
}
}
}
}
} catch (err) {
clearTimeout(timeoutId);
createToastWithText('Unable to find tooltip due to an error');
console.error(err);
reject(err);
}
});
// Configure the observer to look for added nodes in the popup layer
observer.observe(popupLayerElement, {
childList: true, // Look for added/removed child nodes
subtree: true, // Include all descendants in the search
});
} else {
reject(new Error('unable to find popup layer element'));
}
});
const getServiceFeeAndEstimatedTaxFromTooltip = async () => {
const targetTooltipElementPromise = findTooltipByKeywordsWithObserver(['Service Fee', 'Estimated Tax']);
const triggerHoverEnd = triggerHover(findFeesAndEstimatedTaxTooltipTriggerElement());
const targetTooltipElement = await targetTooltipElementPromise;
const targetTooltipElementInnerText = targetTooltipElement?.innerText;
triggerHoverEnd?.();
const targetTooltipElementInnerTextLines = targetTooltipElementInnerText?.split('\n')?.map(line => line.trim());
const serviceFee = targetTooltipElementInnerTextLines?.find(line => line.startsWith('Service Fee'));
const estimatedTax = targetTooltipElementInnerTextLines?.find(line => line.startsWith('Estimated Tax'));
return [serviceFee, estimatedTax];
};
const getServiceFeeAndEstimatedTaxFromTooltipMemorized = (() => {
let serviceFee = 'Fee: $0.0';
let estimatedTax = 'Tax: $0.0';
let isFirstRun = true;
return async () => {
const serviceFeeAndEstimatedTax = findFeesAndEstimatedTaxTotalElement().innerText;
// Be careful about float point number precision error
if (parseFloat(getNumberOnly(serviceFeeAndEstimatedTax)) * 100 !== parseFloat(getNumberOnly(serviceFee)) * 100 + parseFloat(getNumberOnly(estimatedTax)) * 100) {
if (isFirstRun) {
[serviceFee, estimatedTax] = [getInitialServiceFee(), getInitialEstimatedTax()];
isFirstRun = false;
} else {
[serviceFee, estimatedTax] = await getServiceFeeAndEstimatedTaxFromTooltip();
}
}
return [serviceFee, estimatedTax];
};
})();
const getNumberOnly = (text) => {
if (typeof text !== 'string') {
throw new TypeError('text must be string');
}
const numberPart = text.substring(text.lastIndexOf('$') + 1);
return numberPart;
};
const getTotalCost = () => {
const cartTotalElement = findCartTotalElement();
const cartTotal = parseFloat(getNumberOnly(cartTotalElement.innerText));
let totalCost = cartTotal;
const doorDashCreditsTotalElement = findDoorDashCreditsTotalElement();
if (doorDashCreditsTotalElement instanceof HTMLElement) {
const doorDashCreditsTotal = Math.abs(parseFloat(getNumberOnly(doorDashCreditsTotalElement.innerText)));
totalCost += doorDashCreditsTotal;
}
return totalCost;
};
const insertNewRow = (description, total, insertAfterElement, testid = '') => {
const newRowElement = findSubtotalElement().cloneNode(true);
newRowElement.dataset.testid = testid;
const descriptionElement = Array.from(newRowElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('Subtotal'));
descriptionElement.innerText = description;
const totalElement = Array.from(newRowElement.querySelectorAll('span')).find(spanElement => spanElement.innerText.includes('$'));
totalElement.innerText = total;
if (insertAfterElement.nextSibling) {
insertAfterElement.parentNode.insertBefore(newRowElement, insertAfterElement.nextSibling);
} else {
insertAfterElement.parentNode.appendChild(newRowElement);
}
return newRowElement;
};
const calcCostPercentage = (cost) => {
const subtotalTotalElement = findSubtotalTotalElement();
const subtotal = parseFloat(getNumberOnly(subtotalTotalElement.innerText));
const percentage = Math.ceil(cost * 10000 / subtotal) / 100;
return percentage;
};
const setColorRecursively = (parentElement, color = 'red') => {
Array.from(parentElement.querySelectorAll('span, div')).forEach(spanOrDivElement => {
Object.assign(
spanOrDivElement.style,
{
color: color,
},
);
});
};
const updateLineItems = async () => {
const feesAndEstimatedTaxElement = findFeesAndEstimatedTaxElement();
const [serviceFee, estimatedTax] = await getServiceFeeAndEstimatedTaxFromTooltipMemorized();
if (feesAndEstimatedTaxElement instanceof HTMLElement) {
const serviceFeePercentage = calcCostPercentage(parseFloat(getNumberOnly(serviceFee.split(': ')[1])));
const feesElementTestid = 'Fees';
D.querySelector(`[data-testid="${feesElementTestid}"]`)?.remove();
const feesElement = insertNewRow(serviceFee.split(': ')[0], `${serviceFeePercentage}% = ${serviceFee.split(': ')[1]}`, feesAndEstimatedTaxElement, feesElementTestid);
setColorRecursively(feesElement, 'red');
const estimatedTaxPercentage = calcCostPercentage(parseFloat(getNumberOnly(estimatedTax.split(': ')[1])));
const estimatedTaxElementTestid = 'Estimated Tax';
D.querySelector(`[data-testid="${estimatedTaxElementTestid}"]`)?.remove();
const estimatedTaxElement = insertNewRow(estimatedTax.split(': ')[0], `${estimatedTaxPercentage}% = ${estimatedTax.split(': ')[1]}`, feesElement, estimatedTaxElementTestid);
setColorRecursively(feesAndEstimatedTaxElement, 'darkgray');
}
const deliveryFeeElement = findDeliveryFeeElement();
if (deliveryFeeElement instanceof HTMLElement) {
const deliveryFeeTotalElement = findDeliveryFeeElementTotalElement();
deliveryFeeTotalElement.innerText = `${calcCostPercentage(parseFloat(getNumberOnly(deliveryFeeTotalElement.innerText)))}% = $${getNumberOnly(deliveryFeeTotalElement.innerText)}`;
setColorRecursively(deliveryFeeElement, 'red');
}
const longDistanceFeeElement = findLongDistanceFeeElement();
if (longDistanceFeeElement instanceof HTMLElement) {
const longDistanceFeeElement = findLongDistanceFeeTotalElement();
longDistanceFeeElement.innerText = `${calcCostPercentage(parseFloat(getNumberOnly(longDistanceFeeElement.innerText)))}% = $${getNumberOnly(longDistanceFeeElement.innerText)}`;
setColorRecursively(longDistanceFeeElement, 'red');
}
const dasherTipElement = findDasherTipElement();
if (dasherTipElement instanceof HTMLElement) {
const dasherTipTotalElement = findDasherTipTotalElement();
dasherTipTotalElement.innerText = `${calcCostPercentage(parseFloat(getNumberOnly(dasherTipTotalElement.innerText)))}% = $${getNumberOnly(dasherTipTotalElement.innerText)}`;
setColorRecursively(dasherTipElement, 'red');
}
const totalElement = findTotalElement();
if (totalElement instanceof HTMLElement) {
const totalCost = getTotalCost();
const cartTotalElement = findCartTotalElement();
cartTotalElement.innerText = `${calcCostPercentage(totalCost - getNumberOnly(estimatedTax.split(': ')[1]))}% (Pretax) / ${calcCostPercentage(totalCost)}% = $${getNumberOnly(cartTotalElement.innerText)}`;
const businessName = getInitialBusinessName();
if (KNOWN_MARKUP_BUSINESS_NAMES.includes(businessName)) {
const markUpWarningTestid = 'Markup Warning';
D.querySelector(`[data-testid="${markUpWarningTestid}"]`)?.remove();
const markUpWarningElement = insertNewRow('This business is known to charge more on DoorDash, the actual pretax subtotal would be even lower in store.', ' ', totalElement, markUpWarningTestid);
setColorRecursively(markUpWarningElement, 'red');
}
}
};
const main = () => {
const timeoutId = setTimeout(() => {
createToastWithText('Failed to display Fees & Estimated Tax before timeout');
}, GLOBAL_TIMEOUT);
const observer = new MutationObserver(async (mutationsList, observer) => {
try {
// Iterate over all mutations
for (let mutation of mutationsList) {
// Check if nodes were added to the DOM
if (mutation.type === 'childList') {
// Look for the element with the specific data-testid
const lineItemsElement = findLineItemsElement();
if (lineItemsElement instanceof HTMLElement) {
// disconnect the observer once the element is found
observer.disconnect();
try {
await updateLineItems();
} catch (err) {
console.error(err);
}
const lineItemsElementObserverOptions = {
childList: true, // Look for added/removed child nodes
subtree: true, // Include all descendants in the search
characterData: true,
};
const lineItemsObserver = new MutationObserver(async (lineItemsMutationsList, lineItemsObserver) => {
// Avoid recursive calls
lineItemsObserver.disconnect();
try {
await updateLineItems();
} catch (err) {
console.error(err);
}
lineItemsObserver.observe(lineItemsElement, lineItemsElementObserverOptions);
});
lineItemsObserver.observe(lineItemsElement, lineItemsElementObserverOptions);
clearTimeout(timeoutId);
createToastWithText('Fees & Estimated Tax rate calculated');
break;
}
}
}
} catch (err) {
clearTimeout(timeoutId);
createToastWithText('Failed to display Fees & Estimated Tax due to an error');
console.error(err);
}
});
// Configure the observer to look for added nodes in the entire document
observer.observe(D.body, {
childList: true, // Look for added/removed child nodes
subtree: true, // Include all descendants in the search
});
};
main();
})();