// ==UserScript== // @name PS Store Avatar Adder // @namespace https://github.com/yungtry/psn-avatar-unlocker // @version 6.3.0 // @description Adds PS3/PS4 avatars to the PlayStation Store cart. Paste the avatar ID and click the button. // @author yungtry // @match https://store.playstation.com/* // @match https://checkout.playstation.com/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @connect web.np.playstation.com // @run-at document-start // @icon https://store.playstation.com/favicon.ico // ==/UserScript== (function () { 'use strict'; console.log("[PSA] Script loaded on:", window.location.href); // ========================================================================= // NAMESPACES // ========================================================================= const Config = {}; const Utils = {}; const State = {}; const Interceptor = {}; const ApiService = {}; const UiComponents = {}; const EventHandlers = {}; const App = {}; // ========================================================================= // CONFIG // ========================================================================= Object.assign(Config, { GQL_URL: 'https://web.np.playstation.com/api/graphql/v1//op', CLIENT_NAME: '@sie-ppr-web-checkout/app', CLIENT_VERSION: '2.176.0', OPERATION_NAME: 'addToCart', HASH_KEY: 'psa_addToCart_hash', DEFAULT_HASH: '' }); // ========================================================================= // STATE // ========================================================================= Object.assign(State, { getClientName() { return GM_getValue('psa_client_name', Config.CLIENT_NAME); }, setClientName(val) { GM_setValue('psa_client_name', val); }, getClientVersion() { return GM_getValue('psa_client_version', Config.CLIENT_VERSION); }, setClientVersion(val) { GM_setValue('psa_client_version', val); }, getHash() { return GM_getValue(Config.HASH_KEY, Config.DEFAULT_HASH); }, setHash(val) { GM_setValue(Config.HASH_KEY, val); } }); // ========================================================================= // UTILS // ========================================================================= Object.assign(Utils, { detectLocale() { try { const m = window.location.pathname.match(/^\/([a-z]{2})-([a-z]{2})\//i); if (m) return { country: m[2].toUpperCase(), language: `${m[1]}-${m[2]}` }; } catch (_) { } return { country: 'PL', language: 'pl-pl' }; }, uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); }, logMessage(level, text) { const log = document.getElementById('psa-log'); if (!log) return; const e = document.createElement('div'); e.className = `psa-log-entry psa-log-${level}`; const d = document.createElement('span'); d.textContent = text; e.innerHTML = ``; e.appendChild(d); log.appendChild(e); log.scrollTop = log.scrollHeight; }, clearLog() { const log = document.getElementById('psa-log'); if (log) log.innerHTML = ''; }, updateUIHash(hash) { const dot = document.getElementById('psa-hash-dot'); const text = document.getElementById('psa-hash-text'); const manualInput = document.getElementById('psa-manual-hash'); if (dot) dot.className = hash ? '' : 'psa-hash-missing'; if (text) text.textContent = hash ? `Hash: ${hash.substring(0, 20)}...` : 'missing (add product to cart)'; if (manualInput) manualInput.value = hash; } }); // ========================================================================= // INTERCEPTOR (FETCH & XHR) // ========================================================================= Object.assign(Interceptor, { init() { const pageWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; // ─── Hook fetch ─── if (pageWindow.fetch) { const originalFetch = pageWindow.fetch; pageWindow.fetch = function (...args) { try { const [resource, init] = args; const url = typeof resource === 'string' ? resource : resource?.url; if (url) { Interceptor.interceptUrl(url); if (init?.headers) Interceptor.interceptHeaders(init.headers); if (init?.body) Interceptor.interceptBody(init.body); } } catch (_) { } return originalFetch.apply(this, args); }; } // ─── Hook XHR ─── if (pageWindow.XMLHttpRequest) { const originalOpen = pageWindow.XMLHttpRequest.prototype.open; const originalSend = pageWindow.XMLHttpRequest.prototype.send; const originalSetHeader = pageWindow.XMLHttpRequest.prototype.setRequestHeader; pageWindow.XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._psaUrl = url; if (url) Interceptor.interceptUrl(url); return originalOpen.call(this, method, url, ...rest); }; pageWindow.XMLHttpRequest.prototype.setRequestHeader = function (name, value, ...rest) { try { const lowerName = name.toLowerCase(); if (lowerName === 'apollographql-client-name') { State.setClientName(value); } else if (lowerName === 'apollographql-client-version') { State.setClientVersion(value); } } catch (_) { } return originalSetHeader.call(this, name, value, ...rest); }; pageWindow.XMLHttpRequest.prototype.send = function (body) { if (this._psaUrl && body) Interceptor.interceptBody(body); return originalSend.call(this, body); }; } }, interceptHeaders(headers) { if (!headers) return; try { let clientName = null; let clientVersion = null; if (typeof headers.get === 'function') { clientName = headers.get('apollographql-client-name'); clientVersion = headers.get('apollographql-client-version'); } else if (typeof headers === 'object') { for (const key of Object.keys(headers)) { const lowerKey = key.toLowerCase(); if (lowerKey === 'apollographql-client-name') { clientName = headers[key]; } else if (lowerKey === 'apollographql-client-version') { clientVersion = headers[key]; } } } if (clientName && typeof clientName === 'string') { State.setClientName(clientName.trim()); } if (clientVersion && typeof clientVersion === 'string') { State.setClientVersion(clientVersion.trim()); } } catch (_) { } }, interceptUrl(url) { try { if (!url.includes('graphql') && !url.includes('np.playstation.com')) return; const decoded = decodeURIComponent(url); const opMatch = decoded.match(/operationName[=:]([A-Za-z_]+)/); const hashMatch = decoded.match(/sha256Hash['":\s]*["']?([a-f0-9]{64})/i); if (opMatch && hashMatch) { Interceptor.sendInterceptionNotice(opMatch[1], hashMatch[1]); } } catch (_) { } }, interceptBody(raw) { try { const body = typeof raw === 'string' ? JSON.parse(raw) : raw; const hash = body?.extensions?.persistedQuery?.sha256Hash; const op = body?.operationName; if (hash && op) { Interceptor.sendInterceptionNotice(op, hash); } } catch (_) { } }, sendInterceptionNotice(op, hash) { if (op === Config.OPERATION_NAME) { if (/^[a-f0-9]{64}$/.test(hash)) { State.setHash(hash); } } // Pass to top window if intercepted inside an iframe if (window.self !== window.top) { window.top.postMessage({ type: 'PSA_OP_INTERCEPTED', op: op, hash: hash }, '*'); } else { EventHandlers.handleInterceptedOp(op, hash); } } }); // ========================================================================= // API SERVICE // ========================================================================= Object.assign(ApiService, { addToCartGQL(sku, hash, country, language) { return new Promise((resolve) => { const locale = `${language.split('-')[0]}-${country}`; const clientName = State.getClientName(); const clientVersion = State.getClientVersion(); GM_xmlhttpRequest({ method: 'POST', url: Config.GQL_URL, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'apollographql-client-name': clientName, 'apollographql-client-version': clientVersion, 'x-psn-app-ver': `${clientName}/v${clientVersion}`, 'x-psn-correlation-id': Utils.uuid(), 'x-psn-request-id': Utils.uuid(), 'x-psn-storefront-type': 'checkout:store', 'x-psn-store-locale-override': locale, 'x-psn-store-country': country, 'x-psn-store-language': language.split('-')[0], 'Origin': 'https://checkout.playstation.com', 'Referer': 'https://checkout.playstation.com/', }, data: JSON.stringify({ operationName: 'addToCart', variables: { skus: [{ skuId: sku, rewardId: 'OUTRIGHT' }] }, extensions: { persistedQuery: { version: 1, sha256Hash: hash } } }), anonymous: false, onload: (resp) => { try { resolve(JSON.parse(resp.responseText)); } catch (e) { resolve({ errors: [{ message: `HTTP ${resp.status}: ${resp.statusText}` }] }); } }, onerror: () => resolve({ errors: [{ message: 'Network error' }] }), ontimeout: () => resolve({ errors: [{ message: 'Timeout' }] }) }); }); } }); // ========================================================================= // UI COMPONENTS // ========================================================================= Object.assign(UiComponents, { injectStyles() { GM_addStyle(` #psa-panel { position: fixed; bottom: 24px; left: 24px; z-index: 999999; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; width: 380px; transition: transform 0.35s cubic-bezier(0.4,0,0.2,1), opacity 0.35s cubic-bezier(0.4,0,0.2,1); } #psa-panel.psa-hidden { transform: translateY(20px); opacity: 0; pointer-events: none; } #psa-card { background: #0b101d; border: 1px solid rgba(0, 114, 206, 0.4); border-radius: 12px; padding: 24px; box-shadow: 0 12px 40px rgba(0,0,0,0.65), 0 0 20px rgba(0, 114, 206, 0.15); color: #f3f4f6; } #psa-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; } #psa-title { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 700; color: #ffffff; text-transform: uppercase; letter-spacing: 0.05em; } #psa-close-btn { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: #9ca3af; cursor: pointer; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; transition: all 0.2s; padding: 0; } #psa-close-btn:hover { background: #ef4444; color: #ffffff; border-color: #ef4444; } #psa-input-group { display: flex; flex-direction: column; gap: 8px; margin-bottom: 14px; } #psa-input-label { font-size: 11px; font-weight: 600; color: #9ca3af; text-transform: uppercase; letter-spacing: 0.08em; } #psa-avatar-input { background: #161c2c; border: 1px solid #1f293d; border-radius: 6px; padding: 12px 14px; color: #ffffff; font-size: 13px; font-family: 'SF Mono','Fira Code',monospace; outline: none; transition: border-color 0.25s, box-shadow 0.25s; width: 100%; box-sizing: border-box; } #psa-avatar-input::placeholder { color: #4b5563; } #psa-avatar-input:focus { border-color: #0072ce; box-shadow: 0 0 0 3px rgba(0, 114, 206, 0.25); } #psa-country-group { display: flex; gap: 8px; margin-bottom: 16px; } #psa-country-select, #psa-lang-input { background: #161c2c; border: 1px solid #1f293d; border-radius: 6px; padding: 10px 12px; color: #ffffff; font-size: 12px; outline: none; flex: 1; box-sizing: border-box; } #psa-country-select option { background: #0b101d; color: #ffffff; } #psa-add-btn { width: 100%; padding: 12px 20px; border: none; border-radius: 24px; font-size: 14px; font-weight: 700; cursor: pointer; background: #0072ce; color: white; box-shadow: 0 4px 12px rgba(0, 114, 206, 0.35); transition: all 0.2s; position: relative; overflow: hidden; text-transform: uppercase; letter-spacing: 0.05em; } #psa-add-btn:hover:not(:disabled) { background: #0082eb; transform: translateY(-1px); box-shadow: 0 6px 16px rgba(0, 114, 206, 0.5); } #psa-add-btn:disabled { opacity: 0.4; cursor: not-allowed; } #psa-add-btn.psa-loading { color: transparent; } #psa-add-btn.psa-loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 20px; height: 20px; margin: -10px 0 0 -10px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: psa-spin 0.6s linear infinite; } @keyframes psa-spin { to { transform: rotate(360deg); } } #psa-log { margin-top: 14px; max-height: 180px; overflow-y: auto; font-size: 11px; font-family: 'SF Mono','Fira Code',monospace; line-height: 1.5; scrollbar-width: thin; background: #070a12; padding: 10px; border-radius: 6px; border: 1px solid #131926; } .psa-log-entry { padding: 3px 0; display: flex; align-items: flex-start; gap: 6px; } .psa-log-entry .psa-dot { width: 6px; height: 6px; border-radius: 50%; margin-top: 5px; flex-shrink: 0; } .psa-log-info .psa-dot { background: #0072ce; } .psa-log-ok .psa-dot { background: #10b981; } .psa-log-warn .psa-dot { background: #f59e0b; } .psa-log-err .psa-dot { background: #ef4444; } .psa-log-info { color: #9ca3af; } .psa-log-ok { color: #34d399; } .psa-log-warn { color: #fbbf24; } .psa-log-err { color: #f87171; } #psa-hash-status { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #9ca3af; margin-bottom: 14px; padding: 8px 12px; background: #161c2c; border-radius: 6px; border: 1px solid #1f293d; } #psa-hash-dot { width: 8px; height: 8px; border-radius: 50%; background: #10b981; box-shadow: 0 0 6px rgba(16,185,129,0.5); } #psa-hash-dot.psa-hash-missing { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.5); } #psa-toggle-btn { position: fixed; bottom: 24px; left: 24px; z-index: 999998; width: 52px; height: 52px; border-radius: 50%; border: 2px solid #0072ce; background: #0b101d; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 6px 20px rgba(0, 114, 206, 0.4); transition: all 0.3s; } #psa-toggle-btn:hover { transform: scale(1.08) rotate(15deg); box-shadow: 0 8px 25px rgba(0, 114, 206, 0.65); } #psa-toggle-btn.psa-hidden { transform: scale(0); opacity: 0; pointer-events: none; } `); }, createUI() { UiComponents.injectStyles(); const locale = Utils.detectLocale(); const toggleBtn = document.createElement('button'); toggleBtn.id = 'psa-toggle-btn'; toggleBtn.innerHTML = ` `; toggleBtn.title = 'PS Avatar Adder'; document.body.appendChild(toggleBtn); const currentHashVal = State.getHash(); const panel = document.createElement('div'); panel.id = 'psa-panel'; panel.classList.add('psa-hidden'); panel.innerHTML = `