import AppKit import CodexBarCore import WebKit @MainActor final class OpenAICreditsPurchaseWindowController: NSWindowController, WKNavigationDelegate, WKScriptMessageHandler { private static let defaultSize = NSSize(width: 980, height: 760) private static let logHandlerName = "codexbarLog" private static let debugLogURL = URL(fileURLWithPath: NSTemporaryDirectory()) .appendingPathComponent("codexbar-buy-credits.log") private static let autoStartScript = """ (() => { if (window.__codexbarAutoBuyCreditsStarted) return 'already'; const log = (...args) => { try { window.webkit?.messageHandlers?.codexbarLog?.postMessage(args); } catch {} }; const buttonSelector = 'button, a, [role="button"], input[type="button"], input[type="submit"]'; const isVisible = (el) => { if (!el || !el.getBoundingClientRect) return false; const rect = el.getBoundingClientRect(); if (rect.width < 2 || rect.height < 2) return false; const style = window.getComputedStyle ? window.getComputedStyle(el) : null; if (style) { if (style.display === 'none' || style.visibility === 'hidden') return false; if (parseFloat(style.opacity || '1') === 0) return false; } return true; }; const textOf = el => { const raw = el && (el.innerText || el.textContent) ? String(el.innerText || el.textContent) : ''; return raw.trim(); }; const matches = text => { const lower = String(text || '').toLowerCase(); if (!lower.includes('credit')) return false; return ( lower.includes('buy') || lower.includes('add') || lower.includes('purchase') || lower.includes('top up') || lower.includes('top-up') ); }; const matchesAddMore = text => { const lower = String(text || '').toLowerCase(); return lower.includes('add more'); }; const labelFor = el => { if (!el) return ''; return textOf(el) || el.getAttribute('aria-label') || el.getAttribute('title') || el.value || ''; }; const summarize = el => { if (!el) return null; return { tag: el.tagName, type: el.getAttribute('type'), role: el.getAttribute('role'), label: labelFor(el), aria: el.getAttribute('aria-label'), disabled: isDisabled(el), href: el.getAttribute('href'), testId: el.getAttribute('data-testid'), className: (el.className && String(el.className).slice(0, 120)) || '' }; }; const collectButtons = () => { const results = new Set(); const addAll = (root) => { if (!root || !root.querySelectorAll) return; root.querySelectorAll(buttonSelector).forEach(el => results.add(el)); }; addAll(document); document.querySelectorAll('*').forEach(el => { if (el.shadowRoot) addAll(el.shadowRoot); }); document.querySelectorAll('iframe').forEach(frame => { try { const doc = frame.contentDocument; if (!doc) return; addAll(doc); doc.querySelectorAll('*').forEach(el => { if (el.shadowRoot) addAll(el.shadowRoot); }); } catch {} }); return Array.from(results); }; const findDialogNextButton = () => { const dialog = document.querySelector('[role=\"dialog\"], dialog, [aria-modal=\"true\"]'); if (!dialog) return null; const buttons = Array.from(dialog.querySelectorAll(buttonSelector)); const labeled = buttons.filter(btn => labelFor(btn).toLowerCase().startsWith('next')); const visible = labeled.find(isVisible); return visible || labeled[0] || null; }; const clickButton = (el) => { if (!el) return false; try { el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); } catch { try { el.click(); } catch { return false; } } return true; }; const triggerPointerClick = (el) => { if (!el) return false; const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null; if (!rect) return false; const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const events = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']; for (const type of events) { try { el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y })); } catch { return false; } } return true; }; const pickLikelyButton = (buttons) => { if (!buttons || buttons.length === 0) return null; const labeled = buttons.find(btn => { const label = labelFor(btn); if (matches(label) || matchesAddMore(label)) return true; const aria = String(btn.getAttribute('aria-label') || '').toLowerCase(); return aria.includes('credit') || aria.includes('buy') || aria.includes('add'); }); return labeled || buttons[0]; }; const findAddMoreButton = () => { const buttons = collectButtons(); return buttons.find(btn => matchesAddMore(labelFor(btn))) || null; }; const findNextButton = () => { const dialogNext = findDialogNextButton(); if (dialogNext) return dialogNext; const buttons = collectButtons(); const labeled = buttons.filter(btn => { const label = labelFor(btn).toLowerCase(); return label === 'next' || label.startsWith('next '); }); const visible = labeled.find(isVisible); if (visible) return visible; const submit = buttons.find(btn => btn.type && String(btn.type).toLowerCase() === 'submit' && isVisible(btn)); return submit || labeled[0] || null; }; const isDisabled = (el) => { if (!el) return true; if (el.disabled) return true; const ariaDisabled = String(el.getAttribute('aria-disabled') || '').toLowerCase(); if (ariaDisabled === 'true') return true; if (el.classList && (el.classList.contains('disabled') || el.classList.contains('is-disabled'))) { return true; } return false; }; const forceClickElement = (el) => { if (!el) return false; const rect = el.getBoundingClientRect ? el.getBoundingClientRect() : null; if (rect) { const x = rect.left + rect.width / 2; const y = rect.top + rect.height / 2; const target = document.elementFromPoint(x, y); if (target) { try { target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); return true; } catch { return false; } } } return false; }; const requestSubmit = (el) => { if (!el || !el.closest) return false; const form = el.closest('form'); if (!form) return false; if (typeof form.requestSubmit === 'function') { form.requestSubmit(el); return true; } if (typeof form.submit === 'function') { form.submit(); return true; } return false; }; const clickNextIfReady = (attempts) => { const nextButton = findNextButton(); if (!nextButton) { if (attempts && attempts % 5 === 0) log('next_missing', { attempts }); return false; } if (isDisabled(nextButton)) { if (attempts && attempts % 5 === 0) log('next_disabled', summarize(nextButton)); return false; } if (!isVisible(nextButton)) { if (attempts && attempts % 5 === 0) log('next_hidden', summarize(nextButton)); return false; } nextButton.focus?.(); if (requestSubmit(nextButton)) { log('next_submit', summarize(nextButton)); return true; } if (triggerPointerClick(nextButton)) { log('next_pointer', summarize(nextButton)); return true; } if (clickButton(nextButton)) { log('next_click', summarize(nextButton)); return true; } return forceClickElement(nextButton); }; const startNextPolling = (initialDelay = 500, interval = 500, maxAttempts = 90) => { if (window.__codexbarNextPolling) return; window.__codexbarNextPolling = true; log('start_next_poll', { initialDelay, interval, maxAttempts }); setTimeout(() => { let attempts = 0; const nextTimer = setInterval(() => { attempts += 1; if (attempts % 5 === 0) { const nextButton = findNextButton(); log('next_poll', { attempts, found: Boolean(nextButton), summary: summarize(nextButton) }); } if (clickNextIfReady(attempts) || attempts >= maxAttempts) { clearInterval(nextTimer); } }, interval); }, initialDelay); }; const observeNextButton = () => { if (window.__codexbarNextObserver || !window.MutationObserver) return; const observer = new MutationObserver(() => { if (clickNextIfReady(1)) { observer.disconnect(); window.__codexbarNextObserver = null; } }); observer.observe(document.body, { subtree: true, childList: true, attributes: true }); window.__codexbarNextObserver = observer; }; const findCreditsCardButton = () => { const nodes = Array.from(document.querySelectorAll('h1,h2,h3,div,span,p')); const labelMatch = nodes.find(node => { const lower = textOf(node).toLowerCase(); return lower === 'credits remaining' || (lower.includes('credits') && lower.includes('remaining')); }); if (!labelMatch) return null; let cur = labelMatch; for (let i = 0; i < 6 && cur; i++) { const buttons = Array.from(cur.querySelectorAll(buttonSelector)); const picked = pickLikelyButton(buttons); if (picked) return picked; cur = cur.parentElement; } return null; }; const findAndClick = () => { const addMoreButton = findAddMoreButton(); if (addMoreButton) { log('add_more_click', summarize(addMoreButton)); clickButton(addMoreButton); return true; } const cardButton = findCreditsCardButton(); if (!cardButton) return false; log('credits_card_click', summarize(cardButton)); return clickButton(cardButton); }; const logDialogButtons = () => { const dialog = document.querySelector('[role=\"dialog\"], dialog, [aria-modal=\"true\"]'); if (dialog) { const buttons = Array.from(dialog.querySelectorAll(buttonSelector)).map(summarize).filter(Boolean); if (buttons.length) { log('dialog_buttons', { count: buttons.length, buttons: buttons.slice(0, 6) }); } const nextButton = findDialogNextButton(); if (nextButton) { log('dialog_next', summarize(nextButton)); setTimeout(() => clickNextIfReady(1), 100); } return; } const candidates = collectButtons() .map(summarize) .filter(Boolean) .filter(entry => { const label = (entry.label || '').toLowerCase(); return label.includes('next') || label.includes('continue') || label.includes('confirm') || label.includes('buy'); }); if (candidates.length) { log('button_candidates', { count: candidates.length, buttons: candidates.slice(0, 8) }); } }; log('auto_start', { href: location.href, ready: document.readyState }); const iframeSources = Array.from(document.querySelectorAll('iframe')) .map(frame => frame.getAttribute('src') || '') .filter(Boolean) .slice(0, 6); if (iframeSources.length) { log('iframes', iframeSources); } const shadowHostCount = Array.from(document.querySelectorAll('*')).filter(el => el.shadowRoot).length; if (shadowHostCount > 0) { log('shadow_roots', { count: shadowHostCount }); } if (findAndClick()) { window.__codexbarAutoBuyCreditsStarted = true; startNextPolling(); observeNextButton(); logDialogButtons(); return 'clicked'; } startNextPolling(500); observeNextButton(); logDialogButtons(); let attempts = 0; const maxAttempts = 14; const timer = setInterval(() => { attempts += 1; if (findAndClick()) { logDialogButtons(); startNextPolling(); clearInterval(timer); return; } if (attempts >= maxAttempts) { clearInterval(timer); } }, 500); window.__codexbarAutoBuyCreditsStarted = true; return 'scheduled'; })(); """ private let logger = CodexBarLog.logger(LogCategories.creditsPurchase) private var webView: WKWebView? private var accountEmail: String? private var cacheScope: CookieHeaderCache.Scope? private var pendingAutoStart = false private let logHandler = WeakScriptMessageHandler() init() { super.init(window: nil) self.logHandler.delegate = self } @available(*, unavailable) required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func show( purchaseURL: URL, accountEmail: String?, cacheScope: CookieHeaderCache.Scope?, autoStartPurchase: Bool) { guard Self.canOpenPurchaseWindow(accountEmail: accountEmail, cacheScope: cacheScope) else { self.close() self.accountEmail = nil self.cacheScope = nil self.logger.error("Buy credits blocked: scoped account email unavailable") return } let normalizedEmail = Self.normalizeEmail(accountEmail) if self.window == nil || normalizedEmail != self.accountEmail || cacheScope != self.cacheScope { self.accountEmail = normalizedEmail self.cacheScope = cacheScope self.buildWindow() } Self.resetDebugLog() let accountValue = normalizedEmail == nil ? "none" : "set" let sanitizedURL = Self.sanitizedURLString(purchaseURL) Self.appendDebugLog( "show autoStart=\(autoStartPurchase) url=\(sanitizedURL) account=\(accountValue)") self.logger.info("Buy credits window opened") self.logger.debug("Auto-start purchase", metadata: ["enabled": autoStartPurchase ? "1" : "0"]) self.logger.debug("Purchase URL", metadata: ["url": sanitizedURL]) self.logger.debug("Account email", metadata: ["state": accountValue]) self.pendingAutoStart = autoStartPurchase self.load(url: purchaseURL) self.window?.center() self.showWindow(nil) NSApp.activate(ignoringOtherApps: true) } private func buildWindow() { let config = WKWebViewConfiguration() config.userContentController.add(self.logHandler, name: Self.logHandlerName) config.websiteDataStore = OpenAIDashboardWebsiteDataStore.store( forAccountEmail: self.accountEmail, scope: self.cacheScope) let webView = WKWebView(frame: .zero, configuration: config) webView.navigationDelegate = self webView.allowsBackForwardNavigationGestures = true webView.translatesAutoresizingMaskIntoConstraints = false let container = NSView(frame: .zero) container.addSubview(webView) NSLayoutConstraint.activate([ webView.leadingAnchor.constraint(equalTo: container.leadingAnchor), webView.trailingAnchor.constraint(equalTo: container.trailingAnchor), webView.topAnchor.constraint(equalTo: container.topAnchor), webView.bottomAnchor.constraint(equalTo: container.bottomAnchor), ]) let window = NSWindow( contentRect: Self.defaultFrame(), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false) window.title = "Buy Credits" window.isReleasedWhenClosed = false window.collectionBehavior = [.moveToActiveSpace, .fullScreenAuxiliary] window.contentView = container window.center() window.delegate = self self.window = window self.webView = webView } private func load(url: URL) { guard let webView else { return } let request = URLRequest(url: url) webView.load(request) } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { guard self.pendingAutoStart else { return } self.pendingAutoStart = false let currentURL = webView.url?.absoluteString ?? "unknown" Self.appendDebugLog("didFinish url=\(currentURL)") self.logger.debug("Buy credits navigation finished", metadata: ["url": currentURL]) webView.evaluateJavaScript(Self.autoStartScript) { [logger] result, error in if let error { Self.appendDebugLog("autoStart error=\(error.localizedDescription)") logger.error("Auto-start purchase failed", metadata: ["error": error.localizedDescription]) return } if let result { Self.appendDebugLog("autoStart result=\(String(describing: result))") logger.debug("Auto-start purchase result", metadata: ["result": String(describing: result)]) } } } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { guard message.name == Self.logHandlerName else { return } let payload = String(describing: message.body) Self.appendDebugLog("js \(payload)") self.logger.debug("Auto-buy log", metadata: ["payload": payload]) } private static func normalizeEmail(_ email: String?) -> String? { guard let raw = email?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else { return nil } return raw.lowercased() } static func canOpenPurchaseWindow(accountEmail: String?, cacheScope: CookieHeaderCache.Scope?) -> Bool { cacheScope == nil || self.normalizeEmail(accountEmail) != nil } private static func defaultFrame() -> NSRect { let visible = NSScreen.main?.visibleFrame ?? NSRect(x: 0, y: 0, width: 1200, height: 900) let width = min(Self.defaultSize.width, visible.width * 0.92) let height = min(Self.defaultSize.height, visible.height * 0.88) let origin = NSPoint(x: visible.midX - width / 2, y: visible.midY - height / 2) return NSRect(origin: origin, size: NSSize(width: width, height: height)) } private static func appendDebugLog(_ message: String) { let timestamp = ISO8601DateFormatter().string(from: Date()) let line = "[\(timestamp)] \(LogRedactor.redact(message))\n" guard let data = line.data(using: .utf8) else { return } if FileManager.default.fileExists(atPath: Self.debugLogURL.path) { if let handle = try? FileHandle(forWritingTo: Self.debugLogURL) { handle.seekToEndOfFile() handle.write(data) try? handle.close() } } else { try? data.write(to: Self.debugLogURL, options: .atomic) } } private static func resetDebugLog() { try? FileManager.default.removeItem(at: self.debugLogURL) } private static func sanitizedURLString(_ url: URL) -> String { guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return url.absoluteString } components.query = nil components.fragment = nil return components.string ?? url.absoluteString } } private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { weak var delegate: WKScriptMessageHandler? func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { self.delegate?.userContentController(userContentController, didReceive: message) } } // MARK: - NSWindowDelegate extension OpenAICreditsPurchaseWindowController: NSWindowDelegate { func windowWillClose(_ notification: Notification) { guard let window = self.window else { return } let webView = self.webView self.pendingAutoStart = false self.webView = nil self.window = nil self.logger.info("Buy credits window closing") WebKitTeardown.scheduleCleanup(owner: window, window: window, webView: webView) } }