// ==UserScript== // @name App Store: Auto Expand IAP and Owned Badge // @namespace https://apps.apple.com/ // @version 1.0.1 // @description Auto expand In-App Purchases on Apple App Store pages and mark owned apps via AppRaven. // @author Neurogram & Codex // @icon https://apps.apple.com/assets/favicon/favicon-180.png // @match https://apps.apple.com/*/app/*/id* // @match https://apps.apple.com/*/app/id* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect appraven.net // @run-at document-idle // @updateURL https://raw.githubusercontent.com/Neurogram-R/Tampermonkey/refs/heads/main/AppStore-Enhancer.user.js // @downloadURL https://raw.githubusercontent.com/Neurogram-R/Tampermonkey/refs/heads/main/AppStore-Enhancer.user.js // ==/UserScript== (function () { 'use strict'; const GRAPHQL_URL = 'https://appraven.net/appraven/graphql'; const OWNED_TEXT = 'Owned'; const NOT_OWNED_TEXT = 'Get'; const STORAGE_KEYS = { jsessionId: 'appraven.jsessionid', rememberMe: 'appraven.rememberMe' }; const IAP_TEXT_PATTERNS = [ /in[\s-]?app purchases?/i, /app\s*\u5185\u8d2d\u4e70\u9879\u76ee/i, /app\s*\u5167\u8cfc\u8cb7/i, /\u5185\u8d2d/i, /\u5167\u8cfc/i, /\u30a2\u30d7\u30ea\u5185\u8ab2\u91d1/i, /\uc571\s*\ub0b4\s*\uad6c\uc785/i, /achats int[e\u00e9]gr[e\u00e9]s/i, /achats dans l['\u2019]app/i, /compras dentro de la app/i, /compras integradas/i, /compras dentro do app/i, /in[\s-]?app[\s-]?k[a\u00e4]ufe/i, /acquisti in[\s-]?app/i, /in[\s-]?app aankopen/i, /\u043f\u043e\u043a\u0443\u043f\u043a\u0438 \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438/i, /\u0639\u0645\u0644\u064a\u0627\u062a \u0627\u0644\u0634\u0631\u0627\u0621 \u062f\u0627\u062e\u0644 \u0627\u0644\u062a\u0637\u0628\u064a\u0642/i ]; const SHARE_TEXT_PATTERNS = [ /^share$/i, /^\u5206\u4eab$/, /^\u5171\u6709$/, /^\uacf5\uc720$/, /^partager$/i, /^teilen$/i, /^compartir$/i, /^compartilhar$/i, /^condividi$/i, /^\u043f\u043e\u0434\u0435\u043b\u0438\u0442\u044c\u0441\u044f$/i ]; const ownedCache = new Map(); let currentITunesId = null; let routeRunId = 0; let pageObserver = null; function getITunesId() { const match = location.pathname.match(/\/id(\d+)(?:[/?#]|$)/); return match ? match[1] : null; } function getAppRavenCookies() { return { jsessionId: String(GM_getValue(STORAGE_KEYS.jsessionId, '') || '').trim(), rememberMe: String(GM_getValue(STORAGE_KEYS.rememberMe, '') || '').trim() }; } function hasAppRavenCookie() { const cookies = getAppRavenCookies(); return Boolean(cookies.jsessionId && cookies.rememberMe); } function registerConfigMenu() { GM_registerMenuCommand('Set AppRaven cookies', () => { const current = getAppRavenCookies(); const jsessionId = window.prompt('AppRaven JSESSIONID:', current.jsessionId); if (jsessionId === null) return; const rememberMe = window.prompt('AppRaven remember-me:', current.rememberMe); if (rememberMe === null) return; GM_setValue(STORAGE_KEYS.jsessionId, jsessionId.trim()); GM_setValue(STORAGE_KEYS.rememberMe, rememberMe.trim()); ownedCache.clear(); currentITunesId = null; handleRouteChange(true); }); GM_registerMenuCommand('Clear AppRaven cookies', () => { GM_deleteValue(STORAGE_KEYS.jsessionId); GM_deleteValue(STORAGE_KEYS.rememberMe); ownedCache.clear(); currentITunesId = null; restoreOwnedMarkers(); }); } function appRavenRequest(operationName, query, variables) { const cookies = getAppRavenCookies(); const cookieHeader = `JSESSIONID=${cookies.jsessionId}; remember-me=${cookies.rememberMe}`; return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: GRAPHQL_URL, data: JSON.stringify({ operationName, query, variables }), cookie: cookieHeader, headers: { 'content-type': 'application/json', 'accept': '*/*', 'accept-language': 'en-US,en;q=0.9', 'apollographql-client-name': 'net.appraven.app-apollo-ios', 'apollographql-client-version': '2.2.11-2', 'x-apollo-operation-name': operationName, 'x-apollo-operation-type': 'query', 'cookie': cookieHeader }, responseType: 'json', timeout: 20000, onload(response) { if (response.status < 200 || response.status >= 300) { reject(new Error(`AppRaven HTTP ${response.status}`)); return; } const body = response.response || safeJsonParse(response.responseText); if (!body || body.errors) { reject(new Error(`AppRaven GraphQL error: ${JSON.stringify(body && body.errors)}`)); return; } resolve(body.data); }, onerror: reject, ontimeout: () => reject(new Error('AppRaven request timed out')) }); }); } function safeJsonParse(text) { try { return JSON.parse(text); } catch { return null; } } async function isOwnedByITunesId(iTunesId) { if (ownedCache.has(iTunesId)) return ownedCache.get(iTunesId); const idData = await appRavenRequest( 'GetAppIdByITunesId', `query GetAppIdByITunesId($iTunesId: Long!) { appByITunesId(iTunesId: $iTunesId) { __typename id } }`, { iTunesId } ); const appRavenId = idData && idData.appByITunesId && idData.appByITunesId.id; if (!appRavenId) { ownedCache.set(iTunesId, false); return false; } const detailData = await appRavenRequest( 'GetAppDetail', `query GetAppDetail($id: ID!) { app(id: $id) { __typename id youOwn } }`, { id: appRavenId } ); const owned = Boolean(detailData && detailData.app && detailData.app.youOwn); ownedCache.set(iTunesId, owned); return owned; } function textMatchesAny(text, patterns) { const normalized = String(text || '').replace(/\s+/g, ' ').trim(); return patterns.some((pattern) => pattern.test(normalized)); } function getVisibleText(element) { return (element.innerText || element.textContent || '').replace(/\s+/g, ' ').trim(); } function isIapLabel(element) { return textMatchesAny(getVisibleText(element), IAP_TEXT_PATTERNS); } function expandDetails(details) { if (!details || details.open || details.dataset.appStoreIapExpanded === '1') return; details.dataset.appStoreIapExpanded = '1'; const summary = details.querySelector('summary'); if (summary) summary.click(); if (!details.open) details.open = true; } function expandDefinitionListIap() { for (const dt of document.querySelectorAll('dt')) { if (!isIapLabel(dt)) continue; const row = dt.parentElement; const details = row && row.querySelector('dd details'); expandDetails(details); } } function isProbablyIapArea(element) { const container = element.closest('section, div, li, dd, dl') || element; const text = getVisibleText(container); const ariaText = [ element.getAttribute('aria-label'), element.getAttribute('aria-controls'), element.id, element.className, container.id, container.className ].join(' '); return textMatchesAny(`${text} ${ariaText}`, IAP_TEXT_PATTERNS); } function expandButtonBasedIap() { const candidates = [ ...document.querySelectorAll('button[aria-expanded="false"], [role="button"][aria-expanded="false"]'), ...document.querySelectorAll('button.we-truncate__button, button[class*="truncate"], button[class*="more"]') ]; for (const button of candidates) { if (button.dataset.appStoreIapExpanded === '1') continue; if (!isProbablyIapArea(button)) continue; button.dataset.appStoreIapExpanded = '1'; button.click(); } } function expandInAppPurchases() { expandDefinitionListIap(); expandButtonBasedIap(); } function findShareButton() { const selectors = [ '[data-test-share-button]', 'button[data-metrics-click*="share" i]', 'button[aria-label*="share" i]', 'a[aria-label*="share" i]', '.we-button--share', '.product-header__share' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element) return element; } const buttons = [...document.querySelectorAll('button, a[role="button"]')]; return buttons.find((button) => { const text = getVisibleText(button); const aria = button.getAttribute('aria-label') || ''; return textMatchesAny(text, SHARE_TEXT_PATTERNS) || /share/i.test(aria); }); } function findShareTextNode(element) { const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, { acceptNode(node) { return textMatchesAny(node.nodeValue, SHARE_TEXT_PATTERNS) || [OWNED_TEXT, NOT_OWNED_TEXT].includes(node.nodeValue.trim()) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } }); return walker.nextNode(); } function restoreOwnedMarkers() { for (const element of document.querySelectorAll('[data-app-store-owned-marked="1"]')) { const originalText = element.dataset.appStoreOriginalText || 'Share'; const originalAria = element.dataset.appStoreOriginalAria || originalText; const textNode = findShareTextNode(element); if (textNode) textNode.nodeValue = originalText; else element.textContent = originalText; element.setAttribute('aria-label', originalAria); delete element.dataset.appStoreOwnedMarked; delete element.dataset.appStoreOwnedId; delete element.dataset.appStoreOwnedText; delete element.dataset.appStoreOriginalText; delete element.dataset.appStoreOriginalAria; } } function markShareAsResult(iTunesId, text) { const shareButton = findShareButton(); if (!shareButton) return false; if ( shareButton.dataset.appStoreOwnedMarked === '1' && shareButton.dataset.appStoreOwnedId === iTunesId && shareButton.dataset.appStoreOwnedText === text ) { return true; } const textNode = findShareTextNode(shareButton); shareButton.dataset.appStoreOriginalText = textNode ? textNode.nodeValue : getVisibleText(shareButton); shareButton.dataset.appStoreOriginalAria = shareButton.getAttribute('aria-label') || ''; if (textNode) textNode.nodeValue = text; else shareButton.textContent = text; shareButton.setAttribute('aria-label', text); shareButton.dataset.appStoreOwnedMarked = '1'; shareButton.dataset.appStoreOwnedId = iTunesId; shareButton.dataset.appStoreOwnedText = text; return true; } function watchPage() { if (pageObserver) return; let pending = false; const run = () => { pending = false; expandInAppPurchases(); handleRouteChange(); }; pageObserver = new MutationObserver(() => { if (pending) return; pending = true; window.setTimeout(run, 150); }); pageObserver.observe(document.documentElement, { childList: true, subtree: true }); run(); } function watchHistory() { for (const methodName of ['pushState', 'replaceState']) { const original = history[methodName]; history[methodName] = function (...args) { const result = original.apply(this, args); window.dispatchEvent(new Event('app-store-url-change')); return result; }; } window.addEventListener('popstate', () => window.dispatchEvent(new Event('app-store-url-change'))); window.addEventListener('app-store-url-change', () => { window.setTimeout(() => { expandInAppPurchases(); handleRouteChange(true); }, 150); }); } async function handleRouteChange(force) { const iTunesId = getITunesId(); if (!iTunesId) return; if (!force && iTunesId === currentITunesId) return; currentITunesId = iTunesId; const runId = ++routeRunId; restoreOwnedMarkers(); if (!hasAppRavenCookie()) return; try { const owned = await isOwnedByITunesId(iTunesId); if (runId !== routeRunId || iTunesId !== getITunesId()) return; const resultText = owned ? OWNED_TEXT : NOT_OWNED_TEXT; if (markShareAsResult(iTunesId, resultText)) return; const markerObserver = new MutationObserver(() => { if (runId !== routeRunId || iTunesId !== getITunesId()) { markerObserver.disconnect(); return; } if (markShareAsResult(iTunesId, resultText)) markerObserver.disconnect(); }); markerObserver.observe(document.documentElement, { childList: true, subtree: true }); window.setTimeout(() => markerObserver.disconnect(), 15000); } catch (error) { console.warn('[App Store Owned IAP] Failed to query AppRaven ownership:', error); } } function main() { registerConfigMenu(); watchHistory(); watchPage(); } main(); })();