├── .cursorrules ├── .gitignore ├── README.md ├── babel.config.js ├── builds └── .gitkeep ├── docs └── GAMEPASS.md ├── extension ├── LICENSE ├── assets │ ├── chrome.png │ ├── icon.png │ ├── icon128.png │ ├── icon16.png │ ├── icon32.png │ ├── icon48.png │ └── impuestito.svg ├── config │ ├── config.css │ ├── menu.js │ └── sidepanel.html ├── core │ ├── background.js │ ├── helpers.js │ ├── helpers.test.js │ └── index.js ├── gamepass │ ├── gamepass.css │ ├── index.js │ ├── menu.js │ └── steam.js └── prices │ ├── dekudeals.js │ ├── ea.js │ ├── epic.js │ ├── gog.js │ ├── green-man-gaming.js │ ├── humble-bundle.js │ ├── isthereanydeal.js │ ├── nintendo.js │ ├── ntdeals.js │ ├── playstation.js │ ├── prices.css │ ├── psdeals.js │ ├── steam.js │ ├── ubisoft.js │ ├── xbdeals.js │ └── xbox.js ├── manifest-chromium.json ├── manifest-firefox.json ├── media ├── chrome-store-1.jpg ├── chrome-store-2.jpg ├── chrome-store-3.jpg ├── chrome-store-4.jpg ├── chrome-store-5.jpg ├── chrome-store-6.jpg ├── chrome-store-7.jpg └── chrome-store-8.jpg ├── mocks ├── gamepass_api │ ├── ids.js │ └── query.json └── impuestito_api │ └── impuestito.json ├── package-lock.json ├── package.json └── scripts ├── bump-manifest-version.js └── compress-extension.js /.cursorrules: -------------------------------------------------------------------------------- 1 | You are an expert in Chrome Extension Development, JavaScript, TypeScript, HTML, CSS, Shadcn UI, Radix UI, Tailwind and Web APIs.Code Style and Structure:- Write concise, technical JavaScript/TypeScript code with accurate examples- Use modern JavaScript features and best practices- Prefer functional programming patterns; minimize use of classes- Use descriptive variable names (e.g., isExtensionEnabled, hasPermission)- Structure files: manifest.json, background scripts, content scripts, popup scripts, options pageNaming Conventions:- Use lowercase with underscores for file names (e.g., content_script.js, background_worker.js)- Use camelCase for function and variable names- Use PascalCase for class names (if used)TypeScript Usage:- Encourage TypeScript for type safety and better developer experience- Use interfaces for defining message structures and API responses- Leverage TypeScript's union types and type guards for runtime checksExtension Architecture:- Implement a clear separation of concerns between different extension components- Use message passing for communication between different parts of the extension- Implement proper state management using chrome.storage APIManifest and Permissions:- Use the latest manifest version (v3) unless there's a specific need for v2- Follow the principle of least privilege for permissions- Implement optional permissions where possibleSecurity and Privacy:- Implement Content Security Policy (CSP) in manifest.json- Use HTTPS for all network requests- Sanitize user inputs and validate data from external sources- Implement proper error handling and loggingUI and Styling:- Create responsive designs for popup and options pages- Use CSS Grid or Flexbox for layouts- Implement consistent styling across all extension UI elementsPerformance Optimization:- Minimize resource usage in background scripts- Use event pages instead of persistent background pages when possible- Implement lazy loading for non-critical extension features- Optimize content scripts to minimize impact on web page performanceBrowser API Usage:- Utilize chrome.* APIs effectively (e.g., chrome.tabs, chrome.storage, chrome.runtime)- Implement proper error handling for all API calls- Use chrome.alarms for scheduling tasks instead of setIntervalCross-browser Compatibility:- Use WebExtensions API for cross-browser support where possible- Implement graceful degradation for browser-specific featuresTesting and Debugging:- Utilize Chrome DevTools for debugging- Implement unit tests for core extension functionality- Use Chrome's built-in extension loading for testing during developmentContext-Aware Development:- Always consider the whole project context when providing suggestions or generating code- Avoid duplicating existing functionality or creating conflicting implementations- Ensure that new code integrates seamlessly with the existing project structure and architecture- Before adding new features or modifying existing ones, review the current project state to maintain consistency and avoid redundancy- When answering questions or providing solutions, take into account previously discussed or implemented features to prevent contradictions or repetitionsCode Output:- When providing code, always output the entire file content, not just new or modified parts- Include all necessary imports, declarations, and surrounding code to ensure the file is complete and functional- Provide comments or explanations for significant changes or additions within the file- If the file is too large to reasonably include in full, provide the most relevant complete section and clearly indicate where it fits in the larger file structureFollow Chrome Extension documentation for best practices, security guidelines, and API usage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.zip 3 | *.tar 4 | *.tar.gz 5 | node_modules 6 | .env 7 | .env.local 8 | .env.json 9 | *manifest.json 10 | *.pem 11 | *.crx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # impuestito - Te muestra el precio final de todos los videojuegos con impuestos incluidos en más de 14 tiendas gaming 3 | 4 | ![](https://img.shields.io/twitter/follow/impuestito_org) 5 | ![](https://img.shields.io/github/stars/lucasromerodb/impuestito-extension) 6 | ![](https://img.shields.io/chrome-web-store/v/kodbfkngjgckpmipedoomkdhhihioaio) 7 | ![Mozilla Add-on Version](https://img.shields.io/amo/v/impuestito) 8 | ![](https://img.shields.io/chrome-web-store/last-updated/kodbfkngjgckpmipedoomkdhhihioaio) 9 | 10 | 11 | 12 | ![impuestito](/media/chrome-store-2.jpg "Impuestito Cover") 13 | 14 | > [!NOTE] 15 | > Podes ver más en [impuestito.org/gaming](https://impuestito.org/gaming?utm_source=github) 16 | 17 | Impuestito es una extensión para tu navegador que te muestra el precio final de los juegos de +14 tiendas distintas. Vas a ver el precio con todos los impuestos incluidos. 18 | 19 | 20 | ## ⏬ Instrucciones de instalación 21 | 22 | 1. [Instalá la extensión desde acá](https://chrome.google.com/webstore/detail/impuestito/kodbfkngjgckpmipedoomkdhhihioaio/related?hl=es) en tu navegador 23 | 2. ¡Listo! ya podes navegar por más de 14 stores de videojuegos viendo el precio final 24 | 3. [Seguí Impuestito en X (Twitter)](https://twitter.com/impuestito_org) 25 | 4. [Ver más imágenes abajo](#imagenes-promocionales) 🖼️ 26 | 27 | 28 | ## 🛍️ ¿En que tiendas funciona? 29 | 30 | - ✅ [GOG](https://www.gog.com/) 31 | - ✅ [Xbox (PC y Consola)](https://www.xbox.com/es-AR/games/browse) 32 | - ⏳ [Microsoft Store](https://www.microsoft.com/es-ar/store/deals/games/pc) (próximamente) 33 | - ✅ [PlayStation](https://store.playstation.com/es-ar/category/3f772501-f6f8-49b7-abac-874a88ca4897) 34 | - ✅ [Nintendo (AR)](https://www.nintendo.com/es-ar/) 35 | - ✅ [Epic Games Store](https://store.epicgames.com/en-US/) 36 | - ✅ [Green Man Gaming](https://www.greenmangaming.com/) (soporte parcial) 37 | - ✅ [Humble Bundle](https://humblebundle.com) (soporte parcial) 38 | - ✅ [XBDeals](https://xbdeals.net/ar-store) 39 | - ✅ [PSDeals](https://psdeals.net/ar-store) 40 | - ✅ [NTDeals](https://ntdeals.net/us-store) 41 | - ✅ [DekuDeals](https://www.dekudeals.com/) 42 | - ✅ [Ubisoft](https://store.ubisoft.com/ofertas/home?lang=es_AR) (soporte parcial) 43 | - ✅ [EA Play](https://www.ea.com/es-es/ea-play) 44 | - ✅ [Is There Any Deal](https://isthereanydeal.com/) (soporte parcial) 45 | - ⏳ [Battle.net](https://us.shop.battle.net/en-us) (próximamente) 46 | 47 | ### 🌎 Navegadores soportados 48 | 49 | - Google Chrome 50 | - Microsoft Edge 51 | - OperaGX 52 | - Opera 53 | - Brave 54 | - Chromium 55 | 56 | > Nota: debería ser compatible con cualquier browser basado en [Chromium]() 57 | 58 | ## 🤔 ¿Qué hace técnicamente Impuestito? 59 | 60 | Impuestito escanea el precio original de cada videojuego en más de 14 stores y hace un cálculo automáticamente en base a la cotización del dólar y los impuestos que te cobran (pueden ser IVA, Ganancias, IIBB) y luego reemplaza el valor original para mostrar el precio final de cada juego en la tienda sin que necesites hacer cálculos. Vas a poder ver todos los precios finales al mismo tiempo. Si tiene el logo de impuestito, quiere decir que está funcionando! 61 | 62 | > Los impuestos que se calculan en la extensión corresponden al impuesto IVA, Ganancias e IIBB (el porcentaje de impuestos puede variar dependiendo el lugar donde vivas) 63 | 64 | [![Invitame un café en cafecito.app](https://cdn.cafecito.app/imgs/buttons/button_2.svg)](https://cafecito.app/impuestito) 65 | 66 | 67 | ### 🎯 Roadmap de funcionalidades 68 | 69 | - ✅ Compatible con más de 14 tiendas y portales 70 | - ⏳ Completar el soporte para tiendas que tienen soporte parcial 71 | - ⏳ Personalización de impuestos 72 | - ⏳ Integración de detección para saber si el juego está en Game Pass 73 | - ⏳ Comparador de juegos con otras tiendas 74 | - ⏳ Versión para Firefox 75 | - ⏳ Soporte para Amazon 76 | - ⏳ Soporte para Tiendamia 77 | - ⏳ Soporte para AliExpress 78 | - ⏳ Soporte para Steam (solo para recomendar Steamcito) 79 | 80 | 81 | ## ¿Tenés alguna idea o problema? 82 | 83 | Si tenés alguna idea interesante o encontraste algún problema con la extensión: 84 | 85 | - 🐞 Podés hacerlo [creando un issue acá](https://github.com/lucasromerodb/impuestito/issues/new/choose) 86 | - 🗨️ También podés [escribime en Twitter](https://twitter.com/impuestito_org) 87 | 88 | ![](https://img.shields.io/twitter/follow/impuestito_org) 89 | 90 | 91 | ## ¿Buscas algo similar para Steam? 92 | 93 | Hace tiempo existe 👑 [Steamcito](https://github.com/emilianog94/Steamcito-Precios-Steam-Argentina-Impuestos-Incluidos), la extensión que calcula los precios con impuestos y que además te permite personalizarlos. 94 | 95 | > Nota: Impuestito se basa en la misma idea que Steamcito pero para otras tiendas. 96 | 97 | ## Imagenes promocionales 98 | 99 | ![Impuestito](/media/chrome-store-4.jpg "Impuestito Cover") 100 | ![Impuestito](/media/chrome-store-5.jpg "Impuestito Cover") 101 | ![Impuestito](/media/chrome-store-6.jpg "Impuestito Cover") 102 | ![Impuestito](/media/chrome-store-7.jpg "Impuestito Cover") 103 | ![Impuestito](/media/chrome-store-8.jpg "Impuestito Cover") 104 | ![Impuestito](/media/chrome-store-3.jpg "Impuestito Cover") 105 | 106 | ![](https://img.shields.io/twitter/follow/impuestito_org) -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { 4 | targets: { 5 | node: 'current', 6 | }, 7 | }], 8 | ], 9 | }; -------------------------------------------------------------------------------- /builds/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/builds/.gitkeep -------------------------------------------------------------------------------- /docs/GAMEPASS.md: -------------------------------------------------------------------------------- 1 | # Available on Game Pass 2 | 3 | List of all GP Console games: 4 | https://catalog.gamepass.com/sigls/v2?id=f6f1f99f-9b49-4ccd-b3bf-4d9767a77f5e&language=en-us&market=US 5 | 6 | List of all GP PC games: 7 | https://catalog.gamepass.com/sigls/v2?id=fdd9e2a7-0fee-49f6-ad69-4354098401ff&language=en-us&market=US 8 | 9 | List of EA Play games: 10 | https://catalog.gamepass.com/sigls/v2?id=fdd9e2a7-0fee-49f6-ad69-4354098401ff&language=en-us&market=US 11 | 12 | Play without a controller (Cloud): 13 | https://catalog.gamepass.com/sigls/v2?id=7d8e8d56-c02f-4711-afec-73a80d8e9261&language=en-us&market=US 14 | 15 | List of all GP games: 16 | https://catalog.gamepass.com/sigls/v2?id=29a81209-df6f-41fd-a528-2ae6b91f719c&language=en-us&market=US 17 | 18 | --- 19 | 20 | ## API requests 21 | 22 | POST request to 23 | 24 | ``` 25 | https://catalog.gamepass.com/products?market=US&language=en-US&hydration=MobileDetailsForConsole 26 | ``` 27 | 28 | POST body request 29 | 30 | ```json 31 | { 32 | "Products": ["BQ1W1T1FC14W", "C3KLDKZBHNCZ", "BS6WJ2L56B10"] 33 | } 34 | ``` -------------------------------------------------------------------------------- /extension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Luke ✨ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extension/assets/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/extension/assets/chrome.png -------------------------------------------------------------------------------- /extension/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/extension/assets/icon.png -------------------------------------------------------------------------------- /extension/assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/extension/assets/icon128.png -------------------------------------------------------------------------------- /extension/assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/extension/assets/icon16.png -------------------------------------------------------------------------------- /extension/assets/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/extension/assets/icon32.png -------------------------------------------------------------------------------- /extension/assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/extension/assets/icon48.png -------------------------------------------------------------------------------- /extension/assets/impuestito.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /extension/config/config.css: -------------------------------------------------------------------------------- 1 | 2 | /* Trigger */ 3 | 4 | .impuestito-menu__trigger { 5 | z-index: 999; 6 | position: fixed; 7 | bottom: 16px; 8 | left: 50%; 9 | transform: translateX(-50%) !important; 10 | 11 | display: flex; 12 | background-color: white; 13 | border: 1px solid silver; 14 | box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.3); 15 | 16 | padding: 8px 16px; 17 | border-radius: 100px; 18 | 19 | transition: all 0.15s ease-out; 20 | cursor: pointer; 21 | 22 | animation: slideUpBounce 0.75s cubic-bezier(0.68, -0.6, 0.32, 1.6) forwards, bounce 2s ease-in-out infinite 1s !important; 23 | } 24 | 25 | .impuestito-menu__trigger:hover { 26 | padding: 16px 28px; 27 | } 28 | 29 | .impuestito-menu__trigger svg { 30 | width: 100px; 31 | height: 22px; 32 | } 33 | 34 | /* Menu */ 35 | 36 | .impuestito-menu__config { 37 | z-index: 998; 38 | position: fixed; 39 | bottom: 80px; 40 | left: 50%; 41 | 42 | display: flex; 43 | flex-direction: row; 44 | background-color: white; 45 | border: 1px solid silver; 46 | box-shadow: 0 10px 100px rgba(0, 0, 0, 0.4); 47 | border-radius: 20px; 48 | overflow: hidden; 49 | 50 | max-width: 480px; 51 | transform: translateX(-50%) translateY(0) scale(1) !important; 52 | color: black !important; 53 | 54 | transition: all 0.5s cubic-bezier(0.85, 0, 0.15, 1); 55 | } 56 | 57 | .impuestito-menu__config.hidden { 58 | bottom: 16px; 59 | transform: translateX(-50%) translateY(15px) scale(0.2) !important; 60 | transform-origin: bottom center; 61 | border-radius: 600px; 62 | opacity: 0; 63 | pointer-events: none; 64 | } 65 | 66 | .impuestito-menu__config a, .impuestito-menu__config label { 67 | font-size: 14px !important; 68 | } 69 | 70 | .impuestito-menu__config__section { 71 | display: flex; 72 | flex-direction: column; 73 | align-items: flex-start; 74 | justify-content: start; 75 | 76 | width: 260px; 77 | padding: 24px; 78 | 79 | font-size: 14px; 80 | } 81 | 82 | .impuestito-menu__config__section--left { 83 | gap: 20px !important; 84 | border-right: 1px solid silver; 85 | } 86 | 87 | .impuestito-menu__config__section--right { 88 | gap: 12px !important; 89 | } 90 | 91 | .impuestito-menu__config__form-group { 92 | width: 100%; 93 | } 94 | 95 | .impuestito-menu__config__form-group label { 96 | display: block; 97 | font-size: 18px; 98 | margin-bottom: 8px; 99 | } 100 | 101 | .impuestito-menu__config__form-group select { 102 | width: 100% !important; 103 | padding: 10px 12px !important; 104 | 105 | border: 1px solid silver; 106 | border-radius: 8px; 107 | 108 | background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); 109 | background-repeat: no-repeat; 110 | background-position: right 15px center; 111 | background-size: 15px; 112 | 113 | appearance: none; 114 | cursor: pointer; 115 | } 116 | 117 | .impuestito-menu__config__form-group select:focus:not(:disabled), 118 | .impuestito-menu__config__form-group select:hover:not(:disabled) { 119 | border-color: black; 120 | box-shadow: 0 0 2px black; 121 | } 122 | 123 | .impuestito-menu__config__form-group select:disabled { 124 | cursor: not-allowed; 125 | } 126 | 127 | .impuestito-menu__config a { 128 | display: block; 129 | text-decoration: none; 130 | color: black !important; 131 | } 132 | 133 | .impuestito-menu__config a:hover { 134 | color: #00BE5C !important; 135 | text-decoration: underline; 136 | } 137 | 138 | .impuestito-menu__config__title { 139 | font-size: 18px; 140 | font-weight: bold; 141 | } 142 | 143 | .impuestito-menu__config__version { 144 | padding: 4px 12px; 145 | background-color: silver; 146 | border-radius: 50px; 147 | 148 | margin-bottom: 12px; 149 | } 150 | 151 | .impuestito-menu__overlay { 152 | z-index: 997; 153 | position: fixed; 154 | bottom: 0; 155 | left: 50%; 156 | transform: translateX(-50%) translateY(50%) scale(1) !important; 157 | 158 | width: 600px; 159 | height: 600px; 160 | background: radial-gradient(circle, rgba(0, 190, 92, 0.7) 0%, transparent 70%); 161 | pointer-events: none !important; 162 | transition: all 0.5s cubic-bezier(0.85, 0, 0.15, 1); 163 | } 164 | 165 | .impuestito-menu__overlay.hidden { 166 | transform: translateX(-50%) translateY(50%) scale(0) !important; 167 | opacity: 0; 168 | pointer-events: none !important; 169 | } 170 | 171 | .impuestito-menu__amazon-recommendation { 172 | display: flex; 173 | flex-direction: row; 174 | gap: 8px !important; 175 | } 176 | 177 | .impuestito-menu__amazon-recommendation a { 178 | width: 50px; 179 | height: 50px; 180 | 181 | border-radius: 6px; 182 | border: 1px solid silver; 183 | 184 | overflow: hidden; 185 | transition: all 0.15s ease-out; 186 | } 187 | 188 | .impuestito-menu__amazon-recommendation a:hover { 189 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 190 | transform: scale(1.05); 191 | } 192 | 193 | .impuestito-menu__amazon-recommendation img { 194 | width: 100%; 195 | height: 100%; 196 | object-fit: cover; 197 | } 198 | 199 | @media screen and (max-width: 480px) { 200 | .impuestito-menu__config { 201 | flex-direction: column; 202 | width: 95%; 203 | } 204 | 205 | .impuestito-menu__trigger:hover { 206 | padding: 8px 16px; 207 | } 208 | 209 | .impuestito-menu__config__section { 210 | width: auto; 211 | } 212 | 213 | .impuestito-menu__config__section--left { 214 | border-right: none; 215 | } 216 | } 217 | 218 | @keyframes slideUpBounce { 219 | 0% { 220 | transform: translateX(-50%) translateY(100%); 221 | opacity: 0; 222 | } 223 | 100% { 224 | transform: translateX(-50%) translateY(0); 225 | opacity: 1; 226 | } 227 | } 228 | 229 | @keyframes bounce { 230 | 0%, 100% { 231 | transform: translateX(-50%) translateY(0); 232 | } 233 | 25% { 234 | transform: translateX(-50%) translateY(-4px); 235 | } 236 | } 237 | 238 | -------------------------------------------------------------------------------- /extension/config/menu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a new div element with the class "impuestito-menu" and appends it to the body of the document. 3 | * This function is used to dynamically add a playground area to the webpage for the "impuestito-extension". 4 | */ 5 | async function initMenu(text) { 6 | // sync: is used to store or get data across devices 7 | // local: is used to store or get data like localstorage 8 | const responseSync = await chrome.storage.sync.get(['userConfig','market']) 9 | const responseLocal = await chrome.storage.local.get(['impuestito']) 10 | 11 | // If no response, try to get data again 12 | if(!responseSync || !responseLocal) { 13 | console.log('📎 Intentando obtener datos nuevamente') 14 | initMenu() 15 | }; 16 | 17 | 18 | 19 | // Initialize user config if not set 20 | if (!responseSync.userConfig) { 21 | console.log("🙋‍♂️ Initializing user config"); 22 | chrome.storage.sync.set({ 23 | userConfig: { 24 | selectedProvince: 'AR-C', 25 | selectedPaymentMethod: 'tarjeta', 26 | } 27 | }) 28 | } 29 | 30 | // Create elements 31 | const trigger = document.createElement("div"); 32 | const menu = document.createElement("div"); 33 | const overlay = document.createElement("svg"); 34 | 35 | // Add classes and event listeners 36 | overlay.classList.add("impuestito-menu__overlay", "hidden"); 37 | overlay.addEventListener("click", () => { 38 | menu.classList.toggle("hidden"); 39 | overlay.classList.toggle("hidden"); 40 | }); 41 | 42 | // Add classes and event listeners 43 | trigger.classList.add("impuestito-menu__trigger"); 44 | trigger.addEventListener("click", () => { 45 | // Toggle menu 46 | menu.classList.toggle("hidden"); 47 | overlay.classList.toggle("hidden"); 48 | }); 49 | 50 | // Close menu when clicking outside 51 | document.addEventListener("click", (event) => { 52 | const isClickInside = menu.contains(event.target) || trigger.contains(event.target); 53 | if (!isClickInside) { 54 | menu.classList.add("hidden"); 55 | overlay.classList.add("hidden"); 56 | } 57 | }); 58 | 59 | // Close menu when pressing escape key 60 | document.addEventListener("keydown", (event) => { 61 | if (event.key === "Escape") { 62 | menu.classList.add("hidden"); 63 | overlay.classList.add("hidden"); 64 | } 65 | }); 66 | 67 | // Add SVG icon (impuestito logo) 68 | trigger.innerHTML = ` 69 | 70 | 71 | 72 | 73 | 74 | 75 | ` 76 | 77 | // Menu markup 78 | const menuHTML = ` 79 |
80 | 81 |
Configuración
82 | 83 |
84 | 85 | 87 |
88 | 89 |
90 | 91 | 93 |
94 | 95 |
96 | 97 |
98 | ${ 99 | isFirefox() 100 | ? ` 101 | Mozilla Add-on Version 102 | ` 103 | : ` 104 | Chrome Web Store Version 105 | ` 106 | } 107 | 108 | X (formerly Twitter) Follow 109 | 110 | 121 | Más accesorios gaming 122 | Amazon Prime Gaming 123 |
124 | ` 125 | 126 | menu.classList.add("impuestito-menu__config", "hidden"); 127 | menu.innerHTML = menuHTML; 128 | 129 | // Insert elements into the DOM 130 | document.querySelector("body").insertAdjacentElement("beforeend", trigger); 131 | document.querySelector("body").insertAdjacentElement("beforeend", menu); 132 | document.querySelector("body").insertAdjacentElement("beforeend", overlay); 133 | 134 | // Log welcome message 135 | logWelcomeMessage({ store: text || "tu navegador" }); 136 | 137 | // Set data-impuestito attribute 138 | document.querySelector('body').setAttribute('data-impuestito', Date.now()); 139 | 140 | // Initialize select options 141 | if (responseLocal.impuestito) { 142 | const provinces = Object.fromEntries( 143 | Object.entries(responseLocal.impuestito.province).sort(([,a], [,b]) => a.name.localeCompare(b.name)) 144 | ) 145 | 146 | // Initialize select options 147 | const provinceSelect = menu.querySelector('#impuestito-menu__config__select-province'); 148 | const paymentMethodSelect = menu.querySelector('#impuestito-menu__config__select-payment-method'); 149 | 150 | Object.keys(provinces).map(key => { 151 | provinceSelect.appendChild(new Option(`${provinces[key].name} (${(provinces[key].tax*100).toFixed(1)}%)`, key)); 152 | }); 153 | 154 | handleProvinceChange(responseSync.userConfig.selectedProvince); 155 | 156 | paymentMethodSelect.appendChild(new Option('Tarjeta', 'tarjeta', true, true)); 157 | paymentMethodSelect.appendChild(new Option('Dólar MEP', 'mep')); 158 | paymentMethodSelect.appendChild(new Option('Crypto', 'crypto')); 159 | } 160 | 161 | // Province change 162 | menu.querySelector('#impuestito-menu__config__select-province').addEventListener('change', (e) => { 163 | console.log("🗺️ Changing province", e.target.value); 164 | 165 | handleProvinceChange(e.target.value); 166 | 167 | chrome.storage.sync.set({ 168 | userConfig: { 169 | ...responseSync.userConfig, 170 | selectedProvince: e.target.value, 171 | } 172 | }); 173 | 174 | // Force window reload to apply province change 175 | window.location.reload(); 176 | }); 177 | } 178 | 179 | function handleProvinceChange(province) { 180 | const menu = document.querySelector('.impuestito-menu__config'); 181 | const provinceSelect = menu.querySelector('#impuestito-menu__config__select-province'); 182 | // Remove selected attribute from all options 183 | Array.from(provinceSelect.options).forEach(option => { 184 | option.selected = false; 185 | if (option.value === province) { 186 | option.selected = true; 187 | } 188 | }); 189 | } 190 | 191 | function isFirefox() { 192 | return navigator.userAgent.toLowerCase().includes('firefox'); 193 | } 194 | 195 | // Hacer una donación 196 | // Ver más en el sitio web -------------------------------------------------------------------------------- /extension/config/sidepanel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Impuestito 7 | 52 | 53 | 54 | 55 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /extension/core/background.js: -------------------------------------------------------------------------------- 1 | const API_URL_REGEX = /^(?:\/)?(https?:\/\/[^\/]+)(?:\/)?$/; 2 | const IMPUESTITO_API_URL = chrome.runtime.getManifest().web_accessible_resources[0].resources[0].replace(API_URL_REGEX, '$1'); 3 | const GAMEPASS_API_URL = chrome.runtime.getManifest().web_accessible_resources[0].resources[1].replace(API_URL_REGEX, '$1'); 4 | 5 | console.log(IMPUESTITO_API_URL); 6 | console.log(GAMEPASS_API_URL); 7 | 8 | if (chrome.sidePanel) { 9 | chrome.sidePanel 10 | .setPanelBehavior({ openPanelOnActionClick: true }) 11 | .catch((error) => console.error(error)); 12 | } 13 | 14 | // COUNTRY LOCALES: https://saimana.com/list-of-country-locale-code/ 15 | // Xbox supported markets regions 16 | const markets = [ 17 | { name: "Albania", lang: "sq", region: "al", tax: 1 }, 18 | { name: "Algeria", lang: "ar", region: "dz", tax: 1 }, 19 | { name: "Argentina", lang: "es", region: "ar", tax: 2.55 }, 20 | { name: "Australia", lang: "en", region: "au", tax: 1 }, 21 | { name: "Austria", lang: "de", region: "at", tax: 1 }, 22 | { name: "Bahrain", lang: "ar", region: "bh", tax: 1 }, 23 | { name: "Belgium", lang: "nl", region: "be", tax: 1 }, 24 | { name: "Bolivia", lang: "es", region: "bo", tax: 1 }, 25 | { name: "Bosnia and Herzegovina", lang: "bs", region: "ba", tax: 1 }, 26 | { name: "Brazil", lang: "pt", region: "br", tax: 1 }, 27 | { name: "Bulgaria", lang: "bg", region: "bg", tax: 1 }, 28 | { name: "Canada", lang: "en", region: "ca", tax: 1 }, 29 | { name: "Chile", lang: "es", region: "cl", tax: 1 }, 30 | // { name: "China", lang: "zh", region: "cn", tax: 1 }, 31 | { name: "Colombia", lang: "es", region: "co", tax: 1 }, 32 | { name: "Costa Rica", lang: "es", region: "cr", tax: 1 }, 33 | { name: "Croatia", lang: "hr", region: "hr", tax: 1 }, 34 | { name: "Cyprus", lang: "el", region: "cy", tax: 1 }, 35 | { name: "Czechia", lang: "cs", region: "cz", tax: 1 }, 36 | { name: "Denmark", lang: "da", region: "dk", tax: 1 }, 37 | { name: "Ecuador", lang: "es", region: "ec", tax: 1 }, 38 | { name: "Egypt", lang: "ar", region: "eg", tax: 1 }, 39 | { name: "El Salvador", lang: "es", region: "sv", tax: 1 }, 40 | { name: "Estonia", lang: "et", region: "ee", tax: 1 }, 41 | { name: "Finland", lang: "fi", region: "fi", tax: 1 }, 42 | { name: "France", lang: "fr", region: "fr", tax: 1 }, 43 | { name: "Georgia", lang: "ka", region: "ge", tax: 1 }, 44 | { name: "Germany", lang: "de", region: "de", tax: 1 }, 45 | { name: "Greece", lang: "el", region: "gr", tax: 1 }, 46 | { name: "Guatemala", lang: "es", region: "gt", tax: 1 }, 47 | { name: "Honduras", lang: "es", region: "hn", tax: 1 }, 48 | { name: "Hong Kong SAR", lang: "zh", region: "hk", tax: 1 }, 49 | { name: "Hungary", lang: "hu", region: "hu", tax: 1 }, 50 | { name: "Iceland", lang: "is", region: "is", tax: 1 }, 51 | { name: "India", lang: "hi", region: "in", tax: 1 }, 52 | { name: "Indonesia ", lang: "id", region: "id", tax: 1 }, 53 | { name: "Ireland", lang: "en", region: "ie", tax: 1 }, 54 | { name: "Israel", lang: "he", region: "il", tax: 1 }, 55 | { name: "Italy", lang: "it", region: "it", tax: 1 }, 56 | { name: "Japan", lang: "ja", region: "jp", tax: 1 }, 57 | { name: "Korea", lang: "ko", region: "kr", tax: 1 }, 58 | { name: "Kuwait", lang: "ar", region: "kw", tax: 1 }, 59 | { name: "Latvia", lang: "lv", region: "lv", tax: 1 }, 60 | { name: "Libya", lang: "ar", region: "ly", tax: 1 }, 61 | { name: "Liechtenstein", lang: "de", region: "li", tax: 1 }, 62 | { name: "Lithuania", lang: "lt", region: "lt", tax: 1 }, 63 | { name: "Luxembourg", lang: "fr", region: "lu", tax: 1 }, 64 | { name: "Malaysia ", lang: "ms", region: "my", tax: 1 }, 65 | { name: "Malta", lang: "mt", region: "mt", tax: 1 }, 66 | { name: "Mexico", lang: "es", region: "mx", tax: 1 }, 67 | { name: "Moldova", lang: "ro", region: "md", tax: 1 }, 68 | { name: "Montenegro", lang: "sr", region: "me", tax: 1 }, 69 | { name: "Morocco", lang: "ar", region: "ma", tax: 1 }, 70 | { name: "Netherlands", lang: "nl", region: "nl", tax: 1 }, 71 | { name: "New Zealand", lang: "en", region: "nz", tax: 1 }, 72 | { name: "Nicaragua", lang: "es", region: "ni", tax: 1 }, 73 | { name: "Norway", lang: "no", region: "no", tax: 1 }, 74 | { name: "Oman", lang: "ar", region: "om", tax: 1 }, 75 | { name: "Panama", lang: "es", region: "pa", tax: 1 }, 76 | { name: "Paraguay", lang: "es", region: "py", tax: 1 }, 77 | { name: "Peru", lang: "es", region: "pe", tax: 1 }, 78 | { name: "Philippines ", lang: "fil", region: "ph", tax: 1 }, 79 | { name: "Poland", lang: "pl", region: "pl", tax: 1 }, 80 | { name: "Portugal", lang: "pt", region: "pt", tax: 1 }, 81 | { name: "Qatar", lang: "ar", region: "qa", tax: 1 }, 82 | { name: "Republic of North Macedonia", lang: "mk", region: "mk", tax: 1 }, 83 | { name: "Romania", lang: "ro", region: "ro", tax: 1 }, 84 | { name: "Russia", lang: "ru", region: "ru", tax: 1 }, 85 | { name: "Saudi Arabia", lang: "ar", region: "sa", tax: 1 }, 86 | { name: "Serbia", lang: "sr", region: "rs", tax: 1 }, 87 | { name: "Singapore", lang: "en", region: "sg", tax: 1 }, 88 | { name: "Slovakia", lang: "sk", region: "sk", tax: 1 }, 89 | { name: "Slovenia", lang: "sl", region: "si", tax: 1 }, 90 | { name: "South Africa", lang: "en", region: "za", tax: 1 }, 91 | { name: "Spain", lang: "es", region: "es", tax: 1 }, 92 | { name: "Sweden", lang: "sv", region: "se", tax: 1 }, 93 | { name: "Switzerland", lang: "de", region: "ch", tax: 1 }, 94 | { name: "Taiwan", lang: "zh", region: "tw", tax: 1 }, 95 | { name: "Thailand ", lang: "th", region: "th", tax: 1 }, 96 | { name: "Tunisia", lang: "ar", region: "tn", tax: 1 }, 97 | { name: "Turkey", lang: "tr", region: "tr", tax: 1 }, 98 | { name: "Ukraine", lang: "uk", region: "ua", tax: 1 }, 99 | { name: "United Arab Emirates", lang: "ar", region: "ae", tax: 1 }, 100 | { name: "United Kingdom", lang: "en", region: "gb", tax: 1 }, 101 | { name: "United States", lang: "en", region: "us", tax: 1 }, 102 | { name: "Uruguay", lang: "es", region: "uy", tax: 1 }, 103 | { name: "Vietnam", lang: "vi", region: "vn", tax: 1 }, 104 | ]; 105 | 106 | async function requestTaxes() { 107 | console.log("Requesting taxes...⏳"); 108 | try { 109 | const response = await fetch(`${IMPUESTITO_API_URL}/impuestito`, {}); 110 | const data = await response.json(); 111 | chrome.storage.local.set({ impuestito: data }); 112 | console.log("Requesting taxes done ✅"); 113 | } catch (error) { 114 | console.log("Error ❌"); 115 | console.error("Error fetching taxes:", error); 116 | } 117 | } 118 | 119 | async function requestGamePass() { 120 | console.log("Requesting gamepass...⏳"); 121 | try { 122 | // This Game Pass API is Open Source and you can host on your own! https://github.com/lucasromerodb/xbox-store-api 123 | const response = await fetch(`${GAMEPASS_API_URL}/api/gamepass/extension`, {}); 124 | const data = await response.json(); 125 | chrome.storage.local.set({ gamepass: data }); 126 | console.log("Requesting gamepass done ✅"); 127 | } catch (error) { 128 | console.log("Error ❌"); 129 | console.error("Error fetching gamepass:", error); 130 | } 131 | } 132 | 133 | async function requestMarket() { 134 | console.log("Requesting markets...⏳"); 135 | chrome.storage.sync.set({ market: { name: "Argentina", lang: "es", region: "ar", tax: 1 }}); 136 | chrome.storage.local.set({ markets: markets }); 137 | console.log("Requesting market done ✅"); 138 | } 139 | 140 | 141 | chrome.runtime.onInstalled.addListener(async () => { 142 | chrome.storage.sync.set({ updates: "IMPUESTITO_HAS_UPDATES" }); 143 | 144 | // create alarm after extension is installed / upgraded 145 | // https://levelup.gitconnected.com/how-to-use-background-script-to-fetch-data-in-chrome-extension-ef9d7f69625d 146 | // https://www.section.io/engineering-education/how-to-build-a-chrome-extension-using-javascript/ 147 | chrome.alarms.create("requestTaxes", { periodInMinutes: 4320 }); // Ej: minutes = hours * 60 148 | chrome.alarms.create("requestGamePass", { periodInMinutes: 4320 }); // Ej: minutes = hours * 60 149 | await requestTaxes(); 150 | await requestGamePass(); 151 | await requestMarket(); 152 | }); 153 | 154 | chrome.alarms.onAlarm.addListener(async (alarm) => { 155 | console.log("⏰ Alarm:", alarm); 156 | await requestTaxes(); 157 | await requestGamePass(); 158 | await requestMarket(); 159 | }); 160 | 161 | 162 | -------------------------------------------------------------------------------- /extension/core/helpers.js: -------------------------------------------------------------------------------- 1 | const logos = { 2 | impuestito: { 3 | name: "Impuestito", 4 | icon: ` 5 | 6 | 7 | 8 | `, 9 | } 10 | } 11 | /** 12 | * Retrieves data from Chrome's local storage. 13 | * 14 | * @async 15 | * @function getServerData 16 | * @returns {Promise<*>} A promise that resolves to the data from storage if it exists, or null if no data is found or an error occurs. 17 | * @throws Will log an error message to the console if there is an issue retrieving the data. 18 | */ 19 | async function getServerData() { 20 | // sync: is used to store or get data across devices 21 | // local: is used to store or get data like localstorage 22 | const responseSync = await chrome.storage.sync.get(["userConfig","market"]); 23 | const responseLocal = await chrome.storage.local.get(["impuestito", "gamepass"]); 24 | 25 | if (responseSync.userConfig && responseLocal.impuestito && responseLocal.gamepass && responseSync.market) { 26 | // console.log('❇️ DATA from Storage', response); 27 | 28 | // TODO: refactor the server response 29 | // NOTE: this is a temporary solution to normalize data from the server 30 | return { 31 | dollar: responseLocal.impuestito.dollar, 32 | taxes: responseLocal.impuestito.taxes, 33 | province: responseLocal.impuestito.province, 34 | market: responseSync.market, // name, lang, region, tax 35 | gamepass: responseLocal.gamepass, // gamepass games 36 | userConfig: responseSync.userConfig, 37 | }; 38 | } else { 39 | console.warn('🐞 No data found in storage, trying again...'); 40 | getServerData(); 41 | return null; 42 | } 43 | } 44 | 45 | /** 46 | * @param {string | object} gamesSelector 47 | * @param {string} className 48 | * @param {function} callback 49 | */ 50 | function handleMutations(gamesSelector, className, callback) { 51 | const games = document.querySelectorAll(gamesSelector); 52 | 53 | if (games && games.length > 0) { 54 | for (let i = 0; i < games.length; i++) { 55 | const game = games[i]; 56 | 57 | if (game.className.includes("impuestito")) return; 58 | 59 | callback(game, i); 60 | 61 | game.classList.add("impuestito", className); 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Checks if the given element has already been scanned by looking for the "impuestito" class name. 68 | * 69 | * @param {Element} e - The DOM element to check. 70 | * @returns {boolean} - Returns true if the element's class name includes "impuestito", otherwise false. 71 | */ 72 | const alreadyScanned = (e) => e.className.includes("impuestito") 73 | 74 | /** 75 | * Checks if the given element has already been processed. 76 | * 77 | * This function determines if the provided element has a class name 78 | * that includes "impuestito-done", indicating that it has already 79 | * been processed. 80 | * 81 | * @param {HTMLElement} e - The element to check. 82 | * @returns {boolean} - Returns true if the element has been processed, otherwise false. 83 | */ 84 | const alreadyProcessed = (e) => e.className.includes("impuestito-done") 85 | 86 | /** 87 | * @param {string} originalPrice 88 | * @param {string} currency 89 | * @param {object} data 90 | * @returns {number} 91 | */ 92 | function getNewPrice(originalPrice, currency = "ARS", data) { 93 | if (!data.taxes.ganancias || !data.userConfig.selectedProvince) return; 94 | 95 | const exceptions = ["Free", "FREE", "Gratuito", "Gratis", "Gratis+", "No disponible", "Prueba del juego", "--", "", "NaN", "Incluido", "Anunciados", "Ver juego", "Coming Soon", "Available", "Under"]; 96 | const priceTextNaN = exceptions.some((exception) => exception.toLowerCase() === originalPrice.toLowerCase()); 97 | const provinceTax = data.userConfig.selectedProvince ? data.province[data.userConfig.selectedProvince].tax : data.province[data.province["AR-C"]].tax; 98 | const priceWithTaxes = (p) => (p + p * (data.taxes.iva + data.taxes.ganancias + provinceTax)).toFixed(2); 99 | 100 | if (priceTextNaN) { 101 | // console.log("👁️ 1 getNewPrice: priceTextNaN", priceTextNaN); 102 | return 0; 103 | } 104 | 105 | const priceNumber = sanitizePricePunctuation(sanitizePriceSigns(originalPrice)); 106 | // console.log("👁️ 2 priceNumber", priceNumber); 107 | 108 | if (priceNumber === 0) { 109 | // console.log("👁️ 3 getNewPrice: priceNumber is 0"); 110 | return 0; 111 | } 112 | 113 | if (currency === "US") { 114 | const newPrice = priceNumber * sanitizePricePunctuation(data.dollar.bancos); 115 | // console.log("👁️ 4 getNewPrice: newPrice", newPrice); 116 | return priceWithTaxes(newPrice); 117 | } 118 | 119 | // console.log("👁️ 5 getNewPrice: priceWithTaxes", priceWithTaxes(priceNumber)); 120 | return priceWithTaxes(priceNumber); 121 | } 122 | 123 | /** 124 | * @param {object} priceElement 125 | * @param {object} eventElement 126 | * @param {string} originalPrice 127 | * @param {number} newPrice 128 | * @param {boolean} showEmoji 129 | */ 130 | function replacePrice(priceElement, eventElement = priceElement, originalPrice, newPrice, showEmoji = true) { 131 | const originalEmoji = showEmoji ? "❌ " : ""; 132 | // const newEmoji = showEmoji ? "❇️ " : ""; 133 | const newEmoji = showEmoji ? logos.impuestito.icon : ""; 134 | 135 | priceElement.innerHTML = `${newEmoji}${priceFormatter(newPrice)}`; 136 | priceElement.classList.add("priceWithTaxes"); 137 | priceElement.setAttribute("title", `El valor original es ${originalPrice}`); 138 | 139 | // eventElement.addEventListener("mouseenter", (e) => { 140 | // e.preventDefault(); 141 | // priceElement.setAttribute("title", "Precio original"); 142 | // priceElement.textContent = `${originalEmoji}${originalPrice}`; 143 | // }); 144 | 145 | // eventElement.addEventListener("mouseleave", (e) => { 146 | // e.preventDefault(); 147 | // priceElement.setAttribute("title", "Precio (AR$) con impuestos incluidos"); 148 | // priceElement.textContent = `${newEmoji}${priceFormatter(newPrice)}`; 149 | // }); 150 | } 151 | 152 | /** 153 | * @param {object} {priceElement 154 | * @param {object} eventElement 155 | * @param {string} currency 156 | * @param {boolean} showEmoji 157 | * @param {boolean} isDiscount} 158 | */ 159 | async function scrapper({ priceElement, eventElement, currency, showEmoji, isDiscount = false }) { 160 | const data = await getServerData(); 161 | 162 | if (priceElement && data) { 163 | isDiscount ? priceElement.classList.add("price-discount") : priceElement.classList.add("price-regular"); 164 | const originalPrice = priceElement.textContent; 165 | // console.log("✨ scrapper: originalPrice", { originalPrice }); 166 | const newPrice = getNewPrice(originalPrice, currency, data); 167 | // console.log("✨ scrapper: newPrice", { newPrice }); 168 | newPrice && replacePrice(priceElement, eventElement, originalPrice, newPrice, showEmoji); 169 | } 170 | } 171 | 172 | /** 173 | * @param {number} price 174 | * @param {object} targetDOMElement 175 | */ 176 | function drawBadge(price, targetDOMElement) { 177 | const badge = document.createElement("p"); 178 | badge.innerText = price === 0 ? " Gratis" : `AR${priceFormatter(price)}`; 179 | badge.setAttribute("title", "Este es el precio real que vas a pagar (incluye impuestos)"); 180 | badge.classList.add("priceWithTaxesBadge"); 181 | 182 | targetDOMElement.appendChild(badge); 183 | } 184 | 185 | /** 186 | * @param {number} price 187 | * @param {string} format 188 | * @param {string} currency 189 | * @returns {string} 190 | */ 191 | function priceFormatter(price, format = "es-AR", currency = "ARS") { 192 | const formatter = new Intl.NumberFormat(format, { 193 | style: "currency", 194 | currency: currency, 195 | }); 196 | 197 | return formatter.format(price); 198 | } 199 | 200 | 201 | /** 202 | * Removes currency symbols and extra spaces from a price string 203 | * @param {string} price - Price string with currency symbol (e.g. "US$ 1,222.43", "ARS$ 1,222.43") 204 | * @returns {string} Price string without currency symbol (e.g. "1,222.43") 205 | */ 206 | function sanitizePriceSigns(price) { 207 | // Remove leading and trailing whitespace from the price string 208 | const cleanedNumber = price 209 | .trim() 210 | // Remove occurrences of "ARS" or "US" (case insensitive) 211 | .replace(/ARS|MSRP|US|Ahorra|Under|under|con|Xbox|Game|Pass|:/gi, "") 212 | // Remove all dollar signs 213 | .replace(/\$+/gi, "") 214 | // Remove all whitespace characters 215 | .replace(/\s+/gi, "") 216 | // Remove all plus signs 217 | .replace(/\+/gi, "") 218 | // Remove occurrences of "/mes" 219 | .replace(/\s?\/mes/gi, "") 220 | .trim(); 221 | 222 | return cleanedNumber; 223 | } 224 | 225 | /** 226 | * Converts price string with various decimal/thousands separators to a number 227 | * @param {string} price - Price string with decimal/thousands separators (e.g. "1.234,55", "1,234.55", "1234.55") 228 | * @returns {number} Price as a number (e.g. 1234.55) 229 | */ 230 | function sanitizePricePunctuation(price) { 231 | if (Boolean(price) === false) { 232 | console.warn('🐞 Price is required:', price, typeof price); 233 | throw new Error('Price is required') 234 | }; 235 | if (typeof price === 'string') { 236 | if (price.trim() === '') { 237 | console.warn('🐞 Price is empty:', price, typeof price); 238 | throw new Error('Price is empty') 239 | }; 240 | if (price.match(/[a-zA-Z]/gi)) { 241 | console.warn('🐞 Price contains letters:', price, typeof price); 242 | throw new Error('Price contains letters') 243 | }; 244 | if (price.match(/^\d+\.\d+\.\d+$/gi)) { 245 | console.warn('🐞 Price contains multiple decimal points:', price, typeof price); 246 | throw new Error('Price contains multiple decimal points') 247 | }; 248 | if (price.match(/^\d+\,\d+\,\d+$/gi)) { 249 | console.warn('🐞 Price contains multiple commas:', price, typeof price); 250 | throw new Error('Price contains multiple commas') 251 | }; 252 | 253 | const formatedPrice = price 254 | .trim() 255 | .replace(/(\d+)?[\.|\,]?(.+)[\,|\.](\d{1,2})/gi, "$1$2.$3"); 256 | 257 | if (isNaN(+formatedPrice)) { 258 | console.warn('🐞 Price is not a number after formatting:', price, typeof price); 259 | throw new Error('Price is not a number after formatting') 260 | }; 261 | 262 | // console.log("\n💵 sanitizePricePunctuation", price, typeof price, +formatedPrice) 263 | return +formatedPrice; 264 | 265 | } else if (typeof price === 'number') { 266 | return price; 267 | } else { 268 | console.warn('🐞 Price is not a string or number:', price, typeof price); 269 | throw new Error('Price is not a string or number'); 270 | } 271 | } 272 | 273 | /** 274 | * @param {array} arr - Array of strings 275 | * @param {string} url - URL string 276 | */ 277 | function someURL(arr, url) { 278 | if (!arr || arr.length === 0) return false; 279 | if (!url) return false; 280 | return arr.some((w) => url.includes(w)); 281 | } 282 | -------------------------------------------------------------------------------- /extension/core/helpers.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * EXPORT FUNCTIONS TO TEST THIS FILE WITH JEST 3 | */ 4 | import { sanitizePricePunctuation, sanitizePriceSigns, someURL } from './helpers'; 5 | 6 | describe('sanitizePriceSigns', () => { 7 | const testCases = [ 8 | { input: '3 US$', expected: '3'}, 9 | { input: '33 US$', expected: '33'}, 10 | { input: '3,9 US$', expected: '3,9'}, 11 | { input: '36,9 US$', expected: '36,9'}, 12 | { input: '36,99 US$', expected: '36,99'}, 13 | { input: '366,99 US$', expected: '366,99'}, 14 | { input: '36,99 US$', expected: '36,99'}, 15 | { input: '3.666 US$', expected: '3.666'}, 16 | { input: '3.666,99 US$', expected: '3.666,99'}, 17 | { input: ' 3 US$', expected: '3'}, 18 | { input: ' 33 US$', expected: '33'}, 19 | { input: ' 3,9 US$', expected: '3,9'}, 20 | { input: ' 36,9 US$', expected: '36,9'}, 21 | { input: ' 36,99 US$', expected: '36,99'}, 22 | { input: ' 366,99 US$', expected: '366,99'}, 23 | { input: ' 36,99 US$', expected: '36,99'}, 24 | { input: ' 3.666 US$', expected: '3.666'}, 25 | { input: ' 3.666,99 US$', expected: '3.666,99'}, 26 | { input: 'ARS$ 1,222.43', expected: '1,222.43'}, 27 | { input: 'US$ 1,222.43', expected: '1,222.43'}, 28 | { input: '$ 1,222.43', expected: '1,222.43'}, 29 | { input: ' ARS$ 1,222.43', expected: '1,222.43'}, 30 | { input: 'ARS$1,222.43', expected: '1,222.43'}, 31 | { input: 'ARS$ 1,222.43+', expected: '1,222.43'}, 32 | { input: 'US$ 1,222.43+', expected: '1,222.43'}, 33 | { input: '$ 1,222.43+', expected: '1,222.43'}, 34 | { input: ' ARS$ 1,222.43 +', expected: '1,222.43'}, 35 | { input: 'ARS$1,222.43 +', expected: '1,222.43'}, 36 | { input: ' ARS$ 1,222.43 +', expected: '1,222.43'}, 37 | { input: 'US$ 1,222.43 +', expected: '1,222.43'}, 38 | { input: '$ 1,222.43 +', expected: '1,222.43'}, 39 | { input: ' ARS$ 1,222.43 +', expected: '1,222.43'}, 40 | { input: 'ARS$1,222.43 +', expected: '1,222.43'}, 41 | { input: 'ARS$1,222.43/mes', expected: '1,222.43'}, 42 | { input: 'ARS$1,222.43 /mes', expected: '1,222.43'}, 43 | ]; 44 | 45 | test.each(testCases)('converts "$input" to "$expected"', ({ input, expected }) => { 46 | expect(sanitizePriceSigns(input)).toBe(expected); 47 | }); 48 | 49 | // Additional edge cases 50 | test('handles empty string', () => { 51 | expect(sanitizePriceSigns('')).toBe(''); 52 | }); 53 | 54 | test('handles string with only currency symbol', () => { 55 | expect(sanitizePriceSigns('US$')).toBe(''); 56 | }); 57 | 58 | test('handles string with only spaces', () => { 59 | expect(sanitizePriceSigns(' ')).toBe(''); 60 | }); 61 | 62 | test('handles string with only plus sign', () => { 63 | expect(sanitizePriceSigns('+')).toBe(''); 64 | }); 65 | 66 | test('handles null input', () => { 67 | expect(() => sanitizePriceSigns(null)).toThrow(); 68 | }); 69 | 70 | test('handles undefined input', () => { 71 | expect(() => sanitizePriceSigns(undefined)).toThrow(); 72 | }); 73 | }); 74 | 75 | describe('sanitizePricePunctuation', () => { 76 | const testCases = [ 77 | { input: '1.234,55', expected: 1234.55 }, 78 | { input: '1,234.55', expected: 1234.55 }, 79 | { input: '1234,55', expected: 1234.55 }, 80 | { input: '1234.55', expected: 1234.55 }, 81 | { input: '111,22', expected: 111.22 }, 82 | { input: '111.22', expected: 111.22 }, 83 | { input: '11,22', expected: 11.22 }, 84 | { input: '11.22', expected: 11.22 }, 85 | { input: '1,22', expected: 1.22 }, 86 | { input: '1.22', expected: 1.22 }, 87 | { input: '1,2', expected: 1.2 }, 88 | { input: '1.2', expected: 1.2 }, 89 | { input: 1.2, expected: 1.2}, 90 | { input: 1, expected: 1}, 91 | ]; 92 | 93 | test.each(testCases)('converts "$input" to "$expected"', ({ input, expected }) => { 94 | expect(sanitizePricePunctuation(input)).toBe(expected); 95 | }); 96 | 97 | // Additional edge cases 98 | test('handles empty string', () => { 99 | expect(() => sanitizePricePunctuation('')).toThrow(); 100 | }); 101 | 102 | test('handles string with only spaces', () => { 103 | expect(() => sanitizePricePunctuation(' ')).toThrow(); 104 | }); 105 | 106 | test('handles string with only decimal separator', () => { 107 | expect(() => sanitizePricePunctuation('.')).toThrow(); 108 | expect(() => sanitizePricePunctuation(',')).toThrow(); 109 | }); 110 | 111 | 112 | test('handles string with invalid characters', () => { 113 | expect(() => sanitizePricePunctuation('abc')).toThrow(); 114 | expect(() => sanitizePricePunctuation('12.34.56')).toThrow(); 115 | expect(() => sanitizePricePunctuation('12,34,56')).toThrow(); 116 | }); 117 | 118 | test('handles null input', () => { 119 | expect(() => sanitizePricePunctuation(null)).toThrow(); 120 | }); 121 | 122 | test('handles undefined input', () => { 123 | expect(() => sanitizePricePunctuation(undefined)).toThrow(); 124 | }); 125 | }); 126 | 127 | describe('someURL', () => { 128 | const testCases = [ 129 | { arr: ['page'], url: 'https://mypage.com/', expected: true }, 130 | { arr: ['page'], url: 'https://mypage.com/subpage', expected: true }, 131 | 132 | { arr: ['mypage'], url: 'https://mypage.com/page/subpage', expected: true }, 133 | { arr: ['subpage'], url: 'https://mypage.com/page/subpage', expected: true }, 134 | { arr: ['/page'], url: 'https://mypage.com/page/subpage', expected: true }, 135 | { arr: ['page/subpage'], url: 'https://mypage.com/page/subpage', expected: true }, 136 | { arr: ['/page', '/subpage'], url: 'https://mypage.com/page/subpage', expected: true }, 137 | { arr: ['com'], url: 'https://mypage.com/page/subpage', expected: true }, 138 | 139 | { arr: ['test'], url: 'https://mypage.com/page/subpage', expected: false }, 140 | { arr: [], url: 'http://mypage.com/page', expected: false }, 141 | { arr: ['mypage.com'], url: '', expected: false }, 142 | { arr: [], url: '', expected: false }, 143 | { arr: ['mypage'], url: null, expected: false }, 144 | { arr: ['mypage'], url: undefined, expected: false }, 145 | 146 | { arr: null, url: 'https://mypage.com', expected: false }, 147 | { arr: undefined, url: 'https://mypage.com', expected: false }, 148 | { arr: [null], url: 'https://mypage.com', expected: false }, 149 | { arr: [undefined], url: 'https://mypage.com', expected: false }, 150 | { arr: [null, undefined], url: 'https://mypage.com', expected: false }, 151 | { arr: null, url: null, expected: false }, 152 | { arr: null, url: null, expected: false }, 153 | { arr: undefined, url: undefined, expected: false }, 154 | ]; 155 | 156 | test.each(testCases)('returns $expected for array $arr and url $url', ({ arr, url, expected }) => { 157 | expect(someURL(arr, url)).toBe(expected); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /extension/core/index.js: -------------------------------------------------------------------------------- 1 | const hostname = window.location.hostname; 2 | const pathname = window.location.pathname; 3 | const href = window.location.href; 4 | 5 | function logWelcomeMessage({ store }) { 6 | const logo = ` 7 | 11317 1111 8 | 13333333 3333331 9 | 333333333 3333333 10 | 13333333 333333 11 | 13331 333331 12 | 133331 13 | 33333333331 131 14 | 1333333331 13333 15 | 13333331 1333331 16 | 3331 13333 17 | ` 18 | console.log(`\n\n\n${logo}\n✅ Estás usando la extensión de impuestito v${chrome.runtime.getManifest().version} (funcionando en ${store}).\n❇️ Visitá https://impuestito.org para más cálculos e información de compras en el exterior y suscripciones.\n\n\n\n\n\n`); 19 | } 20 | 21 | /** 22 | * Debounce function to limit the rate at which a function can fire. 23 | * @param {Function} func - The function to debounce. 24 | * @param {number} wait - The number of milliseconds to delay. 25 | * @returns {Function} - The debounced function. 26 | */ 27 | function debounce(func, wait = 300) { 28 | let timeout; 29 | return function(...args) { 30 | clearTimeout(timeout); 31 | timeout = setTimeout(() => func.apply(this, args), wait); 32 | }; 33 | } 34 | 35 | // Watch HTML mutations 36 | function observeInit(targetElement, handleScrapperInit, options = { subtree: true, attributes: true, childList: true }) { 37 | if (!targetElement) { 38 | console.error("🔴 Missing targetElement to scrap"); 39 | return; 40 | } 41 | 42 | if (!handleScrapperInit) { 43 | console.error("🔴 Missing handleScrapperInit function"); 44 | return; 45 | } 46 | 47 | MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 48 | const observer = new MutationObserver(debounce((mutations) => { 49 | mutations.length && handleScrapperInit(); 50 | }, 100)); // Adjust the debounce delay as needed 51 | 52 | observer.observe(targetElement, { subtree: options.subtree, attributes: options.attributes, childList: options.childList }); 53 | } -------------------------------------------------------------------------------- /extension/gamepass/gamepass.css: -------------------------------------------------------------------------------- 1 | .page_game-pass--banner-wrapper { 2 | /* display: flex; */ 3 | } 4 | 5 | .page_game-pass--banner { 6 | position: relative; 7 | 8 | padding-top: 65px; 9 | padding-left: 20px; 10 | padding-right: 20px; 11 | padding-bottom: 90px; 12 | 13 | margin-top: 5px; 14 | margin-bottom: 5px; 15 | 16 | border-radius: 4px; 17 | 18 | background: linear-gradient(-45deg, #16ab16, #139113, #0c5a0c, #139113, #0c5a0c); 19 | background-size: 600%; 20 | animation: animated-background 20s linear infinite; 21 | 22 | overflow: hidden; 23 | } 24 | 25 | .page_game-pass--banner .header { 26 | z-index: 1; 27 | position: absolute; 28 | left: 0; 29 | right: 0; 30 | top: 0; 31 | 32 | padding: 10px; 33 | 34 | border-radius: 4px 4px 0 0; 35 | 36 | color: white; 37 | text-align: center; 38 | } 39 | 40 | .gp-available .header, 41 | .gp-fallback.gp-available .header { 42 | background: linear-gradient(to right, #435876, #253142, #435876, #253142); 43 | background-size: 600%; 44 | animation: animated-background 20s linear infinite; 45 | } 46 | 47 | .gp-leaving .header, 48 | .gp-fallback.gp-leaving .header { 49 | background: #b43636; 50 | background-size: 600%; 51 | animation: animated-background 20s linear infinite; 52 | color: white; 53 | } 54 | 55 | .gp-coming .header, 56 | .gp-fallback.gp-coming .header { 57 | background: linear-gradient(to right, #6aec6a, #e9fde9, #6aec6a, #e9fde9); 58 | background-size: 600%; 59 | animation: animated-background 20s linear infinite; 60 | color: #052405; 61 | } 62 | 63 | .page_game-pass--banner .footer { 64 | box-sizing: border-box; 65 | 66 | z-index: 1; 67 | position: absolute; 68 | left: 0; 69 | right: 0; 70 | bottom: 0; 71 | 72 | padding: 22px; 73 | 74 | /* background-color: #052405; */ 75 | /* background-color: rgb(5, 36, 5, 0.5); */ 76 | background: linear-gradient(-45deg, rgba(5, 36, 5, 0.6), rgba(5, 36, 5, 0.1)); 77 | border-radius: 0 0 4px 4px; 78 | /* border-top: 1px solid #0a510a; */ 79 | 80 | font-size: 14px; 81 | color: white; 82 | text-align: center; 83 | } 84 | 85 | /* .page_game-pass--banner .footer div { 86 | display: flex; 87 | align-items: flex-start; 88 | flex-direction: column; 89 | 90 | color: #6aec6a; 91 | } */ 92 | 93 | /* .gp-ms-store--config { 94 | display: flex; 95 | justify-content: flex-start; 96 | align-items: center; 97 | } 98 | 99 | .gp-ms-store--button { 100 | display: flex; 101 | justify-content: flex-end; 102 | align-items: center; 103 | } */ 104 | 105 | .page_game-pass--banner .footer .store { 106 | box-sizing: border-box; 107 | 108 | display: flex; 109 | align-items: center; 110 | justify-content: center; 111 | 112 | width: 100%; 113 | 114 | padding: 8px 14px; 115 | background: transparent; 116 | border-radius: 20px; 117 | border: 1px solid #6aec6a; 118 | 119 | color: #6aec6a; 120 | text-decoration: none; 121 | } 122 | 123 | .page_game-pass--banner .footer a { 124 | color: #6aec6a; 125 | text-decoration: underline; 126 | } 127 | 128 | .page_game-pass--banner .footer a:hover { 129 | color: #e9fde9; 130 | } 131 | 132 | .page_game-pass--banner .footer .store svg { 133 | width: 10px; 134 | height: 10px; 135 | 136 | padding: 2px; 137 | margin-right: 6px; 138 | margin-left: 6px; 139 | 140 | background-color: white; 141 | border-radius: 4px; 142 | } 143 | 144 | .page_game-pass--banner .footer .store strong { 145 | margin-right: 6px; 146 | } 147 | 148 | .page_game-pass--banner .footer .store:hover { 149 | color: #e9fde9; 150 | border: 1px solid #e9fde9; 151 | } 152 | 153 | .page_game-pass--banner .footer .region-change { 154 | background-color: transparent; 155 | border: none; 156 | 157 | color: #e9fde9; 158 | text-decoration: none; 159 | 160 | cursor: pointer; 161 | } 162 | 163 | .page_game-pass--banner .footer .region-change:hover { 164 | opacity: 0.5; 165 | } 166 | 167 | .page_game-pass--banner .footer .extension { 168 | font-size: 12px; 169 | margin-top: 2px; 170 | } 171 | 172 | /* .page_game-pass--banner .footer .market { 173 | margin-top: 2px; 174 | 175 | color: #6aec6a; 176 | font-size: 12px; 177 | } */ 178 | 179 | .page_game-pass--banner .content { 180 | z-index: 1; 181 | display: flex; 182 | justify-content: space-between; 183 | align-content: center; 184 | align-items: center; 185 | } 186 | 187 | .playable-badges { 188 | z-index: 1; 189 | display: flex; 190 | align-content: center; 191 | justify-content: center; 192 | align-items: center; 193 | 194 | color: white; 195 | } 196 | 197 | .playable-badges div { 198 | margin-left: 3px; 199 | margin-right: 3px; 200 | } 201 | 202 | .page_game-pass--badge { 203 | display: flex; 204 | justify-content: center; 205 | align-content: center; 206 | align-items: center; 207 | 208 | padding: 8px 12px; 209 | background: #052405; 210 | border-radius: 20px; 211 | 212 | font-size: 12px; 213 | } 214 | 215 | .page_game-pass--badge svg { 216 | margin-right: 6px; 217 | } 218 | 219 | .page_game-pass--badge svg { 220 | margin-right: 6px; 221 | } 222 | 223 | .page_game-pass--button { 224 | background: linear-gradient(-45deg, #16ab16, #139113, #0c5a0c, #139113, #0c5a0c) !important; 225 | background-size: 600% !important; 226 | animation: animated-background 20s linear infinite !important; 227 | color: #fff; 228 | } 229 | 230 | .device-icon { 231 | fill: white; 232 | } 233 | 234 | .game-pass-logo { 235 | position: relative; 236 | height: 34px; 237 | } 238 | 239 | .ultimate { 240 | position: absolute; 241 | padding: 2px; 242 | 243 | transform: translate(116px, 154%); 244 | border: 1px solid white; 245 | 246 | font-size: 6px; 247 | font-weight: 600; 248 | color: white; 249 | letter-spacing: 2px; 250 | } 251 | 252 | .gp-game_area_purchase_game { 253 | background: linear-gradient(to right, #052405, #020e02) !important; 254 | border: 1px solid #139113; 255 | } 256 | 257 | @keyframes animated-background { 258 | 0% { 259 | background-position: 0% 50%; 260 | } 261 | 50% { 262 | background-position: 100% 50%; 263 | } 264 | 100% { 265 | background-position: 0% 50%; 266 | } 267 | } 268 | 269 | .updates { 270 | padding: 8px; 271 | background: linear-gradient(-45deg, #16ab16, #139113, #0c5a0c, #139113, #0c5a0c, #16ab16, #139113, #0c5a0c); 272 | background-size: 600%; 273 | animation: animated-background 20s linear infinite; 274 | 275 | text-shadow: 0 1px 3px #0a510a; 276 | text-align: center; 277 | color: white; 278 | } 279 | 280 | .updates a, 281 | .updates a:hover { 282 | color: #a0e840; 283 | font-weight: 600; 284 | } 285 | 286 | .updated-at { 287 | padding: 15px 25px; 288 | 289 | backdrop-filter: blur(20px); 290 | background: rgba(0, 0, 0, 0.5); 291 | 292 | font-size: 12px; 293 | color: #16ab16; 294 | } 295 | 296 | .gp-menu { 297 | z-index: 999; 298 | position: fixed; 299 | display: none; 300 | 301 | top: 0; 302 | bottom: 0; 303 | left: 0; 304 | right: 0; 305 | 306 | justify-content: center; 307 | align-items: center; 308 | 309 | backdrop-filter: blur(20px); 310 | 311 | /* animation: animated-updateAt 0.15s ease-out forwards; */ 312 | } 313 | 314 | .gp-menu.visible { 315 | display: flex; 316 | } 317 | 318 | .gp-menu_box { 319 | padding: 30px; 320 | 321 | border-radius: 5px; 322 | box-shadow: 0 10px 20px rgba(5, 36, 5, 0.5); 323 | border: 1px solid #6aec6a; 324 | 325 | background: linear-gradient(-45deg, #16ab16, #139113, #0c5a0c, #139113, #0c5a0c); 326 | background-size: 600%; 327 | animation: animated-background 20s linear infinite; 328 | } 329 | 330 | .gp-menu select { 331 | padding: 10px; 332 | border-radius: 3px; 333 | } 334 | 335 | @keyframes animated-updateAt { 336 | 0% { 337 | transform: translateY(100%); 338 | } 339 | 100% { 340 | transform: translateY(0); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /extension/gamepass/index.js: -------------------------------------------------------------------------------- 1 | // Changelog: https://lucasromerodb.notion.site/Available-on-Game-Pass-notes-0a1105a741c4454a99fb5d9927f6950d 2 | 3 | // Assign the correct method to handle website mutations 4 | async function handleMutationsInit() { 5 | // sync: is used to store or get data across devices 6 | // local: is used to store or get data like localstorage 7 | const responseSync = await chrome.storage.sync.get(["market", "userConfig"]); 8 | const responseLocal = await chrome.storage.local.get(["gamepass", "impuestito", "markets"]); 9 | 10 | if (responseLocal.gamepass && responseSync.market && responseSync.userConfig && responseLocal.impuestito && responseLocal.markets) { 11 | handlePageMutations( 12 | responseLocal.gamepass, 13 | { 14 | dollar: responseLocal.impuestito.dollar, 15 | taxes: responseLocal.impuestito.taxes, 16 | province: responseLocal.impuestito.province, 17 | market: responseSync.market, // name, lang, region, tax 18 | gamepass: responseLocal.gamepass, // gamepass games 19 | userConfig: responseSync.userConfig, 20 | } 21 | ); 22 | } 23 | 24 | const menuElement = document.querySelector(".gp-menu"); 25 | if (!menuElement) { 26 | responseLocal.markets && drawMenu(responseLocal.markets); 27 | 28 | const regionSelectElement = document.querySelector(".gp-menu select"); 29 | regionSelectElement && regionSelectElement.addEventListener("change", (e) => { 30 | selectCountry(e.target.value); 31 | }); 32 | } 33 | } 34 | 35 | document.addEventListener("keyup", (e) => { 36 | if (e.key === "Escape") { 37 | toggleMenu(false); 38 | } 39 | }); 40 | 41 | document.addEventListener("click", (e) => { 42 | const menuElement = document.querySelector(".gp-menu"); 43 | let clickedElement = e.target; // clicked element 44 | 45 | if (menuElement && menuElement.classList.contains("visible")) { 46 | if (menuElement == clickedElement) { 47 | toggleMenu(false); 48 | } 49 | } 50 | }); 51 | 52 | function writePlayground() { 53 | const div = document.createElement("div"); 54 | div.classList.add("available-on-game-pass-playground"); 55 | document.querySelector("body").insertAdjacentElement("beforeend", div); 56 | } 57 | 58 | if (someURL(["steampowered"], hostname)) { 59 | // initMenu("Epic Games Store"); 60 | if (someURL(["app"], pathname)) { 61 | initMenu("Steam"); 62 | writePlayground(); 63 | handleMutationsInit(); 64 | 65 | // Watch HTML mutations 66 | MutationObserver = window.MutationObserver || window.WebKitMutationObserver; 67 | const observer = new MutationObserver(handleMutationsInit); 68 | const observerOptions = { subtree: true, attributes: true }; 69 | 70 | document.querySelector(".leftcol.game_description_column") && observer.observe(document.querySelector(".leftcol.game_description_column"), observerOptions); // STORE PAGE => https://store.steampowered.com/app/1294810/Redfall/ 71 | document.querySelector("#wishlist_ctn") && observer.observe(document.querySelector("#wishlist_ctn"), observerOptions); // USER WISHLIST => https://store.steampowered.com/wishlist/id/USER/ 72 | document.querySelector(".page_content .leftcol.large") && observer.observe(document.querySelector(".page_content .leftcol.large"), observerOptions); // SEARCH => https://store.steampowered.com/search/ 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /extension/gamepass/menu.js: -------------------------------------------------------------------------------- 1 | function toggleMenu(visibility = true) { 2 | const menuElement = document.querySelector(".gp-menu"); 3 | visibility ? menuElement.classList.add("visible") : menuElement.classList.remove("visible"); 4 | } 5 | 6 | function selectCountry(region) { 7 | console.log(region); 8 | if (!region) return; 9 | 10 | chrome.storage.local.get(["markets"], (data) => { 11 | const market = data.markets.find((m) => region === m.region); 12 | 13 | chrome.storage.sync.set({ market: market }); 14 | window.location.reload(); 15 | }); 16 | } 17 | 18 | function drawMenu(markets) { 19 | const options = markets.map((m) => ``); 20 | 21 | const newElement = ` 22 |
23 |
24 | 28 |
29 |
30 | `; 31 | 32 | const targetElement = document.getElementsByTagName("body")[0]; 33 | targetElement.insertAdjacentHTML("afterbegin", newElement); 34 | } 35 | -------------------------------------------------------------------------------- /extension/gamepass/steam.js: -------------------------------------------------------------------------------- 1 | const devices = { 2 | "xbox-one": { 3 | name: "Xbox One", 4 | icon: '', 5 | }, 6 | "xbox-series": { 7 | name: "Xbox Series X|S", 8 | icon: '', 9 | }, 10 | "xbox-windows": { 11 | name: "PC", 12 | icon: '', 13 | }, 14 | "xbox-cloud": { 15 | name: "Cloud", 16 | icon: '', 17 | }, 18 | }; 19 | 20 | const headerText = { 21 | available: "This game is available on Game Pass and playable on the following platforms", 22 | leaving: "This game is leaving Game Pass soon. Give it a last try!", 23 | coming: "This game is coming to Game Pass soon. Can't wait?", 24 | }; 25 | 26 | const GamePassLogo = 27 | ''; 28 | 29 | async function drawPageUI(targetPosition, targetElement, config) { 30 | const { game, availability, updatedAt, options, hasSteamcito } = config; 31 | 32 | const badge = (device) => `
${device.icon || ""}${device.name || ""}
`; 33 | 34 | const one = game.platforms.one ? badge(devices["xbox-one"]) : ""; 35 | const series = game.platforms.series ? badge(devices["xbox-series"]) : ""; 36 | const pc = game.platforms.windows ? badge(devices["xbox-windows"]) : ""; 37 | const cloud = game.platforms.cloud ? badge(devices["xbox-cloud"]) : ""; 38 | const date = new Date(updatedAt); 39 | 40 | const content = { 41 | availability: `
${headerText[availability]}
`, 42 | gamepass: `
${GamePassLogo}${game.EAPlay ? 'ULTIMATE' : ""}`, 43 | badges: `
${one + series + pc + cloud}
`, 44 | updateInfo: `Updated at: ${date}`, 45 | author: `Developed by Lukekix`, 46 | chromeLinkGamePass: `Available on Game Pass v${chrome.runtime.getManifest().version} »`, 47 | }; 48 | 49 | const changeRegionButton = ``; 50 | 51 | const banner = ` 52 |
53 |
54 | ${content.availability} 55 | ${content.gamepass} 56 | ${content.badges} 57 | 62 |
63 | 64 |
65 | `; 66 | 67 | targetElement.insertAdjacentHTML(targetPosition, banner); 68 | 69 | // SHOW MENU 70 | const changeRegionButtonElement = document.querySelector(".page_game-pass--banner .region-change"); 71 | changeRegionButtonElement && changeRegionButtonElement.addEventListener("click", () => toggleMenu(true)); 72 | 73 | // MICROSOFT STORE BUTTON 74 | // This Game Pass API is Open Source and you can host on your own! https://github.com/lucasromerodb/xbox-store-api 75 | const responseXboxStore = await fetch(`${chrome.runtime.getManifest().web_accessible_resources[0].resources[1]}/api/gamepass/search/price/${game.id}/${options.market.region}`, {}); 76 | const dataXboxStore = await responseXboxStore.json(); 77 | console.log("DATA: ", dataXboxStore); 78 | const price = dataXboxStore.price && { MSRP: dataXboxStore.price.MSRP, SalePrice: dataXboxStore.price.SalePrice }; 79 | 80 | const provinceTax = options.userConfig.selectedProvince ? options.province[options.userConfig.selectedProvince].tax : options.province[options.province["AR-C"]].tax; 81 | const priceWithTaxes = (p) => (p + p * (options.taxes.iva + options.taxes.ganancias + provinceTax)).toFixed(2); 82 | 83 | if (price && hasSteamcito && options.market.region === "ar") { 84 | 85 | const priceMSRP = dataXboxStore.price.MSRP ? sanitizePricePunctuation(sanitizePriceSigns(dataXboxStore.price.MSRP)) : null; 86 | const priceSalePrice = dataXboxStore.price.SalePrice ? sanitizePricePunctuation(sanitizePriceSigns(dataXboxStore.price.SalePrice)) : null; 87 | 88 | price.MSRP = priceMSRP ? priceFormatter(priceWithTaxes(priceMSRP)) : null; 89 | price.SalePrice = priceSalePrice ? priceFormatter(priceWithTaxes(priceSalePrice)) : null; 90 | } 91 | 92 | console.log("PRICE: ", price, "MARKET: ", options.market); 93 | 94 | const storeLink = 95 | price && (price.SalePrice || price.MSRP) 96 | ? ` 97 | ${price.SalePrice ? "En oferta" : "Comprar"} a ${logos.impuestito.icon} ${ 98 | price.SalePrice ? price.SalePrice : price.MSRP 99 | } en Microsoft Store — Precio final con impuestos incluidos` 100 | : `Comprar en Microsoft Store`; 101 | 102 | const msStoreButtonContainerElement = document.querySelector(".gp-ms-store--button"); 103 | msStoreButtonContainerElement.innerHTML = storeLink; 104 | } 105 | 106 | function clearTitle(title) { 107 | return title 108 | .replace(/Standard Edition\b/gi, "") 109 | .replace(/Xbox Edition\b/gi, "") 110 | .replace(/Xbox One Edition\b/gi, "") 111 | .replace(/Xbox One & Xbox Series X\|S\b/gi, "") 112 | .replace(/Xbox One & Xbox Series X \| S\b/gi, "") 113 | .replace(/Xbox One\b|^Xbox Series X\|S\b/gi, "") 114 | .replace(/^Buy\b|^Pre-Purchase\b/gi, "") 115 | .replace(/:|-|®|™|'|’|\./gi, "") 116 | .replace(/\s+(?=\s)/gi, "") 117 | .trim() 118 | .toLowerCase(); 119 | } 120 | 121 | function targetElementValidator({ games, element, insertPosition, targetElement, type, options }) { 122 | if (!element.classList.contains("game-pass")) { 123 | const gameTitle = type === "regular" ? element.querySelector("h1").innerText : element.innerText; 124 | 125 | // This is only for Argentina 126 | const hasSteamcito = document.querySelector(".menu-steamcito"); 127 | 128 | const available = games.all.find((g) => clearTitle(g.title) === clearTitle(gameTitle)); 129 | const leaving = games.leaving.find((g) => clearTitle(g.title) === clearTitle(gameTitle)); 130 | const coming = games.coming.find((g) => clearTitle(g.title) === clearTitle(gameTitle)); 131 | 132 | if (available || leaving || coming) { 133 | element.classList.add("game-pass"); 134 | 135 | if (type === "regular") { 136 | element.querySelector(".game_area_purchase_game").classList.add("gp-game_area_purchase_game"); 137 | } 138 | 139 | if (leaving) { 140 | element.classList.add("gp-leaving"); 141 | type === "fallback" && document.querySelector(".game_description_column").classList.add("gp-fallback", "gp-leaving"); 142 | const config = { game: leaving, availability: "leaving", updatedAt: games.updated_at, hasSteamcito, options }; 143 | drawPageUI(insertPosition, targetElement, config); 144 | } else { 145 | if (available) { 146 | element.classList.add("gp-available"); 147 | type === "fallback" && document.querySelector(".game_description_column").classList.add("gp-fallback", "gp-available"); 148 | const config = { game: available, availability: "available", updatedAt: games.updated_at, hasSteamcito, options }; 149 | drawPageUI(insertPosition, targetElement, config); 150 | } 151 | } 152 | 153 | if (coming) { 154 | element.classList.add("gp-coming"); 155 | type === "fallback" && document.querySelector(".game_description_column").classList.add("gp-fallback", "gp-coming"); 156 | const config = { game: coming, availability: "coming", updatedAt: games.updated_at, hasSteamcito, options }; 157 | drawPageUI(insertPosition, targetElement, config); 158 | } 159 | } 160 | } 161 | } 162 | 163 | function handlePageMutations(games = {}, options = {}) { 164 | const banner = document.querySelector(".game-pass--ui"); 165 | 166 | // Main targets (buying option titles) 167 | const elements = document.querySelectorAll(".game_area_purchase_game_wrapper"); 168 | 169 | if (games.all && games.all.length > 0 && !banner) { 170 | if (elements.length > 0) { 171 | for (const element of elements) { 172 | const handleConfig = { 173 | games, 174 | element, 175 | insertPosition: "beforebegin", 176 | targetElement: element.querySelector(".game_area_purchase_game"), 177 | type: "regular", 178 | options, 179 | }; 180 | 181 | targetElementValidator(handleConfig); 182 | } 183 | } 184 | } 185 | 186 | // Fallback target (main title) 187 | const appHubAppName = document.getElementById("appHubAppName"); 188 | const gamePass = document.querySelector(".game-pass"); 189 | 190 | if (games.all && games.all.length > 0 && !gamePass) { 191 | if (appHubAppName) { 192 | const handleConfig = { 193 | games, 194 | element: appHubAppName, 195 | insertPosition: "afterbegin", 196 | targetElement: document.querySelector(".leftcol.game_description_column"), 197 | type: "fallback", 198 | options, 199 | }; 200 | targetElementValidator(handleConfig); 201 | document.querySelector(".page_content").classList.add("gp-fallback"); 202 | } 203 | } 204 | } 205 | 206 | /** 207 | * TODO: ISSUES 208 | * 209 | * TODO: Update notifier! 210 | * TODO: How Long To Beat integration! 211 | * TODO: a way to report a missing game if it doesn't have a banner 212 | */ 213 | -------------------------------------------------------------------------------- /extension/prices/dekudeals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * XB Deals Web Store 3 | */ 4 | function handleDekuDealsMutations() { 5 | if (someURL(["dekudeals.com"], hostname)) { 6 | initMenu("Deku Deals Store") 7 | DekuDealsScrapper(); 8 | 9 | // This is here because DekuDeals have issues with some elements 10 | // dekuDealsScrapper(); 11 | // dekuDealsRelatedScrapper(); 12 | 13 | // observeInit(document, dekuDealsScrapper); 14 | 15 | if (someURL(["items"], pathname)) { 16 | observeInit(document, dekuDealsRelatedScrapper); 17 | } 18 | } 19 | } 20 | 21 | function DekuDealsScrapper() { 22 | const targets = [ 23 | "div.row.item-grid2 .card-badge > s", 24 | "div.row.item-grid2 .card-badge > strong", 25 | ]; 26 | 27 | const elements = [...document.querySelectorAll(targets)] 28 | .filter((e) => e.innerText.includes("$")) 29 | .filter((e) => !alreadyScanned(e)) 30 | .map((e) => { 31 | e.classList.add("impuestito", "impuestito-dekudeals") 32 | return e; 33 | }); 34 | 35 | const priceElementsTarget = elements; 36 | if (priceElementsTarget.length > 0) { 37 | for (const element of priceElementsTarget) { 38 | if (!element.className.includes("impuestito-done")) { 39 | scrapper({ 40 | priceElement: element, 41 | eventElement: element, 42 | currency: "US", 43 | showEmoji: true, 44 | isDiscount: element.classList.contains("strikethrough"), 45 | }); 46 | } 47 | element.classList.add("impuestito-done"); 48 | } 49 | } 50 | } 51 | 52 | function dekuDealsScrapper() { 53 | const elements = []; 54 | 55 | const priceElements = [...document.querySelectorAll("div.card-badge strong"), ...document.querySelectorAll("div.card-badge s")]; 56 | if (priceElements.length > 0) { 57 | for (const element of priceElements) { 58 | if (element.className.includes("impuestito")) return; 59 | element.classList.add("impuestito", "impuestito-dekudeals"); 60 | elements.push(element); 61 | } 62 | } 63 | 64 | const soloPriceElements = [...[...document.querySelectorAll("main .search-main .row > div > .position-relative")].filter((element) => !element.childNodes[3].classList.contains("card-badge"))]; 65 | if (soloPriceElements.length > 0) { 66 | for (const element of soloPriceElements) { 67 | if (element.childNodes[2].nodeName.toLowerCase() === "span" && element.childNodes[2].className.includes("impuestito")) return; 68 | 69 | const span = document.createElement("span"); 70 | span.append(element.childNodes[2]); 71 | element.childNodes[2].remove; 72 | element.childNodes[1].insertAdjacentElement("afterend", span); 73 | element.childNodes[2].classList.add("impuestito", "impuestito-dekudeals"); 74 | elements.push(element.childNodes[2]); 75 | } 76 | } 77 | 78 | const priceElementsTarget = elements; 79 | if (priceElementsTarget.length > 0) { 80 | for (const element of priceElementsTarget) { 81 | if (!element.className.includes("impuestito-done")) { 82 | scrapper({ 83 | priceElement: element, 84 | eventElement: element, 85 | currency: "ARS", 86 | showEmoji: true, 87 | isDiscount: element.classList.contains("text-muted"), 88 | }); 89 | } 90 | element.classList.add("impuestito-done"); 91 | } 92 | } 93 | } 94 | 95 | function dekuDealsRelatedScrapper() { 96 | const elements = []; 97 | 98 | const buttonPriceElements = [...document.querySelectorAll(".btn.btn-block.btn-primary")]; 99 | if (buttonPriceElements.length > 0) { 100 | for (const element of buttonPriceElements) { 101 | if (element.childNodes[0].nodeName.toLowerCase() === "span" && element.childNodes[0].className.includes("impuestito")) return; 102 | element.style.cssText = "background: black; color: white"; 103 | const span = document.createElement("span"); 104 | span.append(element.childNodes[0]); 105 | element.childNodes[0].remove; 106 | element.insertAdjacentElement("afterbegin", span); 107 | element.childNodes[0].classList.add("impuestito", "impuestito-dekudeals"); 108 | elements.push(element.childNodes[0]); 109 | } 110 | } 111 | 112 | const soloPriceElements = [...[...document.querySelectorAll(".row.item-grid2 > div > .position-relative")].filter((element) => !element.childNodes[3].classList.contains("card-badge"))]; 113 | if (soloPriceElements.length > 0) { 114 | for (const element of soloPriceElements) { 115 | if (element.childNodes[2].nodeName.toLowerCase() === "span" && element.childNodes[2].className.includes("impuestito")) return; 116 | 117 | const span = document.createElement("span"); 118 | span.append(element.childNodes[2]); 119 | element.childNodes[2].remove; 120 | element.childNodes[1].insertAdjacentElement("afterend", span); 121 | element.childNodes[2].classList.add("impuestito", "impuestito-dekudeals"); 122 | elements.push(element.childNodes[2]); 123 | } 124 | } 125 | 126 | const priceElementsTarget = elements; 127 | if (priceElementsTarget.length > 0) { 128 | for (const element of priceElementsTarget) { 129 | if (!element.className.includes("impuestito-done")) { 130 | scrapper({ 131 | priceElement: element, 132 | eventElement: element, 133 | currency: "ARS", 134 | showEmoji: true, 135 | isDiscount: element.classList.contains("text-muted"), 136 | }); 137 | } 138 | element.classList.add("impuestito-done"); 139 | } 140 | } 141 | } 142 | 143 | // Init 144 | handleDekuDealsMutations(); 145 | -------------------------------------------------------------------------------- /extension/prices/ea.js: -------------------------------------------------------------------------------- 1 | /** 2 | * EA Store 3 | */ 4 | function handleEAMutations() { 5 | if (someURL(["ea.com"], hostname)) { 6 | if (someURL(["ea-play"], pathname)) { 7 | initMenu("EA Store"); 8 | setTimeout(() => { 9 | observeInit(document, EAScrapper); 10 | }, 200); 11 | } 12 | } 13 | } 14 | 15 | /** 16 | * EA Scrapper 17 | * 18 | * Tested on: 19 | * 20 | * https://www.ea.com/ea-play 21 | * https://www.ea.com/games/ea-sports-wrc/wrc-24/buy 22 | */ 23 | function EAScrapper() { 24 | console.log('🔃 Runnig:', arguments.callee.name); 25 | 26 | const finalPriceElements = [ 27 | ...document.querySelector("ea-play-tile").shadowRoot.querySelectorAll("span.eapl-subscription-tile__price-number"), 28 | ...document.querySelector("ea-play-pro-tile").shadowRoot.querySelectorAll("span.eapl-subscription-tile__price-number"), 29 | ] 30 | .filter((e) => !alreadyScanned(e)) 31 | .map((e) => { 32 | e.classList.add("impuestito", "impuestito-ea") 33 | return e; 34 | }); 35 | 36 | const priceElementsTarget = finalPriceElements.filter((e) => !alreadyProcessed(e)); 37 | if (priceElementsTarget.length > 0) { 38 | for (const element of priceElementsTarget) { 39 | scrapper({ 40 | priceElement: element, 41 | eventElement: element, 42 | currency: "US", 43 | showEmoji: true, 44 | isDiscount: false, 45 | }); 46 | element.classList.add("impuestito-done"); 47 | } 48 | } 49 | } 50 | 51 | // Init 52 | handleEAMutations(); 53 | -------------------------------------------------------------------------------- /extension/prices/epic.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Epic Games Web Store 3 | */ 4 | function handleEpicMutations() { 5 | if (someURL(["epicgames"], hostname)) { 6 | initMenu("Epic Games Store"); 7 | observeInit(document.body, EpicScrapper); 8 | } 9 | } 10 | 11 | /** 12 | * Epic Scrapper 13 | * 14 | * Tested on: 15 | * https://store.epicgames.com/en-US/ 16 | * https://store.epicgames.com/en-US/p/kingdom-come-deliverance-2-664783 17 | * https://store.epicgames.com/en-US/browse?q=assassins&sortBy=relevancy&sortDir=DESC&count=40 18 | * 19 | */ 20 | function EpicScrapper() { 21 | console.log('🔃 Runnig:', arguments.callee.name); 22 | 23 | const finalPriceElements = [...document.querySelectorAll("div, span, b")] 24 | .filter((e) => e.innerText.includes("$") && e.innerText.length < 15 && e.innerText === e.innerHTML) 25 | .filter((e) => !alreadyScanned(e)) 26 | .map((e) => { 27 | e.classList.add("impuestito", "impuestito-epic") 28 | return e; 29 | }); 30 | 31 | const priceElementsTarget = finalPriceElements.filter((e) => !alreadyProcessed(e)); 32 | if (priceElementsTarget.length > 0) { 33 | for (const element of priceElementsTarget) { 34 | scrapper({ 35 | priceElement: element, 36 | eventElement: element, 37 | currency: "US", 38 | showEmoji: true, 39 | isDiscount: element.classList.contains("price-discount"), 40 | }); 41 | element.classList.add("impuestito-done"); 42 | } 43 | } 44 | } 45 | 46 | // Init 47 | handleEpicMutations(); 48 | -------------------------------------------------------------------------------- /extension/prices/gog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * GOG (Good Old Games) Store 3 | */ 4 | function handleGOGMutations() { 5 | if (someURL(["gog.com"], hostname)) { 6 | initMenu("GOG Store"); 7 | observeInit(document, GOGScrapper); 8 | } 9 | } 10 | 11 | /** 12 | * GOG Scrapper 13 | */ 14 | function GOGScrapper() { 15 | console.log('🔃 Runnig:', arguments.callee.name); 16 | 17 | const targets = [ 18 | ".final-value", 19 | ".base-value", 20 | ".big-spot__price-amount", 21 | ".product-tile__price-discounted", 22 | ".product-actions-price__final-amount._price", 23 | ".menu-inside-category ._price", 24 | ".menu-cart-item__price ._price", 25 | ".series__buy-all-price-final._price", 26 | ".product-tile__price._price", 27 | ".product-actions-price__base-amount._price", 28 | ".series__buy-all-price-base._price", 29 | ].join(", "); 30 | 31 | const elements = [...document.querySelectorAll(targets)] 32 | .filter((e) => !alreadyScanned(e)) 33 | .map((e) => { 34 | e.classList.add("impuestito", "impuestito-gog") 35 | return e; 36 | }); 37 | 38 | // console.log(elements); 39 | 40 | const targetElements = elements.filter((e) => !alreadyProcessed(e)); 41 | if (targetElements.length > 0) { 42 | for (const element of targetElements) { 43 | scrapper({ 44 | priceElement: element, 45 | eventElement: element, 46 | currency: "US", 47 | showEmoji: true, 48 | isDiscount: element.classList.contains("price-discount"), 49 | }); 50 | element.classList.add("impuestito-done"); 51 | } 52 | } 53 | } 54 | 55 | // Init 56 | handleGOGMutations(); 57 | -------------------------------------------------------------------------------- /extension/prices/green-man-gaming.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Green Man Gaming Store 3 | */ 4 | function handleGreenManGamingMutations() { 5 | if (someURL(["greenmangaming.com"], hostname)) { 6 | initMenu("Green Man Gaming Store"); 7 | observeInit(document.body, GreenManGamingScrapper); 8 | } 9 | } 10 | 11 | /** 12 | * Tested on: 13 | * https://www.greenmangaming.com/ 14 | * https://www.greenmangaming.com/games/ 15 | * https://www.greenmangaming.com/games/railway-empire-2-bella-italia-pc/ 16 | */ 17 | function GreenManGamingScrapper() { 18 | console.log('🔃 Runnig:', arguments.callee.name); 19 | 20 | const toRemove = [ 21 | ".prev-price > span", 22 | ".current-price > span", 23 | ].join(", "); 24 | 25 | [...document.querySelectorAll(toRemove)] 26 | .map((e) => { 27 | e.remove(); 28 | }) 29 | 30 | const targets = [ 31 | "gmgprice", 32 | ].join(", ") 33 | 34 | const priceElements = [...document.querySelectorAll(targets)] 35 | .filter((e) => e.innerText.includes("$")) 36 | .filter((e) => !alreadyScanned(e)) 37 | .map((e) => { 38 | e.classList.add("impuestito", "impuestito-green-man-gaming"); 39 | return e; 40 | }); 41 | 42 | const targetElements = priceElements.filter((e) => !alreadyProcessed(e)); 43 | if (targetElements.length > 0) { 44 | for (const element of targetElements) { 45 | scrapper({ 46 | priceElement: element, 47 | eventElement: element, 48 | currency: "US", 49 | showEmoji: true, 50 | isDiscount: element.classList.contains("price-discount"), 51 | }); 52 | element.classList.add("impuestito-done"); 53 | } 54 | } 55 | } 56 | 57 | // Init 58 | handleGreenManGamingMutations(); 59 | -------------------------------------------------------------------------------- /extension/prices/humble-bundle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Humble Bundle Store 3 | */ 4 | function handleHumbleBundleMutations() { 5 | if (someURL(["humblebundle.com"], hostname)) { 6 | initMenu("Humble Bundle Store"); 7 | observeInit(document.body, HumbleBundleScrapper); 8 | } 9 | } 10 | 11 | /** 12 | * Tested on: 13 | * https://www.humblebundle.com/store 14 | * https://www.humblebundle.com/store/astroneer 15 | */ 16 | function HumbleBundleScrapper() { 17 | console.log('🔃 Runnig:', arguments.callee.name); 18 | [...document.querySelectorAll(".price-button")] 19 | .map((e) => { 20 | e.style.width = "auto"; 21 | e.style.paddingLeft = "5px"; 22 | e.style.paddingRight = "5px"; 23 | }) 24 | 25 | const targets = [ 26 | ".price", 27 | ".full-price", 28 | ".current-price", 29 | ].join(", ") 30 | 31 | const priceElements = [...document.querySelectorAll(targets)] 32 | .filter((e) => e.innerText.includes("$")) 33 | .filter((e) => !alreadyScanned(e)) 34 | .map((e) => { 35 | e.classList.add("impuestito", "impuestito-humble-bundle"); 36 | return e; 37 | }); 38 | 39 | const targetElements = priceElements.filter((e) => !alreadyProcessed(e)); 40 | if (targetElements.length > 0) { 41 | for (const element of targetElements) { 42 | scrapper({ 43 | priceElement: element, 44 | eventElement: element, 45 | currency: "US", 46 | showEmoji: true, 47 | isDiscount: element.classList.contains("price-discount"), 48 | }); 49 | element.classList.add("impuestito-done"); 50 | } 51 | } 52 | } 53 | 54 | // Init 55 | handleHumbleBundleMutations(); 56 | -------------------------------------------------------------------------------- /extension/prices/isthereanydeal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Epic Games Web Store 3 | */ 4 | function handleIsThereAnyDealMutations() { 5 | if (someURL(["isthereanydeal"], hostname)) { 6 | initMenu("Is There Any Deal Store"); 7 | observeInit(document.body, IsThereAnyDealScrapper); 8 | } 9 | } 10 | 11 | /** 12 | * Epic Scrapper 13 | * 14 | * Tested on: 15 | * https://store.epicgames.com/en-US/ 16 | * https://store.epicgames.com/en-US/p/kingdom-come-deliverance-2-664783 17 | * https://store.epicgames.com/en-US/browse?q=assassins&sortBy=relevancy&sortDir=DESC&count=40 18 | * 19 | */ 20 | function IsThereAnyDealScrapper() { 21 | console.log('🔃 Runnig:', arguments.callee.name); 22 | 23 | const targets = [ 24 | "span", 25 | "div" 26 | ].join(", ") 27 | 28 | const finalPriceElements = [...document.querySelectorAll(targets)] 29 | .filter((e) => !alreadyScanned(e)) 30 | .filter((e) => e.innerText.includes("$")) 31 | .map((e) => { 32 | e.classList.add("impuestito", "impuestito-isthereanydeal") 33 | return e; 34 | }); 35 | 36 | const priceElementsTarget = finalPriceElements.filter((e) => !alreadyProcessed(e)); 37 | if (priceElementsTarget.length > 0) { 38 | for (const element of priceElementsTarget) { 39 | scrapper({ 40 | priceElement: element, 41 | eventElement: element, 42 | currency: "US", 43 | showEmoji: true, 44 | isDiscount: element.classList.contains("price-discount"), 45 | }); 46 | element.classList.add("impuestito-done"); 47 | } 48 | } 49 | } 50 | 51 | // Init 52 | handleIsThereAnyDealMutations(); 53 | -------------------------------------------------------------------------------- /extension/prices/nintendo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Nintendo Store 3 | */ 4 | function handleNintendoARMutations() { 5 | if (someURL(["nintendo"], hostname)) { 6 | initMenu("Nintendo Store"); 7 | 8 | if (someURL(["/es-ar"], pathname)) { 9 | observeInit(document, NintendoARScrapper, { subtree: true, attributes: false, childList: true }); 10 | } 11 | 12 | // if (someURL(["/us", "/en-us"], pathname)) { 13 | // observeInit(document, NintendoUSScrapper, { subtree: true, attributes: false, childList: true }); 14 | // } 15 | } 16 | } 17 | 18 | /** 19 | * 20 | * Tested on: 21 | * https://www.nintendo.com/es-ar/store/games/ 22 | * https://www.nintendo.com/es-ar/store/games/#show=0&p=1&sort=df&f=topLevelFilters&topLevelFilters=Ofertas 23 | * https://www.nintendo.com/es-ar/store/products/sonic-x-shadow-generations-switch/ 24 | * https://www.nintendo.com/es-ar/store/products/super-mario-bros-wonder-switch/ 25 | */ 26 | 27 | function NintendoARScrapper() { 28 | console.log('🔃 Runnig:', arguments.callee.name); 29 | 30 | const targets = [ 31 | ".W990N.SH2al._5auKz", 32 | ".o2BsP.SH2al", 33 | ".W990N.SH2al", 34 | ".W990N.QS4uJ", 35 | ".W990N.QS4uJ._5auKz", 36 | ".o2BsP.QS4uJ" 37 | ].join(", "); 38 | 39 | const elements = [...document.querySelectorAll(targets)] 40 | .filter((e) => e.innerText.includes("$")) 41 | // .filter((e) => e.innerText.includes('Precio normal') || e.innerText.includes('Precio promocional')) 42 | .filter((e) => !alreadyScanned(e)) 43 | .map((e) => { 44 | e.classList.add("impuestito", "impuestito-nintendo") 45 | e.innerText = e.innerText.replace(/(precio(\s)?(normal|promocional)(:)?)/gi, ""); 46 | return e; 47 | }); 48 | 49 | const versions = [...document.querySelectorAll("main > section:first-child > div > div > div > div > div > p")] 50 | .filter((e) => e.innerText.includes("$")) 51 | .filter((e) => !alreadyScanned(e)) 52 | .map((e) => { 53 | e.classList.add("impuestito", "impuestito-nintendo") 54 | return e; 55 | }); 56 | 57 | 58 | const targetElements = [...elements, ...versions].filter((e) => !alreadyProcessed(e)); 59 | if (targetElements.length > 0) { 60 | for (const element of targetElements) { 61 | scrapper({ 62 | priceElement: element, 63 | eventElement: element, 64 | currency: "ARS", 65 | showEmoji: true, 66 | isDiscount: false, 67 | }); 68 | element.classList.add("impuestito-done"); 69 | } 70 | } 71 | } 72 | 73 | function NintendoUSScrapper() { 74 | console.log('🔃 Runnig:', arguments.callee.name); 75 | 76 | const targets = [ 77 | ".W990N.SH2al._5auKz", 78 | ".o2BsP.SH2al", 79 | ".W990N.SH2al", 80 | ".W990N.QS4uJ", 81 | ".W990N.QS4uJ._5auKz", 82 | ".o2BsP.QS4uJ" 83 | ].join(", "); 84 | 85 | const elements = [...document.querySelectorAll(targets)] 86 | .filter((e) => e.innerText.includes("$")) 87 | // .filter((e) => e.innerText.includes('Precio normal') || e.innerText.includes('Precio promocional')) 88 | .filter((e) => !alreadyScanned(e)) 89 | .map((e) => { 90 | e.classList.add("impuestito", "impuestito-nintendo") 91 | e.innerText = e.innerText.replace(/((regular|current)(\s)?price(:)?)/gi, ""); 92 | return e; 93 | }); 94 | 95 | const versions = [...document.querySelectorAll("main > section:first-child > div > div > div > div > div > p")] 96 | .filter((e) => e.innerText.includes("$")) 97 | .filter((e) => !alreadyScanned(e)) 98 | .map((e) => { 99 | e.classList.add("impuestito", "impuestito-nintendo") 100 | return e; 101 | }); 102 | 103 | 104 | const targetElements = [...elements, ...versions].filter((e) => !alreadyProcessed(e)); 105 | if (targetElements.length > 0) { 106 | for (const element of targetElements) { 107 | scrapper({ 108 | priceElement: element, 109 | eventElement: element, 110 | currency: "US", 111 | showEmoji: true, 112 | isDiscount: false, 113 | }); 114 | element.classList.add("impuestito-done"); 115 | } 116 | } 117 | } 118 | 119 | // TODO: https://www.nintendo.com/es-ar/store/products/monster-hunter-rise-plus-sunbreak-switch/ 120 | // Dropdown 121 | 122 | // Init 123 | handleNintendoARMutations(); 124 | -------------------------------------------------------------------------------- /extension/prices/ntdeals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * NT Deals Web Store 3 | */ 4 | function handleNTDealsMutations() { 5 | if (someURL(["ntdeals.net"], hostname) && someURL(["/us-store"], pathname)) { 6 | initMenu("NT Deals Store"); 7 | observeInit(document, ntdealsScrapper); 8 | } 9 | } 10 | 11 | function ntdealsScrapper() { 12 | const targets = [ 13 | ".game-collection-item-price", 14 | ".game-collection-item-price-discount", 15 | ".game-collection-item-price-bonus", 16 | ".game-buy-button-price", 17 | ".game-buy-button-price-discount", 18 | ".game-buy-button-price-bonus", 19 | ]; 20 | 21 | const elements = [...document.querySelectorAll(targets)] 22 | .filter((e) => e.innerText.includes("$")) 23 | .filter((e) => !alreadyScanned(e)) 24 | .map((e) => { 25 | e.classList.add("impuestito", "impuestito-ntdeals") 26 | return e; 27 | }); 28 | 29 | const priceElementsTarget = elements; 30 | if (priceElementsTarget.length > 0) { 31 | for (const element of priceElementsTarget) { 32 | if (!element.className.includes("impuestito-done")) { 33 | scrapper({ 34 | priceElement: element, 35 | eventElement: element, 36 | currency: "US", 37 | showEmoji: true, 38 | isDiscount: element.classList.contains("strikethrough"), 39 | }); 40 | } 41 | element.classList.add("impuestito-done"); 42 | } 43 | } 44 | } 45 | 46 | // Init 47 | handleNTDealsMutations(); 48 | -------------------------------------------------------------------------------- /extension/prices/playstation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * PlayStation Store 3 | */ 4 | function handlePlaystationMutations() { 5 | if (someURL(["playstation"], hostname)) { 6 | initMenu("PlayStation Store"); 7 | observeInit(document, PlaystationScrapper); 8 | } 9 | } 10 | 11 | /** 12 | * Scrapes PlayStation price elements from the document, processes them, and applies specific classes. 13 | * 14 | * This function selects all `span` and `s` elements that contain a dollar sign (`$`) in their inner text, 15 | * ensures the text length is less than 15 characters, and that the inner text matches the inner HTML. 16 | * It then filters out elements that have already been scanned, adds specific classes to the remaining elements, 17 | * and processes them if they haven't been processed yet. 18 | * 19 | * The processed elements are passed to the `scrapper` function with specific parameters, and a class indicating 20 | * completion is added to each processed element. 21 | * 22 | * Tested on: 23 | * https://store.playstation.com/es-ar/category/024029c7-f61b-4bef-a4d7-06270ed12b56 24 | * https://store.playstation.com/es-ar/pages/latest 25 | * https://store.playstation.com/es-ar/pages/collections 26 | * https://store.playstation.com/es-ar/pages/deals 27 | * https://store.playstation.com/es-ar/pages/subscriptions 28 | * https://www.playstation.com/es-ar/ps-plus/whats-new/ 29 | * https://www.playstation.com/es-ar/ps-plus/this-month-on-ps-plus/ 30 | * https://store.playstation.com/es-ar/product/UP1004-CUSA03041_00-RDR2ULTMEDTNBUND 31 | * https://store.playstation.com/es-ar/product/UP2047-CUSA01164_00-PLAGUEOFFROGSPAC 32 | * https://store.playstation.com/es-ar/concept/10000649 33 | * https://www.playstation.com/es-ar/games/god-of-war/ 34 | * https://www.playstation.com/es-ar/games/horizon-zero-dawn/ 35 | * https://www.playstation.com/es-ar/games/god-of-war/ 36 | * https://www.playstation.com/es-ar/games/ea-sports-fifa-22/ 37 | * 38 | * @function 39 | */ 40 | function PlaystationScrapper() { 41 | const elements = [...document.querySelectorAll('span, s')] 42 | .filter((e) => e.innerText.includes("$")) 43 | .filter((e) => !alreadyScanned(e)) 44 | .map((e) => { 45 | e.classList.add("impuestito", "impuestito-playstation") 46 | return e; 47 | }); 48 | 49 | const targetElements = elements.filter((e) => !alreadyProcessed(e)); 50 | if (targetElements.length > 0) { 51 | for (const element of targetElements) { 52 | scrapper({ 53 | priceElement: element, 54 | eventElement: element, 55 | currency: "US", 56 | showEmoji: true, 57 | isDiscount: false, 58 | }); 59 | element.classList.add("impuestito-done"); 60 | } 61 | } 62 | } 63 | 64 | // Init 65 | handlePlaystationMutations(); 66 | -------------------------------------------------------------------------------- /extension/prices/prices.css: -------------------------------------------------------------------------------- 1 | .priceWithTaxes { 2 | position: inherit; 3 | display: flex !important; 4 | flex-direction: row; 5 | align-items: center; 6 | 7 | font-family: inherit; 8 | font-size: inherit; 9 | font-weight: inherit; 10 | gap: 5px; 11 | cursor: help; 12 | } 13 | 14 | .priceWithTaxes svg { 15 | width: 1rem !important; 16 | height: 1rem !important; 17 | padding: 0.15rem; 18 | background: white; 19 | border-radius: 0.3rem; 20 | } 21 | 22 | /* .priceWithTaxes.price-regular:hover, 23 | .priceWithTaxes .price-regular:hover { 24 | transform: scale(1); 25 | animation: textShine 1s ease-in-out infinite; 26 | } */ 27 | 28 | /* .priceWithTaxes.price-discount { 29 | text-decoration: line-through !important; 30 | } */ 31 | 32 | /* .price-discount { 33 | text-decoration: line-through !important; 34 | } */ 35 | 36 | .playstation--grid .priceWithTaxes { 37 | font-size: 16px !important; 38 | } 39 | 40 | .price-gamepass { 41 | padding: 0 5px; 42 | /* background: none !important; 43 | -webkit-background-clip: unset !important; 44 | background-clip: unset !important; 45 | -webkit-text-fill-color: inherit !important; 46 | text-fill-color: inherit !important; */ 47 | color: inherit; 48 | } 49 | 50 | .impuestito-gog::before, 51 | .impuestito-gog:before, 52 | .big-spot__price:before, 53 | .big-spot__price::before { 54 | display: none !important; 55 | } 56 | 57 | #impuestito-ui { 58 | position: fixed; 59 | z-index: 9999; 60 | bottom: 0; 61 | right: 0; 62 | font-size: 50px; 63 | background-color: white; 64 | } 65 | 66 | .impuestito-trigger-playground { 67 | display: none; 68 | } 69 | 70 | .impuestito-available-on-game-pass { 71 | display: flex; 72 | align-items: center; 73 | justify-content: space-between; 74 | position: relative; 75 | 76 | padding: 20px; 77 | 78 | margin-top: 0; 79 | margin-bottom: 15px; 80 | 81 | border-radius: 4px; 82 | 83 | background: linear-gradient(-45deg, #16ab16, #139113, #0c5a0c, #139113, #0c5a0c); 84 | background-size: 600%; 85 | animation: animated-background 20s linear infinite; 86 | 87 | overflow: hidden; 88 | } 89 | 90 | .impuestito-available-on-game-pass p { 91 | width: 367px; 92 | margin-right: 10px; 93 | 94 | font-size: 16px; 95 | color: rgb(106, 236, 106); 96 | } 97 | 98 | .impuestito-available-on-game-pass p strong { 99 | color: white; 100 | } 101 | 102 | .impuestito-available-on-game-pass a { 103 | display: flex; 104 | align-items: center; 105 | justify-content: space-between; 106 | 107 | width: auto; 108 | padding: 15px; 109 | padding-right: 20px; 110 | 111 | border-radius: 30px; 112 | background-color: black; 113 | 114 | color: white; 115 | } 116 | 117 | .impuestito-available-on-game-pass a:hover { 118 | color: rgb(106, 236, 106) !important; 119 | } 120 | .impuestito-available-on-game-pass a:hover::before { 121 | animation: chromeRotation 1s linear infinite; 122 | } 123 | 124 | .impuestito-available-on-game-pass a::before { 125 | content: ""; 126 | 127 | display: block; 128 | width: 24px; 129 | height: 24px; 130 | 131 | margin-right: 10px; 132 | 133 | background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQASURBVHgBrVVfbFNVGP+dc/p/q2zdKHQjTYnbComaYY0urJXuAYcaxAcN8Q0zQpwxLLyhQdBpDC8m24uLhgyM0QSiLxCMKMHFymLMosmY6TZAyoCuw9LK2GpX7r3Hc3rb0rVb5wxfcu+593zf9/t9/869wCqEP99kHvH5jKvxIZWU1/2P12oZtYdRBEGolxCsBwcoIbdByCS4dopqyhnXL+ORVREkWz01CaP1CAV/i1JqkHtifeBEFrsJzkGVKYc8ofD0igSXfU2PMhjOC41H4ujARKxkWYIcSYRD2ekeDo8tSzD11ObHFPBz4rFBB3oQeaUMiiRqBH923fDY1fxGwSsS2OxSOD+TB/+f0pABOX8t2FqT3zDkH4adlg/brv/jKfXgInf2iB1W39MweDaCmhh4Kgwkvgc0RU+zYAxZEw9zKkcwhANArkSbBnZ4bKp67cRXt1Cd0Qr20rfKvw2Od95H3LQWockFWE0ErW4jGi23oE0eAE/+uFQmKkurTrJjNpHNgFP+8ryYxcG2Wuz/6U7Bytq2FY6PB/DB6TkcvxiHqvGCrjvowMEXvwH5Yzf4nR9KCZhiNuwX63u5DvJX5PKdtxoRh36OCGOoe7sXR8/O4VgotQhcysDQPHpP3wNt6ReFXlOWAiE8mIXOvmnwZjMRJRlod+jRi9LMWNYLoBSWkxMXU4ilnSA1/nIlJ5uyBMHjQYugq8/vj7osGHFbYWpqwe9T91FJVNGu0MQCYGsuVxK+7tN9LhsNX0iwUl1/oA5pqiF9n2MlsZpF2kSWtfxs7HvNrNGZL0bnOfhMsSJexXDSHoe/2QRGKxNsEROF1Lhsmrj0Uw/9/hfpiKR1d04mSh2/zIyDGv7G6+22ZcG7gzYxrlHwxAUdvIhEzMRlaZOLj5cN873MPA6H+vDuTjveCC4mkVl1BWw4+EI1pscPCXcNeuREJ6DiMBLydS4TYMOxTodNw23xUtaP9sYn0RvoAZT6bNNlXwItZhFsEqdGjqKbnYQcw+wxlsce+sVUspF0xCKFzng/6+wTS89SpWAiqqD7GTQ7PFBVFX/evYFfp0fxeeMleC1pPcwicEE4aAjEugoZSHF/4q+1sqrf5LcE/0F2197FYVdM4KlFu9ksIgtp2l713I2o3CnMyNSbPycVhu3CIroSuJ2p6Fo7K7zFBFGT3tyskKi6QF7Kgy8ikHJ177krlBo7RSCRSgR76mbRaJKREx1c/vSoIaIofJt5+81LxbZlUx7ee3ZMbG4VmQwuBd5gVLCnfjYfsbwUrrE+ZuNbLB03r5TaV/zpy884YXyXxvmrYuyahbHzow1x7FozFxOeE5yTIYM91U+emEriYYhvxGfk3zaZV+PzL0tnVmUbMxLBAAAAAElFTkSuQmCC"); 134 | } 135 | 136 | @media screen and (max-width: 1366px) { 137 | .playstation--grid .priceWithTaxes { 138 | font-size: 14px !important; 139 | } 140 | } 141 | 142 | @media screen and (min-width: 1919px) { 143 | .playstation--grid .priceWithTaxes { 144 | font-size: 15px !important; 145 | } 146 | } 147 | 148 | @keyframes textShine { 149 | 0% { 150 | background-position: 0% 50%; 151 | } 152 | 100% { 153 | background-position: 100% 50%; 154 | } 155 | } 156 | 157 | @keyframes chromeRotation { 158 | 0% { 159 | transform: rotate(0); 160 | } 161 | 100% { 162 | transform: rotate(-360deg); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /extension/prices/psdeals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * XB Deals Web Store 3 | */ 4 | function handlePSDealsMutations() { 5 | if (someURL(["psdeals.net"], hostname) && someURL(["/ar-store"], pathname)) { 6 | initMenu("PS Deals (AR) Store"); 7 | observeInit(document, psdealsScrapperAR); 8 | } 9 | } 10 | 11 | function psdealsScrapperAR() { 12 | const targets = [ 13 | ".game-collection-item-price", 14 | ".game-collection-item-price-discount", 15 | ".game-collection-item-price-bonus", 16 | ".game-buy-button-price", 17 | ".game-buy-button-price-discount", 18 | ".game-buy-button-price-bonus", 19 | ]; 20 | 21 | const elements = [...document.querySelectorAll(targets)] 22 | .filter((e) => e.innerText.includes("$")) 23 | .filter((e) => !alreadyScanned(e)) 24 | .map((e) => { 25 | e.classList.add("impuestito", "impuestito-psdeals") 26 | return e; 27 | }); 28 | 29 | const priceElementsTarget = elements; 30 | if (priceElementsTarget.length > 0) { 31 | for (const element of priceElementsTarget) { 32 | if (!element.className.includes("impuestito-done")) { 33 | scrapper({ 34 | priceElement: element, 35 | eventElement: element, 36 | currency: "US", 37 | showEmoji: true, 38 | isDiscount: element.classList.contains("strikethrough"), 39 | }); 40 | } 41 | element.classList.add("impuestito-done"); 42 | } 43 | } 44 | } 45 | 46 | // Init 47 | handlePSDealsMutations(); 48 | -------------------------------------------------------------------------------- /extension/prices/steam.js: -------------------------------------------------------------------------------- 1 | function handleSteamMutations() { 2 | if (someURL(["steampowered"], hostname)) { 3 | // initMenu("Epic Games Store"); 4 | if (someURL(["app"], pathname)) { 5 | console.log("🟢 impuestito is working..."); 6 | const element = document.querySelector(".leftcol.game_description_column"); 7 | element && observeInit(element, steamScrapper); 8 | } 9 | } 10 | } 11 | 12 | function steamScrapper() { 13 | const targetArea = document.querySelector(".leftcol.game_description_column"); 14 | const isAvailableOnGamePassExtension = document.querySelector(".available-on-game-pass-playground"); 15 | 16 | if (targetArea && !isAvailableOnGamePassExtension && !document.querySelector(".impuestito-available-on-game-pass")) { 17 | const banner = document.createElement("div"); 18 | banner.classList.add("impuestito-available-on-game-pass"); 19 | banner.innerHTML = 20 | '

Usá la extensión Available on Game Pass para saber si este juego está disponible en Game Pass

Obtener Extensión'; 21 | 22 | targetArea.insertAdjacentElement("afterbegin", banner); 23 | } 24 | } 25 | 26 | handleSteamMutations(); 27 | -------------------------------------------------------------------------------- /extension/prices/ubisoft.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ubisoft Store 3 | */ 4 | function handleUbisoftMutations() { 5 | if (someURL(["store.ubisoft"], hostname)) { 6 | initMenu('Ubisoft Store') 7 | observeInit(document.body, UbisoftScrapper); 8 | } 9 | } 10 | 11 | /** 12 | * Ubisoft Scrapper 13 | * 14 | * https://store.ubisoft.com/ 15 | * https://store.ubisoft.com/ofertas/home 16 | */ 17 | function UbisoftScrapper() { 18 | 19 | console.log('🔃 Runnig:', arguments.callee.name); 20 | 21 | const targets = [ 22 | "span.product-tiles_ui_components_ProductPrice__price", 23 | "span.product-tiles_ui_components_ProductPrice__regularPrice", 24 | ".standard-price", 25 | ".price-discount", 26 | ".price-item", 27 | ".product-tiles_product-card_components_Content__container span" 28 | ].join(", ") 29 | 30 | 31 | const finalPriceElements = [...document.querySelectorAll(targets)] 32 | .filter((e) => e.innerText.includes("$")) 33 | .filter((e) => !alreadyScanned(e)) 34 | .map((e) => { 35 | e.classList.add("impuestito", "impuestito-ubisoft") 36 | return e; 37 | }); 38 | 39 | const priceElementsTarget = finalPriceElements.filter((e) => !alreadyProcessed(e)); 40 | if (priceElementsTarget.length > 0) { 41 | for (const element of priceElementsTarget) { 42 | scrapper({ 43 | priceElement: element, 44 | eventElement: element, 45 | currency: "US", 46 | showEmoji: true, 47 | isDiscount: element.classList.contains("price-discount"), 48 | }); 49 | element.classList.add("impuestito-done"); 50 | } 51 | } 52 | } 53 | 54 | // Init 55 | handleUbisoftMutations(); 56 | -------------------------------------------------------------------------------- /extension/prices/xbdeals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * XB Deals Web Store 3 | */ 4 | function handleXBDealsMutations() { 5 | if (someURL(["xbdeals.net"], hostname) && someURL(["/ar-store"], pathname)) { 6 | initMenu("XB Deals (AR) Store"); 7 | observeInit(document, XBDealsScrapper); 8 | } 9 | } 10 | 11 | function XBDealsScrapper() { 12 | const targets = [ 13 | ".game-collection-item-price", 14 | ".game-collection-item-price-discount", 15 | ".game-collection-item-price-bonus", 16 | ".game-buy-button-price", 17 | ".game-buy-button-price-discount", 18 | ".game-buy-button-price-bonus", 19 | ]; 20 | 21 | const elements = [...document.querySelectorAll(targets)] 22 | .filter((e) => e.innerText.includes("$")) 23 | .filter((e) => !alreadyScanned(e)) 24 | .map((e) => { 25 | e.classList.add("impuestito", "impuestito-xbdeals") 26 | return e; 27 | }); 28 | 29 | const priceElementsTarget = elements; 30 | if (priceElementsTarget.length > 0) { 31 | for (const element of priceElementsTarget) { 32 | if (!element.className.includes("impuestito-done")) { 33 | scrapper({ 34 | priceElement: element, 35 | eventElement: element, 36 | currency: "ARS", 37 | showEmoji: true, 38 | isDiscount: element.classList.contains("strikethrough"), 39 | }); 40 | } 41 | element.classList.add("impuestito-done"); 42 | } 43 | } 44 | } 45 | 46 | // Init 47 | handleXBDealsMutations(); 48 | -------------------------------------------------------------------------------- /extension/prices/xbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Microsoft Xbox Web Store 3 | */ 4 | function handleXboxMutations() { 5 | if (someURL(["xbox.com"], hostname)) { 6 | initMenu("Xbox Store"); 7 | 8 | if (someURL(["/es-AR", "/es-ar"], pathname)) { 9 | observeInit(document.body, XboxScrapper); 10 | } 11 | } 12 | } 13 | 14 | /** 15 | * Tested on: 16 | * https://www.xbox.com/es-ar/games/all-games [DEPRECATED] 17 | * https://www.xbox.com/es-AR/games/browse 18 | * https://www.xbox.com/es-AR/games/store/grand-theft-auto-v-xbox-series-xs/9NXMBTB02ZSF/0010 19 | * https://www.xbox.com/es-ar/games/store/dragon-ball-xenoverse-2/BX03760D0QGN 20 | * https://www.xbox.com/es-ar/games/store/grand-theft-auto-iv/BRQ2SCZCTXF2 21 | * https://www.xbox.com/es-ar/games/store/lego-los-increbles/BZP3R43F8DNH 22 | * https://www.xbox.com/es-ar/games/store/tom-clancys-rainbow-six-extraction/9P53VF7859PW 23 | * https://www.xbox.com/es-ar/games/store/psychonauts/C5HHPG1TXDNG 24 | * https://www.xbox.com/es-AR/games/store/the-elder-scrolls-v-skyrim-anniversary-edition/9PBN02CTMRTH/0010 25 | * https://www.xbox.com/es-ar/games/store/dragon-ball-xenoverse-2/BX03760D0QGN 26 | * https://www.xbox.com/es-ar/games/store/grand-theft-auto-iv/BRQ2SCZCTXF2 27 | * https://www.xbox.com/es-ar/games/store/lego-los-increbles/BZP3R43F8DNH 28 | * https://www.xbox.com/es-ar/games/store/tom-clancys-rainbow-six-extraction/9P53VF7859PW 29 | * https://www.xbox.com/es-ar/games/store/psychonauts/C5HHPG1TXDNG 30 | * https://www.xbox.com/es-AR/games/halo-infinite 31 | * https://www.xbox.com/es-ar/games/forza-horizon-4 32 | * https://www.xbox.com/es-ar/games/assassins-creed-valhalla 33 | * https://www.xbox.com/es-ar/games/fortnite 34 | * https://www.xbox.com/es-AR/xbox-game-pass 35 | * https://www.xbox.com/es-AR/xbox-game-pass/pc-game-pass 36 | * 37 | * TODO: 38 | * https://www.xbox.com/es-AR/promotions/sales/sales-and-specials 39 | */ 40 | function XboxScrapper() { 41 | console.log('🔃 Runnig:', arguments.callee.name); 42 | 43 | const priceElements = [...document.querySelectorAll("span")] 44 | .filter((e) => !alreadyScanned(e)) 45 | .filter((e) => e.className.includes("Price-module")) 46 | .filter((e) => e.innerText.includes("$")) 47 | .map((e) => { 48 | e.classList.add("impuestito", "impuestito-xbox"); 49 | return e; 50 | }); 51 | 52 | const priceElements2 = [...document.querySelectorAll("p")] 53 | .filter((e) => !alreadyScanned(e)) 54 | .filter((e) => e.innerText.includes("$")) 55 | .map((e) => { 56 | e.classList.add("impuestito", "impuestito-xbox"); 57 | return e; 58 | }); 59 | 60 | const targetElements = [...priceElements, ...priceElements2].filter((e) => !alreadyProcessed(e)); 61 | if (targetElements.length > 0) { 62 | for (const element of targetElements) { 63 | scrapper({ 64 | priceElement: element, 65 | eventElement: element, 66 | currency: "ARS", 67 | showEmoji: true, 68 | }); 69 | element.classList.add("impuestito-done"); 70 | } 71 | } 72 | } 73 | 74 | // Init 75 | handleXboxMutations(); -------------------------------------------------------------------------------- /manifest-chromium.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Impuestito: precio final juegos con impuestos", 4 | "description": "Te dice el precio final y con impuestos de los juegos de Playstation, Xbox, Nintendo, Epic, GOG y más tiendas. (Argentina)", 5 | "version": "1.14", 6 | "side_panel": { 7 | "default_path": "config/sidepanel.html" 8 | }, 9 | "action": { 10 | "default_title": "Abrir panel de impuestito.org" 11 | }, 12 | "background": { 13 | "service_worker": "core/background.js", 14 | "type": "module" 15 | }, 16 | "permissions": [ 17 | "storage", 18 | "alarms", 19 | "sidePanel" 20 | ], 21 | "content_scripts": [ 22 | { 23 | "matches": [ 24 | "*://*.playstation.com/*", 25 | "*://*.xbox.com/*", 26 | "*://*.nintendo.com/*", 27 | "*://*.epicgames.com/*", 28 | "*://*.gog.com/*", 29 | "*://*.ubisoft.com/*", 30 | "*://*.ea.com/*", 31 | "*://*.xbdeals.net/*", 32 | "*://*.psdeals.net/*", 33 | "*://*.ntdeals.net/*", 34 | "*://*.dekudeals.com/*", 35 | "*://*.steampowered.com/*", 36 | "*://*.humblebundle.com/*", 37 | "*://*.greenmangaming.com/*", 38 | "*://*.fanatical.com/*", 39 | "*://*.isthereanydeal.com/*", 40 | "*://*.battle.net/*", 41 | "*://*.amazon.com/*", 42 | "*://*.tiendamia.com/*", 43 | "*://*.aliexpress.com/*", 44 | "*://*.alibaba.com/*", 45 | "*://*.bestbuy.com/*", 46 | "*://*.walmart.com/*", 47 | "*://*.g2a.com/*", 48 | "*://*.instant-gaming.com/*", 49 | "*://*.eneba.com/*", 50 | "*://*.gamivo.com/*", 51 | "*://*.digitalworldpsn.com/*", 52 | "*://*.bonoxs.com/*" 53 | ], 54 | "css": [ 55 | "config/config.css", 56 | "prices/prices.css", 57 | "gamepass/gamepass.css" 58 | ], 59 | "js": [ 60 | "core/index.js", 61 | "core/helpers.js", 62 | "config/menu.js", 63 | "prices/playstation.js", 64 | "prices/xbox.js", 65 | "prices/nintendo.js", 66 | "prices/epic.js", 67 | "prices/gog.js", 68 | "prices/ubisoft.js", 69 | "prices/ea.js", 70 | "prices/xbdeals.js", 71 | "prices/psdeals.js", 72 | "prices/ntdeals.js", 73 | "prices/dekudeals.js", 74 | "prices/steam.js", 75 | "prices/humble-bundle.js", 76 | "prices/green-man-gaming.js", 77 | "prices/isthereanydeal.js", 78 | "gamepass/index.js", 79 | "gamepass/menu.js", 80 | "gamepass/steam.js" 81 | ], 82 | "run_at": "document_idle" 83 | } 84 | ], 85 | "web_accessible_resources": [ 86 | { 87 | "resources": [ "chrome.png" ], 88 | "matches": [ "https://*/*" ] 89 | } 90 | ], 91 | "icons": { 92 | "16": "/assets/icon16.png", 93 | "32": "/assets/icon32.png", 94 | "48": "/assets/icon48.png", 95 | "128": "/assets/icon128.png" 96 | } 97 | } -------------------------------------------------------------------------------- /manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Impuestito: precio final juegos con impuestos", 4 | "description": "Te dice el precio final y con impuestos de los juegos de Playstation, Xbox, Nintendo, Epic, GOG y más tiendas. (Argentina)", 5 | "version": "1.14", 6 | "browser_specific_settings": { 7 | "gecko": { 8 | "id": "{91a9979c-49cc-4748-96b0-54930ce3618c}" 9 | } 10 | }, 11 | "background": { 12 | "scripts": [ 13 | "core/background.js" 14 | ] 15 | }, 16 | "host_permissions": [ 17 | "*://localhost:3000/*", 18 | "https://*.up.railway.app/*", 19 | "https://*.impuestito.org/*" 20 | ], 21 | "permissions": [ 22 | "storage", 23 | "alarms", 24 | "webRequest" 25 | ], 26 | "content_scripts": [ 27 | { 28 | "matches": [ 29 | "*://*.playstation.com/*", 30 | "*://*.xbox.com/*", 31 | "*://*.nintendo.com/*", 32 | "*://*.epicgames.com/*", 33 | "*://*.gog.com/*", 34 | "*://*.ubisoft.com/*", 35 | "*://*.ea.com/*", 36 | "*://*.xbdeals.net/*", 37 | "*://*.psdeals.net/*", 38 | "*://*.ntdeals.net/*", 39 | "*://*.dekudeals.com/*", 40 | "*://*.steampowered.com/*", 41 | "*://*.humblebundle.com/*", 42 | "*://*.greenmangaming.com/*", 43 | "*://*.fanatical.com/*", 44 | "*://*.isthereanydeal.com/*", 45 | "*://*.battle.net/*", 46 | "*://*.amazon.com/*", 47 | "*://*.tiendamia.com/*", 48 | "*://*.aliexpress.com/*", 49 | "*://*.alibaba.com/*", 50 | "*://*.bestbuy.com/*", 51 | "*://*.walmart.com/*", 52 | "*://*.g2a.com/*", 53 | "*://*.instant-gaming.com/*", 54 | "*://*.eneba.com/*", 55 | "*://*.gamivo.com/*", 56 | "*://*.digitalworldpsn.com/*", 57 | "*://*.bonoxs.com/*" 58 | ], 59 | "css": [ 60 | "config/config.css", 61 | "prices/prices.css", 62 | "gamepass/gamepass.css" 63 | ], 64 | "js": [ 65 | "core/index.js", 66 | "core/helpers.js", 67 | "config/menu.js", 68 | "prices/playstation.js", 69 | "prices/xbox.js", 70 | "prices/nintendo.js", 71 | "prices/epic.js", 72 | "prices/gog.js", 73 | "prices/ubisoft.js", 74 | "prices/ea.js", 75 | "prices/xbdeals.js", 76 | "prices/psdeals.js", 77 | "prices/ntdeals.js", 78 | "prices/dekudeals.js", 79 | "prices/steam.js", 80 | "prices/humble-bundle.js", 81 | "prices/green-man-gaming.js", 82 | "prices/isthereanydeal.js", 83 | "gamepass/index.js", 84 | "gamepass/menu.js", 85 | "gamepass/steam.js" 86 | ], 87 | "run_at": "document_idle" 88 | } 89 | ], 90 | "web_accessible_resources": [ 91 | { 92 | "resources": [ "chrome.png" ], 93 | "matches": [ "https://*/*" ] 94 | } 95 | ], 96 | "icons": { 97 | "16": "/assets/icon16.png", 98 | "32": "/assets/icon32.png", 99 | "48": "/assets/icon48.png", 100 | "128": "/assets/icon128.png" 101 | } 102 | } -------------------------------------------------------------------------------- /media/chrome-store-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-1.jpg -------------------------------------------------------------------------------- /media/chrome-store-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-2.jpg -------------------------------------------------------------------------------- /media/chrome-store-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-3.jpg -------------------------------------------------------------------------------- /media/chrome-store-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-4.jpg -------------------------------------------------------------------------------- /media/chrome-store-5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-5.jpg -------------------------------------------------------------------------------- /media/chrome-store-6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-6.jpg -------------------------------------------------------------------------------- /media/chrome-store-7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-7.jpg -------------------------------------------------------------------------------- /media/chrome-store-8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucasromerodb/impuestito-extension/c5bcbed4e251489d885fa4ac6393b6b24b3a2475/media/chrome-store-8.jpg -------------------------------------------------------------------------------- /mocks/gamepass_api/ids.js: -------------------------------------------------------------------------------- 1 | export const data = [ 2 | { 3 | siglId: "29a81209-df6f-41fd-a528-2ae6b91f719c", 4 | title: "All games", 5 | description: "Discover your next favorite game", 6 | requiresShuffling: "False", 7 | imageUrl: "http://store-images.s-microsoft.com/image/global.47673.acentoprodimg.40520333-055e-420a-bd6e-39b85591ccd3.65c04579-754f-40be-a1f2-f0e983dba803", 8 | }, 9 | { id: "BRL7GC0GP1BM" }, 10 | { id: "9NL4KTK0N4CG" }, 11 | { id: "9ND0JVB184XL" }, 12 | { id: "9PG28RXDG9GQ" }, 13 | { id: "BV02TBL15DX3" }, 14 | { id: "9PMM21KVRD72" }, 15 | { id: "9NG07QJNK38J" }, 16 | { id: "9PJPV2PC3MWR" }, 17 | { id: "9PN9WG83X4XF" }, 18 | { id: "9P3MGM8ZCS5D" }, 19 | { id: "9N5JRWWGMS1R" }, 20 | { id: "9P6JQDDZ2MQB" }, 21 | { id: "9NR7XDNVP5SW" }, 22 | { id: "BW9TWC8L4JCS" }, 23 | { id: "BZGJRJC1FGF3" }, 24 | { id: "9NBLGGH43KZB" }, 25 | { id: "BXLL06QN8HVP" }, 26 | { id: "9NC9TNXS9C8G" }, 27 | { id: "BVR64CDJ01FJ" }, 28 | { id: "BSJG7TTSWVJ2" }, 29 | { id: "BX27S00SKW1V" }, 30 | { id: "9MTTMQ6WSDP8" }, 31 | { id: "BSLX1RNXR6H7" }, 32 | { id: "BP15SF17LH13" }, 33 | { id: "BPL68T0XK96W" }, 34 | { id: "BZ2N7TQ0XCF2" }, 35 | { id: "9N7GCF5SGCXC" }, 36 | { id: "9P4R2M0NRWNK" }, 37 | { id: "9N360TZQZ0TR" }, 38 | { id: "9NJWFF2KHL6H" }, 39 | { id: "9PPFBQG81Q5J" }, 40 | { id: "BTPDCJXNFSZL" }, 41 | { id: "9NX6XGVJ0J3D" }, 42 | { id: "BRG51C5MWFSG" }, 43 | { id: "9NR7LV9PB9SD" }, 44 | { id: "BNRX1DN6GXM6" }, 45 | { id: "BRT1K9FV4LRR" }, 46 | { id: "9P0FQ0XCV0LB" }, 47 | { id: "9NV2VTVG0L31" }, 48 | { id: "9N7CJX93ZGWN" }, 49 | { id: "9NN1Z8LHMFBV" }, 50 | { id: "C4GH8N6ZXG5L" }, 51 | { id: "9N6F97F9WGL0" }, 52 | { id: "BX4J85JZNGXQ" }, 53 | { id: "9N17CM38WNN8" }, 54 | { id: "9N1NBJNDD08J" }, 55 | { id: "C2X6ZCNKN2WR" }, 56 | { id: "BSLTQT4L0RLV" }, 57 | { id: "9N3DKKRHSJBT" }, 58 | { id: "BR74RLMH966K" }, 59 | { id: "9NXR6469DM2P" }, 60 | { id: "9NWNX54ZMT1K" }, 61 | { id: "C4B8XR1LCXR5" }, 62 | { id: "9N5Q1VBD1ZWZ" }, 63 | { id: "9NGCPXQG3NLZ" }, 64 | { id: "9MZ8RZSD0NFQ" }, 65 | { id: "9N27NM3BT3ML" }, 66 | { id: "9PMM6V93MRKW" }, 67 | { id: "9N07T7TGP6JG" }, 68 | { id: "BSR9NLHVF1KL" }, 69 | { id: "9NP1G0T8JDMR" }, 70 | { id: "C0N22P73QZ60" }, 71 | { id: "BQSCNS1T8PHQ" }, 72 | { id: "BRJLJ6RPLQTJ" }, 73 | { id: "9P5Z4530318L" }, 74 | { id: "9NDZ7NXFF622" }, 75 | { id: "9NNBPBM2F60W" }, 76 | { id: "9NHFVWX1V7QJ" }, 77 | { id: "C37XBX7DCBZ0" }, 78 | { id: "9P5ZDVMCJMFD" }, 79 | { id: "C2GMBPMTHDDK" }, 80 | { id: "9PC4C9NLP3ZD" }, 81 | { id: "9PJGM0T0827V" }, 82 | { id: "9PK5G9HBH6NH" }, 83 | { id: "BSZM480TSWGP" }, 84 | { id: "C299QVC2BSJF" }, 85 | { id: "C5F2XDQPPJKZ" }, 86 | { id: "9NSF0BGH8D86" }, 87 | { id: "9N6Z8DQXSQWH" }, 88 | { id: "9P544PGZXC0P" }, 89 | { id: "9N6V2181GHLM" }, 90 | { id: "C3QH42WRGM3R" }, 91 | { id: "9PLZPHBNHTMF" }, 92 | { id: "9NSL68D814GC" }, 93 | { id: "9MXND4PQLK3W" }, 94 | { id: "9P5S26314HWQ" }, 95 | { id: "9PLT62LRF9V7" }, 96 | { id: "C2592KJZ0Q1X" }, 97 | { id: "BS9SX4Q6XJRF" }, 98 | { id: "C4G76555T4QN" }, 99 | { id: "C47GZZBMR5WG" }, 100 | { id: "BZRK5C951KK7" }, 101 | { id: "9PFD00CZJ35V" }, 102 | { id: "9PBDC0XZ8TXK" }, 103 | { id: "9NB7FZ2GV5N1" }, 104 | { id: "9P934697Z4W4" }, 105 | { id: "9NMD6VV08WGF" }, 106 | { id: "9P9XS9D0LBPV" }, 107 | { id: "9NN7NF8N5Q5C" }, 108 | { id: "9P3NT9H51RMR" }, 109 | { id: "9NPC5398SCQ6" }, 110 | { id: "9PC12991NZ5N" }, 111 | { id: "9NJG36MFVR1L" }, 112 | { id: "9N6J02VPG635" }, 113 | { id: "C2Q32JM0BPZL" }, 114 | { id: "C2WKJJ9F5936" }, 115 | { id: "BR46KM4D5B9L" }, 116 | { id: "9PL4HXW1H502" }, 117 | { id: "C29HQ887KH4B" }, 118 | { id: "C3KLDKZBHNCZ" }, 119 | { id: "BS6WJ2L56B10" }, 120 | { id: "BX3JNK07Z6QK" }, 121 | { id: "BR7X7MVBBQKM" }, 122 | { id: "9N29VZ9LRNNQ" }, 123 | { id: "9P6SRW1HVW9K" }, 124 | { id: "BQQKG9H2STC0" }, 125 | { id: "9NLM4TTHCWSH" }, 126 | { id: "9P982VRDSV6G" }, 127 | { id: "C4CBF3FHHCND" }, 128 | { id: "9MV2S7Q5PHSW" }, 129 | { id: "9PNJXVCVWD4K" }, 130 | { id: "9NKX70BBCDRN" }, 131 | { id: "9NP539LHJD8S" }, 132 | { id: "9NLX3GWW0HF1" }, 133 | { id: "C2P985H1H42H" }, 134 | { id: "BPQZT43FWD49" }, 135 | { id: "9PP09MJRH2XD" }, 136 | { id: "9P4KMR76PLLQ" }, 137 | { id: "C1SDBNRFXT1D" }, 138 | { id: "BPKDQSSFQ9WV" }, 139 | { id: "9NBLGGH4PBBM" }, 140 | { id: "BSHMMGRP84N4" }, 141 | { id: "BQT21VXFS52F" }, 142 | { id: "9NN3HCKW5TPC" }, 143 | { id: "C20WW4W29FQ1" }, 144 | { id: "9PHK9D8CLQCG" }, 145 | { id: "BV0NM309K1TL" }, 146 | { id: "9N14G09PWG74" }, 147 | { id: "C41M2F4NWB2S" }, 148 | { id: "9PJTHRNVH62H" }, 149 | { id: "9P01JWGQGQ9C" }, 150 | { id: "BRRC2BP0G9P0" }, 151 | { id: "9NP1P1WFS0LB" }, 152 | { id: "C42KCJCLX6MX" }, 153 | { id: "BPRPQSKXTD1L" }, 154 | { id: "BV0K9LMLQ9W5" }, 155 | { id: "9MT8PTGVHX2P" }, 156 | { id: "9ND8C4314ZZG" }, 157 | { id: "C4Z7QM8FSXM2" }, 158 | { id: "9NN82NH949D5" }, 159 | { id: "9MW9469V91LM" }, 160 | { id: "9NBFGKQLMV33" }, 161 | { id: "BSMZH25V6V46" }, 162 | { id: "9P4Q17HQ2WKW" }, 163 | { id: "9PM1905P9LQ6" }, 164 | { id: "C07KJZRH0L7S" }, 165 | { id: "9NK7HNCG0R8D" }, 166 | { id: "BPKVH4C4XV4N" }, 167 | { id: "C17GQF31D617" }, 168 | { id: "9NXVC0482QS5" }, 169 | { id: "BRRVN5198461" }, 170 | { id: "C1ZWH2BZ9TSF" }, 171 | { id: "9MWHMJ0SRBXV" }, 172 | { id: "9P9MC8B7R5FP" }, 173 | { id: "BV83SM3191S5" }, 174 | { id: "9P6FTM76L1S7" }, 175 | { id: "9MZVSHQZ666K" }, 176 | { id: "BVQ3FL3201P8" }, 177 | { id: "9P87CGF1TCCX" }, 178 | { id: "9N5GLFTT40SN" }, 179 | { id: "9NPFJTM4HBH9" }, 180 | { id: "9P8N66DTG10T" }, 181 | { id: "9NFWSNN4JWKB" }, 182 | { id: "9PM9ZVWT18WR" }, 183 | { id: "9MV6MCVLT8GR" }, 184 | { id: "9N8C03GW2TRB" }, 185 | { id: "9NVRJS95FLM9" }, 186 | { id: "9P5B81KVDGP1" }, 187 | { id: "BRX6HS1G5CDK" }, 188 | { id: "9PGLL77C201J" }, 189 | { id: "9PKWHT7G60WQ" }, 190 | { id: "BZPR04V49BMH" }, 191 | { id: "9P66GGSJR71M" }, 192 | { id: "9PB86W3JK8Z5" }, 193 | { id: "9PDDP6ML6XHF" }, 194 | { id: "9PDXJP3805DN" }, 195 | { id: "9MTJ74MKQM46" }, 196 | { id: "9PDV8FKWP3B4" }, 197 | { id: "9PH1Q5TKPQCQ" }, 198 | { id: "9MV8J6MFJNQV" }, 199 | { id: "9N8NJ74FZTG9" }, 200 | { id: "C48LBRJJCP2L" }, 201 | { id: "9MXM0G8RP4H4" }, 202 | { id: "9MZNS9NZ97PF" }, 203 | { id: "9NP4BGBLLLXM" }, 204 | { id: "9P7VCSGBP9KL" }, 205 | { id: "9P8XJRLCLH2P" }, 206 | { id: "9P77VD8MGJX8" }, 207 | { id: "BTC0L0BW6LWC" }, 208 | { id: "9PC2BJDXR2LK" }, 209 | { id: "9P6N58X27150" }, 210 | { id: "9P16M6LF0QFH" }, 211 | { id: "9NDJLXD2X2DM" }, 212 | { id: "BX1FZX1X4132" }, 213 | { id: "9MVVDHS95RCL" }, 214 | { id: "BRZZLBF5T245" }, 215 | { id: "9NMBJQ0265ZK" }, 216 | { id: "9NNSTP6KJTZ9" }, 217 | { id: "9MZN3SMXN824" }, 218 | { id: "9PBGCRBQKXTP" }, 219 | { id: "9P7SL78VHVMF" }, 220 | { id: "9N27DBP8GNNB" }, 221 | { id: "9NZ5QW71X49G" }, 222 | { id: "9PGSC3PW4N8Z" }, 223 | { id: "BQVQTL3PCH05" }, 224 | { id: "9NFZ65KKJ10X" }, 225 | { id: "9NRDVCW02JD7" }, 226 | { id: "9PCCFDH6LMQJ" }, 227 | { id: "C5K89TFLSV19" }, 228 | { id: "9N9606CC950J" }, 229 | { id: "9N6PB00DXQ7H" }, 230 | { id: "9P8WMQ1S4TF9" }, 231 | { id: "9N6HB778ZWP2" }, 232 | { id: "9N4K8K2ZGF1L" }, 233 | { id: "9NKKDCVR3VW9" }, 234 | { id: "BW85KQB8Q31M" }, 235 | { id: "9N8CD0XZKLP4" }, 236 | { id: "C596FKDKMQN7" }, 237 | { id: "C083G6BGJ334" }, 238 | { id: "BVJLKDG2TX8H" }, 239 | { id: "9P6W2Q41BB8V" }, 240 | { id: "9N673ZL1TCS7" }, 241 | { id: "9N3TF03KNTBD" }, 242 | { id: "9NVN8NSXDK41" }, 243 | { id: "9MWBT3HFCZ3Z" }, 244 | { id: "BNCZHKWRZ7BR" }, 245 | { id: "C0SWGV4560W1" }, 246 | { id: "BZF7N4FQWHNR" }, 247 | { id: "9NZDHXL9SJJ8" }, 248 | { id: "9NCBTFHWJQNX" }, 249 | { id: "9P1P5Q3BNM7J" }, 250 | { id: "9N8WRDC25K6J" }, 251 | { id: "9PJD2KMX7TZ6" }, 252 | { id: "BS34VNW7H61F" }, 253 | { id: "BTJ0T8C04ZBV" }, 254 | { id: "C4HZC7LJG6PX" }, 255 | { id: "BNRH7BRC1D02" }, 256 | { id: "9P5VMG8D4P4B" }, 257 | { id: "9NHDJC0NW20M" }, 258 | { id: "BQMVWCMB8P59" }, 259 | { id: "9MZRSLLWKWDV" }, 260 | { id: "9NQ73XB1Q5ZG" }, 261 | { id: "C5HHPG1TXDNG" }, 262 | { id: "9NBR2VXT87SJ" }, 263 | { id: "9P34LH5ZWBVG" }, 264 | { id: "9P1Z43KRNQD4" }, 265 | { id: "9N1JLJR48FBG" }, 266 | { id: "BR27BSZ2M3SR" }, 267 | { id: "C2DCJ95ZXBMS" }, 268 | { id: "9PDRJWHBSK88" }, 269 | { id: "9NRBH9HS807L" }, 270 | { id: "9NBLGGH1Z6FQ" }, 271 | { id: "9NNZJ389GSW2" }, 272 | { id: "9NVBKDF85W8T" }, 273 | { id: "9PL36RW9ZTPW" }, 274 | { id: "9P3PL76N0KWZ" }, 275 | { id: "9N9RMHTJX41P" }, 276 | { id: "9NM3TNRPQXLR" }, 277 | { id: "9P2N57MC619K" }, 278 | { id: "9P9JZLNFQ9PT" }, 279 | { id: "9P7GBPGT90L3" }, 280 | { id: "9N5521ZQMQMJ" }, 281 | { id: "9N6PHQ93Z451" }, 282 | { id: "9PKJTP6DTND3" }, 283 | { id: "9N7V5R3VLMJZ" }, 284 | { id: "9N4PGNQQ7ZFK" }, 285 | { id: "9PLF2RS7JRZL" }, 286 | { id: "BNKDKQXMXRR2" }, 287 | { id: "C3QWNCV55VLL" }, 288 | { id: "9NW4Z3HPJVKW" }, 289 | { id: "9P6W46XMDJM5" }, 290 | { id: "9PHFJ0N31NV1" }, 291 | { id: "BZ5VLVX84STW" }, 292 | { id: "9PP8Q82H79LC" }, 293 | { id: "9PNRSC0J6DT8" }, 294 | { id: "9N1KQPLTR7S5" }, 295 | { id: "C23M2TC1ZFPJ" }, 296 | { id: "9NVD5DB5T3HD" }, 297 | { id: "9N23WV1HGLTQ" }, 298 | { id: "9PM85QK6C13H" }, 299 | { id: "9N0TRF57SMQH" }, 300 | { id: "C2CSDTSCBZ0C" }, 301 | { id: "C0GWTPD0S8S1" }, 302 | { id: "BV9CWVQWNS4P" }, 303 | { id: "C3D891Z6TNQM" }, 304 | { id: "9NT4X7P8B9NB" }, 305 | { id: "BWL72GR7Z7GK" }, 306 | { id: "BX3S1Q5DVHRD" }, 307 | { id: "9NP6V9LFVJ87" }, 308 | { id: "9NVR4ZQKNBSB" }, 309 | { id: "9PN6MTJX8LZD" }, 310 | { id: "9NPPK20VCTGM" }, 311 | { id: "9MZGGWHPKQ4C" }, 312 | { id: "9NS3673HVH41" }, 313 | { id: "9NF83PRZK6K3" }, 314 | { id: "9NLHWTCWLKGX" }, 315 | { id: "BTNPS60N3114" }, 316 | { id: "9P9S593SLMV7" }, 317 | { id: "C27QL5JBKQ8M" }, 318 | { id: "9PN3VDFTB5HZ" }, 319 | { id: "C2B4T86TXLRS" }, 320 | { id: "9MVN4ND41DD3" }, 321 | { id: "9MT8HTJC4GSB" }, 322 | { id: "BXVCFBJBNS17" }, 323 | { id: "BQ1W1T1FC14W" }, 324 | { id: "C2M8HBNVPT1T" }, 325 | { id: "C40860J5R2MP" }, 326 | { id: "9NJ4R763M7TH" }, 327 | { id: "9P008L2LS87F" }, 328 | { id: "9NQTJK43NCQJ" }, 329 | { id: "C41ZDFQ82M1G" }, 330 | { id: "BVTKN6CQ8W5F" }, 331 | { id: "9NTX07HR22TG" }, 332 | { id: "9P94PCKP864B" }, 333 | { id: "9PLFD17M47K3" }, 334 | { id: "BQR7QS0F8SJ3" }, 335 | { id: "C2XNJC9WK15X" }, 336 | { id: "C2DQXX0CB42F" }, 337 | { id: "BW6B077FCH11" }, 338 | { id: "BXBJQ1932138" }, 339 | { id: "9MT6TG9CXR2H" }, 340 | { id: "9NLLP82XVSKH" }, 341 | { id: "C2N9CS4FS1QR" }, 342 | { id: "BXRB4ZH2GJHK" }, 343 | { id: "9NSHNMCM0JR8" }, 344 | { id: "BQND4NQXFFV2" }, 345 | { id: "9PC4RWP34M2D" }, 346 | { id: "9NZG72SH3H4W" }, 347 | { id: "9NFM39PSFXJD" }, 348 | { id: "9NSFGM8J6MBJ" }, 349 | { id: "9PN99PX1P1LX" }, 350 | { id: "9PCQRLT7C2BH" }, 351 | { id: "9MTCRVZQN3GV" }, 352 | { id: "C4KBHNHRPLN6" }, 353 | { id: "9NLRT31Z4RWM" }, 354 | { id: "9N6781PMXC02" }, 355 | { id: "9N0T8V0R7MBC" }, 356 | { id: "9PCW1SMN9RGG" }, 357 | { id: "9PKLF2W8J0TF" }, 358 | { id: "9PJGGX9XJXPB" }, 359 | { id: "9N55W5HG0DSG" }, 360 | { id: "9NH5HN11FG4M" }, 361 | { id: "C4VKLMG1HLZW" }, 362 | { id: "9N7KBCL0NC5H" }, 363 | { id: "BSXSL6WBJ0V7" }, 364 | { id: "BS1NPTPJGD4G" }, 365 | { id: "9P4FCZR21QQK" }, 366 | { id: "C521HDXRTS7F" }, 367 | { id: "BQ9T0JF0D3L4" }, 368 | { id: "9NGH1FK0RJGL" }, 369 | { id: "BSXLFN5QQZSC" }, 370 | { id: "BPR2TBS2KMQJ" }, 371 | { id: "9P0B86JN5X28" }, 372 | { id: "9N232RBCFR2G" }, 373 | { id: "BT9FFLG51VVG" }, 374 | { id: "BPV4GXTDCNSH" }, 375 | { id: "C421ZX7RCG0W" }, 376 | { id: "C4LLMHFQ1BXQ" }, 377 | { id: "C4BZ7X545J1T" }, 378 | { id: "BRJNRZ9N734V" }, 379 | { id: "9NPP17LHJ3MK" }, 380 | { id: "9N3460XCS8BC" }, 381 | { id: "9NXFD44B98P4" }, 382 | { id: "9NK23S9XBMZ6" }, 383 | { id: "9NK3ZFC5R579" }, 384 | { id: "9NBJ51BD0LTH" }, 385 | { id: "9PBJL0NLFMK9" }, 386 | { id: "9NXCSWCQTNFG" }, 387 | { id: "9NCF3MRQ8480" }, 388 | { id: "9PMM5T8C0CN6" }, 389 | { id: "9P47H1RVDWWW" }, 390 | { id: "9PLSCHRN5715" }, 391 | ]; 392 | -------------------------------------------------------------------------------- /mocks/gamepass_api/query.json: -------------------------------------------------------------------------------- 1 | { 2 | "Products": [ 3 | "BRL7GC0GP1BM", 4 | "9NL4KTK0N4CG", 5 | "9ND0JVB184XL", 6 | "9PG28RXDG9GQ", 7 | "BV02TBL15DX3", 8 | "9PMM21KVRD72", 9 | "9NG07QJNK38J", 10 | "9PJPV2PC3MWR", 11 | "9PN9WG83X4XF", 12 | "9P3MGM8ZCS5D", 13 | "9N5JRWWGMS1R", 14 | "9P6JQDDZ2MQB", 15 | "9NR7XDNVP5SW", 16 | "BW9TWC8L4JCS", 17 | "BZGJRJC1FGF3", 18 | "9NBLGGH43KZB", 19 | "BXLL06QN8HVP", 20 | "9NC9TNXS9C8G", 21 | "BVR64CDJ01FJ", 22 | "BSJG7TTSWVJ2", 23 | "BX27S00SKW1V", 24 | "9MTTMQ6WSDP8", 25 | "BSLX1RNXR6H7", 26 | "BP15SF17LH13", 27 | "BPL68T0XK96W", 28 | "BZ2N7TQ0XCF2", 29 | "9N7GCF5SGCXC", 30 | "9P4R2M0NRWNK", 31 | "9N360TZQZ0TR", 32 | "9NJWFF2KHL6H", 33 | "9PPFBQG81Q5J", 34 | "BTPDCJXNFSZL", 35 | "9NX6XGVJ0J3D", 36 | "BRG51C5MWFSG", 37 | "9NR7LV9PB9SD", 38 | "BNRX1DN6GXM6", 39 | "BRT1K9FV4LRR", 40 | "9P0FQ0XCV0LB", 41 | "9NV2VTVG0L31", 42 | "9N7CJX93ZGWN", 43 | "9NN1Z8LHMFBV", 44 | "C4GH8N6ZXG5L", 45 | "9N6F97F9WGL0", 46 | "BX4J85JZNGXQ", 47 | "9N17CM38WNN8", 48 | "9N1NBJNDD08J", 49 | "C2X6ZCNKN2WR", 50 | "BSLTQT4L0RLV", 51 | "9N3DKKRHSJBT", 52 | "BR74RLMH966K", 53 | "9NXR6469DM2P", 54 | "9NWNX54ZMT1K", 55 | "C4B8XR1LCXR5", 56 | "9N5Q1VBD1ZWZ", 57 | "9NGCPXQG3NLZ", 58 | "9MZ8RZSD0NFQ", 59 | "9N27NM3BT3ML", 60 | "9PMM6V93MRKW", 61 | "9N07T7TGP6JG", 62 | "BSR9NLHVF1KL", 63 | "9NP1G0T8JDMR", 64 | "C0N22P73QZ60", 65 | "BQSCNS1T8PHQ", 66 | "BRJLJ6RPLQTJ", 67 | "9P5Z4530318L", 68 | "9NDZ7NXFF622", 69 | "9NNBPBM2F60W", 70 | "9NHFVWX1V7QJ", 71 | "C37XBX7DCBZ0", 72 | "9P5ZDVMCJMFD", 73 | "C2GMBPMTHDDK", 74 | "9PC4C9NLP3ZD", 75 | "9PJGM0T0827V", 76 | "9PK5G9HBH6NH", 77 | "BSZM480TSWGP", 78 | "C299QVC2BSJF", 79 | "C5F2XDQPPJKZ", 80 | "9NSF0BGH8D86", 81 | "9N6Z8DQXSQWH", 82 | "9P544PGZXC0P", 83 | "9N6V2181GHLM", 84 | "C3QH42WRGM3R", 85 | "9PLZPHBNHTMF", 86 | "9NSL68D814GC", 87 | "9MXND4PQLK3W", 88 | "9P5S26314HWQ", 89 | "9PLT62LRF9V7", 90 | "C2592KJZ0Q1X", 91 | "BS9SX4Q6XJRF", 92 | "C4G76555T4QN", 93 | "C47GZZBMR5WG", 94 | "BZRK5C951KK7", 95 | "9PFD00CZJ35V", 96 | "9PBDC0XZ8TXK", 97 | "9NB7FZ2GV5N1", 98 | "9P934697Z4W4", 99 | "9NMD6VV08WGF", 100 | "9P9XS9D0LBPV", 101 | "9NN7NF8N5Q5C", 102 | "9P3NT9H51RMR", 103 | "9NPC5398SCQ6", 104 | "9PC12991NZ5N", 105 | "9NJG36MFVR1L", 106 | "9N6J02VPG635", 107 | "C2Q32JM0BPZL", 108 | "C2WKJJ9F5936", 109 | "BR46KM4D5B9L", 110 | "9PL4HXW1H502", 111 | "C29HQ887KH4B", 112 | "C3KLDKZBHNCZ", 113 | "BS6WJ2L56B10", 114 | "BX3JNK07Z6QK", 115 | "BR7X7MVBBQKM", 116 | "9N29VZ9LRNNQ", 117 | "9P6SRW1HVW9K", 118 | "BQQKG9H2STC0", 119 | "9NLM4TTHCWSH", 120 | "9P982VRDSV6G", 121 | "C4CBF3FHHCND", 122 | "9MV2S7Q5PHSW", 123 | "9PNJXVCVWD4K", 124 | "9NKX70BBCDRN", 125 | "9NP539LHJD8S", 126 | "9NLX3GWW0HF1", 127 | "C2P985H1H42H", 128 | "BPQZT43FWD49", 129 | "9PP09MJRH2XD", 130 | "9P4KMR76PLLQ", 131 | "C1SDBNRFXT1D", 132 | "BPKDQSSFQ9WV", 133 | "9NBLGGH4PBBM", 134 | "BSHMMGRP84N4", 135 | "BQT21VXFS52F", 136 | "9NN3HCKW5TPC", 137 | "C20WW4W29FQ1", 138 | "9PHK9D8CLQCG", 139 | "BV0NM309K1TL", 140 | "9N14G09PWG74", 141 | "C41M2F4NWB2S", 142 | "9PJTHRNVH62H", 143 | "9P01JWGQGQ9C", 144 | "BRRC2BP0G9P0", 145 | "9NP1P1WFS0LB", 146 | "C42KCJCLX6MX", 147 | "BPRPQSKXTD1L", 148 | "BV0K9LMLQ9W5", 149 | "9MT8PTGVHX2P", 150 | "9ND8C4314ZZG", 151 | "C4Z7QM8FSXM2", 152 | "9NN82NH949D5", 153 | "9MW9469V91LM", 154 | "9NBFGKQLMV33", 155 | "BSMZH25V6V46", 156 | "9P4Q17HQ2WKW", 157 | "9PM1905P9LQ6", 158 | "C07KJZRH0L7S", 159 | "9NK7HNCG0R8D", 160 | "BPKVH4C4XV4N", 161 | "C17GQF31D617", 162 | "9NXVC0482QS5", 163 | "BRRVN5198461", 164 | "C1ZWH2BZ9TSF", 165 | "9MWHMJ0SRBXV", 166 | "9P9MC8B7R5FP", 167 | "BV83SM3191S5", 168 | "9P6FTM76L1S7", 169 | "9MZVSHQZ666K", 170 | "BVQ3FL3201P8", 171 | "9P87CGF1TCCX", 172 | "9N5GLFTT40SN", 173 | "9NPFJTM4HBH9", 174 | "9P8N66DTG10T", 175 | "9NFWSNN4JWKB", 176 | "9PM9ZVWT18WR", 177 | "9MV6MCVLT8GR", 178 | "9N8C03GW2TRB", 179 | "9NVRJS95FLM9", 180 | "9P5B81KVDGP1", 181 | "BRX6HS1G5CDK", 182 | "9PGLL77C201J", 183 | "9PKWHT7G60WQ", 184 | "BZPR04V49BMH", 185 | "9P66GGSJR71M", 186 | "9PB86W3JK8Z5", 187 | "9PDDP6ML6XHF", 188 | "9PDXJP3805DN", 189 | "9MTJ74MKQM46", 190 | "9PDV8FKWP3B4", 191 | "9PH1Q5TKPQCQ", 192 | "9MV8J6MFJNQV", 193 | "9N8NJ74FZTG9", 194 | "C48LBRJJCP2L", 195 | "9MXM0G8RP4H4", 196 | "9MZNS9NZ97PF", 197 | "9NP4BGBLLLXM", 198 | "9P7VCSGBP9KL", 199 | "9P8XJRLCLH2P", 200 | "9P77VD8MGJX8", 201 | "BTC0L0BW6LWC", 202 | "9PC2BJDXR2LK", 203 | "9P6N58X27150", 204 | "9P16M6LF0QFH", 205 | "9NDJLXD2X2DM", 206 | "BX1FZX1X4132", 207 | "9MVVDHS95RCL", 208 | "BRZZLBF5T245", 209 | "9NMBJQ0265ZK", 210 | "9NNSTP6KJTZ9", 211 | "9MZN3SMXN824", 212 | "9PBGCRBQKXTP", 213 | "9P7SL78VHVMF", 214 | "9N27DBP8GNNB", 215 | "9NZ5QW71X49G", 216 | "9PGSC3PW4N8Z", 217 | "BQVQTL3PCH05", 218 | "9NFZ65KKJ10X", 219 | "9NRDVCW02JD7", 220 | "9PCCFDH6LMQJ", 221 | "C5K89TFLSV19", 222 | "9N9606CC950J", 223 | "9N6PB00DXQ7H", 224 | "9P8WMQ1S4TF9", 225 | "9N6HB778ZWP2", 226 | "9N4K8K2ZGF1L", 227 | "9NKKDCVR3VW9", 228 | "BW85KQB8Q31M", 229 | "9N8CD0XZKLP4", 230 | "C596FKDKMQN7", 231 | "C083G6BGJ334", 232 | "BVJLKDG2TX8H", 233 | "9P6W2Q41BB8V", 234 | "9N673ZL1TCS7", 235 | "9N3TF03KNTBD", 236 | "9NVN8NSXDK41", 237 | "9MWBT3HFCZ3Z", 238 | "BNCZHKWRZ7BR", 239 | "C0SWGV4560W1", 240 | "BZF7N4FQWHNR", 241 | "9NZDHXL9SJJ8", 242 | "9NCBTFHWJQNX", 243 | "9P1P5Q3BNM7J", 244 | "9N8WRDC25K6J", 245 | "9PJD2KMX7TZ6", 246 | "BS34VNW7H61F", 247 | "BTJ0T8C04ZBV", 248 | "C4HZC7LJG6PX", 249 | "BNRH7BRC1D02", 250 | "9P5VMG8D4P4B", 251 | "9NHDJC0NW20M", 252 | "BQMVWCMB8P59", 253 | "9MZRSLLWKWDV", 254 | "9NQ73XB1Q5ZG", 255 | "C5HHPG1TXDNG", 256 | "9NBR2VXT87SJ", 257 | "9P34LH5ZWBVG", 258 | "9P1Z43KRNQD4", 259 | "9N1JLJR48FBG", 260 | "BR27BSZ2M3SR", 261 | "C2DCJ95ZXBMS", 262 | "9PDRJWHBSK88", 263 | "9NRBH9HS807L", 264 | "9NBLGGH1Z6FQ", 265 | "9NNZJ389GSW2", 266 | "9NVBKDF85W8T", 267 | "9PL36RW9ZTPW", 268 | "9P3PL76N0KWZ", 269 | "9N9RMHTJX41P", 270 | "9NM3TNRPQXLR", 271 | "9P2N57MC619K", 272 | "9P9JZLNFQ9PT", 273 | "9P7GBPGT90L3", 274 | "9N5521ZQMQMJ", 275 | "9N6PHQ93Z451", 276 | "9PKJTP6DTND3", 277 | "9N7V5R3VLMJZ", 278 | "9N4PGNQQ7ZFK", 279 | "9PLF2RS7JRZL", 280 | "BNKDKQXMXRR2", 281 | "C3QWNCV55VLL", 282 | "9NW4Z3HPJVKW", 283 | "9P6W46XMDJM5", 284 | "9PHFJ0N31NV1", 285 | "BZ5VLVX84STW", 286 | "9PP8Q82H79LC", 287 | "9PNRSC0J6DT8", 288 | "9N1KQPLTR7S5", 289 | "C23M2TC1ZFPJ", 290 | "9NVD5DB5T3HD", 291 | "9N23WV1HGLTQ", 292 | "9PM85QK6C13H", 293 | "9N0TRF57SMQH", 294 | "C2CSDTSCBZ0C", 295 | "C0GWTPD0S8S1", 296 | "BV9CWVQWNS4P", 297 | "C3D891Z6TNQM", 298 | "9NT4X7P8B9NB", 299 | "BWL72GR7Z7GK", 300 | "BX3S1Q5DVHRD", 301 | "9NP6V9LFVJ87", 302 | "9NVR4ZQKNBSB", 303 | "9PN6MTJX8LZD", 304 | "9NPPK20VCTGM", 305 | "9MZGGWHPKQ4C", 306 | "9NS3673HVH41", 307 | "9NF83PRZK6K3", 308 | "9NLHWTCWLKGX", 309 | "BTNPS60N3114", 310 | "9P9S593SLMV7", 311 | "C27QL5JBKQ8M", 312 | "9PN3VDFTB5HZ", 313 | "C2B4T86TXLRS", 314 | "9MVN4ND41DD3", 315 | "9MT8HTJC4GSB", 316 | "BXVCFBJBNS17", 317 | "BQ1W1T1FC14W", 318 | "C2M8HBNVPT1T", 319 | "C40860J5R2MP", 320 | "9NJ4R763M7TH", 321 | "9P008L2LS87F", 322 | "9NQTJK43NCQJ", 323 | "C41ZDFQ82M1G", 324 | "BVTKN6CQ8W5F", 325 | "9NTX07HR22TG", 326 | "9P94PCKP864B", 327 | "9PLFD17M47K3", 328 | "BQR7QS0F8SJ3", 329 | "C2XNJC9WK15X", 330 | "C2DQXX0CB42F", 331 | "BW6B077FCH11", 332 | "BXBJQ1932138", 333 | "9MT6TG9CXR2H", 334 | "9NLLP82XVSKH", 335 | "C2N9CS4FS1QR", 336 | "BXRB4ZH2GJHK", 337 | "9NSHNMCM0JR8", 338 | "BQND4NQXFFV2", 339 | "9PC4RWP34M2D", 340 | "9NZG72SH3H4W", 341 | "9NFM39PSFXJD", 342 | "9NSFGM8J6MBJ", 343 | "9PN99PX1P1LX", 344 | "9PCQRLT7C2BH", 345 | "9MTCRVZQN3GV", 346 | "C4KBHNHRPLN6", 347 | "9NLRT31Z4RWM", 348 | "9N6781PMXC02", 349 | "9N0T8V0R7MBC", 350 | "9PCW1SMN9RGG", 351 | "9PKLF2W8J0TF", 352 | "9PJGGX9XJXPB", 353 | "9N55W5HG0DSG", 354 | "9NH5HN11FG4M", 355 | "C4VKLMG1HLZW", 356 | "9N7KBCL0NC5H", 357 | "BSXSL6WBJ0V7", 358 | "BS1NPTPJGD4G", 359 | "9P4FCZR21QQK", 360 | "C521HDXRTS7F", 361 | "BQ9T0JF0D3L4", 362 | "9NGH1FK0RJGL", 363 | "BSXLFN5QQZSC", 364 | "BPR2TBS2KMQJ", 365 | "9P0B86JN5X28", 366 | "9N232RBCFR2G", 367 | "BT9FFLG51VVG", 368 | "BPV4GXTDCNSH", 369 | "C421ZX7RCG0W", 370 | "C4LLMHFQ1BXQ", 371 | "C4BZ7X545J1T", 372 | "BRJNRZ9N734V", 373 | "9NPP17LHJ3MK", 374 | "9N3460XCS8BC", 375 | "9NXFD44B98P4", 376 | "9NK23S9XBMZ6", 377 | "9NK3ZFC5R579", 378 | "9NBJ51BD0LTH", 379 | "9PBJL0NLFMK9", 380 | "9NXCSWCQTNFG", 381 | "9NCF3MRQ8480", 382 | "9PMM5T8C0CN6", 383 | "9P47H1RVDWWW", 384 | "9PLSCHRN5715" 385 | ] 386 | } 387 | -------------------------------------------------------------------------------- /mocks/impuestito_api/impuestito.json: -------------------------------------------------------------------------------- 1 | { 2 | "dollar": { 3 | "bancos": 1098.33, 4 | "cripto": 1245.24, 5 | "mep": 1237.06, 6 | "oficial": 1088 7 | }, 8 | "taxes": { 9 | "iva": 0.21, 10 | "pais": 0, 11 | "ganancias": 0.3, 12 | "iibb": "AR-C", 13 | "defaultTotal": 0.53 14 | }, 15 | "province": { 16 | "AR-C": { 17 | "name": "CABA", 18 | "tax": 0.02 19 | }, 20 | "AR-B": { 21 | "name": "Buenos Aires", 22 | "tax": 0.02 23 | }, 24 | "AR-K": { 25 | "name": "Catamarca", 26 | "tax": 0 27 | }, 28 | "AR-H": { 29 | "name": "Chaco", 30 | "tax": 0.055 31 | }, 32 | "AR-U": { 33 | "name": "Chubut", 34 | "tax": 0 35 | }, 36 | "AR-W": { 37 | "name": "Corrientes", 38 | "tax": 0 39 | }, 40 | "AR-X": { 41 | "name": "Córdoba", 42 | "tax": 0.03 43 | }, 44 | "AR-E": { 45 | "name": "Entre Ríos", 46 | "tax": 0 47 | }, 48 | "AR-P": { 49 | "name": "Formosa", 50 | "tax": 0 51 | }, 52 | "AR-Y": { 53 | "name": "Jujuy", 54 | "tax": 0 55 | }, 56 | "AR-L": { 57 | "name": "La Pampa", 58 | "tax": 0.01 59 | }, 60 | "AR-F": { 61 | "name": "La Rioja", 62 | "tax": 0 63 | }, 64 | "AR-M": { 65 | "name": "Mendoza", 66 | "tax": 0 67 | }, 68 | "AR-N": { 69 | "name": "Misiones", 70 | "tax": 0.025 71 | }, 72 | "AR-Q": { 73 | "name": "Neuquén", 74 | "tax": 0.04 75 | }, 76 | "AR-R": { 77 | "name": "Río Negro", 78 | "tax": 0.05 79 | }, 80 | "AR-J": { 81 | "name": "San Juan", 82 | "tax": 0 83 | }, 84 | "AR-D": { 85 | "name": "San Luis", 86 | "tax": 0 87 | }, 88 | "AR-A": { 89 | "name": "Salta", 90 | "tax": 0.036 91 | }, 92 | "AR-Z": { 93 | "name": "Santa Cruz", 94 | "tax": 0 95 | }, 96 | "AR-S": { 97 | "name": "Santa Fe", 98 | "tax": 0.045 99 | }, 100 | "AR-G": { 101 | "name": "Sgo. del Estero", 102 | "tax": 0 103 | }, 104 | "AR-V": { 105 | "name": "T. del Fuego", 106 | "tax": 0.03 107 | }, 108 | "AR-T": { 109 | "name": "Tucumán", 110 | "tax": 0 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "jest --watch", 4 | "bump": "node scripts/bump-manifest-version.js", 5 | "clean:builds": "rm -rf builds/*.zip", 6 | "dev:chromium": "npm run clean:builds && cp manifest-chromium.json extension/manifest.json && node scripts/compress-extension.js chromium", 7 | "dev:firefox": "npm run clean:builds && cp manifest-firefox.json extension/manifest.json && node scripts/compress-extension.js firefox", 8 | "build:chromium": "cp manifest-chromium.json extension/manifest.json && node scripts/compress-extension.js chromium", 9 | "build:firefox": "cp manifest-firefox.json extension/manifest.json && node scripts/compress-extension.js firefox", 10 | "build": "npm run clean:builds && npm run build:chromium && npm run build:firefox", 11 | "release": "npm run bump && npm run build:chromium && npm run build:firefox" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.26.0", 15 | "@babel/preset-env": "^7.26.0", 16 | "babel-jest": "^29.7.0", 17 | "bestzip": "^2.2.1", 18 | "jest": "^29.7.0" 19 | }, 20 | "jest": { 21 | "transform": { 22 | "^.+\\.js$": "babel-jest" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scripts/bump-manifest-version.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const bumpManifestVersion = (browser) => { 4 | try { 5 | const manifestPath = `./manifest-${browser}.json` 6 | const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); 7 | const currentVersion = manifest.version; 8 | const versionParts = currentVersion.split('.'); 9 | versionParts[versionParts.length - 1] = parseInt(versionParts[versionParts.length - 1]) + 1; 10 | const newVersion = versionParts.join('.'); 11 | manifest.version = newVersion; 12 | fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); 13 | console.log(`Bumped version from ${currentVersion} -> ${newVersion}`); 14 | } catch (err) { 15 | console.error('Error bumping manifest version:', err); 16 | process.exit(1); 17 | } 18 | }; 19 | 20 | bumpManifestVersion('chromium'); 21 | bumpManifestVersion('firefox'); -------------------------------------------------------------------------------- /scripts/compress-extension.js: -------------------------------------------------------------------------------- 1 | var zip = require('bestzip'); 2 | const fs = require('fs'); 3 | 4 | // Get browser from command line arguments 5 | const browser = process.argv[2]; 6 | 7 | if (!browser) { 8 | console.error('\n❌ Please provide a browser argument: firefox or chromium'); 9 | console.error('\n📖 Usage: node compress-extension.js '); 10 | process.exit(1); 11 | } 12 | 13 | if (browser !== 'firefox' && browser !== 'chromium') { 14 | console.error('\n❌ Invalid browser. Please specify "firefox" or "chromium"'); 15 | process.exit(1); 16 | } 17 | 18 | console.log(`\n🔨 Building for ${browser}...`); 19 | 20 | // Copy the appropriate manifest file based on browser selection 21 | const sourceManifest = `./manifest-${browser}.json`; 22 | const targetManifest = './extension/manifest.json'; 23 | 24 | try { 25 | fs.copyFileSync(sourceManifest, targetManifest); 26 | console.log(`\n📝 Copied ${sourceManifest} to ${targetManifest}`); 27 | } catch (err) { 28 | console.error(`\n❌ Error copying manifest file: ${err}`); 29 | process.exit(1); 30 | } 31 | 32 | // Get manifest version 33 | const manifestPath = './extension/manifest.json'; 34 | const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); 35 | const envVars = JSON.parse(fs.readFileSync('./.env.json', 'utf8')); 36 | 37 | console.log("\n⏳ Getting API URLs from environments..."); 38 | const api_taxes = envVars.IMPUESTITO_API_URL || ""; 39 | const api_gamepass = envVars.XBOX_STORE_API_URL || ""; 40 | 41 | 42 | if (api_taxes !== "" || api_gamepass !== "") { 43 | console.log("\n📕 Current manifest version:", manifest.version); 44 | console.log("\n⏳ Setting API URLs..."); 45 | console.log("📝 IMPUESTITO_API_URL:", api_taxes); 46 | console.log("📝 XBOX_STORE_API_URL:", api_gamepass); 47 | manifest.web_accessible_resources[0].resources = [api_taxes, api_gamepass]; 48 | fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); 49 | console.log("✅ API URLs updated"); 50 | } else { 51 | console.error("\n❌ API URLs not found in .env file"); 52 | process.exit(1); 53 | } 54 | 55 | 56 | console.log("\n📦 Compressing extension (.zip)..."); 57 | zip({ 58 | cwd: 'extension/', 59 | source: '*', 60 | destination: `../builds/impuestito-extension-v${manifest.version}-${browser}.zip` 61 | }).then(function() { 62 | console.log('✅ Extension compressed successfully!'); 63 | }).catch(function(err) { 64 | console.error('❌ Error compressing extension:', err.stack); 65 | process.exit(1); 66 | }); 67 | --------------------------------------------------------------------------------