fill-stripe-checkout.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. // background/steps/fill-stripe-checkout.js — Step 7: Fill Stripe checkout form
  2. (function attachBackgroundStep12(root, factory) {
  3. root.MultiPageBackgroundStep12 = factory();
  4. })(typeof self !== 'undefined' ? self : globalThis, function createBackgroundStep12Module() {
  5. const CHECKOUT_STRIPE_SOURCE = 'checkout-stripe';
  6. const CHECKOUT_STRIPE_INJECT_FILES = ['content/activation-utils.js', 'content/utils.js', 'content/checkout-stripe.js'];
  7. const CHECKOUT_URL_PATTERNS = [
  8. 'https://chatgpt.com/checkout/*',
  9. 'https://pay.openai.com/*',
  10. 'https://checkout.stripe.com/*',
  11. ];
  12. const SUBMIT_MAX_ATTEMPTS = 3;
  13. const PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
  14. function createStep12Executor(deps = {}) {
  15. const {
  16. addLog,
  17. chrome,
  18. completeStepFromBackground,
  19. fetchCheckoutAddress,
  20. getTabId,
  21. sendToContentScriptResilient,
  22. } = deps;
  23. async function executeStep12(state) {
  24. await addLog('步骤 7:正在填写 Stripe 结账表单...');
  25. // Fetch a random US address
  26. const addr = await fetchCheckoutAddress();
  27. await addLog(`已获取美国地址: ${addr.street}, ${addr.city}, ${addr.state} ${addr.zip}`);
  28. if (!chrome?.tabs?.sendMessage || !chrome?.webNavigation?.getAllFrames || !chrome?.scripting?.executeScript) {
  29. await addLog('步骤 7:当前环境不支持 checkout iframe 编排,回退到旧版主 frame 填写方式。', 'warn');
  30. await sendToContentScriptResilient(CHECKOUT_STRIPE_SOURCE, {
  31. type: 'EXECUTE_STEP',
  32. step: 7,
  33. source: 'background',
  34. payload: { address: addr },
  35. });
  36. return;
  37. }
  38. const tabId = await resolveCheckoutTabId();
  39. if (!tabId) {
  40. throw new Error('步骤 7:未找到 Stripe/Plus Checkout 标签页,请先完成步骤 6。');
  41. }
  42. await addLog('步骤 7:正在等待 Checkout 页面加载完成...');
  43. await waitForTabComplete(tabId);
  44. await sleep(800);
  45. const readyFrames = await getReadyCheckoutFrames(tabId);
  46. const paymentFrame = await resolvePaymentFrame(tabId, readyFrames);
  47. if (paymentFrame.frameId === null) {
  48. throw new Error(`步骤 7:未在主页面或 iframe 中发现 PayPal 付款方式。frame 摘要:${buildFrameSummary(paymentFrame.inspections)}`);
  49. }
  50. if (paymentFrame.frameId !== 0) {
  51. await addLog(`步骤 7:PayPal 位于 checkout iframe(frameId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
  52. }
  53. await addLog('步骤 7:正在切换 PayPal 付款方式...');
  54. const paymentResult = await sendFrameMessage(tabId, paymentFrame.frameId, {
  55. type: 'CHECKOUT_STRIPE_SELECT_PAYPAL',
  56. source: 'background',
  57. payload: { relaxedActivation: true },
  58. });
  59. if (paymentResult?.error) {
  60. throw new Error(paymentResult.error);
  61. }
  62. const billingFrame = await waitForBillingOrHostedSubmitFrameAfterPaymentSelection(tabId, paymentFrame);
  63. let billingResult = {
  64. countryText: '',
  65. structuredAddress: null,
  66. };
  67. if (billingFrame.frameId !== paymentFrame.frameId) {
  68. await addLog(`步骤 7:账单地址位于 checkout iframe(frameId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
  69. }
  70. await addLog(
  71. billingFrame.autoJsDirectSelectors
  72. ? '步骤 7:未发现独立账单地址 iframe,参考 auto.js 在当前 checkout frame 内直接填写固定账单字段...'
  73. : '步骤 7:正在填写账单地址...'
  74. );
  75. billingResult = await sendFrameMessage(tabId, billingFrame.frameId, {
  76. type: 'CHECKOUT_STRIPE_FILL_BILLING_ADDRESS',
  77. source: 'background',
  78. payload: {
  79. address: addr,
  80. autoJsDirectSelectors: Boolean(billingFrame.autoJsDirectSelectors),
  81. },
  82. });
  83. if (billingResult?.error) {
  84. throw new Error(billingResult.error);
  85. }
  86. let redirectedToPayPal = false;
  87. let lastSubmitError = '';
  88. for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt += 1) {
  89. await addLog(
  90. attempt === 1
  91. ? (billingFrame.autoJsDirectSelectors
  92. ? '步骤 7:账单地址已按 auto.js 方式处理,等待 1.5 秒后提交...'
  93. : '步骤 7:账单地址已填写完成,等待 3 秒让 checkout 完成校验...')
  94. : `步骤 7:准备第 ${attempt}/${SUBMIT_MAX_ATTEMPTS} 次重新提交...`,
  95. attempt === 1 ? 'info' : 'warn'
  96. );
  97. await sleep(billingFrame.autoJsDirectSelectors ? 1500 : 3000);
  98. const submitFrame = await waitForSubmitFrame(tabId, [
  99. { frameId: 0, url: '' },
  100. { frameId: paymentFrame.frameId, url: paymentFrame.frameUrl || '' },
  101. { frameId: billingFrame.frameId, url: billingFrame.frameUrl || '' },
  102. ]);
  103. const submitResult = await sendFrameMessage(tabId, submitFrame.frameId, {
  104. type: 'CHECKOUT_STRIPE_CLICK_SUBMIT',
  105. source: 'background',
  106. payload: {
  107. beforeClickDelayMs: attempt === 1 ? 700 : 1200,
  108. },
  109. });
  110. if (submitResult?.error) {
  111. lastSubmitError = submitResult.error;
  112. await addLog(`步骤 7:点击订阅失败(${attempt}/${SUBMIT_MAX_ATTEMPTS}):${lastSubmitError}`, 'warn');
  113. continue;
  114. }
  115. await addLog(`步骤 7:已提交 checkout,正在等待跳转到 PayPal(${attempt}/${SUBMIT_MAX_ATTEMPTS})...`, 'info');
  116. redirectedToPayPal = await waitForPayPalRedirectAfterSubmit(tabId);
  117. if (redirectedToPayPal) break;
  118. lastSubmitError = `提交后 ${Math.round(PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 PayPal`;
  119. await addLog(`步骤 7:${lastSubmitError},将重试提交。`, 'warn');
  120. }
  121. if (!redirectedToPayPal) {
  122. throw new Error(`步骤 7:多次提交 checkout 后仍未跳转到 PayPal。${lastSubmitError}`);
  123. }
  124. if (typeof completeStepFromBackground === 'function') {
  125. await completeStepFromBackground(7, {
  126. plusBillingCountryText: billingResult?.countryText || '',
  127. plusBillingAddress: billingResult?.structuredAddress || null,
  128. });
  129. }
  130. }
  131. async function resolveCheckoutTabId() {
  132. const registered = await getTabId(CHECKOUT_STRIPE_SOURCE);
  133. if (registered && await isCheckoutTab(registered)) {
  134. return registered;
  135. }
  136. const signupTab = await getTabId('signup-page');
  137. if (signupTab && await isCheckoutTab(signupTab)) {
  138. return signupTab;
  139. }
  140. const tabs = await chrome.tabs.query({ url: CHECKOUT_URL_PATTERNS }).catch(() => []);
  141. const tab = tabs.find((item) => Number.isInteger(item?.id) && isCheckoutUrl(item.url));
  142. return tab?.id || null;
  143. }
  144. async function isCheckoutTab(tabId) {
  145. const tab = await chrome.tabs.get(tabId).catch(() => null);
  146. return Boolean(tab && isCheckoutUrl(tab.url));
  147. }
  148. function isCheckoutUrl(url = '') {
  149. return /^https:\/\/(?:chatgpt\.com\/checkout|pay\.openai\.com|checkout\.stripe\.com)(?:\/|$)/i.test(String(url || ''));
  150. }
  151. async function waitForTabComplete(tabId, timeoutMs = 30000) {
  152. const startedAt = Date.now();
  153. while (Date.now() - startedAt < timeoutMs) {
  154. const tab = await chrome.tabs.get(tabId).catch(() => null);
  155. if (!tab) throw new Error('步骤 7:Checkout 标签页已关闭。');
  156. if (tab.status === 'complete') return tab;
  157. await sleep(300);
  158. }
  159. return chrome.tabs.get(tabId).catch(() => null);
  160. }
  161. async function getCheckoutFrames(tabId) {
  162. const frames = await chrome.webNavigation.getAllFrames({ tabId }).catch(() => null);
  163. if (!Array.isArray(frames) || !frames.length) {
  164. return [{ frameId: 0, url: '' }];
  165. }
  166. return frames
  167. .filter((frame) => Number.isInteger(frame?.frameId))
  168. .sort((left, right) => Number(left.frameId) - Number(right.frameId));
  169. }
  170. async function pingCheckoutFrame(tabId, frameId) {
  171. try {
  172. const pong = await chrome.tabs.sendMessage(tabId, {
  173. type: 'PING',
  174. source: 'background',
  175. payload: {},
  176. }, {
  177. frameId: Number.isInteger(frameId) ? frameId : 0,
  178. });
  179. return Boolean(pong?.ok && (!pong.source || pong.source === CHECKOUT_STRIPE_SOURCE));
  180. } catch {
  181. return false;
  182. }
  183. }
  184. async function ensureCheckoutFrameReady(tabId, frameId) {
  185. if (await pingCheckoutFrame(tabId, frameId)) {
  186. return true;
  187. }
  188. try {
  189. await chrome.scripting.executeScript({
  190. target: { tabId, frameIds: [frameId] },
  191. func: (injectedSource) => {
  192. window.__MULTIPAGE_SOURCE = injectedSource;
  193. },
  194. args: [CHECKOUT_STRIPE_SOURCE],
  195. });
  196. await chrome.scripting.executeScript({
  197. target: { tabId, frameIds: [frameId] },
  198. files: CHECKOUT_STRIPE_INJECT_FILES,
  199. });
  200. } catch {
  201. // Some Stripe helper frames are intentionally not scriptable. They are skipped below.
  202. }
  203. await sleep(500);
  204. return pingCheckoutFrame(tabId, frameId);
  205. }
  206. async function getReadyCheckoutFrames(tabId) {
  207. const frames = await getCheckoutFrames(tabId);
  208. const readyFrames = [];
  209. for (const frame of frames) {
  210. const ready = await ensureCheckoutFrameReady(tabId, frame.frameId);
  211. readyFrames.push({ ...frame, ready });
  212. }
  213. return readyFrames;
  214. }
  215. async function sendFrameMessage(tabId, frameId, message) {
  216. return chrome.tabs.sendMessage(tabId, message, {
  217. frameId: Number.isInteger(frameId) ? frameId : 0,
  218. });
  219. }
  220. async function inspectCheckoutFrame(tabId, frame) {
  221. if (frame.ready === false) {
  222. return { frame, error: 'content-script-not-ready' };
  223. }
  224. try {
  225. const result = await sendFrameMessage(tabId, frame.frameId, {
  226. type: 'CHECKOUT_STRIPE_GET_STATE',
  227. source: 'background',
  228. payload: {},
  229. });
  230. return { frame, result: result || {} };
  231. } catch (error) {
  232. return { frame, error: error?.message || String(error || '') };
  233. }
  234. }
  235. async function inspectCheckoutFrames(tabId, frames) {
  236. const inspections = [];
  237. for (const frame of frames) {
  238. inspections.push(await inspectCheckoutFrame(tabId, frame));
  239. }
  240. return inspections;
  241. }
  242. async function resolvePaymentFrame(tabId, frames) {
  243. const inspections = await inspectCheckoutFrames(tabId, frames);
  244. const picked = pickPaymentFrame(inspections);
  245. if (!picked) {
  246. return { frameId: null, frameUrl: '', inspections };
  247. }
  248. return {
  249. frameId: picked.frame.frameId,
  250. frameUrl: picked.frame.url || '',
  251. inspections,
  252. };
  253. }
  254. function isPaymentFrameUrl(url = '') {
  255. return /elements-inner-payment|componentName=payment/i.test(String(url || ''));
  256. }
  257. function isAddressFrameUrl(url = '') {
  258. return /elements-inner-address|componentName=address/i.test(String(url || ''));
  259. }
  260. function pickPaymentFrame(inspections = []) {
  261. return inspections.find((item) => item.result?.hasPayPal || item.result?.paypalCandidates?.length)
  262. || inspections.find((item) => isPaymentFrameUrl(item.frame.url))
  263. || null;
  264. }
  265. function pickBillingFrame(inspections = []) {
  266. return inspections.find((item) => item.result?.billingFieldsVisible)
  267. || inspections.find((item) => isAddressFrameUrl(item.frame.url))
  268. || null;
  269. }
  270. function pickHostedPayPalSubmitFrame(inspections = []) {
  271. return inspections.find((item) => item.result?.hasPayPal && item.result?.hasSubmitButton)
  272. || null;
  273. }
  274. async function getHostedPayPalSubmitFrame(tabId) {
  275. const frames = await getReadyCheckoutFrames(tabId);
  276. const inspections = await inspectCheckoutFrames(tabId, frames);
  277. const picked = pickHostedPayPalSubmitFrame(inspections);
  278. if (!picked) {
  279. return null;
  280. }
  281. return {
  282. frameId: picked.frame.frameId,
  283. frameUrl: picked.frame.url || '',
  284. inspections,
  285. skipBillingAddress: true,
  286. };
  287. }
  288. async function waitForBillingOrHostedSubmitFrameAfterPaymentSelection(tabId, paymentFrame) {
  289. try {
  290. return await waitForBillingFrame(tabId, 6500);
  291. } catch (firstError) {
  292. const hostedSubmitFrame = await getHostedPayPalSubmitFrame(tabId);
  293. if (hostedSubmitFrame) {
  294. await addLog(`步骤 7:未等到账单地址 iframe,但当前 frame 已有 PayPal 与提交按钮,将参考 auto.js 直接填写当前页面字段。${firstError.message}`, 'warn');
  295. return hostedSubmitFrame;
  296. }
  297. await addLog(`步骤 7:首次等待账单地址未出现,准备按 auto.js 方式重新点击 PayPal。${firstError.message}`, 'warn');
  298. }
  299. const latestFrames = await getReadyCheckoutFrames(tabId);
  300. const latestPaymentFrame = await resolvePaymentFrame(tabId, latestFrames);
  301. const retryFrameId = latestPaymentFrame.frameId ?? paymentFrame.frameId;
  302. if (Number.isInteger(retryFrameId)) {
  303. const retryResult = await sendFrameMessage(tabId, retryFrameId, {
  304. type: 'CHECKOUT_STRIPE_SELECT_PAYPAL',
  305. source: 'background',
  306. payload: { relaxedActivation: true },
  307. });
  308. if (retryResult?.error) {
  309. await addLog(`步骤 7:再次点击 PayPal 失败:${retryResult.error}`, 'warn');
  310. } else {
  311. await addLog('步骤 7:已再次点击 PayPal,继续检测账单地址或 hosted checkout 提交区...', 'info');
  312. }
  313. }
  314. try {
  315. return await waitForBillingFrame(tabId, 6500);
  316. } catch (secondError) {
  317. const hostedSubmitFrame = await getHostedPayPalSubmitFrame(tabId);
  318. if (hostedSubmitFrame) {
  319. await addLog(`步骤 7:二次等待账单地址仍未出现,将参考 auto.js 在当前页面直接填固定账单字段后提交。${secondError.message}`, 'warn');
  320. return hostedSubmitFrame;
  321. }
  322. throw secondError;
  323. }
  324. }
  325. async function waitForBillingFrame(tabId, timeoutMs = 30000) {
  326. const startedAt = Date.now();
  327. let lastInspections = [];
  328. while (Date.now() - startedAt < timeoutMs) {
  329. const frames = await getReadyCheckoutFrames(tabId);
  330. const inspections = await inspectCheckoutFrames(tabId, frames);
  331. lastInspections = inspections;
  332. const picked = pickBillingFrame(inspections);
  333. if (picked) {
  334. return {
  335. frameId: picked.frame.frameId,
  336. frameUrl: picked.frame.url || '',
  337. inspections,
  338. };
  339. }
  340. await sleep(250);
  341. }
  342. throw new Error(`步骤 7:等待账单地址 iframe 超时。frame 摘要:${buildFrameSummary(lastInspections) || '无可检测 frame'}`);
  343. }
  344. async function waitForSubmitFrame(tabId, candidateFrames = [], timeoutMs = 15000) {
  345. const startedAt = Date.now();
  346. while (Date.now() - startedAt < timeoutMs) {
  347. const frameMap = new Map();
  348. for (const item of candidateFrames) {
  349. if (Number.isInteger(item?.frameId)) frameMap.set(item.frameId, item);
  350. }
  351. const readyFrames = await getReadyCheckoutFrames(tabId);
  352. for (const frame of readyFrames) {
  353. if (!frameMap.has(frame.frameId)) frameMap.set(frame.frameId, frame);
  354. }
  355. const inspections = await inspectCheckoutFrames(tabId, Array.from(frameMap.values()));
  356. const picked = inspections.find((item) => item.result?.hasSubmitButton);
  357. if (picked) {
  358. return picked.frame;
  359. }
  360. await sleep(250);
  361. }
  362. throw new Error('步骤 7:等待订阅按钮超时。');
  363. }
  364. async function waitForPayPalRedirectAfterSubmit(tabId) {
  365. const startedAt = Date.now();
  366. while (Date.now() - startedAt < PAYPAL_REDIRECT_TIMEOUT_MS) {
  367. const tab = await chrome.tabs.get(tabId).catch(() => null);
  368. if (!tab) throw new Error('步骤 7:Checkout 标签页已关闭,无法继续等待 PayPal 跳转。');
  369. const url = String(tab.url || '');
  370. if (/paypal\./i.test(url) && !isCheckoutUrl(url)) {
  371. await waitForTabComplete(tabId, 15000);
  372. await sleep(1000);
  373. return true;
  374. }
  375. if (url && !isCheckoutUrl(url)) {
  376. await addLog(`步骤 7:点击订阅后页面跳转到非 PayPal 识别地址:${url}`, 'warn');
  377. return false;
  378. }
  379. await sleep(500);
  380. }
  381. return false;
  382. }
  383. function buildFrameSummary(inspections = []) {
  384. return inspections
  385. .map((item) => {
  386. const flags = [];
  387. if (item.result?.hasPayPal) flags.push('paypal');
  388. if (item.result?.billingFieldsVisible) flags.push('billing');
  389. if (item.result?.hasSubmitButton) flags.push('submit');
  390. if (!flags.length && item.error) flags.push(item.error);
  391. if (!flags.length) flags.push('no-match');
  392. return `${item.frame.frameId}:${item.frame.url || 'about:blank'}:${flags.join(',')}`;
  393. })
  394. .slice(0, 8)
  395. .join(' | ');
  396. }
  397. return { executeStep12 };
  398. }
  399. function sleep(ms) {
  400. return new Promise((resolve) => setTimeout(resolve, ms));
  401. }
  402. return { createStep12Executor };
  403. });