checkout-stripe.js 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130
  1. // content/checkout-stripe.js — Stripe checkout page automation for ChatGPT Plus subscription
  2. (function attachCheckoutStripe() {
  3. if (document.documentElement.hasAttribute('data-multipage-checkout-stripe-listener')) return;
  4. document.documentElement.setAttribute('data-multipage-checkout-stripe-listener', '');
  5. chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  6. if (message.type === 'EXECUTE_STEP' && message.step === 7) {
  7. resetStopState();
  8. runStripeCheckout(message.payload || {}).then(
  9. (result) => sendResponse(result),
  10. (err) => sendResponse({ error: err.message })
  11. );
  12. return true;
  13. }
  14. if (message.type === 'CHECKOUT_STRIPE_SELECT_ADDRESS_SUGGESTION') {
  15. resetStopState();
  16. selectGoogleAddressSuggestionOnly(message.payload || {}).then(
  17. (result) => sendResponse(result),
  18. (err) => sendResponse({ error: err.message })
  19. );
  20. return true;
  21. }
  22. if (message.type === 'CHECKOUT_STRIPE_GET_STATE') {
  23. sendResponse(inspectCheckoutStripeState());
  24. return false;
  25. }
  26. if (message.type === 'CHECKOUT_STRIPE_SELECT_PAYPAL') {
  27. resetStopState();
  28. selectPayPalPaymentMethod(message.payload || {}).then(
  29. (result) => sendResponse(result),
  30. (err) => sendResponse({ error: err.message })
  31. );
  32. return true;
  33. }
  34. if (message.type === 'CHECKOUT_STRIPE_FILL_BILLING_ADDRESS') {
  35. resetStopState();
  36. fillStripeBillingAddress(message.payload || {}).then(
  37. (result) => sendResponse(result),
  38. (err) => sendResponse({ error: err.message })
  39. );
  40. return true;
  41. }
  42. if (message.type === 'CHECKOUT_STRIPE_CLICK_SUBMIT') {
  43. resetStopState();
  44. clickStripeCheckoutSubmit(message.payload || {}).then(
  45. (result) => sendResponse(result),
  46. (err) => sendResponse({ error: err.message })
  47. );
  48. return true;
  49. }
  50. });
  51. async function runStripeCheckout(payload) {
  52. try {
  53. throwIfStopped();
  54. log('开始执行 Stripe 结账页面自动化...');
  55. await sleep(1800);
  56. // 1. Click PayPal option
  57. await selectPayPalPaymentMethod({ relaxedActivation: true });
  58. await sleep(1000);
  59. // 2. Fill billing address
  60. await fillStripeBillingAddress(payload);
  61. // 3. Click submit
  62. await clickStripeCheckoutSubmit({ beforeClickDelayMs: 1500 });
  63. log('Stripe 结账表单已提交');
  64. reportComplete(7, {});
  65. return { ok: true };
  66. } catch (e) {
  67. if (isStopError(e)) throw e;
  68. log('Stripe 结账流程出错: ' + e.message, 'error');
  69. reportError(7, e.message);
  70. return { error: e.message };
  71. }
  72. }
  73. function inspectCheckoutStripeState() {
  74. const addressFields = getStructuredAddressFields();
  75. return {
  76. url: location.href,
  77. readyState: document.readyState,
  78. hasPayPal: Boolean(findPayPalPaymentMethodTarget()),
  79. paypalCandidates: getPayPalCandidateSummaries(),
  80. billingFieldsVisible: hasBillingAddressFields(addressFields),
  81. hasSubmitButton: Boolean(findSubmitButton()),
  82. addressFieldValues: {
  83. address1: addressFields.address1?.value || '',
  84. city: addressFields.city?.value || '',
  85. region: addressFields.region?.value || getSelectText(addressFields.regionSelect) || '',
  86. postalCode: addressFields.postalCode?.value || '',
  87. },
  88. };
  89. }
  90. async function selectPayPalPaymentMethod(options = {}) {
  91. log('正在寻找 PayPal 选项...');
  92. const autoJsTarget = findAutoJsPayPalTarget();
  93. if (autoJsTarget) {
  94. await clickPayPalLikeAutoJs(autoJsTarget);
  95. await sleep(450);
  96. await clickPayPalLikeAutoJs(autoJsTarget);
  97. const autoJsActive = await waitForPayPalPaymentMethodActive(2500);
  98. if (autoJsActive) {
  99. log('已按 auto.js 方式确认 PayPal 选项生效');
  100. } else {
  101. log('auto.js 方式点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn');
  102. }
  103. return {
  104. paymentSelected: autoJsActive,
  105. relaxed: !autoJsActive,
  106. };
  107. }
  108. const target = await waitForPayPalPaymentMethodTarget(10000);
  109. if (!target) {
  110. if (options.relaxedActivation) {
  111. log('未找到 PayPal 选项按钮,当前为宽松模式,继续执行...', 'warn');
  112. return { paymentSelected: false, relaxed: true };
  113. }
  114. throw new Error('未找到 PayPal 付款方式,无法切换。');
  115. }
  116. const clickTargets = getPayPalActivationTargets(target);
  117. for (let attempt = 0; attempt < 2; attempt += 1) {
  118. for (const candidate of clickTargets) {
  119. dispatchRobustActivation(candidate);
  120. await sleep(220);
  121. if (hasSelectedPayPalControl() || hasBillingAddressFields()) {
  122. break;
  123. }
  124. }
  125. await sleep(450);
  126. }
  127. const active = await waitForPayPalPaymentMethodActive(3500);
  128. if (!active) {
  129. log('点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn');
  130. } else {
  131. log('已确认 PayPal 选项生效');
  132. }
  133. return {
  134. paymentSelected: active,
  135. relaxed: !active,
  136. };
  137. }
  138. async function fillStripeBillingAddress(payload = {}) {
  139. const addr = normalizeCheckoutAddress(payload.address || {});
  140. if (payload.autoJsDirectSelectors) {
  141. log('参考 auto.js 直接填写账单字段...');
  142. fillBillingAddressDirectSelectors(addr);
  143. await sleep(800);
  144. hideAutocompleteDropdowns();
  145. await ensureTermsCheckbox();
  146. const latest = getStructuredAddressFields();
  147. return {
  148. countryText: readCountryText(),
  149. selectedAutocompleteAddress: false,
  150. structuredAddress: {
  151. address1: latest.address1?.value || '',
  152. city: latest.city?.value || '',
  153. region: latest.region?.value || getSelectText(latest.regionSelect) || '',
  154. postalCode: latest.postalCode?.value || '',
  155. },
  156. };
  157. }
  158. log('正在填写账单地址...');
  159. const selectedAutocompleteAddress = await fillBillingAddressLine1FromGoogle(addr);
  160. if (!selectedAutocompleteAddress) {
  161. log('未能选择 Google 地址下拉项,改用手动填写地址兜底。', 'warn');
  162. const fields = getStructuredAddressFields();
  163. const address1Input = fields.address1 || findAddressSearchInput();
  164. if (address1Input) {
  165. fillInput(address1Input, addr.street);
  166. } else {
  167. fillBySelector('#billingAddressLine1', addr.street);
  168. }
  169. }
  170. await sleep(900);
  171. const fields = getStructuredAddressFields();
  172. fillMissingControl(fields.city || document.querySelector('#billingLocality'), addr.city);
  173. fillMissingControl(fields.postalCode || document.querySelector('#billingPostalCode'), addr.zip.substring(0, 5));
  174. fillRegionControlByText(fields.regionSelect || fields.region || document.querySelector('#billingAdministrativeArea'), addr.state);
  175. await sleep(800);
  176. hideAutocompleteDropdowns();
  177. await ensureTermsCheckbox();
  178. const latest = getStructuredAddressFields();
  179. return {
  180. countryText: readCountryText(),
  181. selectedAutocompleteAddress,
  182. structuredAddress: {
  183. address1: latest.address1?.value || '',
  184. city: latest.city?.value || '',
  185. region: latest.region?.value || getSelectText(latest.regionSelect) || '',
  186. postalCode: latest.postalCode?.value || '',
  187. },
  188. };
  189. }
  190. async function clickStripeCheckoutSubmit(payload = {}) {
  191. await ensureTermsCheckbox();
  192. await sleep(Math.max(0, Math.floor(Number(payload.beforeClickDelayMs) || 0)));
  193. await clickSubmitButton();
  194. return { clicked: true };
  195. }
  196. function normalizeCheckoutAddress(addr = {}) {
  197. return {
  198. street: normalizeText(addr.street || addr.address1 || '123 Main St'),
  199. city: normalizeText(addr.city || 'New York'),
  200. state: normalizeText(addr.state || addr.region || 'New York'),
  201. zip: normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5),
  202. query: normalizeText(addr.query || addr.autocompleteQuery || ''),
  203. };
  204. }
  205. function getStructuredAddressFields() {
  206. const address1 = getVisibleElementById('billingAddressLine1') || findInputByFieldText([
  207. /address\s*(?:line)?\s*1|address[_-]?line[_-]?1|address\[(?:address_)?line1\]|line\s*1|street|street[_-]?address/i,
  208. /地址\s*1|街道|详细地址|住所/i,
  209. ], {
  210. exclude: (input) => isNonAddressSearchInput(input),
  211. }) || findAddressSearchInput();
  212. const city = getVisibleElementById('billingLocality') || findInputByFieldText([
  213. /city|town|suburb|locality|address[_-]?level[_-]?2|address\[city\]/i,
  214. /城市|市区|区市町村|市区町村|市町村/i,
  215. ]);
  216. const postalCode = getVisibleElementById('billingPostalCode') || findInputByFieldText([
  217. /postal|zip|postcode|postal[_-]?code|zip[_-]?code|address\[postal_code\]/i,
  218. /邮编|邮政|郵便番号/i,
  219. ]);
  220. const regionSelect = getVisibleElementById('billingAdministrativeArea');
  221. const region = regionSelect || findInputByFieldText([
  222. /state|province|region|county|prefecture|administrative|administrative[_-]?area|address[_-]?level[_-]?1|address\[state\]/i,
  223. /省|州|地区|辖区|都道府县|都道府県/i,
  224. ]);
  225. return {
  226. address1,
  227. city,
  228. postalCode,
  229. region,
  230. regionSelect: regionSelect && regionSelect.tagName === 'SELECT' ? regionSelect : null,
  231. };
  232. }
  233. function getVisibleElementById(id) {
  234. const el = document.getElementById(id);
  235. return el && isVisibleNode(el) ? el : null;
  236. }
  237. function hasBillingAddressFields(fields = getStructuredAddressFields()) {
  238. if (fields?.address1 || fields?.city || fields?.postalCode) {
  239. return true;
  240. }
  241. return getVisibleTextInputs().some((input) => {
  242. const text = getFieldText(input);
  243. return /address|street|billing|line\s*1|地址|街道|账单/i.test(text)
  244. && !/card\s*number|card|expiry|expiration|security|cvc|cvv|name|email|e-mail|phone|tel|country|region|postal|zip|city|state|province|银行卡|卡号|有效期|安全码|姓名|邮箱|电话|国家|地区|邮编|城市|省|州/i.test(text);
  245. }) || Boolean(findAddressSearchInput());
  246. }
  247. function readCountryText() {
  248. const country = document.getElementById('billingCountry')
  249. || findInputByFieldText([/country|region/i, /国家|地区/i])
  250. || Array.from(document.querySelectorAll('select')).find((select) => (
  251. isVisibleNode(select) && /country|region|国家|地区/i.test(getFieldText(select))
  252. ));
  253. if (!country) return '';
  254. if (country.tagName === 'SELECT') {
  255. return getSelectText(country) || country.value || '';
  256. }
  257. return country.value || country.textContent || '';
  258. }
  259. function getSelectText(select) {
  260. if (!select || select.tagName !== 'SELECT') return '';
  261. return normalizeText(select.selectedOptions?.[0]?.textContent || select.value || '');
  262. }
  263. function findInputByFieldText(patterns = [], options = {}) {
  264. const excluded = options.exclude || (() => false);
  265. return getVisibleTextInputs().find((input) => {
  266. if (excluded(input)) return false;
  267. return patterns.some((pattern) => pattern.test(getFieldText(input)));
  268. }) || null;
  269. }
  270. function getVisibleControls(selector) {
  271. return Array.from(document.querySelectorAll(selector)).filter((el) => isVisibleNode(el));
  272. }
  273. function getVisibleTextInputs() {
  274. return getVisibleControls('input, textarea')
  275. .filter((el) => {
  276. const type = String(el.getAttribute('type') || el.type || '').trim().toLowerCase();
  277. return !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
  278. });
  279. }
  280. function getVisibleFormControls() {
  281. return Array.from(document.querySelectorAll('input, textarea, select'))
  282. .filter((el) => {
  283. const type = String(el.getAttribute('type') || el.type || '').toLowerCase();
  284. return type !== 'hidden' && isVisibleNode(el);
  285. });
  286. }
  287. function getFieldText(el) {
  288. if (!el) return '';
  289. const id = el.id ? String(el.id) : '';
  290. const labelText = id
  291. ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
  292. : '';
  293. const wrappingLabel = el.closest?.('label')?.textContent || '';
  294. const container = el.closest?.('[data-testid], [class], div, section, fieldset');
  295. return normalizeText([
  296. id,
  297. el.name,
  298. el.getAttribute?.('autocomplete'),
  299. el.getAttribute?.('aria-label'),
  300. el.getAttribute?.('placeholder'),
  301. labelText,
  302. wrappingLabel,
  303. container && !isDocumentLevelContainer(container) ? container.textContent || '' : '',
  304. ].filter(Boolean).join(' '));
  305. }
  306. function getDirectFieldHintText(el) {
  307. if (!el) return '';
  308. const id = el.id ? String(el.id) : '';
  309. const labelText = id
  310. ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
  311. : '';
  312. const wrappingLabel = el.closest?.('label')?.textContent || '';
  313. return normalizeText([
  314. id,
  315. el.name,
  316. el.getAttribute?.('autocomplete'),
  317. el.getAttribute?.('aria-label'),
  318. el.getAttribute?.('placeholder'),
  319. labelText,
  320. wrappingLabel,
  321. ].filter(Boolean).join(' '));
  322. }
  323. function isNonAddressSearchInput(input) {
  324. const directText = getDirectFieldHintText(input);
  325. const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
  326. return /name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(directText)
  327. || ['email', 'tel', 'password'].includes(type);
  328. }
  329. function isLikelyAddressSearchInput(input) {
  330. const text = getFieldText(input);
  331. if (isNonAddressSearchInput(input)) {
  332. return false;
  333. }
  334. if (/name|email|e-mail|phone|tel|password|coupon|promo|country|region|postal|zip|city|state|province|card|card\s*number|expiry|expiration|security|cvc|cvv|cc-|全名|姓名|邮箱|电话|密码|国家|地区|邮编|城市|省|州|银行卡|卡号|有效期|安全码/i.test(text)) {
  335. return false;
  336. }
  337. return /address|street|billing|search|line\s*1|地址|街道|账单/i.test(text);
  338. }
  339. function findAddressSearchInput() {
  340. const direct = findInputByFieldText([
  341. /address|street|billing|search|line\s*1/i,
  342. /地址|街道|账单/i,
  343. ], {
  344. exclude: (input) => isNonAddressSearchInput(input)
  345. || /city|state|province|postal|zip|country|region|城市|省|州|邮编|国家|地区/i.test(getFieldText(input)),
  346. });
  347. if (direct) return direct;
  348. return getVisibleTextInputs().filter(isLikelyAddressSearchInput)[0] || null;
  349. }
  350. function isDocumentLevelContainer(el) {
  351. return !el
  352. || el === document.documentElement
  353. || el === document.body
  354. || ['HTML', 'BODY', 'MAIN'].includes(el.tagName);
  355. }
  356. function cssEscape(value) {
  357. if (window.CSS?.escape) return window.CSS.escape(value);
  358. return String(value || '').replace(/["\\]/g, '\\$&');
  359. }
  360. function findPayPalPaymentMethodTarget() {
  361. const directSelectors = [
  362. '[data-testid="paypal-accordion-item-button"]',
  363. '[data-testid*="paypal" i]',
  364. '.paypal-accordion-item button',
  365. 'button[aria-label*="PayPal" i]',
  366. '[aria-label*="PayPal" i]',
  367. '[title*="PayPal" i]',
  368. '[role="radio"][aria-label*="PayPal" i]',
  369. 'input[type="radio"][value*="paypal" i]',
  370. 'button[value*="paypal" i]',
  371. ];
  372. for (const selector of directSelectors) {
  373. const target = Array.from(document.querySelectorAll(selector)).find((el) => isVisibleNode(el));
  374. if (target) return target;
  375. }
  376. const directClickable = findClickableByText([/paypal/i]);
  377. if (directClickable) return directClickable;
  378. const radios = getVisibleControls('input[type="radio"], [role="radio"]');
  379. const matchedRadio = radios.find((el) => /paypal/i.test(getCombinedSearchText(el)));
  380. if (matchedRadio) return matchedRadio;
  381. for (const candidate of getPayPalSearchCandidates()) {
  382. const interactive = findInteractiveAncestor(candidate);
  383. if (interactive && /paypal/i.test(getCombinedSearchText(interactive))) {
  384. return interactive;
  385. }
  386. const card = findPaymentCardAncestor(candidate, /paypal/i);
  387. if (card) {
  388. return card;
  389. }
  390. }
  391. return null;
  392. }
  393. function getPayPalCandidateSummaries() {
  394. return getPayPalSearchCandidates()
  395. .slice(0, 8)
  396. .map((el) => ({
  397. tag: el.tagName,
  398. id: el.id || '',
  399. role: el.getAttribute?.('role') || '',
  400. text: normalizeText(getCombinedSearchText(el)).slice(0, 120),
  401. visible: isVisibleNode(el),
  402. checked: el.checked === true || el.getAttribute?.('aria-checked') === 'true',
  403. }));
  404. }
  405. function findClickableByText(patterns = []) {
  406. const candidates = getVisibleControls('button, a, [role="button"], [role="radio"], [role="tab"], input[type="button"], input[type="submit"], input[type="radio"], label, [tabindex]');
  407. return candidates.find((el) => {
  408. const text = getCombinedSearchText(el);
  409. return patterns.some((pattern) => pattern.test(text));
  410. }) || null;
  411. }
  412. function getPayPalActivationTargets(target) {
  413. const candidates = [];
  414. const push = (el) => {
  415. if (!el || !isVisibleNode(el) || isDocumentLevelContainer(el)) return;
  416. if (!candidates.includes(el)) candidates.push(el);
  417. };
  418. push(target);
  419. push(target?.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]'));
  420. push(target?.closest?.('input[type="radio"], [role="radio"], button, [role="button"], label, [tabindex]'));
  421. push(findInteractiveAncestor(target));
  422. push(findPaymentCardAncestor(target, /paypal/i));
  423. let current = target;
  424. for (let depth = 0; current && depth < 7; depth += 1, current = current.parentElement) {
  425. if (isDocumentLevelContainer(current)) break;
  426. if (/paypal/i.test(getCombinedSearchText(current))) {
  427. push(current.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]'));
  428. if (isPaymentCardSized(current)) push(current);
  429. if (current.matches?.('button, [role="button"], [role="radio"], label, [tabindex]')) push(current);
  430. }
  431. }
  432. for (const candidate of getPayPalSearchCandidates().slice(0, 8)) {
  433. push(candidate.closest?.('button, [role="button"], [role="radio"], label, [tabindex]'));
  434. push(findInteractiveAncestor(candidate));
  435. push(findPaymentCardAncestor(candidate, /paypal/i));
  436. }
  437. return candidates.slice(0, 10);
  438. }
  439. function getPayPalSearchCandidates() {
  440. const selector = [
  441. 'button',
  442. 'a',
  443. 'label',
  444. '[role="button"]',
  445. '[role="radio"]',
  446. '[role="tab"]',
  447. 'input[type="radio"]',
  448. '[tabindex]',
  449. '[data-testid]',
  450. '[aria-label]',
  451. '[title]',
  452. 'img',
  453. 'svg',
  454. 'span',
  455. 'div',
  456. ].join(', ');
  457. return getVisibleControls(selector)
  458. .filter((el) => /paypal/i.test(getCombinedSearchText(el)))
  459. .sort((left, right) => {
  460. const leftRect = left.getBoundingClientRect();
  461. const rightRect = right.getBoundingClientRect();
  462. return (leftRect.width * leftRect.height) - (rightRect.width * rightRect.height);
  463. });
  464. }
  465. function findInteractiveAncestor(el) {
  466. let current = el;
  467. for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
  468. if (!isVisibleNode(current) || isDocumentLevelContainer(current)) continue;
  469. if (current.matches?.('button, a, label, [role="button"], [role="radio"], [role="tab"], input[type="radio"], [tabindex]')) {
  470. return current;
  471. }
  472. }
  473. return null;
  474. }
  475. function isPaymentCardSized(el) {
  476. if (!isVisibleNode(el) || isDocumentLevelContainer(el)) return false;
  477. const rect = el.getBoundingClientRect();
  478. const maxWidth = Math.max(320, Math.min(window.innerWidth * 0.95, 900));
  479. const maxHeight = Math.max(140, Math.min(window.innerHeight * 0.45, 340));
  480. return rect.width >= 64
  481. && rect.height >= 28
  482. && rect.width <= maxWidth
  483. && rect.height <= maxHeight;
  484. }
  485. function findPaymentCardAncestor(el, pattern) {
  486. let current = el;
  487. for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
  488. if (!isVisibleNode(current)) continue;
  489. if (isDocumentLevelContainer(current)) break;
  490. const text = getCombinedSearchText(current);
  491. if (pattern.test(text) && isPaymentCardSized(current)) {
  492. return current;
  493. }
  494. }
  495. return null;
  496. }
  497. function getCombinedSearchText(el) {
  498. if (!el) return '';
  499. return [
  500. el.textContent,
  501. el.getAttribute?.('aria-label'),
  502. el.getAttribute?.('data-testid'),
  503. el.id,
  504. el.name,
  505. el.value,
  506. el.className && typeof el.className === 'string' ? el.className : '',
  507. ].filter(Boolean).join(' ');
  508. }
  509. async function waitForPayPalPaymentMethodTarget(timeoutMs = 10000) {
  510. const startedAt = Date.now();
  511. while (Date.now() - startedAt < timeoutMs) {
  512. throwIfStopped();
  513. const target = findPayPalPaymentMethodTarget();
  514. if (target) return target;
  515. await sleep(250);
  516. }
  517. return null;
  518. }
  519. async function waitForPayPalPaymentMethodActive(timeoutMs = 3500) {
  520. const startedAt = Date.now();
  521. while (Date.now() - startedAt < timeoutMs) {
  522. throwIfStopped();
  523. if (hasSelectedPayPalControl()) return true;
  524. await sleep(250);
  525. }
  526. return false;
  527. }
  528. function hasSelectedPayPalControl() {
  529. const target = findPayPalPaymentMethodTarget();
  530. let current = target;
  531. for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
  532. if (isDocumentLevelContainer(current)) break;
  533. if (/paypal/i.test(getCombinedSearchText(current)) && hasPaymentMethodSelectionMarker(current)) {
  534. return true;
  535. }
  536. const radio = current.querySelector?.('input[type="radio"], [role="radio"]');
  537. if (
  538. radio
  539. && /paypal/i.test(getCombinedSearchText(current) || getCombinedSearchText(radio))
  540. && hasPaymentMethodSelectionMarker(radio)
  541. ) {
  542. return true;
  543. }
  544. }
  545. return false;
  546. }
  547. function hasPaymentMethodSelectionMarker(el) {
  548. if (!el) return false;
  549. const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
  550. return el.checked === true
  551. || el.getAttribute?.('aria-checked') === 'true'
  552. || el.getAttribute?.('aria-selected') === 'true'
  553. || el.getAttribute?.('data-state') === 'checked'
  554. || el.getAttribute?.('data-selected') === 'true'
  555. || /\b(selected|checked|active)\b/i.test(className);
  556. }
  557. async function ensureTermsCheckbox() {
  558. const cb = document.getElementById('termsOfServiceConsentCheckbox')
  559. || Array.from(document.querySelectorAll('input[type="checkbox"]')).find((input) => /terms|service|agree|consent/i.test(getFieldText(input)));
  560. if (cb && !cb.checked) {
  561. dispatchPointerMouseClick(cb);
  562. log('已勾选服务条款');
  563. await sleep(300);
  564. }
  565. }
  566. function findSubmitButton() {
  567. const direct = document.querySelector('button[data-testid="submit-button"]')
  568. || document.querySelector('button[data-testid="hosted-payment-submit-button"]')
  569. || document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
  570. || document.querySelector('button.SubmitButton--complete');
  571. if (direct && isVisibleNode(direct)) return direct;
  572. const patterns = [/^下一页$/, /^下一步$/, /^next$/i, /subscribe|pay|continue|agree/i, /訂閱|处理中|同意|付款|继续/];
  573. return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((el) => {
  574. if (!isVisibleNode(el) || el.disabled) return false;
  575. const text = normalizeText(el.textContent || el.value || el.getAttribute?.('aria-label') || '');
  576. return patterns.some((pattern) => pattern.test(text));
  577. }) || null;
  578. }
  579. function dispatchRobustActivation(el) {
  580. dispatchPointerMouseClick(el);
  581. if (typeof el.focus === 'function') {
  582. el.focus({ preventScroll: true });
  583. }
  584. [' ', 'Enter'].forEach((key) => {
  585. try {
  586. el.dispatchEvent(new KeyboardEvent('keydown', {
  587. key,
  588. code: key === ' ' ? 'Space' : 'Enter',
  589. bubbles: true,
  590. cancelable: true,
  591. }));
  592. el.dispatchEvent(new KeyboardEvent('keyup', {
  593. key,
  594. code: key === ' ' ? 'Space' : 'Enter',
  595. bubbles: true,
  596. cancelable: true,
  597. }));
  598. } catch {
  599. // Some synthetic keyboard events can be rejected on hardened checkout nodes.
  600. }
  601. });
  602. }
  603. function dispatchPointerMouseClick(el) {
  604. if (!el) throw new Error('无法点击空元素。');
  605. el.scrollIntoView?.({ block: 'center', inline: 'nearest' });
  606. const rect = el.getBoundingClientRect();
  607. const clientX = Math.max(0, Math.floor(rect.left + rect.width / 2));
  608. const clientY = Math.max(0, Math.floor(rect.top + rect.height / 2));
  609. ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
  610. const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
  611. el.dispatchEvent(new EventCtor(type, {
  612. bubbles: true,
  613. cancelable: true,
  614. view: window,
  615. button: 0,
  616. buttons: type === 'pointerup' || type === 'mouseup' || type === 'click' ? 0 : 1,
  617. clientX,
  618. clientY,
  619. pointerId: 1,
  620. pointerType: 'mouse',
  621. isPrimary: true,
  622. }));
  623. });
  624. if (typeof el.click === 'function') {
  625. el.click();
  626. }
  627. }
  628. function fillBySelector(selector, value) {
  629. const el = document.querySelector(selector);
  630. if (el) {
  631. fillInput(el, value);
  632. } else {
  633. log(`未找到元素: ${selector}`, 'warn');
  634. }
  635. }
  636. function fillMissingBySelector(selector, value) {
  637. const el = document.querySelector(selector);
  638. if (!el) {
  639. log(`未找到元素: ${selector}`, 'warn');
  640. return;
  641. }
  642. if (String(el.value || '').trim()) {
  643. return;
  644. }
  645. fillInput(el, value);
  646. }
  647. function fillMissingControl(el, value) {
  648. if (!el) return false;
  649. if (String(el.value || '').trim()) {
  650. return false;
  651. }
  652. fillInput(el, value);
  653. return true;
  654. }
  655. function fillSelectByText(selector, text) {
  656. const el = document.querySelector(selector);
  657. if (!el) {
  658. log(`未找到 select: ${selector}`, 'warn');
  659. return;
  660. }
  661. for (let i = 0; i < el.options.length; i++) {
  662. const opt = el.options[i];
  663. if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
  664. fillSelect(el, opt.value);
  665. return;
  666. }
  667. }
  668. log(`未在 select 中找到匹配项: ${text}`, 'warn');
  669. }
  670. function fillRegionControlByText(el, text) {
  671. if (!el) return false;
  672. if (el.tagName === 'SELECT') {
  673. for (let i = 0; i < el.options.length; i++) {
  674. const opt = el.options[i];
  675. if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
  676. fillSelect(el, opt.value);
  677. return true;
  678. }
  679. }
  680. log(`未在地区 select 中找到匹配项: ${text}`, 'warn');
  681. return false;
  682. }
  683. return fillMissingControl(el, text);
  684. }
  685. function fillBillingAddressDirectSelectors(addr = {}) {
  686. fillBySelector('#billingAddressLine1', addr.street);
  687. fillBySelector('#billingLocality', addr.city);
  688. fillBySelector('#billingPostalCode', addr.zip.substring(0, 5));
  689. fillSelectByText('#billingAdministrativeArea', addr.state);
  690. }
  691. async function fillBillingAddressLine1FromGoogle(addr = {}) {
  692. const input = getStructuredAddressFields().address1 || findAddressSearchInput();
  693. if (!input) {
  694. log('未找到地址栏 1 输入框,无法触发 Google 地址下拉。', 'warn');
  695. return false;
  696. }
  697. const query = buildGoogleAddressQuery(addr);
  698. log(`正在地址栏 1 输入完整地址以触发 Google 下拉: ${query}`);
  699. await typeAddressQueryForAutocomplete(input, query);
  700. const item = await waitForGoogleAddressSuggestion(query, 6500);
  701. if (item) {
  702. const itemText = getSuggestionText(item) || '首个地址建议';
  703. log(`正在选择 Google 地址建议: ${itemText}`);
  704. clickAutocompleteSuggestion(item);
  705. await waitForAddressAutofill(input, query, 2200);
  706. dispatchInputBlur(input);
  707. return true;
  708. }
  709. const externalFrameSelected = await selectAddressSuggestionInExternalFrame(query);
  710. if (externalFrameSelected) {
  711. await waitForAddressAutofill(input, query, 2200);
  712. dispatchInputBlur(input);
  713. return true;
  714. }
  715. log('未检测到可点击的 Google 地址建议,尝试使用键盘选择首个建议...', 'warn');
  716. const keyboardSelected = await chooseAutocompleteWithKeyboard(input, query);
  717. if (keyboardSelected) {
  718. dispatchInputBlur(input);
  719. return true;
  720. }
  721. return false;
  722. }
  723. function findAutoJsPayPalTarget() {
  724. return document.querySelector('[data-testid="paypal-accordion-item-button"]')
  725. || document.querySelector('.paypal-accordion-item button');
  726. }
  727. async function clickPayPalLikeAutoJs(target) {
  728. if (!target) return;
  729. target.scrollIntoView?.({ block: 'center', inline: 'nearest' });
  730. if (typeof target.click === 'function') {
  731. target.click();
  732. }
  733. await sleep(600);
  734. }
  735. async function selectGoogleAddressSuggestionOnly(payload = {}) {
  736. const query = normalizeText(payload.query || '');
  737. const item = await waitForGoogleAddressSuggestion(query, 5500);
  738. if (!item) {
  739. return { error: '未找到 Google 地址建议项' };
  740. }
  741. const itemText = getSuggestionText(item) || '首个地址建议';
  742. log(`正在独立 autocomplete iframe 中选择 Google 地址建议: ${itemText}`);
  743. clickAutocompleteSuggestion(item);
  744. await sleep(900);
  745. return {
  746. ok: true,
  747. selectedAddressText: itemText,
  748. };
  749. }
  750. async function selectAddressSuggestionInExternalFrame(query) {
  751. if (!chrome?.runtime?.sendMessage) {
  752. return false;
  753. }
  754. try {
  755. const response = await chrome.runtime.sendMessage({
  756. type: 'CHECKOUT_STRIPE_SELECT_AUTOCOMPLETE_FRAME',
  757. source: 'checkout-stripe',
  758. payload: { query },
  759. });
  760. if (response?.ok) {
  761. log(`已在独立 Google 地址 iframe 中选择建议: ${response.selectedAddressText || '首个地址建议'}`);
  762. return true;
  763. }
  764. if (response?.error) {
  765. log(`独立 Google 地址 iframe 未完成选择: ${response.error}`, 'warn');
  766. }
  767. } catch (error) {
  768. log(`尝试选择独立 Google 地址 iframe 失败: ${error?.message || error}`, 'warn');
  769. }
  770. return false;
  771. }
  772. function buildGoogleAddressQuery(addr = {}) {
  773. const explicitQuery = normalizeText(addr.query || addr.autocompleteQuery || '');
  774. if (explicitQuery) return explicitQuery;
  775. const street = normalizeText(addr.street || addr.address1 || '123 Main St');
  776. const city = normalizeText(addr.city || 'New York');
  777. const state = normalizeText(addr.state || addr.region || 'New York');
  778. const zip = normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5);
  779. return [street, city, state, zip].filter(Boolean).join(', ');
  780. }
  781. async function typeAddressQueryForAutocomplete(input, query) {
  782. input.scrollIntoView?.({ block: 'center', inline: 'nearest' });
  783. input.focus();
  784. await sleep(150);
  785. setNativeInputValue(input, '');
  786. input.dispatchEvent(new Event('input', { bubbles: true }));
  787. input.dispatchEvent(new Event('change', { bubbles: true }));
  788. await sleep(120);
  789. for (const char of String(query || '')) {
  790. throwIfStopped();
  791. input.dispatchEvent(new KeyboardEvent('keydown', {
  792. key: char,
  793. bubbles: true,
  794. cancelable: true,
  795. }));
  796. setNativeInputValue(input, `${input.value || ''}${char}`);
  797. dispatchAutocompleteInputEvent(input, char);
  798. input.dispatchEvent(new KeyboardEvent('keyup', {
  799. key: char,
  800. bubbles: true,
  801. cancelable: true,
  802. }));
  803. await sleep(18);
  804. }
  805. input.dispatchEvent(new Event('change', { bubbles: true }));
  806. }
  807. function setNativeInputValue(input, value) {
  808. const descriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
  809. if (descriptor?.set) {
  810. descriptor.set.call(input, value);
  811. } else {
  812. input.value = value;
  813. }
  814. }
  815. function dispatchAutocompleteInputEvent(input, data) {
  816. try {
  817. input.dispatchEvent(new InputEvent('input', {
  818. bubbles: true,
  819. cancelable: true,
  820. data,
  821. inputType: 'insertText',
  822. }));
  823. } catch {
  824. input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
  825. }
  826. }
  827. function dispatchInputBlur(input) {
  828. input.dispatchEvent(new Event('change', { bubbles: true }));
  829. input.dispatchEvent(new Event('blur', { bubbles: true }));
  830. }
  831. async function waitForGoogleAddressSuggestion(query, timeoutMs = 5000) {
  832. const start = Date.now();
  833. while (Date.now() - start < timeoutMs) {
  834. throwIfStopped();
  835. const items = getVisibleAutocompleteSuggestions(query);
  836. if (items.length) {
  837. const matching = findBestAddressSuggestion(items, query);
  838. return matching || items[0];
  839. }
  840. await sleep(250);
  841. }
  842. return null;
  843. }
  844. function getVisibleAutocompleteSuggestions(query = '') {
  845. const selectors = [
  846. { selector: '.pac-container .pac-item', generic: false },
  847. { selector: '#billing-address-autocomplete-results [role="option"]', generic: false },
  848. { selector: '.AddressAutocomplete-results [role="option"]', generic: false },
  849. { selector: '[class*="AddressAutocomplete"] [role="option"]', generic: false },
  850. { selector: '[data-testid*="address" i] [role="option"]', generic: false },
  851. { selector: '[role="listbox"] [role="option"]', generic: false },
  852. { selector: '[role="option"]', generic: false },
  853. { selector: '[role="listbox"] li', generic: true },
  854. { selector: '.autocomplete-dropdown [role="option"]', generic: false },
  855. { selector: '.autocomplete-dropdown li', generic: true },
  856. { selector: 'li', generic: true },
  857. ];
  858. const seen = new Set();
  859. const items = [];
  860. for (const config of selectors) {
  861. document.querySelectorAll(config.selector).forEach((item) => {
  862. if (seen.has(item) || !isVisibleNode(item)) return;
  863. if (!isLikelyAddressSuggestion(item, query, config.generic)) return;
  864. seen.add(item);
  865. items.push(item);
  866. });
  867. }
  868. return items;
  869. }
  870. function findBestAddressSuggestion(items, query) {
  871. let best = null;
  872. let bestScore = 0;
  873. for (const item of items) {
  874. const score = scoreAddressSuggestion(item, query);
  875. if (score > bestScore) {
  876. best = item;
  877. bestScore = score;
  878. }
  879. }
  880. return best || null;
  881. }
  882. function getSuggestionText(item) {
  883. return normalizeText(item?.textContent || item?.getAttribute?.('aria-label') || '');
  884. }
  885. function normalizeText(value = '') {
  886. return String(value || '').replace(/\s+/g, ' ').trim();
  887. }
  888. function getAddressQueryTokens(query = '') {
  889. return normalizeText(query)
  890. .toLowerCase()
  891. .split(/[^a-z0-9]+/i)
  892. .map((part) => part.trim())
  893. .filter((part) => part.length >= 3);
  894. }
  895. function scoreAddressSuggestion(item, query = '') {
  896. const text = getSuggestionText(item).toLowerCase();
  897. if (!text) return 0;
  898. const tokens = getAddressQueryTokens(query);
  899. let score = 0;
  900. tokens.forEach((token) => {
  901. if (text.includes(token)) score += 1;
  902. });
  903. if (/\d/.test(text)) score += 2;
  904. if (/street|st\.?|avenue|ave\.?|road|rd\.?|drive|dr\.?|boulevard|blvd\.?|lane|ln\.?|way|court|ct\.?/i.test(text)) {
  905. score += 2;
  906. }
  907. return score;
  908. }
  909. function isLikelyAddressSuggestion(item, query = '', generic = false) {
  910. const text = getSuggestionText(item);
  911. if (!text || text.length < 3) return false;
  912. if (!generic) return true;
  913. const lowered = text.toLowerCase();
  914. if (/terms|privacy|subscribe|paypal|card|payment|email|phone|下一步|提交|付款|订阅/i.test(lowered)) {
  915. return false;
  916. }
  917. return scoreAddressSuggestion(item, query) > 0;
  918. }
  919. function isVisibleNode(node) {
  920. const rect = node.getBoundingClientRect();
  921. const style = window.getComputedStyle(node);
  922. return rect.width > 0
  923. && rect.height > 0
  924. && style.visibility !== 'hidden'
  925. && style.display !== 'none';
  926. }
  927. function clickAutocompleteSuggestion(item) {
  928. item.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
  929. ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
  930. const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
  931. item.dispatchEvent(new EventCtor(type, {
  932. bubbles: true,
  933. cancelable: true,
  934. view: window,
  935. }));
  936. });
  937. if (typeof item.click === 'function') {
  938. item.click();
  939. }
  940. }
  941. async function chooseAutocompleteWithKeyboard(input, query = '') {
  942. input.focus();
  943. input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true }));
  944. input.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true }));
  945. await sleep(250);
  946. input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
  947. input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
  948. return waitForAddressAutofill(input, query, 2200);
  949. }
  950. async function waitForAddressAutofill(input, query = '', timeoutMs = 1800) {
  951. const startedAt = Date.now();
  952. const originalQuery = normalizeText(query);
  953. while (Date.now() - startedAt < timeoutMs) {
  954. throwIfStopped();
  955. const line1 = normalizeText(input?.value || '');
  956. const fields = getStructuredAddressFields();
  957. const city = normalizeText(fields.city?.value || document.querySelector('#billingLocality')?.value || '');
  958. const zip = normalizeText(fields.postalCode?.value || document.querySelector('#billingPostalCode')?.value || '');
  959. if (city || zip || (line1 && line1 !== originalQuery)) {
  960. return true;
  961. }
  962. await sleep(200);
  963. }
  964. return false;
  965. }
  966. function hideAutocompleteDropdowns() {
  967. log('正在隐藏剩余 Google 地址补全框...');
  968. document.querySelectorAll([
  969. '.pac-container',
  970. '.pac-item',
  971. 'div[role="listbox"]',
  972. '.AddressAutocomplete-results',
  973. '[class*="AddressAutocomplete"]',
  974. '#billing-address-autocomplete-results',
  975. ].join(', ')).forEach((el) => {
  976. el.style.setProperty('display', 'none', 'important');
  977. el.style.setProperty('visibility', 'hidden', 'important');
  978. el.style.setProperty('height', '0', 'important');
  979. el.style.setProperty('overflow', 'hidden', 'important');
  980. });
  981. }
  982. async function clickSubmitButton(retries = 0) {
  983. throwIfStopped();
  984. const btn = findSubmitButton();
  985. if (btn) {
  986. const rect = btn.getBoundingClientRect();
  987. if (btn.disabled || rect.height === 0) {
  988. log('提交按钮被禁用或不可见,等待中...');
  989. if (retries < 12) {
  990. await sleep(800);
  991. return clickSubmitButton(retries + 1);
  992. }
  993. throw new Error('提交按钮一直不可用,已超时');
  994. }
  995. log(`正在点击提交按钮: ${btn.textContent.trim()}`);
  996. dispatchPointerMouseClick(btn);
  997. } else {
  998. if (retries < 12) {
  999. log(`未找到提交按钮,重试中... (${retries + 1})`);
  1000. await sleep(800);
  1001. return clickSubmitButton(retries + 1);
  1002. }
  1003. throw new Error('在 Stripe 页面上未找到提交按钮');
  1004. }
  1005. }
  1006. document.documentElement.setAttribute('data-multipage-checkout-stripe-ready', '');
  1007. })();