markpdfdown 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4358,10 +4358,58 @@ class AuthManager {
4358
4358
  }
4359
4359
  }
4360
4360
  const authManager = AuthManager.getInstance();
4361
+ const PAYMENT_STATUSES = /* @__PURE__ */ new Set(["pending", "completed", "failed", "refunded"]);
4362
+ const PAYMENT_PROVIDER_STATUSES = /* @__PURE__ */ new Set([
4363
+ "pending",
4364
+ "processing",
4365
+ "completed",
4366
+ "failed",
4367
+ "canceled",
4368
+ "expired",
4369
+ "refunded",
4370
+ "unknown"
4371
+ ]);
4361
4372
  class CloudService {
4362
4373
  static instance;
4363
4374
  constructor() {
4364
4375
  }
4376
+ normalizeCheckoutStatus(data) {
4377
+ if (!data || typeof data !== "object") {
4378
+ return null;
4379
+ }
4380
+ const sessionId = typeof data.session_id === "string" ? data.session_id.trim() : "";
4381
+ const status = typeof data.status === "string" ? data.status : "";
4382
+ const providerStatus = typeof data.provider_status === "string" ? data.provider_status : "";
4383
+ const amountUsd = Number(data.amount_usd);
4384
+ const creditsAdded = Number(data.credits_added);
4385
+ const createdAt = typeof data.created_at === "string" ? data.created_at : "";
4386
+ if (!sessionId || !PAYMENT_STATUSES.has(status)) {
4387
+ return null;
4388
+ }
4389
+ if (!providerStatus || !PAYMENT_PROVIDER_STATUSES.has(providerStatus)) {
4390
+ return null;
4391
+ }
4392
+ if (!Number.isFinite(amountUsd) || !Number.isFinite(creditsAdded)) {
4393
+ return null;
4394
+ }
4395
+ if (typeof data.is_final !== "boolean" || typeof data.changed !== "boolean") {
4396
+ return null;
4397
+ }
4398
+ if (!createdAt) {
4399
+ return null;
4400
+ }
4401
+ return {
4402
+ session_id: sessionId,
4403
+ order_id: typeof data.order_id === "string" ? data.order_id : void 0,
4404
+ status,
4405
+ provider_status: providerStatus,
4406
+ is_final: data.is_final,
4407
+ changed: data.changed,
4408
+ amount_usd: amountUsd,
4409
+ credits_added: creditsAdded,
4410
+ created_at: createdAt
4411
+ };
4412
+ }
4365
4413
  static getInstance() {
4366
4414
  if (!CloudService.instance) {
4367
4415
  CloudService.instance = new CloudService();
@@ -4688,6 +4736,116 @@ class CloudService {
4688
4736
  };
4689
4737
  }
4690
4738
  }
4739
+ /**
4740
+ * Create payment checkout session
4741
+ */
4742
+ async createCheckout(amountUsd) {
4743
+ try {
4744
+ if (!Number.isFinite(amountUsd) || amountUsd <= 0) {
4745
+ return { success: false, error: "Invalid top-up amount" };
4746
+ }
4747
+ const res = await authManager.fetchWithAuth(`${API_BASE_URL}/api/v1/payment/checkout`, {
4748
+ method: "POST",
4749
+ headers: { "Content-Type": "application/json" },
4750
+ body: JSON.stringify({ amount_usd: amountUsd })
4751
+ });
4752
+ const responseJson = await res.json().catch(() => null);
4753
+ if (!res.ok || !responseJson?.success) {
4754
+ const serverMessage = responseJson?.error?.message;
4755
+ const allowedAmounts = responseJson?.error?.details?.allowed_amounts_usd;
4756
+ const allowedSuffix = Array.isArray(allowedAmounts) && allowedAmounts.length > 0 ? ` (allowed: ${allowedAmounts.join(", ")})` : "";
4757
+ return {
4758
+ success: false,
4759
+ error: serverMessage ? `${serverMessage}${allowedSuffix}` : `Failed to create checkout session: ${res.status}`
4760
+ };
4761
+ }
4762
+ if (!responseJson.data?.checkout_url || !responseJson.data?.session_id) {
4763
+ return { success: false, error: "Invalid checkout response" };
4764
+ }
4765
+ return { success: true, data: responseJson.data };
4766
+ } catch (error) {
4767
+ console.error("[CloudService] createCheckout error:", error);
4768
+ return {
4769
+ success: false,
4770
+ error: error instanceof Error ? error.message : String(error)
4771
+ };
4772
+ }
4773
+ }
4774
+ /**
4775
+ * Query checkout order status by session_id (supports long polling)
4776
+ */
4777
+ async getCheckoutStatus(sessionId, waitSeconds = 10) {
4778
+ try {
4779
+ const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
4780
+ if (!normalizedSessionId) {
4781
+ return { success: false, error: "Invalid checkout session id" };
4782
+ }
4783
+ if (!Number.isFinite(waitSeconds)) {
4784
+ return { success: false, error: "Invalid wait_seconds" };
4785
+ }
4786
+ const normalizedWaitSeconds = Math.min(30, Math.max(0, Math.floor(waitSeconds)));
4787
+ const params = new URLSearchParams({
4788
+ wait_seconds: String(normalizedWaitSeconds)
4789
+ });
4790
+ const requestTimeoutMs = Math.max((normalizedWaitSeconds + 20) * 1e3, 3e4);
4791
+ const res = await authManager.fetchWithAuth(
4792
+ `${API_BASE_URL}/api/v1/payment/checkout/${encodeURIComponent(normalizedSessionId)}/status?${params.toString()}`,
4793
+ {},
4794
+ { timeoutMs: requestTimeoutMs }
4795
+ );
4796
+ const responseJson = await res.json().catch(() => null);
4797
+ if (!res.ok || !responseJson?.success) {
4798
+ return {
4799
+ success: false,
4800
+ error: responseJson?.error?.message || `Failed to query checkout status: ${res.status}`
4801
+ };
4802
+ }
4803
+ const data = this.normalizeCheckoutStatus(responseJson.data);
4804
+ if (!data) {
4805
+ return { success: false, error: "Invalid checkout status response" };
4806
+ }
4807
+ return { success: true, data };
4808
+ } catch (error) {
4809
+ console.error("[CloudService] getCheckoutStatus error:", error);
4810
+ return {
4811
+ success: false,
4812
+ error: error instanceof Error ? error.message : String(error)
4813
+ };
4814
+ }
4815
+ }
4816
+ /**
4817
+ * Trigger proactive reconciliation for a checkout session
4818
+ */
4819
+ async reconcileCheckout(sessionId) {
4820
+ try {
4821
+ const normalizedSessionId = typeof sessionId === "string" ? sessionId.trim() : "";
4822
+ if (!normalizedSessionId) {
4823
+ return { success: false, error: "Invalid checkout session id" };
4824
+ }
4825
+ const res = await authManager.fetchWithAuth(
4826
+ `${API_BASE_URL}/api/v1/payment/checkout/${encodeURIComponent(normalizedSessionId)}/reconcile`,
4827
+ { method: "POST" }
4828
+ );
4829
+ const responseJson = await res.json().catch(() => null);
4830
+ if (!res.ok || !responseJson?.success) {
4831
+ return {
4832
+ success: false,
4833
+ error: responseJson?.error?.message || `Failed to reconcile checkout: ${res.status}`
4834
+ };
4835
+ }
4836
+ const data = this.normalizeCheckoutStatus(responseJson.data);
4837
+ if (!data) {
4838
+ return { success: false, error: "Invalid checkout reconcile response" };
4839
+ }
4840
+ return { success: true, data };
4841
+ } catch (error) {
4842
+ console.error("[CloudService] reconcileCheckout error:", error);
4843
+ return {
4844
+ success: false,
4845
+ error: error instanceof Error ? error.message : String(error)
4846
+ };
4847
+ }
4848
+ }
4691
4849
  /**
4692
4850
  * Get credits info from the cloud API
4693
4851
  */
@@ -4753,6 +4911,42 @@ class CloudService {
4753
4911
  };
4754
4912
  }
4755
4913
  }
4914
+ /**
4915
+ * Get payment history from the cloud API
4916
+ */
4917
+ async getPaymentHistory(page = 1, pageSize = 20) {
4918
+ try {
4919
+ const params = new URLSearchParams({
4920
+ page: String(page),
4921
+ page_size: String(pageSize)
4922
+ });
4923
+ const res = await authManager.fetchWithAuth(
4924
+ `${API_BASE_URL}/api/v1/payment/history?${params.toString()}`
4925
+ );
4926
+ if (!res.ok) {
4927
+ const errorBody = await res.json().catch(() => null);
4928
+ return {
4929
+ success: false,
4930
+ error: errorBody?.error?.message || `Failed to fetch payment history: ${res.status}`
4931
+ };
4932
+ }
4933
+ const responseJson = await res.json();
4934
+ if (!responseJson.success) {
4935
+ return { success: false, error: responseJson.error?.message || "Invalid payment history response" };
4936
+ }
4937
+ return {
4938
+ success: true,
4939
+ data: responseJson.data,
4940
+ pagination: responseJson.pagination
4941
+ };
4942
+ } catch (error) {
4943
+ console.error("[CloudService] getPaymentHistory error:", error);
4944
+ return {
4945
+ success: false,
4946
+ error: error instanceof Error ? error.message : String(error)
4947
+ };
4948
+ }
4949
+ }
4756
4950
  /**
4757
4951
  * Delete a cloud task (only terminal states can be deleted)
4758
4952
  * Terminal states: FAILED=0, COMPLETED=6, CANCELLED=7, PARTIAL_FAILED=8
@@ -5200,6 +5394,54 @@ function registerCloudHandlers() {
5200
5394
  };
5201
5395
  }
5202
5396
  });
5397
+ ipcMain.handle("cloud:createCheckout", async (_, params) => {
5398
+ try {
5399
+ if (!params || typeof params.amountUsd !== "number" || !Number.isFinite(params.amountUsd) || params.amountUsd <= 0) {
5400
+ return { success: false, error: "Invalid amountUsd" };
5401
+ }
5402
+ return await cloudService.createCheckout(params.amountUsd);
5403
+ } catch (error) {
5404
+ console.error("[IPC] cloud:createCheckout error:", error);
5405
+ return {
5406
+ success: false,
5407
+ error: error instanceof Error ? error.message : String(error)
5408
+ };
5409
+ }
5410
+ });
5411
+ ipcMain.handle("cloud:getCheckoutStatus", async (_, params) => {
5412
+ try {
5413
+ const sessionId = typeof params?.sessionId === "string" ? params.sessionId.trim() : "";
5414
+ if (!sessionId) {
5415
+ return { success: false, error: "Invalid sessionId" };
5416
+ }
5417
+ const waitSeconds = params?.waitSeconds ?? 10;
5418
+ if (typeof waitSeconds !== "number" || !Number.isFinite(waitSeconds)) {
5419
+ return { success: false, error: "Invalid waitSeconds" };
5420
+ }
5421
+ return await cloudService.getCheckoutStatus(sessionId, waitSeconds);
5422
+ } catch (error) {
5423
+ console.error("[IPC] cloud:getCheckoutStatus error:", error);
5424
+ return {
5425
+ success: false,
5426
+ error: error instanceof Error ? error.message : String(error)
5427
+ };
5428
+ }
5429
+ });
5430
+ ipcMain.handle("cloud:reconcileCheckout", async (_, params) => {
5431
+ try {
5432
+ const sessionId = typeof params?.sessionId === "string" ? params.sessionId.trim() : "";
5433
+ if (!sessionId) {
5434
+ return { success: false, error: "Invalid sessionId" };
5435
+ }
5436
+ return await cloudService.reconcileCheckout(sessionId);
5437
+ } catch (error) {
5438
+ console.error("[IPC] cloud:reconcileCheckout error:", error);
5439
+ return {
5440
+ success: false,
5441
+ error: error instanceof Error ? error.message : String(error)
5442
+ };
5443
+ }
5444
+ });
5203
5445
  ipcMain.handle("cloud:getCredits", async () => {
5204
5446
  try {
5205
5447
  return await cloudService.getCredits();
@@ -5222,6 +5464,23 @@ function registerCloudHandlers() {
5222
5464
  };
5223
5465
  }
5224
5466
  });
5467
+ ipcMain.handle("cloud:getPaymentHistory", async (_, params) => {
5468
+ try {
5469
+ if (!params || typeof params.page !== "number" || !Number.isFinite(params.page) || params.page < 1) {
5470
+ return { success: false, error: "Invalid page" };
5471
+ }
5472
+ if (!params || typeof params.pageSize !== "number" || !Number.isFinite(params.pageSize) || params.pageSize < 1) {
5473
+ return { success: false, error: "Invalid pageSize" };
5474
+ }
5475
+ return await cloudService.getPaymentHistory(params.page, params.pageSize);
5476
+ } catch (error) {
5477
+ console.error("[IPC] cloud:getPaymentHistory error:", error);
5478
+ return {
5479
+ success: false,
5480
+ error: error instanceof Error ? error.message : String(error)
5481
+ };
5482
+ }
5483
+ });
5225
5484
  ipcMain.handle("cloud:sseConnect", async () => {
5226
5485
  try {
5227
5486
  await cloudSSEManager.connect();
@@ -5494,6 +5753,29 @@ function getIconPath() {
5494
5753
  return possiblePaths[0];
5495
5754
  }
5496
5755
  const PROTOCOL_NAME = "markpdfdown";
5756
+ const PAYMENT_CALLBACK_EVENT = "payment:callback";
5757
+ let pendingPaymentCallback = null;
5758
+ function toNullableNumber(value) {
5759
+ if (value === null || value.trim() === "") {
5760
+ return null;
5761
+ }
5762
+ const parsed = Number(value);
5763
+ return Number.isFinite(parsed) ? parsed : null;
5764
+ }
5765
+ function dispatchPaymentCallback(payload) {
5766
+ if (!mainWindow) {
5767
+ pendingPaymentCallback = payload;
5768
+ return;
5769
+ }
5770
+ const sendPayload = () => {
5771
+ mainWindow?.webContents.send(PAYMENT_CALLBACK_EVENT, payload);
5772
+ };
5773
+ if (mainWindow.webContents.isLoadingMainFrame()) {
5774
+ mainWindow.webContents.once("did-finish-load", sendPayload);
5775
+ return;
5776
+ }
5777
+ sendPayload();
5778
+ }
5497
5779
  protocol.registerSchemesAsPrivileged([
5498
5780
  {
5499
5781
  scheme: "local-file",
@@ -5518,15 +5800,18 @@ function handleProtocolUrl(url) {
5518
5800
  console.warn("[Main] Ignoring URL with unexpected scheme");
5519
5801
  return;
5520
5802
  }
5803
+ let parsed;
5804
+ let host;
5805
+ let pathname;
5521
5806
  try {
5522
- const parsed = new URL(url);
5523
- const host = parsed.host.toLowerCase();
5524
- const pathname = parsed.pathname.replace(/\/+/g, "/").replace(/\/+$/, "");
5807
+ parsed = new URL(url);
5808
+ host = parsed.host.toLowerCase();
5809
+ pathname = parsed.pathname.replace(/\/+/g, "/").replace(/\/+$/, "");
5525
5810
  if (parsed.host.includes("%")) {
5526
5811
  console.warn("[Main] Ignoring protocol URL with encoded host");
5527
5812
  return;
5528
5813
  }
5529
- const isAllowed = host === "auth" && pathname === "/callback" || host === "auth" && pathname === "";
5814
+ const isAllowed = host === "auth" && pathname === "/callback" || host === "auth" && pathname === "" || host === "payment" && pathname === "/callback";
5530
5815
  if (!isAllowed) {
5531
5816
  console.warn(`[Main] Ignoring protocol URL with unexpected path: ${host}${pathname}`);
5532
5817
  return;
@@ -5541,7 +5826,23 @@ function handleProtocolUrl(url) {
5541
5826
  }
5542
5827
  mainWindow.focus();
5543
5828
  }
5544
- authManager.checkDeviceTokenStatus();
5829
+ if (host === "auth") {
5830
+ authManager.checkDeviceTokenStatus();
5831
+ return;
5832
+ }
5833
+ if (host === "payment" && pathname === "/callback") {
5834
+ const payload = {
5835
+ url,
5836
+ status: parsed.searchParams.get("status"),
5837
+ sessionId: parsed.searchParams.get("session_id"),
5838
+ amountUsd: toNullableNumber(parsed.searchParams.get("amount_usd")),
5839
+ creditsToAdd: toNullableNumber(parsed.searchParams.get("credits_to_add")),
5840
+ query: Object.fromEntries(parsed.searchParams.entries()),
5841
+ receivedAt: (/* @__PURE__ */ new Date()).toISOString()
5842
+ };
5843
+ console.log("[Main] Payment callback received:", payload.status || "unknown");
5844
+ dispatchPaymentCallback(payload);
5845
+ }
5545
5846
  }
5546
5847
  app.on("open-url", (event, url) => {
5547
5848
  event.preventDefault();
@@ -5688,6 +5989,10 @@ function createWindow() {
5688
5989
  mainWindow = null;
5689
5990
  windowManager.setMainWindow(null);
5690
5991
  });
5992
+ if (pendingPaymentCallback) {
5993
+ dispatchPaymentCallback(pendingPaymentCallback);
5994
+ pendingPaymentCallback = null;
5995
+ }
5691
5996
  }
5692
5997
  app.whenReady().then(async () => {
5693
5998
  try {
@@ -68,8 +68,12 @@ electron.contextBridge.exposeInMainWorld("api", {
68
68
  getTaskResult: (id) => electron.ipcRenderer.invoke("cloud:getTaskResult", id),
69
69
  downloadPdf: (id) => electron.ipcRenderer.invoke("cloud:downloadPdf", id),
70
70
  getPageImage: (params) => electron.ipcRenderer.invoke("cloud:getPageImage", params),
71
+ createCheckout: (params) => electron.ipcRenderer.invoke("cloud:createCheckout", params),
72
+ getCheckoutStatus: (params) => electron.ipcRenderer.invoke("cloud:getCheckoutStatus", params),
73
+ reconcileCheckout: (params) => electron.ipcRenderer.invoke("cloud:reconcileCheckout", params),
71
74
  getCredits: () => electron.ipcRenderer.invoke("cloud:getCredits"),
72
75
  getCreditHistory: (params) => electron.ipcRenderer.invoke("cloud:getCreditHistory", params),
76
+ getPaymentHistory: (params) => electron.ipcRenderer.invoke("cloud:getPaymentHistory", params),
73
77
  sseConnect: () => electron.ipcRenderer.invoke("cloud:sseConnect"),
74
78
  sseDisconnect: () => electron.ipcRenderer.invoke("cloud:sseDisconnect"),
75
79
  sseResetAndDisconnect: () => electron.ipcRenderer.invoke("cloud:sseResetAndDisconnect")
@@ -150,6 +154,18 @@ electron.contextBridge.exposeInMainWorld("api", {
150
154
  return () => {
151
155
  electron.ipcRenderer.removeListener("cloud:taskEvent", handler);
152
156
  };
157
+ },
158
+ /**
159
+ * 监听支付回跳事件
160
+ * @param callback 事件回调函数
161
+ * @returns 清理函数
162
+ */
163
+ onPaymentCallback: (callback) => {
164
+ const handler = (_event, data) => callback(data);
165
+ electron.ipcRenderer.on("payment:callback", handler);
166
+ return () => {
167
+ electron.ipcRenderer.removeListener("payment:callback", handler);
168
+ };
153
169
  }
154
170
  },
155
171
  // ==================== Platform APIs ====================