| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130 |
- // content/checkout-stripe.js — Stripe checkout page automation for ChatGPT Plus subscription
- (function attachCheckoutStripe() {
- if (document.documentElement.hasAttribute('data-multipage-checkout-stripe-listener')) return;
- document.documentElement.setAttribute('data-multipage-checkout-stripe-listener', '');
- chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
- if (message.type === 'EXECUTE_STEP' && message.step === 7) {
- resetStopState();
- runStripeCheckout(message.payload || {}).then(
- (result) => sendResponse(result),
- (err) => sendResponse({ error: err.message })
- );
- return true;
- }
- if (message.type === 'CHECKOUT_STRIPE_SELECT_ADDRESS_SUGGESTION') {
- resetStopState();
- selectGoogleAddressSuggestionOnly(message.payload || {}).then(
- (result) => sendResponse(result),
- (err) => sendResponse({ error: err.message })
- );
- return true;
- }
- if (message.type === 'CHECKOUT_STRIPE_GET_STATE') {
- sendResponse(inspectCheckoutStripeState());
- return false;
- }
- if (message.type === 'CHECKOUT_STRIPE_SELECT_PAYPAL') {
- resetStopState();
- selectPayPalPaymentMethod(message.payload || {}).then(
- (result) => sendResponse(result),
- (err) => sendResponse({ error: err.message })
- );
- return true;
- }
- if (message.type === 'CHECKOUT_STRIPE_FILL_BILLING_ADDRESS') {
- resetStopState();
- fillStripeBillingAddress(message.payload || {}).then(
- (result) => sendResponse(result),
- (err) => sendResponse({ error: err.message })
- );
- return true;
- }
- if (message.type === 'CHECKOUT_STRIPE_CLICK_SUBMIT') {
- resetStopState();
- clickStripeCheckoutSubmit(message.payload || {}).then(
- (result) => sendResponse(result),
- (err) => sendResponse({ error: err.message })
- );
- return true;
- }
- });
- async function runStripeCheckout(payload) {
- try {
- throwIfStopped();
- log('开始执行 Stripe 结账页面自动化...');
- await sleep(1800);
- // 1. Click PayPal option
- await selectPayPalPaymentMethod({ relaxedActivation: true });
- await sleep(1000);
- // 2. Fill billing address
- await fillStripeBillingAddress(payload);
- // 3. Click submit
- await clickStripeCheckoutSubmit({ beforeClickDelayMs: 1500 });
- log('Stripe 结账表单已提交');
- reportComplete(7, {});
- return { ok: true };
- } catch (e) {
- if (isStopError(e)) throw e;
- log('Stripe 结账流程出错: ' + e.message, 'error');
- reportError(7, e.message);
- return { error: e.message };
- }
- }
- function inspectCheckoutStripeState() {
- const addressFields = getStructuredAddressFields();
- return {
- url: location.href,
- readyState: document.readyState,
- hasPayPal: Boolean(findPayPalPaymentMethodTarget()),
- paypalCandidates: getPayPalCandidateSummaries(),
- billingFieldsVisible: hasBillingAddressFields(addressFields),
- hasSubmitButton: Boolean(findSubmitButton()),
- addressFieldValues: {
- address1: addressFields.address1?.value || '',
- city: addressFields.city?.value || '',
- region: addressFields.region?.value || getSelectText(addressFields.regionSelect) || '',
- postalCode: addressFields.postalCode?.value || '',
- },
- };
- }
- async function selectPayPalPaymentMethod(options = {}) {
- log('正在寻找 PayPal 选项...');
- const autoJsTarget = findAutoJsPayPalTarget();
- if (autoJsTarget) {
- await clickPayPalLikeAutoJs(autoJsTarget);
- await sleep(450);
- await clickPayPalLikeAutoJs(autoJsTarget);
- const autoJsActive = await waitForPayPalPaymentMethodActive(2500);
- if (autoJsActive) {
- log('已按 auto.js 方式确认 PayPal 选项生效');
- } else {
- log('auto.js 方式点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn');
- }
- return {
- paymentSelected: autoJsActive,
- relaxed: !autoJsActive,
- };
- }
- const target = await waitForPayPalPaymentMethodTarget(10000);
- if (!target) {
- if (options.relaxedActivation) {
- log('未找到 PayPal 选项按钮,当前为宽松模式,继续执行...', 'warn');
- return { paymentSelected: false, relaxed: true };
- }
- throw new Error('未找到 PayPal 付款方式,无法切换。');
- }
- const clickTargets = getPayPalActivationTargets(target);
- for (let attempt = 0; attempt < 2; attempt += 1) {
- for (const candidate of clickTargets) {
- dispatchRobustActivation(candidate);
- await sleep(220);
- if (hasSelectedPayPalControl() || hasBillingAddressFields()) {
- break;
- }
- }
- await sleep(450);
- }
- const active = await waitForPayPalPaymentMethodActive(3500);
- if (!active) {
- log('点击 PayPal 后未观察到标准选中标记,继续按 hosted checkout 宽松模式执行。', 'warn');
- } else {
- log('已确认 PayPal 选项生效');
- }
- return {
- paymentSelected: active,
- relaxed: !active,
- };
- }
- async function fillStripeBillingAddress(payload = {}) {
- const addr = normalizeCheckoutAddress(payload.address || {});
- if (payload.autoJsDirectSelectors) {
- log('参考 auto.js 直接填写账单字段...');
- fillBillingAddressDirectSelectors(addr);
- await sleep(800);
- hideAutocompleteDropdowns();
- await ensureTermsCheckbox();
- const latest = getStructuredAddressFields();
- return {
- countryText: readCountryText(),
- selectedAutocompleteAddress: false,
- structuredAddress: {
- address1: latest.address1?.value || '',
- city: latest.city?.value || '',
- region: latest.region?.value || getSelectText(latest.regionSelect) || '',
- postalCode: latest.postalCode?.value || '',
- },
- };
- }
- log('正在填写账单地址...');
- const selectedAutocompleteAddress = await fillBillingAddressLine1FromGoogle(addr);
- if (!selectedAutocompleteAddress) {
- log('未能选择 Google 地址下拉项,改用手动填写地址兜底。', 'warn');
- const fields = getStructuredAddressFields();
- const address1Input = fields.address1 || findAddressSearchInput();
- if (address1Input) {
- fillInput(address1Input, addr.street);
- } else {
- fillBySelector('#billingAddressLine1', addr.street);
- }
- }
- await sleep(900);
- const fields = getStructuredAddressFields();
- fillMissingControl(fields.city || document.querySelector('#billingLocality'), addr.city);
- fillMissingControl(fields.postalCode || document.querySelector('#billingPostalCode'), addr.zip.substring(0, 5));
- fillRegionControlByText(fields.regionSelect || fields.region || document.querySelector('#billingAdministrativeArea'), addr.state);
- await sleep(800);
- hideAutocompleteDropdowns();
- await ensureTermsCheckbox();
- const latest = getStructuredAddressFields();
- return {
- countryText: readCountryText(),
- selectedAutocompleteAddress,
- structuredAddress: {
- address1: latest.address1?.value || '',
- city: latest.city?.value || '',
- region: latest.region?.value || getSelectText(latest.regionSelect) || '',
- postalCode: latest.postalCode?.value || '',
- },
- };
- }
- async function clickStripeCheckoutSubmit(payload = {}) {
- await ensureTermsCheckbox();
- await sleep(Math.max(0, Math.floor(Number(payload.beforeClickDelayMs) || 0)));
- await clickSubmitButton();
- return { clicked: true };
- }
- function normalizeCheckoutAddress(addr = {}) {
- return {
- street: normalizeText(addr.street || addr.address1 || '123 Main St'),
- city: normalizeText(addr.city || 'New York'),
- state: normalizeText(addr.state || addr.region || 'New York'),
- zip: normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5),
- query: normalizeText(addr.query || addr.autocompleteQuery || ''),
- };
- }
- function getStructuredAddressFields() {
- const address1 = getVisibleElementById('billingAddressLine1') || findInputByFieldText([
- /address\s*(?:line)?\s*1|address[_-]?line[_-]?1|address\[(?:address_)?line1\]|line\s*1|street|street[_-]?address/i,
- /地址\s*1|街道|详细地址|住所/i,
- ], {
- exclude: (input) => isNonAddressSearchInput(input),
- }) || findAddressSearchInput();
- const city = getVisibleElementById('billingLocality') || findInputByFieldText([
- /city|town|suburb|locality|address[_-]?level[_-]?2|address\[city\]/i,
- /城市|市区|区市町村|市区町村|市町村/i,
- ]);
- const postalCode = getVisibleElementById('billingPostalCode') || findInputByFieldText([
- /postal|zip|postcode|postal[_-]?code|zip[_-]?code|address\[postal_code\]/i,
- /邮编|邮政|郵便番号/i,
- ]);
- const regionSelect = getVisibleElementById('billingAdministrativeArea');
- const region = regionSelect || findInputByFieldText([
- /state|province|region|county|prefecture|administrative|administrative[_-]?area|address[_-]?level[_-]?1|address\[state\]/i,
- /省|州|地区|辖区|都道府县|都道府県/i,
- ]);
- return {
- address1,
- city,
- postalCode,
- region,
- regionSelect: regionSelect && regionSelect.tagName === 'SELECT' ? regionSelect : null,
- };
- }
- function getVisibleElementById(id) {
- const el = document.getElementById(id);
- return el && isVisibleNode(el) ? el : null;
- }
- function hasBillingAddressFields(fields = getStructuredAddressFields()) {
- if (fields?.address1 || fields?.city || fields?.postalCode) {
- return true;
- }
- return getVisibleTextInputs().some((input) => {
- const text = getFieldText(input);
- return /address|street|billing|line\s*1|地址|街道|账单/i.test(text)
- && !/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);
- }) || Boolean(findAddressSearchInput());
- }
- function readCountryText() {
- const country = document.getElementById('billingCountry')
- || findInputByFieldText([/country|region/i, /国家|地区/i])
- || Array.from(document.querySelectorAll('select')).find((select) => (
- isVisibleNode(select) && /country|region|国家|地区/i.test(getFieldText(select))
- ));
- if (!country) return '';
- if (country.tagName === 'SELECT') {
- return getSelectText(country) || country.value || '';
- }
- return country.value || country.textContent || '';
- }
- function getSelectText(select) {
- if (!select || select.tagName !== 'SELECT') return '';
- return normalizeText(select.selectedOptions?.[0]?.textContent || select.value || '');
- }
- function findInputByFieldText(patterns = [], options = {}) {
- const excluded = options.exclude || (() => false);
- return getVisibleTextInputs().find((input) => {
- if (excluded(input)) return false;
- return patterns.some((pattern) => pattern.test(getFieldText(input)));
- }) || null;
- }
- function getVisibleControls(selector) {
- return Array.from(document.querySelectorAll(selector)).filter((el) => isVisibleNode(el));
- }
- function getVisibleTextInputs() {
- return getVisibleControls('input, textarea')
- .filter((el) => {
- const type = String(el.getAttribute('type') || el.type || '').trim().toLowerCase();
- return !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
- });
- }
- function getVisibleFormControls() {
- return Array.from(document.querySelectorAll('input, textarea, select'))
- .filter((el) => {
- const type = String(el.getAttribute('type') || el.type || '').toLowerCase();
- return type !== 'hidden' && isVisibleNode(el);
- });
- }
- function getFieldText(el) {
- if (!el) return '';
- const id = el.id ? String(el.id) : '';
- const labelText = id
- ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
- : '';
- const wrappingLabel = el.closest?.('label')?.textContent || '';
- const container = el.closest?.('[data-testid], [class], div, section, fieldset');
- return normalizeText([
- id,
- el.name,
- el.getAttribute?.('autocomplete'),
- el.getAttribute?.('aria-label'),
- el.getAttribute?.('placeholder'),
- labelText,
- wrappingLabel,
- container && !isDocumentLevelContainer(container) ? container.textContent || '' : '',
- ].filter(Boolean).join(' '));
- }
- function getDirectFieldHintText(el) {
- if (!el) return '';
- const id = el.id ? String(el.id) : '';
- const labelText = id
- ? Array.from(document.querySelectorAll(`label[for="${cssEscape(id)}"]`)).map((label) => label.textContent || '').join(' ')
- : '';
- const wrappingLabel = el.closest?.('label')?.textContent || '';
- return normalizeText([
- id,
- el.name,
- el.getAttribute?.('autocomplete'),
- el.getAttribute?.('aria-label'),
- el.getAttribute?.('placeholder'),
- labelText,
- wrappingLabel,
- ].filter(Boolean).join(' '));
- }
- function isNonAddressSearchInput(input) {
- const directText = getDirectFieldHintText(input);
- const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
- 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)
- || ['email', 'tel', 'password'].includes(type);
- }
- function isLikelyAddressSearchInput(input) {
- const text = getFieldText(input);
- if (isNonAddressSearchInput(input)) {
- return false;
- }
- 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)) {
- return false;
- }
- return /address|street|billing|search|line\s*1|地址|街道|账单/i.test(text);
- }
- function findAddressSearchInput() {
- const direct = findInputByFieldText([
- /address|street|billing|search|line\s*1/i,
- /地址|街道|账单/i,
- ], {
- exclude: (input) => isNonAddressSearchInput(input)
- || /city|state|province|postal|zip|country|region|城市|省|州|邮编|国家|地区/i.test(getFieldText(input)),
- });
- if (direct) return direct;
- return getVisibleTextInputs().filter(isLikelyAddressSearchInput)[0] || null;
- }
- function isDocumentLevelContainer(el) {
- return !el
- || el === document.documentElement
- || el === document.body
- || ['HTML', 'BODY', 'MAIN'].includes(el.tagName);
- }
- function cssEscape(value) {
- if (window.CSS?.escape) return window.CSS.escape(value);
- return String(value || '').replace(/["\\]/g, '\\$&');
- }
- function findPayPalPaymentMethodTarget() {
- const directSelectors = [
- '[data-testid="paypal-accordion-item-button"]',
- '[data-testid*="paypal" i]',
- '.paypal-accordion-item button',
- 'button[aria-label*="PayPal" i]',
- '[aria-label*="PayPal" i]',
- '[title*="PayPal" i]',
- '[role="radio"][aria-label*="PayPal" i]',
- 'input[type="radio"][value*="paypal" i]',
- 'button[value*="paypal" i]',
- ];
- for (const selector of directSelectors) {
- const target = Array.from(document.querySelectorAll(selector)).find((el) => isVisibleNode(el));
- if (target) return target;
- }
- const directClickable = findClickableByText([/paypal/i]);
- if (directClickable) return directClickable;
- const radios = getVisibleControls('input[type="radio"], [role="radio"]');
- const matchedRadio = radios.find((el) => /paypal/i.test(getCombinedSearchText(el)));
- if (matchedRadio) return matchedRadio;
- for (const candidate of getPayPalSearchCandidates()) {
- const interactive = findInteractiveAncestor(candidate);
- if (interactive && /paypal/i.test(getCombinedSearchText(interactive))) {
- return interactive;
- }
- const card = findPaymentCardAncestor(candidate, /paypal/i);
- if (card) {
- return card;
- }
- }
- return null;
- }
- function getPayPalCandidateSummaries() {
- return getPayPalSearchCandidates()
- .slice(0, 8)
- .map((el) => ({
- tag: el.tagName,
- id: el.id || '',
- role: el.getAttribute?.('role') || '',
- text: normalizeText(getCombinedSearchText(el)).slice(0, 120),
- visible: isVisibleNode(el),
- checked: el.checked === true || el.getAttribute?.('aria-checked') === 'true',
- }));
- }
- function findClickableByText(patterns = []) {
- const candidates = getVisibleControls('button, a, [role="button"], [role="radio"], [role="tab"], input[type="button"], input[type="submit"], input[type="radio"], label, [tabindex]');
- return candidates.find((el) => {
- const text = getCombinedSearchText(el);
- return patterns.some((pattern) => pattern.test(text));
- }) || null;
- }
- function getPayPalActivationTargets(target) {
- const candidates = [];
- const push = (el) => {
- if (!el || !isVisibleNode(el) || isDocumentLevelContainer(el)) return;
- if (!candidates.includes(el)) candidates.push(el);
- };
- push(target);
- push(target?.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]'));
- push(target?.closest?.('input[type="radio"], [role="radio"], button, [role="button"], label, [tabindex]'));
- push(findInteractiveAncestor(target));
- push(findPaymentCardAncestor(target, /paypal/i));
- let current = target;
- for (let depth = 0; current && depth < 7; depth += 1, current = current.parentElement) {
- if (isDocumentLevelContainer(current)) break;
- if (/paypal/i.test(getCombinedSearchText(current))) {
- push(current.querySelector?.('input[type="radio"], [role="radio"], button, [role="button"]'));
- if (isPaymentCardSized(current)) push(current);
- if (current.matches?.('button, [role="button"], [role="radio"], label, [tabindex]')) push(current);
- }
- }
- for (const candidate of getPayPalSearchCandidates().slice(0, 8)) {
- push(candidate.closest?.('button, [role="button"], [role="radio"], label, [tabindex]'));
- push(findInteractiveAncestor(candidate));
- push(findPaymentCardAncestor(candidate, /paypal/i));
- }
- return candidates.slice(0, 10);
- }
- function getPayPalSearchCandidates() {
- const selector = [
- 'button',
- 'a',
- 'label',
- '[role="button"]',
- '[role="radio"]',
- '[role="tab"]',
- 'input[type="radio"]',
- '[tabindex]',
- '[data-testid]',
- '[aria-label]',
- '[title]',
- 'img',
- 'svg',
- 'span',
- 'div',
- ].join(', ');
- return getVisibleControls(selector)
- .filter((el) => /paypal/i.test(getCombinedSearchText(el)))
- .sort((left, right) => {
- const leftRect = left.getBoundingClientRect();
- const rightRect = right.getBoundingClientRect();
- return (leftRect.width * leftRect.height) - (rightRect.width * rightRect.height);
- });
- }
- function findInteractiveAncestor(el) {
- let current = el;
- for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
- if (!isVisibleNode(current) || isDocumentLevelContainer(current)) continue;
- if (current.matches?.('button, a, label, [role="button"], [role="radio"], [role="tab"], input[type="radio"], [tabindex]')) {
- return current;
- }
- }
- return null;
- }
- function isPaymentCardSized(el) {
- if (!isVisibleNode(el) || isDocumentLevelContainer(el)) return false;
- const rect = el.getBoundingClientRect();
- const maxWidth = Math.max(320, Math.min(window.innerWidth * 0.95, 900));
- const maxHeight = Math.max(140, Math.min(window.innerHeight * 0.45, 340));
- return rect.width >= 64
- && rect.height >= 28
- && rect.width <= maxWidth
- && rect.height <= maxHeight;
- }
- function findPaymentCardAncestor(el, pattern) {
- let current = el;
- for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
- if (!isVisibleNode(current)) continue;
- if (isDocumentLevelContainer(current)) break;
- const text = getCombinedSearchText(current);
- if (pattern.test(text) && isPaymentCardSized(current)) {
- return current;
- }
- }
- return null;
- }
- function getCombinedSearchText(el) {
- if (!el) return '';
- return [
- el.textContent,
- el.getAttribute?.('aria-label'),
- el.getAttribute?.('data-testid'),
- el.id,
- el.name,
- el.value,
- el.className && typeof el.className === 'string' ? el.className : '',
- ].filter(Boolean).join(' ');
- }
- async function waitForPayPalPaymentMethodTarget(timeoutMs = 10000) {
- const startedAt = Date.now();
- while (Date.now() - startedAt < timeoutMs) {
- throwIfStopped();
- const target = findPayPalPaymentMethodTarget();
- if (target) return target;
- await sleep(250);
- }
- return null;
- }
- async function waitForPayPalPaymentMethodActive(timeoutMs = 3500) {
- const startedAt = Date.now();
- while (Date.now() - startedAt < timeoutMs) {
- throwIfStopped();
- if (hasSelectedPayPalControl()) return true;
- await sleep(250);
- }
- return false;
- }
- function hasSelectedPayPalControl() {
- const target = findPayPalPaymentMethodTarget();
- let current = target;
- for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
- if (isDocumentLevelContainer(current)) break;
- if (/paypal/i.test(getCombinedSearchText(current)) && hasPaymentMethodSelectionMarker(current)) {
- return true;
- }
- const radio = current.querySelector?.('input[type="radio"], [role="radio"]');
- if (
- radio
- && /paypal/i.test(getCombinedSearchText(current) || getCombinedSearchText(radio))
- && hasPaymentMethodSelectionMarker(radio)
- ) {
- return true;
- }
- }
- return false;
- }
- function hasPaymentMethodSelectionMarker(el) {
- if (!el) return false;
- const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
- return el.checked === true
- || el.getAttribute?.('aria-checked') === 'true'
- || el.getAttribute?.('aria-selected') === 'true'
- || el.getAttribute?.('data-state') === 'checked'
- || el.getAttribute?.('data-selected') === 'true'
- || /\b(selected|checked|active)\b/i.test(className);
- }
- async function ensureTermsCheckbox() {
- const cb = document.getElementById('termsOfServiceConsentCheckbox')
- || Array.from(document.querySelectorAll('input[type="checkbox"]')).find((input) => /terms|service|agree|consent/i.test(getFieldText(input)));
- if (cb && !cb.checked) {
- dispatchPointerMouseClick(cb);
- log('已勾选服务条款');
- await sleep(300);
- }
- }
- function findSubmitButton() {
- const direct = document.querySelector('button[data-testid="submit-button"]')
- || document.querySelector('button[data-testid="hosted-payment-submit-button"]')
- || document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
- || document.querySelector('button.SubmitButton--complete');
- if (direct && isVisibleNode(direct)) return direct;
- const patterns = [/^下一页$/, /^下一步$/, /^next$/i, /subscribe|pay|continue|agree/i, /訂閱|处理中|同意|付款|继续/];
- return Array.from(document.querySelectorAll('button, [role="button"], input[type="submit"]')).find((el) => {
- if (!isVisibleNode(el) || el.disabled) return false;
- const text = normalizeText(el.textContent || el.value || el.getAttribute?.('aria-label') || '');
- return patterns.some((pattern) => pattern.test(text));
- }) || null;
- }
- function dispatchRobustActivation(el) {
- dispatchPointerMouseClick(el);
- if (typeof el.focus === 'function') {
- el.focus({ preventScroll: true });
- }
- [' ', 'Enter'].forEach((key) => {
- try {
- el.dispatchEvent(new KeyboardEvent('keydown', {
- key,
- code: key === ' ' ? 'Space' : 'Enter',
- bubbles: true,
- cancelable: true,
- }));
- el.dispatchEvent(new KeyboardEvent('keyup', {
- key,
- code: key === ' ' ? 'Space' : 'Enter',
- bubbles: true,
- cancelable: true,
- }));
- } catch {
- // Some synthetic keyboard events can be rejected on hardened checkout nodes.
- }
- });
- }
- function dispatchPointerMouseClick(el) {
- if (!el) throw new Error('无法点击空元素。');
- el.scrollIntoView?.({ block: 'center', inline: 'nearest' });
- const rect = el.getBoundingClientRect();
- const clientX = Math.max(0, Math.floor(rect.left + rect.width / 2));
- const clientY = Math.max(0, Math.floor(rect.top + rect.height / 2));
- ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
- const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
- el.dispatchEvent(new EventCtor(type, {
- bubbles: true,
- cancelable: true,
- view: window,
- button: 0,
- buttons: type === 'pointerup' || type === 'mouseup' || type === 'click' ? 0 : 1,
- clientX,
- clientY,
- pointerId: 1,
- pointerType: 'mouse',
- isPrimary: true,
- }));
- });
- if (typeof el.click === 'function') {
- el.click();
- }
- }
- function fillBySelector(selector, value) {
- const el = document.querySelector(selector);
- if (el) {
- fillInput(el, value);
- } else {
- log(`未找到元素: ${selector}`, 'warn');
- }
- }
- function fillMissingBySelector(selector, value) {
- const el = document.querySelector(selector);
- if (!el) {
- log(`未找到元素: ${selector}`, 'warn');
- return;
- }
- if (String(el.value || '').trim()) {
- return;
- }
- fillInput(el, value);
- }
- function fillMissingControl(el, value) {
- if (!el) return false;
- if (String(el.value || '').trim()) {
- return false;
- }
- fillInput(el, value);
- return true;
- }
- function fillSelectByText(selector, text) {
- const el = document.querySelector(selector);
- if (!el) {
- log(`未找到 select: ${selector}`, 'warn');
- return;
- }
- for (let i = 0; i < el.options.length; i++) {
- const opt = el.options[i];
- if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
- fillSelect(el, opt.value);
- return;
- }
- }
- log(`未在 select 中找到匹配项: ${text}`, 'warn');
- }
- function fillRegionControlByText(el, text) {
- if (!el) return false;
- if (el.tagName === 'SELECT') {
- for (let i = 0; i < el.options.length; i++) {
- const opt = el.options[i];
- if (opt.text.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
- fillSelect(el, opt.value);
- return true;
- }
- }
- log(`未在地区 select 中找到匹配项: ${text}`, 'warn');
- return false;
- }
- return fillMissingControl(el, text);
- }
- function fillBillingAddressDirectSelectors(addr = {}) {
- fillBySelector('#billingAddressLine1', addr.street);
- fillBySelector('#billingLocality', addr.city);
- fillBySelector('#billingPostalCode', addr.zip.substring(0, 5));
- fillSelectByText('#billingAdministrativeArea', addr.state);
- }
- async function fillBillingAddressLine1FromGoogle(addr = {}) {
- const input = getStructuredAddressFields().address1 || findAddressSearchInput();
- if (!input) {
- log('未找到地址栏 1 输入框,无法触发 Google 地址下拉。', 'warn');
- return false;
- }
- const query = buildGoogleAddressQuery(addr);
- log(`正在地址栏 1 输入完整地址以触发 Google 下拉: ${query}`);
- await typeAddressQueryForAutocomplete(input, query);
- const item = await waitForGoogleAddressSuggestion(query, 6500);
- if (item) {
- const itemText = getSuggestionText(item) || '首个地址建议';
- log(`正在选择 Google 地址建议: ${itemText}`);
- clickAutocompleteSuggestion(item);
- await waitForAddressAutofill(input, query, 2200);
- dispatchInputBlur(input);
- return true;
- }
- const externalFrameSelected = await selectAddressSuggestionInExternalFrame(query);
- if (externalFrameSelected) {
- await waitForAddressAutofill(input, query, 2200);
- dispatchInputBlur(input);
- return true;
- }
- log('未检测到可点击的 Google 地址建议,尝试使用键盘选择首个建议...', 'warn');
- const keyboardSelected = await chooseAutocompleteWithKeyboard(input, query);
- if (keyboardSelected) {
- dispatchInputBlur(input);
- return true;
- }
- return false;
- }
- function findAutoJsPayPalTarget() {
- return document.querySelector('[data-testid="paypal-accordion-item-button"]')
- || document.querySelector('.paypal-accordion-item button');
- }
- async function clickPayPalLikeAutoJs(target) {
- if (!target) return;
- target.scrollIntoView?.({ block: 'center', inline: 'nearest' });
- if (typeof target.click === 'function') {
- target.click();
- }
- await sleep(600);
- }
- async function selectGoogleAddressSuggestionOnly(payload = {}) {
- const query = normalizeText(payload.query || '');
- const item = await waitForGoogleAddressSuggestion(query, 5500);
- if (!item) {
- return { error: '未找到 Google 地址建议项' };
- }
- const itemText = getSuggestionText(item) || '首个地址建议';
- log(`正在独立 autocomplete iframe 中选择 Google 地址建议: ${itemText}`);
- clickAutocompleteSuggestion(item);
- await sleep(900);
- return {
- ok: true,
- selectedAddressText: itemText,
- };
- }
- async function selectAddressSuggestionInExternalFrame(query) {
- if (!chrome?.runtime?.sendMessage) {
- return false;
- }
- try {
- const response = await chrome.runtime.sendMessage({
- type: 'CHECKOUT_STRIPE_SELECT_AUTOCOMPLETE_FRAME',
- source: 'checkout-stripe',
- payload: { query },
- });
- if (response?.ok) {
- log(`已在独立 Google 地址 iframe 中选择建议: ${response.selectedAddressText || '首个地址建议'}`);
- return true;
- }
- if (response?.error) {
- log(`独立 Google 地址 iframe 未完成选择: ${response.error}`, 'warn');
- }
- } catch (error) {
- log(`尝试选择独立 Google 地址 iframe 失败: ${error?.message || error}`, 'warn');
- }
- return false;
- }
- function buildGoogleAddressQuery(addr = {}) {
- const explicitQuery = normalizeText(addr.query || addr.autocompleteQuery || '');
- if (explicitQuery) return explicitQuery;
- const street = normalizeText(addr.street || addr.address1 || '123 Main St');
- const city = normalizeText(addr.city || 'New York');
- const state = normalizeText(addr.state || addr.region || 'New York');
- const zip = normalizeText(addr.zip || addr.postalCode || '10001').substring(0, 5);
- return [street, city, state, zip].filter(Boolean).join(', ');
- }
- async function typeAddressQueryForAutocomplete(input, query) {
- input.scrollIntoView?.({ block: 'center', inline: 'nearest' });
- input.focus();
- await sleep(150);
- setNativeInputValue(input, '');
- input.dispatchEvent(new Event('input', { bubbles: true }));
- input.dispatchEvent(new Event('change', { bubbles: true }));
- await sleep(120);
- for (const char of String(query || '')) {
- throwIfStopped();
- input.dispatchEvent(new KeyboardEvent('keydown', {
- key: char,
- bubbles: true,
- cancelable: true,
- }));
- setNativeInputValue(input, `${input.value || ''}${char}`);
- dispatchAutocompleteInputEvent(input, char);
- input.dispatchEvent(new KeyboardEvent('keyup', {
- key: char,
- bubbles: true,
- cancelable: true,
- }));
- await sleep(18);
- }
- input.dispatchEvent(new Event('change', { bubbles: true }));
- }
- function setNativeInputValue(input, value) {
- const descriptor = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value');
- if (descriptor?.set) {
- descriptor.set.call(input, value);
- } else {
- input.value = value;
- }
- }
- function dispatchAutocompleteInputEvent(input, data) {
- try {
- input.dispatchEvent(new InputEvent('input', {
- bubbles: true,
- cancelable: true,
- data,
- inputType: 'insertText',
- }));
- } catch {
- input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
- }
- }
- function dispatchInputBlur(input) {
- input.dispatchEvent(new Event('change', { bubbles: true }));
- input.dispatchEvent(new Event('blur', { bubbles: true }));
- }
- async function waitForGoogleAddressSuggestion(query, timeoutMs = 5000) {
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- throwIfStopped();
- const items = getVisibleAutocompleteSuggestions(query);
- if (items.length) {
- const matching = findBestAddressSuggestion(items, query);
- return matching || items[0];
- }
- await sleep(250);
- }
- return null;
- }
- function getVisibleAutocompleteSuggestions(query = '') {
- const selectors = [
- { selector: '.pac-container .pac-item', generic: false },
- { selector: '#billing-address-autocomplete-results [role="option"]', generic: false },
- { selector: '.AddressAutocomplete-results [role="option"]', generic: false },
- { selector: '[class*="AddressAutocomplete"] [role="option"]', generic: false },
- { selector: '[data-testid*="address" i] [role="option"]', generic: false },
- { selector: '[role="listbox"] [role="option"]', generic: false },
- { selector: '[role="option"]', generic: false },
- { selector: '[role="listbox"] li', generic: true },
- { selector: '.autocomplete-dropdown [role="option"]', generic: false },
- { selector: '.autocomplete-dropdown li', generic: true },
- { selector: 'li', generic: true },
- ];
- const seen = new Set();
- const items = [];
- for (const config of selectors) {
- document.querySelectorAll(config.selector).forEach((item) => {
- if (seen.has(item) || !isVisibleNode(item)) return;
- if (!isLikelyAddressSuggestion(item, query, config.generic)) return;
- seen.add(item);
- items.push(item);
- });
- }
- return items;
- }
- function findBestAddressSuggestion(items, query) {
- let best = null;
- let bestScore = 0;
- for (const item of items) {
- const score = scoreAddressSuggestion(item, query);
- if (score > bestScore) {
- best = item;
- bestScore = score;
- }
- }
- return best || null;
- }
- function getSuggestionText(item) {
- return normalizeText(item?.textContent || item?.getAttribute?.('aria-label') || '');
- }
- function normalizeText(value = '') {
- return String(value || '').replace(/\s+/g, ' ').trim();
- }
- function getAddressQueryTokens(query = '') {
- return normalizeText(query)
- .toLowerCase()
- .split(/[^a-z0-9]+/i)
- .map((part) => part.trim())
- .filter((part) => part.length >= 3);
- }
- function scoreAddressSuggestion(item, query = '') {
- const text = getSuggestionText(item).toLowerCase();
- if (!text) return 0;
- const tokens = getAddressQueryTokens(query);
- let score = 0;
- tokens.forEach((token) => {
- if (text.includes(token)) score += 1;
- });
- if (/\d/.test(text)) score += 2;
- if (/street|st\.?|avenue|ave\.?|road|rd\.?|drive|dr\.?|boulevard|blvd\.?|lane|ln\.?|way|court|ct\.?/i.test(text)) {
- score += 2;
- }
- return score;
- }
- function isLikelyAddressSuggestion(item, query = '', generic = false) {
- const text = getSuggestionText(item);
- if (!text || text.length < 3) return false;
- if (!generic) return true;
- const lowered = text.toLowerCase();
- if (/terms|privacy|subscribe|paypal|card|payment|email|phone|下一步|提交|付款|订阅/i.test(lowered)) {
- return false;
- }
- return scoreAddressSuggestion(item, query) > 0;
- }
- function isVisibleNode(node) {
- const rect = node.getBoundingClientRect();
- const style = window.getComputedStyle(node);
- return rect.width > 0
- && rect.height > 0
- && style.visibility !== 'hidden'
- && style.display !== 'none';
- }
- function clickAutocompleteSuggestion(item) {
- item.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
- ['pointerdown', 'mouseover', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((type) => {
- const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function' ? PointerEvent : MouseEvent;
- item.dispatchEvent(new EventCtor(type, {
- bubbles: true,
- cancelable: true,
- view: window,
- }));
- });
- if (typeof item.click === 'function') {
- item.click();
- }
- }
- async function chooseAutocompleteWithKeyboard(input, query = '') {
- input.focus();
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true }));
- input.dispatchEvent(new KeyboardEvent('keyup', { key: 'ArrowDown', code: 'ArrowDown', bubbles: true, cancelable: true }));
- await sleep(250);
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
- input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Enter', code: 'Enter', bubbles: true, cancelable: true }));
- return waitForAddressAutofill(input, query, 2200);
- }
- async function waitForAddressAutofill(input, query = '', timeoutMs = 1800) {
- const startedAt = Date.now();
- const originalQuery = normalizeText(query);
- while (Date.now() - startedAt < timeoutMs) {
- throwIfStopped();
- const line1 = normalizeText(input?.value || '');
- const fields = getStructuredAddressFields();
- const city = normalizeText(fields.city?.value || document.querySelector('#billingLocality')?.value || '');
- const zip = normalizeText(fields.postalCode?.value || document.querySelector('#billingPostalCode')?.value || '');
- if (city || zip || (line1 && line1 !== originalQuery)) {
- return true;
- }
- await sleep(200);
- }
- return false;
- }
- function hideAutocompleteDropdowns() {
- log('正在隐藏剩余 Google 地址补全框...');
- document.querySelectorAll([
- '.pac-container',
- '.pac-item',
- 'div[role="listbox"]',
- '.AddressAutocomplete-results',
- '[class*="AddressAutocomplete"]',
- '#billing-address-autocomplete-results',
- ].join(', ')).forEach((el) => {
- el.style.setProperty('display', 'none', 'important');
- el.style.setProperty('visibility', 'hidden', 'important');
- el.style.setProperty('height', '0', 'important');
- el.style.setProperty('overflow', 'hidden', 'important');
- });
- }
- async function clickSubmitButton(retries = 0) {
- throwIfStopped();
- const btn = findSubmitButton();
- if (btn) {
- const rect = btn.getBoundingClientRect();
- if (btn.disabled || rect.height === 0) {
- log('提交按钮被禁用或不可见,等待中...');
- if (retries < 12) {
- await sleep(800);
- return clickSubmitButton(retries + 1);
- }
- throw new Error('提交按钮一直不可用,已超时');
- }
- log(`正在点击提交按钮: ${btn.textContent.trim()}`);
- dispatchPointerMouseClick(btn);
- } else {
- if (retries < 12) {
- log(`未找到提交按钮,重试中... (${retries + 1})`);
- await sleep(800);
- return clickSubmitButton(retries + 1);
- }
- throw new Error('在 Stripe 页面上未找到提交按钮');
- }
- }
- document.documentElement.setAttribute('data-multipage-checkout-stripe-ready', '');
- })();
|