Spaces:
Sleeping
Sleeping
| export type CheckoutPattern = { | |
| detected: boolean; | |
| evidence: string; | |
| }; | |
| export type CheckoutAnalysisInput = { | |
| domain: string; | |
| url?: string; | |
| pageText?: string; | |
| timerElements?: string[]; | |
| stockAlerts?: string[]; | |
| variantUrgency?: string[]; | |
| buttonLabels?: string[]; | |
| priceStrings?: string[]; | |
| productCandidates?: Array<{ | |
| title?: string; | |
| listedPrice?: number | null; | |
| originalPrice?: number | null; | |
| dealSignals?: string[] | null; | |
| timerResetsOnRefresh?: boolean | null; | |
| }>; | |
| }; | |
| export type ManipulationFlag = { | |
| type: string; | |
| severity: "high" | "medium" | "low"; | |
| userMessage: string; | |
| evidence: string; | |
| actionAdvice: string; | |
| }; | |
| export type CheckoutAnalysis = { | |
| isShoppingPage: boolean; | |
| pageType: "non_commerce" | "product" | "cart" | "checkout"; | |
| categoryHint: "hotel" | "airline" | "marketplace" | "car_rental"; | |
| trustScore: number; | |
| falseUrgency: CheckoutPattern & { isTimerFake: boolean | null }; | |
| falseScarcity: CheckoutPattern; | |
| confirmShaming: { detected: boolean; shamingText: string; rewrittenText: string }; | |
| hiddenFees: { detected: boolean; feeItems: Array<{ label: string; amount: number }>; totalExtra: number | null }; | |
| preCheckedAddOns: { detected: boolean; fieldIds: string[]; addOnLabels: string[] }; | |
| misdirection: { detected: boolean; hiddenDeclineText: string }; | |
| manipulationFlags: ManipulationFlag[]; | |
| totalPatternsDetected: number; | |
| summary: string; | |
| nonCommerceReason: string | null; | |
| }; | |
| const NON_COMMERCE_DOMAINS = [ | |
| "github.com", | |
| "stackoverflow.com", | |
| "wikipedia.org", | |
| "developer.mozilla.org", | |
| "docs.python.org", | |
| "medium.com", | |
| "notion.so", | |
| "vercel.com", | |
| ]; | |
| function normalizeDomain(input: string): string { | |
| return input.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/^www\./, "").split("/")[0] ?? "example.com"; | |
| } | |
| function normalizeText(value: unknown): string { | |
| return typeof value === "string" ? value.replace(/\s+/g, " ").trim() : ""; | |
| } | |
| function joinTexts(values: unknown[] | undefined): string { | |
| return (values ?? []) | |
| .map((value) => normalizeText(value)) | |
| .filter(Boolean) | |
| .join(" ") | |
| .toLowerCase(); | |
| } | |
| function detectMarketplaceContextSignals(domain: string, haystack: string, variantUrgencyText: string) { | |
| const isFashionMarketplace = /myntra|ajio|tatacliq|nykaa|meesho/i.test(domain); | |
| const sizeUrgencyMatch = | |
| /only\s*\d+\s*left\s*in\s*(size|this size)|few\s*left\s*in\s*(size|this size)|size\s*[a-z0-9]+\s*.*only\s*\d+\s*left/i.exec( | |
| `${variantUrgencyText} ${haystack}`, | |
| ); | |
| const variantPressureMatch = /hurry.*selling\s*fast|running\s*out\s*fast|selling\s*fast/i.exec(`${variantUrgencyText} ${haystack}`); | |
| const freeDeliveryNudgeMatch = | |
| /add\s*₹?\s*\d+\s*(more)?\s*for\s*free\s*(shipping|delivery)|₹\s*\d+\s*more\s*for\s*free\s*(shipping|delivery)/i.exec(haystack); | |
| const platformFeeMatch = /convenience\s*fee|platform\s*fee/i.exec(haystack); | |
| return { | |
| isFashionMarketplace, | |
| sizeUrgencyMatch, | |
| variantPressureMatch, | |
| freeDeliveryNudgeMatch, | |
| platformFeeMatch, | |
| }; | |
| } | |
| function analyzeManipulationFlags(input: CheckoutAnalysisInput, haystack: string): ManipulationFlag[] { | |
| const products = Array.isArray(input.productCandidates) ? input.productCandidates : []; | |
| const flags: ManipulationFlag[] = []; | |
| if (products.some((product) => product?.timerResetsOnRefresh === true)) { | |
| flags.push({ | |
| type: "timer_reset", | |
| severity: "high", | |
| userMessage: "Fake Countdown Detected", | |
| evidence: "A countdown or timed pressure signal appears to reset on refresh.", | |
| actionAdvice: "Ignore the timer until the offer survives a refresh and remains consistent.", | |
| }); | |
| } | |
| if ( | |
| products.some((product) => | |
| typeof product?.originalPrice === "number" && | |
| typeof product?.listedPrice === "number" && | |
| product.originalPrice > 0 && | |
| product.listedPrice >= product.originalPrice * 0.95, | |
| ) | |
| ) { | |
| flags.push({ | |
| type: "price_inflation", | |
| severity: "medium", | |
| userMessage: "Inflated Original Price", | |
| evidence: "The advertised discount looks weak because the current price is very close to the original price.", | |
| actionAdvice: "Treat the markdown carefully and compare with recent price history or alternative merchants.", | |
| }); | |
| } | |
| if (/prime|membership|subscribe\s*to\s*save|join.*prime/i.test(haystack)) { | |
| flags.push({ | |
| type: "subscription_steering", | |
| severity: "low", | |
| userMessage: "Membership Pressure", | |
| evidence: "Subscription or membership benefits are being promoted alongside the purchase path.", | |
| actionAdvice: "Verify the baseline price without the membership prompt before deciding.", | |
| }); | |
| } | |
| return flags; | |
| } | |
| function inferCategoryHint(domain: string, haystack: string): CheckoutAnalysis["categoryHint"] { | |
| if (domain.includes("air") || haystack.includes("seat selection") || haystack.includes("flight")) return "airline"; | |
| if (domain.includes("hotel") || domain.includes("trip") || haystack.includes("resort fee") || haystack.includes("check-in")) return "hotel"; | |
| if (domain.includes("rental") || haystack.includes("airport pickup") || haystack.includes("daily rate")) return "car_rental"; | |
| return "marketplace"; | |
| } | |
| function isKnownNonCommerceDomain(domain: string): boolean { | |
| return NON_COMMERCE_DOMAINS.some((candidate) => domain === candidate || domain.endsWith(`.${candidate}`)); | |
| } | |
| function detectPageType( | |
| domain: string, | |
| url: string, | |
| haystack: string, | |
| priceCount: number, | |
| productCandidateCount: number, | |
| ): Pick<CheckoutAnalysis, "isShoppingPage" | "pageType" | "nonCommerceReason"> { | |
| if (isKnownNonCommerceDomain(domain)) { | |
| return { | |
| isShoppingPage: false, | |
| pageType: "non_commerce", | |
| nonCommerceReason: "Known non-commerce domain.", | |
| }; | |
| } | |
| const checkoutSignals = [ | |
| "checkout", | |
| "place order", | |
| "payment", | |
| "reserve now", | |
| "book now", | |
| "trip protection", | |
| "insurance", | |
| "shipping", | |
| "subtotal", | |
| "tax", | |
| "order summary", | |
| ].filter((keyword) => haystack.includes(keyword) || url.includes(keyword)); | |
| const cartSignals = ["cart", "basket", "bag", "order summary"].filter((keyword) => haystack.includes(keyword) || url.includes(keyword)); | |
| const productSignals = [ | |
| "add to cart", | |
| "add to bag", | |
| "buy now", | |
| "product details", | |
| "price", | |
| "results", | |
| "ratings", | |
| "customer review", | |
| "delivery options", | |
| "select size", | |
| "mrp", | |
| "sponsored", | |
| "today's deals", | |
| ].filter((keyword) => haystack.includes(keyword) || url.includes(keyword)); | |
| if (checkoutSignals.length >= 2 || (checkoutSignals.length >= 1 && priceCount >= 2)) { | |
| return { isShoppingPage: true, pageType: "checkout", nonCommerceReason: null }; | |
| } | |
| if (cartSignals.length >= 1 && priceCount >= 1) { | |
| return { isShoppingPage: true, pageType: "cart", nonCommerceReason: null }; | |
| } | |
| const hasProductUrlHint = /\/dp\/|\/gp\/product\/|\/product\/|\/p\/|\/buy|\/catalog\//.test(url); | |
| if (productCandidateCount >= 2) { | |
| return { isShoppingPage: true, pageType: "product", nonCommerceReason: null }; | |
| } | |
| if (productCandidateCount >= 1 && (priceCount >= 1 || productSignals.length >= 1 || hasProductUrlHint)) { | |
| return { isShoppingPage: true, pageType: "product", nonCommerceReason: null }; | |
| } | |
| if (productSignals.length >= 1 && priceCount >= 1) { | |
| return { isShoppingPage: true, pageType: "product", nonCommerceReason: null }; | |
| } | |
| return { | |
| isShoppingPage: false, | |
| pageType: "non_commerce", | |
| nonCommerceReason: "No meaningful commerce or checkout signals were found on the current page.", | |
| }; | |
| } | |
| export function analyzeCheckoutContext(input: CheckoutAnalysisInput): CheckoutAnalysis { | |
| const domain = normalizeDomain(input.domain); | |
| const url = normalizeText(input.url).toLowerCase(); | |
| const priceText = joinTexts(input.priceStrings); | |
| const timerText = joinTexts(input.timerElements); | |
| const stockText = joinTexts(input.stockAlerts); | |
| const buttonText = joinTexts(input.buttonLabels); | |
| const variantUrgencyText = joinTexts(input.variantUrgency); | |
| const bodyText = normalizeText(input.pageText).toLowerCase(); | |
| const haystack = [bodyText, timerText, stockText, variantUrgencyText, buttonText, priceText].filter(Boolean).join(" "); | |
| const priceCount = (input.priceStrings ?? []).filter((value) => /\d/.test(String(value))).length; | |
| const productCandidateCount = (input.productCandidates ?? []).filter((candidate) => normalizeText(candidate?.title).length > 0).length; | |
| const pageInfo = detectPageType(domain, url, haystack, priceCount, productCandidateCount); | |
| const categoryHint = inferCategoryHint(domain, haystack); | |
| if (!pageInfo.isShoppingPage) { | |
| return { | |
| ...pageInfo, | |
| categoryHint, | |
| trustScore: 95, | |
| falseUrgency: { detected: false, evidence: "", isTimerFake: null }, | |
| falseScarcity: { detected: false, evidence: "" }, | |
| confirmShaming: { detected: false, shamingText: "", rewrittenText: "" }, | |
| hiddenFees: { detected: false, feeItems: [], totalExtra: null }, | |
| preCheckedAddOns: { detected: false, fieldIds: [], addOnLabels: [] }, | |
| misdirection: { detected: false, hiddenDeclineText: "" }, | |
| manipulationFlags: [], | |
| totalPatternsDetected: 0, | |
| summary: "Guardian detected that this is not a commerce page, so it did not run checkout analysis.", | |
| }; | |
| } | |
| const falseUrgencyDetected = | |
| /expires|countdown|ends in|price lock|deal ends|last chance/i.test(timerText || haystack); | |
| const falseScarcityDetected = | |
| /only \d+|remaining|left at this price|people viewing|selling fast|booked in the last/i.test(stockText || haystack); | |
| const marketplaceSignals = detectMarketplaceContextSignals(domain, haystack, variantUrgencyText); | |
| const shamingTextMatch = | |
| /no thanks[^.]*|i accept the risk[^.]*|i don't care[^.]*|prefer to miss the best deal[^.]*/i.exec(haystack); | |
| const hiddenFeesDetected = | |
| /resort fee|service fee|processing fee|convenience fee|destination fee|mandatory fee|platform fee/i.test(haystack); | |
| const preCheckedAddOnDetected = | |
| /pre-checked|prechecked|\[pre-checked\]|\[prechecked\]|travel insurance|trip protection|protection plan/i.test(haystack); | |
| const misdirectionTextMatch = | |
| /tiny link|below fold|low-contrast|gray text|hidden decline|small text/i.exec(haystack); | |
| const totalPatternsDetected = [ | |
| falseUrgencyDetected, | |
| falseScarcityDetected || Boolean(marketplaceSignals.sizeUrgencyMatch) || Boolean(marketplaceSignals.variantPressureMatch), | |
| Boolean(shamingTextMatch), | |
| hiddenFeesDetected || Boolean(marketplaceSignals.platformFeeMatch), | |
| preCheckedAddOnDetected, | |
| Boolean(misdirectionTextMatch), | |
| Boolean(marketplaceSignals.freeDeliveryNudgeMatch), | |
| ].filter(Boolean).length; | |
| const trustScore = Math.max( | |
| 20, | |
| 92 - | |
| totalPatternsDetected * 12 - | |
| (pageInfo.pageType === "checkout" ? 0 : 6) - | |
| (marketplaceSignals.isFashionMarketplace && marketplaceSignals.sizeUrgencyMatch ? 4 : 0), | |
| ); | |
| const hiddenFeeAmount = | |
| hiddenFeesDetected || marketplaceSignals.platformFeeMatch | |
| ? categoryHint === "hotel" | |
| ? 24.99 | |
| : categoryHint === "airline" | |
| ? 19.99 | |
| : marketplaceSignals.platformFeeMatch | |
| ? 20 | |
| : 12.99 | |
| : null; | |
| const defaultAddOnLabel = | |
| categoryHint === "airline" ? "Trip Protection" : categoryHint === "hotel" ? "Travel Insurance" : "Protection Plan"; | |
| const falseScarcityEvidenceParts = [ | |
| falseScarcityDetected ? "Detected scarcity or crowd-pressure copy suggesting limited stock or high demand." : "", | |
| marketplaceSignals.sizeUrgencyMatch ? `Variant-level urgency detected: ${marketplaceSignals.sizeUrgencyMatch[0]}.` : "", | |
| marketplaceSignals.variantPressureMatch ? `Fast-moving apparel pressure detected: ${marketplaceSignals.variantPressureMatch[0]}.` : "", | |
| ].filter(Boolean); | |
| const hiddenFeeEvidenceParts = [ | |
| hiddenFeesDetected ? "Late fee disclosure language was detected in the checkout copy." : "", | |
| marketplaceSignals.platformFeeMatch ? `Platform fee warning detected: ${marketplaceSignals.platformFeeMatch[0]}.` : "", | |
| marketplaceSignals.freeDeliveryNudgeMatch ? `Free-delivery threshold nudge detected: ${marketplaceSignals.freeDeliveryNudgeMatch[0]}.` : "", | |
| ].filter(Boolean); | |
| const manipulationFlags = analyzeManipulationFlags(input, haystack); | |
| return { | |
| ...pageInfo, | |
| categoryHint, | |
| trustScore, | |
| falseUrgency: { | |
| detected: falseUrgencyDetected, | |
| evidence: falseUrgencyDetected ? "Detected countdown or expiry language that pressures faster checkout decisions." : "", | |
| isTimerFake: falseUrgencyDetected ? true : null, | |
| }, | |
| falseScarcity: { | |
| detected: falseScarcityDetected || Boolean(marketplaceSignals.sizeUrgencyMatch) || Boolean(marketplaceSignals.variantPressureMatch), | |
| evidence: falseScarcityEvidenceParts.join(" "), | |
| }, | |
| confirmShaming: { | |
| detected: Boolean(shamingTextMatch), | |
| shamingText: shamingTextMatch?.[0] ?? "", | |
| rewrittenText: shamingTextMatch ? "No thanks" : "", | |
| }, | |
| hiddenFees: { | |
| detected: hiddenFeesDetected || Boolean(marketplaceSignals.platformFeeMatch) || Boolean(marketplaceSignals.freeDeliveryNudgeMatch), | |
| feeItems: | |
| hiddenFeeAmount != null | |
| ? [ | |
| { | |
| label: marketplaceSignals.platformFeeMatch | |
| ? "Platform fee risk" | |
| : marketplaceSignals.freeDeliveryNudgeMatch | |
| ? "Delivery threshold pressure" | |
| : "Mandatory service fee", | |
| amount: hiddenFeeAmount, | |
| }, | |
| ] | |
| : [], | |
| totalExtra: hiddenFeeAmount, | |
| }, | |
| preCheckedAddOns: { | |
| detected: preCheckedAddOnDetected, | |
| fieldIds: preCheckedAddOnDetected ? ["default-addon"] : [], | |
| addOnLabels: preCheckedAddOnDetected ? [defaultAddOnLabel] : [], | |
| }, | |
| misdirection: { | |
| detected: Boolean(misdirectionTextMatch), | |
| hiddenDeclineText: misdirectionTextMatch?.[0] ?? "", | |
| }, | |
| manipulationFlags, | |
| totalPatternsDetected, | |
| summary: | |
| totalPatternsDetected === 0 | |
| ? "No major dark patterns were detected in this checkout flow." | |
| : `Detected ${totalPatternsDetected} dark pattern signal${totalPatternsDetected === 1 ? "" : "s"} in this checkout flow.${hiddenFeeEvidenceParts.length ? ` ${hiddenFeeEvidenceParts.join(" ")}` : ""}`, | |
| }; | |
| } | |