您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display DoorDash fees in a clearer way.
当前为
// ==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(); })();