- // ==UserScript==
- // @name DoorDash fees buster
- // @namespace http://tampermonkey.net/
- // @version 2024-12-31.4
- // @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('$'));
- 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('$'));
- 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 popup layer element before timeout');
- reject(new Error('unable to find popup layer 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 = parseFloat(getNumberOnly(findFeesAndEstimatedTaxTotalElement().innerText));
- // Be careful about float point number precision error
- if (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();
- })();