| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- import Foundation
- enum RedditLoginBridgeStyle {
- static let messageHandlerName = "reddoraLogin"
- /// Intercepts Log In and blocks Reddit's embedded auth modal, which cannot complete
- /// credential sign-in reliably in WKWebView ("Server error. Try again later.").
- static let injectionScript = """
- (function() {
- if (window.__reddoraLoginBridgeInstalled) return;
- window.__reddoraLoginBridgeInstalled = true;
- const LOGIN_PATTERN = /log\\s*in/i;
- const HEADER_SELECTORS = 'header, [role="banner"], #header, faceplate-header, nav, shreddit-header-action';
- const CLICKABLE_SELECTORS = 'button, a, [role="button"], faceplate-tracker, faceplate-button';
- const AUTH_MODAL_SELECTORS = [
- 'auth-flow-modal',
- '[data-testid="login-modal"]',
- '#credentials-login-popup',
- 'shreddit-signup-drawer',
- 'faceplate-modal[slot="login"]',
- 'faceplate-modal[open]'
- ].join(', ');
- const boundLoginTriggers = new WeakSet();
- function nodeLabel(node) {
- if (!(node instanceof Element)) return '';
- return (
- (node.textContent || '') + ' ' +
- (node.getAttribute('aria-label') || '') + ' ' +
- (node.getAttribute('title') || '') + ' ' +
- (node.getAttribute('href') || '')
- ).trim();
- }
- function isLoginPage() {
- const host = window.location.hostname.toLowerCase();
- return (host === 'www.reddit.com' || host === 'reddit.com' || host === 'new.reddit.com')
- && /^\\/login\\/?$/i.test(window.location.pathname);
- }
- function requestInAppLogin() {
- if (isLoginPage()) return;
- if (window.__reddoraLoginPanelRequested) return;
- window.__reddoraLoginPanelRequested = true;
- setTimeout(function() {
- window.__reddoraLoginPanelRequested = false;
- }, 1000);
- if (
- window.webkit &&
- window.webkit.messageHandlers &&
- window.webkit.messageHandlers.reddoraLogin
- ) {
- window.webkit.messageHandlers.reddoraLogin.postMessage('open');
- }
- }
- function interceptLoginEvent(event) {
- if (isLoginPage()) return false;
- if (eventMatchesLoginTrigger(event)) {
- event.preventDefault();
- event.stopImmediatePropagation();
- window.__reddoraCloseRedditAuthModal && window.__reddoraCloseRedditAuthModal();
- requestInAppLogin();
- return true;
- }
- const loginButton = document.getElementById('login-button');
- if (loginButton && eventMatchesButtonBounds(event, loginButton)) {
- event.preventDefault();
- event.stopImmediatePropagation();
- window.__reddoraCloseRedditAuthModal && window.__reddoraCloseRedditAuthModal();
- requestInAppLogin();
- return true;
- }
- return false;
- }
- function eventMatchesButtonBounds(event, button) {
- if (!(button instanceof Element)) return false;
- if (!(event instanceof MouseEvent) && event.type !== 'pointerdown' && event.type !== 'mousedown') {
- return false;
- }
- if (button.contains(event.target)) return false;
- const rect = button.getBoundingClientRect();
- const x = event.clientX;
- const y = event.clientY;
- return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
- }
- function eventMatchesLoginTrigger(event) {
- const path = event.composedPath ? event.composedPath() : [event.target];
- for (let i = 0; i < path.length; i++) {
- if (resolveLoginTrigger(path[i])) return true;
- }
- return false;
- }
- function resolveLoginTrigger(node) {
- if (!(node instanceof Element)) return null;
- if (node.id === 'login-button') return node;
- if (node.getAttribute('data-testid') === 'login-button') return node;
- const clickable = node.matches(CLICKABLE_SELECTORS)
- ? node
- : node.closest(CLICKABLE_SELECTORS);
- if (!clickable) return null;
- if (clickable.id === 'login-button') return clickable;
- if (clickable.getAttribute('data-testid') === 'login-button') return clickable;
- const href = (clickable.getAttribute('href') || '').toLowerCase();
- if (href.includes('/login')) return clickable;
- const label = nodeLabel(clickable);
- if (!LOGIN_PATTERN.test(label)) return null;
- if (clickable.closest(HEADER_SELECTORS)) return clickable;
- const rect = clickable.getBoundingClientRect();
- if (rect.top >= 0 && rect.top < 96 && rect.width > 0 && rect.height > 0) {
- return clickable;
- }
- return null;
- }
- function isLoginTriggerElement(node) {
- return resolveLoginTrigger(node) !== null;
- }
- function isAuthModal(element) {
- if (!(element instanceof Element)) return false;
- if (element.matches(
- 'auth-flow-modal, [data-testid="login-modal"], #credentials-login-popup, shreddit-signup-drawer'
- )) {
- return true;
- }
- if (element.matches('shreddit-overlay-display')) {
- return !!element.querySelector(
- 'shreddit-signup-drawer, auth-flow-modal, input[type="password"]'
- );
- }
- if (element.matches('faceplate-modal')) {
- const label = nodeLabel(element);
- return LOGIN_PATTERN.test(label) || !!element.querySelector('input[type="password"]');
- }
- return false;
- }
- window.__reddoraCloseRedditAuthModal = function() {
- document.querySelectorAll(AUTH_MODAL_SELECTORS).forEach(function(element) {
- if (!isAuthModal(element)) return;
- const closeButton = element.querySelector(
- '[aria-label="Close"], [aria-label="close"], button[close]'
- );
- if (closeButton) {
- closeButton.click();
- } else {
- element.remove();
- }
- });
- };
- function bindDirectLoginListeners(root) {
- function walk(node) {
- if (!(node instanceof Element)) return;
- const trigger = resolveLoginTrigger(node);
- if (trigger && !boundLoginTriggers.has(trigger)) {
- boundLoginTriggers.add(trigger);
- ['pointerdown', 'mousedown', 'click'].forEach(function(type) {
- trigger.addEventListener(type, interceptLoginEvent, true);
- });
- }
- if (node.shadowRoot) walk(node.shadowRoot);
- const children = node.children;
- for (let i = 0; i < children.length; i++) {
- walk(children[i]);
- }
- }
- walk(root || document.documentElement);
- }
- function installAuthModalBlocker() {
- if (isLoginPage()) return;
- const STYLE_ID = 'reddora-auth-block-style';
- if (!document.getElementById(STYLE_ID)) {
- const style = document.createElement('style');
- style.id = STYLE_ID;
- style.textContent = [
- 'auth-flow-modal,',
- '[data-testid="login-modal"],',
- '#credentials-login-popup,',
- 'shreddit-signup-drawer {',
- ' display: none !important;',
- ' pointer-events: none !important;',
- '}'
- ].join('\\n');
- (document.head || document.documentElement).appendChild(style);
- }
- const observer = new MutationObserver(function() {
- if (isLoginPage()) return;
- bindDirectLoginListeners(document.documentElement);
- let foundAuthModal = false;
- document.querySelectorAll(AUTH_MODAL_SELECTORS).forEach(function(element) {
- if (!isAuthModal(element)) return;
- foundAuthModal = true;
- element.remove();
- });
- if (foundAuthModal) {
- requestInAppLogin();
- }
- });
- observer.observe(document.documentElement, { childList: true, subtree: true });
- }
- ['pointerdown', 'mousedown', 'click'].forEach(function(type) {
- document.addEventListener(type, interceptLoginEvent, true);
- });
- bindDirectLoginListeners(document.documentElement);
- installAuthModalBlocker();
- [250, 750, 1500, 3000].forEach(function(delay) {
- setTimeout(function() {
- bindDirectLoginListeners(document.documentElement);
- }, delay);
- });
- })();
- """
- static let closeAuthModalScript = "window.__reddoraCloseRedditAuthModal && window.__reddoraCloseRedditAuthModal();"
- }
|