checkout-paypal.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. // content/checkout-paypal.js — PayPal page automation for login and checkout
  2. (function attachCheckoutPaypal() {
  3. if (document.documentElement.hasAttribute('data-multipage-checkout-paypal-listener')) return;
  4. document.documentElement.setAttribute('data-multipage-checkout-paypal-listener', '');
  5. let _cleaned = false;
  6. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  7. if (message.type === 'EXECUTE_STEP') {
  8. resetStopState();
  9. const payload = message.payload || {};
  10. if (message.step === 8) {
  11. runPaypalLogin(payload).then(
  12. (result) => sendResponse(result),
  13. (err) => sendResponse({ error: err.message })
  14. );
  15. return true;
  16. }
  17. if (message.step === 9) {
  18. runPaypalPayment(payload).then(
  19. (result) => sendResponse(result),
  20. (err) => sendResponse({ error: err.message })
  21. );
  22. return true;
  23. }
  24. }
  25. });
  26. // ========== PayPal Login (Step 8) ==========
  27. async function runPaypalLogin(payload) {
  28. try {
  29. throwIfStopped();
  30. log('开始执行 PayPal 登录页面自动化...');
  31. clearPaypalSession();
  32. await sleep(2000);
  33. const email = payload.email || randEmail();
  34. log(`正在填写邮箱: ${email}`);
  35. fillById('email', email);
  36. await sleep(1200);
  37. await clickNextButton();
  38. log('PayPal 登录邮箱已提交');
  39. reportComplete(8, { email });
  40. return { ok: true };
  41. } catch (e) {
  42. if (isStopError(e)) throw e;
  43. log('PayPal 登录流程出错: ' + e.message, 'error');
  44. reportError(8, e.message);
  45. return { error: e.message };
  46. }
  47. }
  48. // ========== PayPal Checkout (Step 9) ==========
  49. async function runPaypalPayment(payload) {
  50. try {
  51. throwIfStopped();
  52. log('开始执行 PayPal 结账页面自动化...');
  53. await sleep(2000);
  54. if (isPaypalHostedReviewPage()) {
  55. await clickHostedReviewConsent();
  56. log('PayPal 二次确认页已提交');
  57. reportComplete(9, { reviewSubmitted: true });
  58. return { ok: true, reviewSubmitted: true };
  59. }
  60. // Switch country to US
  61. const country = document.getElementById('country');
  62. if (country && country.value !== 'US') {
  63. log('检测到国家非 US,正在切换...');
  64. fillSelect(country, 'US');
  65. country.dispatchEvent(new Event('change', { bubbles: true }));
  66. await sleep(3000);
  67. } else {
  68. log('国家已是 US');
  69. }
  70. // Fill card info from payload
  71. const card = payload.card || {};
  72. const addr = payload.address || {};
  73. const email = payload.email || randEmail();
  74. const password = payload.password || randPass();
  75. const phone = payload.phone || normalizePaypalPhoneForInput(payload.paypalPhone || '+15822201173');
  76. fillById('email', email);
  77. fillById('phone', phone);
  78. fillById('cardNumber', card.Credit_Card_Number || '');
  79. fillById('cardExpiry', normalizeExpiry(card.Expires || ''));
  80. fillById('cardCvv', card.CVV2 || '');
  81. fillById('password', password);
  82. fillById('firstName', 'James');
  83. fillById('lastName', 'Smith');
  84. fillById('billingLine1', addr.street || '123 Main St');
  85. fillById('billingCity', addr.city || 'New York');
  86. fillById('billingPostalCode', (addr.zip || '10001').substring(0, 5));
  87. fillSelectById('billingState', addr.state || 'New York');
  88. log('PayPal 表单已填充完毕');
  89. await sleep(1200);
  90. await clickPaypalSubmit();
  91. const postSubmit = await waitForPaypalPostSubmitDecision(payload);
  92. log('PayPal 结账表单已提交');
  93. reportComplete(9, postSubmit);
  94. return { ok: true, ...postSubmit };
  95. } catch (e) {
  96. if (isStopError(e)) throw e;
  97. log('PayPal 结账流程出错: ' + e.message, 'error');
  98. reportError(9, e.message);
  99. return { error: e.message };
  100. }
  101. }
  102. // ========== Helpers ==========
  103. function fillById(id, val) {
  104. const el = document.getElementById(id);
  105. if (el) {
  106. fillInput(el, val);
  107. }
  108. }
  109. function fillSelectById(id, text) {
  110. const el = document.getElementById(id);
  111. if (!el) return;
  112. for (let i = 0; i < el.options.length; i++) {
  113. const opt = el.options[i];
  114. if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
  115. fillSelect(el, opt.value);
  116. return;
  117. }
  118. }
  119. }
  120. function clearPaypalSession() {
  121. if (_cleaned) return;
  122. _cleaned = true;
  123. try { localStorage.clear(); } catch (e) {}
  124. try { sessionStorage.clear(); } catch (e) {}
  125. try {
  126. const host = window.location.hostname;
  127. const domains = [host, '.' + host];
  128. const parts = host.split('.');
  129. for (let i = 1; i < parts.length - 1; i++) {
  130. domains.push('.' + parts.slice(i).join('.'));
  131. }
  132. const paths = ['/', window.location.pathname];
  133. const cookies = document.cookie ? document.cookie.split(';') : [];
  134. cookies.forEach((c) => {
  135. const name = c.split('=')[0].trim();
  136. if (!name) return;
  137. paths.forEach((p) => {
  138. domains.forEach((d) => {
  139. document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=' + p + '; domain=' + d;
  140. });
  141. document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=' + p;
  142. });
  143. });
  144. log('已清理 PayPal 前端 cookie / storage');
  145. } catch (e) {
  146. log('清理 cookie 时出错: ' + e.message, 'warn');
  147. }
  148. }
  149. function randEmail() {
  150. const c = 'abcdefghijklmnopqrstuvwxyz0123456789';
  151. let e = '';
  152. for (let i = 0; i < 16; i++) e += c[Math.floor(Math.random() * c.length)];
  153. return e + '@gmail.com';
  154. }
  155. function randPass() {
  156. const L = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
  157. const D = '0123456789';
  158. const S = '!@#$%^';
  159. const A = L + D + S;
  160. let p = L[Math.floor(Math.random() * 26)] + L[26 + Math.floor(Math.random() * 26)] + D[Math.floor(Math.random() * 10)] + S[Math.floor(Math.random() * 6)];
  161. for (let i = 4; i < 14; i++) p += A[Math.floor(Math.random() * A.length)];
  162. return p.split('').sort(() => Math.random() - 0.5).join('');
  163. }
  164. function normalizePaypalPhoneForInput(value = '') {
  165. const raw = String(value || '').trim();
  166. const digits = raw.replace(/\D+/g, '');
  167. if (digits.length === 11 && digits.startsWith('1')) {
  168. return digits.slice(1);
  169. }
  170. return digits || raw;
  171. }
  172. function normalizeExpiry(exp) {
  173. if (!exp) return '';
  174. const m = String(exp).match(/^(\d{1,2})\s*\/\s*(\d{2,4})$/);
  175. if (!m) return exp;
  176. const mm = m[1].padStart(2, '0');
  177. const yy = m[2].length === 4 ? m[2].slice(2) : m[2];
  178. return mm + ' / ' + yy;
  179. }
  180. function isPaypalHostedReviewPage() {
  181. const path = String(location.pathname || '');
  182. const text = document.body?.innerText || '';
  183. return /\/webapps\/hermes/i.test(path)
  184. || Boolean(document.getElementById('consentButton'))
  185. || /set up once\. pay faster next time|agree and continue/i.test(text);
  186. }
  187. async function waitForPaypalPostSubmitDecision(payload = {}, timeoutMs = 60000) {
  188. const startedAt = Date.now();
  189. const startUrl = location.href;
  190. while (Date.now() - startedAt < timeoutMs) {
  191. throwIfStopped();
  192. if (isPaypalHostedReviewPage()) {
  193. await clickHostedReviewConsent();
  194. return { reviewSubmitted: true };
  195. }
  196. if (isPaypalSmsVerificationPage()) {
  197. const result = await completePaypalSmsVerification(payload);
  198. return { smsVerified: true, ...result };
  199. }
  200. if (!/paypal\./i.test(String(location.host || ''))) {
  201. return { leftPayPal: true };
  202. }
  203. if (location.href !== startUrl && /return|success|complete/i.test(location.href)) {
  204. return { redirected: true };
  205. }
  206. await sleep(1000);
  207. }
  208. log('提交后未检测到 PayPal 二次确认页或外部跳转,按已提交继续。', 'warn');
  209. return { postSubmitTimeout: true };
  210. }
  211. function isPaypalSmsVerificationPage() {
  212. return Boolean(findPaypalSmsCodeInput())
  213. || Boolean(findPaypalSplitSmsCodeInputs().length >= 4)
  214. || /verification\s*code|confirm.{0,40}phone|verify.{0,40}phone|sent.{0,40}code|text\s*message|短信|验证码|安全代码/i.test(document.body?.innerText || '');
  215. }
  216. async function completePaypalSmsVerification(payload = {}) {
  217. await clickPaypalSmsSendCodeButtonIfPresent();
  218. const inputState = await waitForPaypalSmsCodeInput(20000);
  219. if (!inputState) {
  220. throw new Error('PayPal 短信验证码页面已出现,但未找到验证码输入框。');
  221. }
  222. const code = await requestPaypalSmsCode(payload);
  223. fillPaypalSmsCodeInput(inputState, code);
  224. await sleep(500);
  225. const submitButton = findPaypalSmsSubmitButton();
  226. if (!submitButton) {
  227. throw new Error('PayPal 短信验证码已填写,但未找到继续/验证按钮。');
  228. }
  229. submitButton.click();
  230. log('PayPal 短信验证码已填写并提交。');
  231. await sleep(2500);
  232. return { smsCodeSubmitted: true };
  233. }
  234. async function requestPaypalSmsCode(payload = {}) {
  235. const response = await chrome.runtime.sendMessage({
  236. type: 'PAYPAL_FETCH_SMS_CODE',
  237. source: 'checkout-paypal',
  238. payload: {
  239. phone: payload.paypalPhone || payload.phone || '+15822201173',
  240. smsApiUrl: payload.paypalSmsApiUrl || '',
  241. },
  242. });
  243. if (response?.error) {
  244. throw new Error(response.error);
  245. }
  246. if (!response?.code) {
  247. throw new Error('PayPal 短信收码接口未返回验证码。');
  248. }
  249. log(`PayPal 短信验证码已获取(第 ${response.attempt || 1} 次轮询)。`);
  250. return String(response.code);
  251. }
  252. async function clickPaypalSmsSendCodeButtonIfPresent() {
  253. const button = findPaypalButtonByPatterns([
  254. /send\s*(?:me\s*)?(?:a\s*)?code/i,
  255. /text\s*(?:me|code)/i,
  256. /resend/i,
  257. /发送.*验证码|短信|重新发送/i,
  258. ]);
  259. if (button) {
  260. button.click();
  261. log('已点击 PayPal 发送/重新发送短信验证码按钮。');
  262. await sleep(1200);
  263. }
  264. }
  265. async function waitForPaypalSmsCodeInput(timeoutMs = 20000) {
  266. const startedAt = Date.now();
  267. while (Date.now() - startedAt < timeoutMs) {
  268. throwIfStopped();
  269. const input = findPaypalSmsCodeInput();
  270. if (input) {
  271. return { input, splitInputs: [] };
  272. }
  273. const splitInputs = findPaypalSplitSmsCodeInputs();
  274. if (splitInputs.length >= 4) {
  275. return { input: null, splitInputs };
  276. }
  277. await sleep(400);
  278. }
  279. return null;
  280. }
  281. function fillPaypalSmsCodeInput(inputState, code) {
  282. const normalizedCode = String(code || '').replace(/\D+/g, '');
  283. if (!normalizedCode) {
  284. throw new Error('PayPal 短信验证码为空。');
  285. }
  286. if (inputState.input) {
  287. fillInput(inputState.input, normalizedCode);
  288. return;
  289. }
  290. inputState.splitInputs.forEach((input, index) => {
  291. if (normalizedCode[index]) {
  292. fillInput(input, normalizedCode[index]);
  293. }
  294. });
  295. }
  296. function findPaypalSmsCodeInput() {
  297. const selectors = [
  298. 'input[autocomplete="one-time-code"]',
  299. 'input[name*="code" i]',
  300. 'input[id*="code" i]',
  301. 'input[aria-label*="code" i]',
  302. 'input[placeholder*="code" i]',
  303. 'input[inputmode="numeric"]',
  304. 'input[maxlength="6"]',
  305. ];
  306. const candidates = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector)));
  307. return candidates.find((input) => {
  308. if (!isClickable(input)) return false;
  309. const text = getPaypalFieldText(input);
  310. if (/card|cvv|cvc|postal|zip|phone|tel|amount|卡|邮编|电话/i.test(text)) {
  311. return false;
  312. }
  313. return /code|otp|security|verification|验证码|安全/i.test(text)
  314. || String(input.getAttribute('maxlength') || '') === '6'
  315. || String(input.getAttribute('autocomplete') || '').toLowerCase() === 'one-time-code';
  316. }) || null;
  317. }
  318. function findPaypalSplitSmsCodeInputs() {
  319. return Array.from(document.querySelectorAll('input'))
  320. .filter((input) => {
  321. if (!isClickable(input)) return false;
  322. const maxLength = Number(input.getAttribute('maxlength') || input.maxLength || 0);
  323. const text = getPaypalFieldText(input);
  324. return maxLength === 1
  325. && /code|otp|security|verification|验证码|安全/i.test(text);
  326. })
  327. .slice(0, 8);
  328. }
  329. function findPaypalSmsSubmitButton() {
  330. return findPaypalButtonByPatterns([
  331. /continue/i,
  332. /verify/i,
  333. /confirm/i,
  334. /submit/i,
  335. /next/i,
  336. /继续|验证|确认|提交|下一步/i,
  337. ]);
  338. }
  339. function findPaypalButtonByPatterns(patterns) {
  340. return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((button) => {
  341. if (!isClickable(button)) return false;
  342. const text = String(button.textContent || button.value || button.getAttribute?.('aria-label') || '').trim();
  343. return patterns.some((pattern) => pattern.test(text));
  344. }) || null;
  345. }
  346. function getPaypalFieldText(el) {
  347. if (!el) return '';
  348. const id = el.id ? String(el.id) : '';
  349. const labelText = id
  350. ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
  351. : '';
  352. return [
  353. id,
  354. el.name,
  355. el.getAttribute?.('autocomplete'),
  356. el.getAttribute?.('aria-label'),
  357. el.getAttribute?.('placeholder'),
  358. labelText,
  359. el.closest?.('label')?.textContent || '',
  360. el.closest?.('[data-testid], [class], div, section, fieldset')?.textContent || '',
  361. ].filter(Boolean).join(' ');
  362. }
  363. function cssEscape(value) {
  364. if (window.CSS?.escape) return window.CSS.escape(value);
  365. return String(value || '').replace(/["\\]/g, '\\$&');
  366. }
  367. async function clickHostedReviewConsent() {
  368. log(`PayPal Hermes:开始等待账单确认按钮。当前 URL:${location.href}`, 'info');
  369. let waited = 0;
  370. while (waited < 30) {
  371. throwIfStopped();
  372. waited += 1;
  373. const button = findHostedReviewConsentButton();
  374. if (button) {
  375. log('PayPal Hermes:已找到确认按钮,准备点击 Agree and Continue。', 'info');
  376. button.click();
  377. await sleep(1000);
  378. return true;
  379. }
  380. if (waited === 1 || waited % 5 === 0) {
  381. log(`PayPal Hermes:尚未找到确认按钮,继续等待(${waited}/30)。`, 'info');
  382. }
  383. await sleep(1000);
  384. }
  385. throw new Error('PayPal hosted checkout 二次确认页超时,未找到确认按钮。');
  386. }
  387. function findHostedReviewConsentButton() {
  388. const direct = document.getElementById('consentButton')
  389. || document.querySelector('button[data-testid="consentButton"]');
  390. if (direct && isClickable(direct)) return direct;
  391. const patterns = [
  392. /agree\s*(?:and|&)\s*continue/i,
  393. /continue/i,
  394. /pay\s*now/i,
  395. /同意|继续|付款/i,
  396. ];
  397. return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((button) => {
  398. if (!isClickable(button)) return false;
  399. const text = String(button.textContent || button.value || button.getAttribute?.('aria-label') || '').trim();
  400. return patterns.some((pattern) => pattern.test(text));
  401. }) || null;
  402. }
  403. function isClickable(el) {
  404. if (!el || el.disabled) return false;
  405. const rect = el.getBoundingClientRect();
  406. const style = window.getComputedStyle(el);
  407. return rect.width > 0
  408. && rect.height > 0
  409. && style.visibility !== 'hidden'
  410. && style.display !== 'none';
  411. }
  412. async function clickPaypalSubmit(retries = 0) {
  413. throwIfStopped();
  414. if (retries >= 12) throw new Error('未找到 PayPal 提交按钮,已超时');
  415. const btn = document.querySelector('button[data-testid="submit-button"]')
  416. || document.querySelector('button[data-testid="hosted-payment-submit-button"]');
  417. if (btn) {
  418. const rect = btn.getBoundingClientRect();
  419. if (btn.disabled || rect.height === 0) {
  420. log('PayPal 提交按钮被禁用或不可见,等待中...');
  421. await sleep(800);
  422. return clickPaypalSubmit(retries + 1);
  423. }
  424. log(`正在点击 PayPal 提交: ${btn.textContent.trim()}`);
  425. btn.click();
  426. } else {
  427. const all = document.querySelectorAll('button');
  428. for (let i = 0; i < all.length; i++) {
  429. const t = all[i].textContent.trim();
  430. if (['Agree & Create Account', 'Agree and Pay', 'Continue', 'Pay Now'].includes(t) || t.includes('同意')) {
  431. all[i].click();
  432. log(`已点击: ${t}`);
  433. return;
  434. }
  435. }
  436. log(`未找到提交按钮,重试中... (${retries + 1})`);
  437. await sleep(800);
  438. return clickPaypalSubmit(retries + 1);
  439. }
  440. }
  441. async function clickNextButton(retries = 0) {
  442. throwIfStopped();
  443. if (retries >= 12) throw new Error('未找到 PayPal 下一步按钮,已超时');
  444. const all = document.querySelectorAll('button');
  445. for (let i = 0; i < all.length; i++) {
  446. const t = all[i].textContent.trim();
  447. if (['下一页', '下一步', 'Next', 'Continue'].includes(t)) {
  448. if (!all[i].disabled && all[i].getBoundingClientRect().height > 0) {
  449. all[i].click();
  450. log(`已点击: ${t}`);
  451. return;
  452. }
  453. }
  454. }
  455. log(`未找到"下一步"按钮,重试中... (${retries + 1})`);
  456. await sleep(800);
  457. return clickNextButton(retries + 1);
  458. }
  459. if (isPaypalHostedReviewPage()) {
  460. setTimeout(() => {
  461. clickHostedReviewConsent().catch((error) => {
  462. log(`PayPal Hermes 自动确认失败: ${error?.message || error}`, 'warn');
  463. });
  464. }, 0);
  465. }
  466. document.documentElement.setAttribute('data-multipage-checkout-paypal-ready', '');
  467. })();