RedditLoginBridgeStyle.swift 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import Foundation
  2. enum RedditLoginBridgeStyle {
  3. static let messageHandlerName = "reddoraLogin"
  4. /// Intercepts Log In and blocks Reddit's embedded auth modal, which cannot complete
  5. /// credential sign-in reliably in WKWebView ("Server error. Try again later.").
  6. static let injectionScript = """
  7. (function() {
  8. if (window.__reddoraLoginBridgeInstalled) return;
  9. window.__reddoraLoginBridgeInstalled = true;
  10. const LOGIN_PATTERN = /log\\s*in/i;
  11. const HEADER_SELECTORS = 'header, [role="banner"], #header, faceplate-header, nav';
  12. const AUTH_MODAL_SELECTORS = [
  13. 'auth-flow-modal',
  14. '[data-testid="login-modal"]',
  15. '#credentials-login-popup',
  16. 'faceplate-modal[slot="login"]',
  17. 'faceplate-modal[open]'
  18. ].join(', ');
  19. function nodeLabel(node) {
  20. if (!(node instanceof Element)) return '';
  21. return (
  22. (node.textContent || '') + ' ' +
  23. (node.getAttribute('aria-label') || '') + ' ' +
  24. (node.getAttribute('title') || '')
  25. ).trim();
  26. }
  27. function isHeaderLoginTrigger(node) {
  28. if (!(node instanceof Element)) return false;
  29. const clickable = node.matches('button, a, [role="button"], faceplate-tracker, faceplate-button')
  30. ? node
  31. : node.closest('button, a, [role="button"], faceplate-tracker, faceplate-button');
  32. if (!clickable) return false;
  33. if (clickable.id === 'login-button') return true;
  34. if (clickable.getAttribute('data-testid') === 'login-button') return true;
  35. const label = nodeLabel(clickable);
  36. if (!LOGIN_PATTERN.test(label)) return false;
  37. return !!clickable.closest(HEADER_SELECTORS);
  38. }
  39. function isLoginPage() {
  40. const host = window.location.hostname.toLowerCase();
  41. return (host === 'www.reddit.com' || host === 'reddit.com' || host === 'new.reddit.com')
  42. && /^\\/login\\/?$/i.test(window.location.pathname);
  43. }
  44. function requestInAppLogin() {
  45. if (isLoginPage()) return;
  46. if (window.__reddoraLoginPanelRequested) return;
  47. window.__reddoraLoginPanelRequested = true;
  48. setTimeout(function() {
  49. window.__reddoraLoginPanelRequested = false;
  50. }, 1000);
  51. if (
  52. window.webkit &&
  53. window.webkit.messageHandlers &&
  54. window.webkit.messageHandlers.reddoraLogin
  55. ) {
  56. window.webkit.messageHandlers.reddoraLogin.postMessage('open');
  57. }
  58. }
  59. function isAuthModal(element) {
  60. if (!(element instanceof Element)) return false;
  61. if (element.matches('auth-flow-modal, [data-testid="login-modal"], #credentials-login-popup')) {
  62. return true;
  63. }
  64. if (element.matches('faceplate-modal')) {
  65. const label = nodeLabel(element);
  66. return LOGIN_PATTERN.test(label) || !!element.querySelector('input[type="password"]');
  67. }
  68. return false;
  69. }
  70. window.__reddoraCloseRedditAuthModal = function() {
  71. document.querySelectorAll(AUTH_MODAL_SELECTORS).forEach(function(element) {
  72. if (!isAuthModal(element)) return;
  73. const closeButton = element.querySelector(
  74. '[aria-label="Close"], [aria-label="close"], button[close]'
  75. );
  76. if (closeButton) {
  77. closeButton.click();
  78. } else {
  79. element.remove();
  80. }
  81. });
  82. };
  83. function installAuthModalBlocker() {
  84. if (isLoginPage()) return;
  85. const STYLE_ID = 'reddora-auth-block-style';
  86. if (!document.getElementById(STYLE_ID)) {
  87. const style = document.createElement('style');
  88. style.id = STYLE_ID;
  89. style.textContent = [
  90. 'auth-flow-modal,',
  91. '[data-testid="login-modal"],',
  92. '#credentials-login-popup {',
  93. ' display: none !important;',
  94. ' pointer-events: none !important;',
  95. '}'
  96. ].join('\\n');
  97. (document.head || document.documentElement).appendChild(style);
  98. }
  99. const observer = new MutationObserver(function() {
  100. if (isLoginPage()) return;
  101. let foundAuthModal = false;
  102. document.querySelectorAll(AUTH_MODAL_SELECTORS).forEach(function(element) {
  103. if (!isAuthModal(element)) return;
  104. foundAuthModal = true;
  105. element.remove();
  106. });
  107. if (foundAuthModal) {
  108. requestInAppLogin();
  109. }
  110. });
  111. observer.observe(document.documentElement, { childList: true, subtree: true });
  112. }
  113. document.addEventListener('click', function(event) {
  114. if (isLoginPage()) return;
  115. const path = event.composedPath();
  116. for (let i = 0; i < path.length; i++) {
  117. if (!isHeaderLoginTrigger(path[i])) continue;
  118. event.preventDefault();
  119. event.stopImmediatePropagation();
  120. window.__reddoraCloseRedditAuthModal();
  121. requestInAppLogin();
  122. return;
  123. }
  124. }, true);
  125. installAuthModalBlocker();
  126. })();
  127. """
  128. static let closeAuthModalScript = "window.__reddoraCloseRedditAuthModal && window.__reddoraCloseRedditAuthModal();"
  129. }