@itpay/cli 0.2.1 → 0.2.3

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.
package/README.md CHANGED
@@ -158,6 +158,19 @@ Before starting a new purchase, agents should inspect recoverable local state:
158
158
  itp status --refresh --json
159
159
  ```
160
160
 
161
+ Humans can use the default account overview:
162
+
163
+ ```bash
164
+ itp status
165
+ ```
166
+
167
+ ```text
168
+ Account: buyer_7xK2mP9vQ4
169
+ Linked: alipay, wechat
170
+ Orders: 12
171
+ Device: Codex on MacBook-Pro (active)
172
+ ```
173
+
161
174
  If an unfinished run exists, continue it:
162
175
 
163
176
  ```bash
@@ -208,7 +221,7 @@ itp buyer checkout status <checkout_id> --json
208
221
  For the one-command buyer helper:
209
222
 
210
223
  ```bash
211
- itp buy var_itpay_enterprise_fuzzy_search_cny01 --email buyer@example.com --input company_name=阿里 --json
224
+ itp buy var_itpay_enterprise_fuzzy_search_cny01 --email buyer@example.com --input company_name=阿里 --display agent --no-wait-payment --json
212
225
  ```
213
226
 
214
227
  For multi-item cart tests:
@@ -226,6 +239,7 @@ Payment QR rules:
226
239
  - Otherwise render the ItPay-hosted `qr_png_url` / `preferred_qr_url`.
227
240
  - Use `mobile_wallet_url` only as a human mobile fallback.
228
241
  - Do not generate your own QR from payment URLs.
242
+ - In agent app clients, send `payment_handoff.markdown` to the human first, then run `payment_handoff.wait_command`.
229
243
  - Treat only `payment_intent.verified` as payment success.
230
244
 
231
245
  If the human wants the agent to analyze delivered content, the human must reveal
package/bin/itp CHANGED
@@ -93,7 +93,7 @@ async function main() {
93
93
  output({
94
94
  version: VERSION,
95
95
  commands: [
96
- "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json",
96
+ "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --display agent --no-wait-payment --json",
97
97
  "buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --no-wait --json",
98
98
  "buyer catalog search --query 企业工商 --category business_data_api --provider itpay_enterprise_data --json",
99
99
  "buyer catalog get --variant var_pubg_couple_skin_cny20 --json",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  {
70
70
  "intent": "do both steps through the high-level command",
71
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --json",
71
+ "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
72
72
  "success_signal": "response.status is waiting_human_auth, waiting_user_payment, payment_verified, or delivery_claimable"
73
73
  }
74
74
  ],
@@ -17,8 +17,8 @@
17
17
  "commands": [
18
18
  {
19
19
  "intent": "create payment intent through the high-level buy flow",
20
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --json",
21
- "success_signal": "response.payment_intent.human_action.local_qr_path or response.payment_intent.human_action.qr_png_url is present"
20
+ "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
21
+ "success_signal": "response.payment_handoff.markdown and response.payment_handoff.wait_command are present"
22
22
  },
23
23
  {
24
24
  "intent": "recover a scanner order-not-found display problem",
@@ -33,6 +33,8 @@
33
33
  "Payment QR display requires both a valid/saved buyer session and payment_intent_id. If payment_intent_id appears before buyer_session is saved, run buyer checkout resume/status instead of showing payment QR.",
34
34
  "Do not stop after showing auth_qr unless the human explicitly asks you to pause. Keep waiting/resuming the same checkout so the post-auth payment handoff can continue.",
35
35
  "Do not describe auth_qr as payment success or payment proof. Only payment_intent.verified proves payment.",
36
+ "In Codex or Claude Code app clients, the default UX is show-then-wait: show response.payment_handoff.markdown first, then run response.payment_handoff.wait_command.",
37
+ "For Telegram-style message clients, show response.payment_handoff.markdown and stop unless the human asks you to keep waiting or returns with a payment question.",
36
38
  "MUST show local_qr_path first when present. Many agent clients do not reliably render remote QR images; local_qr_path is the most reliable desktop/chat display artifact.",
37
39
  "If local_qr_path is not present, show qr_png_url or preferred_qr_url as the primary scannable QR. This is an ItPay-hosted human QR image; it may render the native provider payment code for scanner reliability, but the agent must not request or decode the raw provider payload.",
38
40
  "Use qr_image_url/SVG only as a fallback when PNG/local rendering is unavailable.",
@@ -40,7 +42,8 @@
40
42
  "payment_entry_url is the stable ItPay status/payment page. qr_png_url/local_qr_path is usually the better desktop scanner artifact.",
41
43
  "In Alipay sandbox, the provider can briefly show order not found even after precreate succeeded. Tell the human to wait 30-60 seconds and retry the same QR/page. Do not create a new checkout, do not create a new payment intent, and do not repeatedly refresh unless the ItPay page or CLI explicitly asks for recovery.",
42
44
  "Opening the payment page must not be treated as paid.",
43
- "After showing QR, immediately wait for payment verification."
45
+ "Do not run payment wait before the QR/link has been sent to the human.",
46
+ "After showing QR, run payment_handoff.wait_command or buyer payment wait for the same payment_intent_id."
44
47
  ],
45
48
  "forbidden": [
46
49
  "Do not encode payment_entry_url into a new QR yourself when qr_png_url/local_qr_path is available.",
@@ -27,9 +27,11 @@
27
27
  }
28
28
  ],
29
29
  "agent_rules": [
30
+ "Start payment wait only after the QR image/link has already been sent to the human.",
30
31
  "Payment wait is a recovery loop around the same payment_intent_id, not a one-shot command.",
31
32
  "wait.timeout is a heartbeat, not a payment failure.",
32
- "Continue waiting with the same payment_intent_id unless the user explicitly cancels.",
33
+ "In Codex or Claude Code app clients, after showing QR, wait online for a short visible cycle first. If the human still has not paid, explain that the same QR/link remains valid and pause for their reply.",
34
+ "Continue waiting with the same payment_intent_id unless the user explicitly cancels or asks you to pause.",
33
35
  "If the network drops, the process is interrupted, or there is no response, rerun payment wait with the same payment_intent_id or run checkout status/resume with the same checkout_id.",
34
36
  "If status is unclear, tell the human it is unclear and continue querying the same payment_intent_id/checkout_id. Do not create a new checkout.",
35
37
  "Payment success is payment_intent.verified only.",
@@ -36,8 +36,8 @@
36
36
  },
37
37
  {
38
38
  "intent": "buy a selected variant with the high-level cart-first flow",
39
- "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --json",
40
- "success_signal": "response.status becomes waiting_human_auth, waiting_user_payment, payment_verified, or delivery_claimable"
39
+ "command": "itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json",
40
+ "success_signal": "response.status becomes waiting_human_auth or waiting_user_payment; if waiting_user_payment, response.payment_handoff contains markdown and wait_command"
41
41
  }
42
42
  ],
43
43
  "agent_rules": [
@@ -54,8 +54,9 @@
54
54
  "After showing auth_qr, keep running/resuming the same checkout unless the human explicitly asks you to pause. Do not stop merely because a QR was displayed.",
55
55
  "When a buyer command returns buyer_session.status=buyer_session_saved, the CLI has stored the buyer account session for this agent device. You may continue with checkout/payment or `buyer vault` commands without asking the human for a token.",
56
56
  "Vault grants are only for reading approved delivered artifacts. They are not buyer session credentials and do not authorize refunds or account/order management.",
57
- "Use high-level itp buy when possible; it is designed to show the first-purchase entry, wait/resume, continue to payment, wait for verification, and poll delivery.",
58
- "Show the returned payment QR exactly as provided.",
57
+ "Use high-level itp buy when possible, but in agent app clients split payment into two visible steps: first return and show payment_handoff, then run payment_handoff.wait_command.",
58
+ "If response.payment_handoff.markdown is present, send it to the human immediately in the same user-visible message. It includes the QR image and payment link.",
59
+ "Show the returned payment QR exactly as provided before starting payment wait.",
59
60
  "Payment truth comes only from payment_intent.verified.",
60
61
  "Secure delivery goes to the human first; report redacted delivery status only."
61
62
  ],
@@ -0,0 +1,157 @@
1
+ import { coreApi } from "./http.js";
2
+ import { noRecoverableContext } from "./buyer.js";
3
+ import { cliCommand, output, readConfig, readCredentials, readSessionToken, safeErrorMessage } from "./env.js";
4
+
5
+ async function accountOverviewStatus(flags = {}) {
6
+ const config = readConfig();
7
+ const credentials = readCredentials();
8
+ const hasLocalSession = Boolean(readSessionToken(credentials) || flags.access_token);
9
+ const base = {
10
+ schema_version: "itp.agent.v1",
11
+ status: hasLocalSession ? "account_status_unavailable" : "unauthenticated",
12
+ phase: hasLocalSession ? "account_status_unavailable" : "unauthenticated",
13
+ authenticated: false,
14
+ session_verified: !hasLocalSession,
15
+ auth_source: hasLocalSession ? "local_files_unverified" : "local_files",
16
+ account_id: config.account_id || null,
17
+ buyer_account_id: config.account_id || null,
18
+ device_id: config.device_id || null,
19
+ agent_device_id: config.device_id || null,
20
+ linked_providers: [],
21
+ order_count: 0,
22
+ created_at: null,
23
+ device: config.device_id ? { agent_device_id: config.device_id } : null,
24
+ next: hasLocalSession
25
+ ? { type: "retry_status", command: cliCommand("status", "--json"), safe_for_agent: true }
26
+ : { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true },
27
+ recoverable_context: noRecoverableContext(),
28
+ agent_next_actions: hasLocalSession
29
+ ? ["run_status_refresh", "view_orders", "create_refund_if_needed", "list_agent_read_grants"]
30
+ : ["start_human_auth_if_unauthenticated"],
31
+ secrets: { raw_key_included: false, session_token_included: false }
32
+ };
33
+ if (!hasLocalSession) return base;
34
+
35
+ try {
36
+ const me = await coreApi("/v1/me", { method: "GET" }, flags);
37
+ const authenticated = Boolean(me.authenticated);
38
+ const accountID = me.buyer_account_id || config.account_id || null;
39
+ const deviceID = me.agent_device_id || me.device?.agent_device_id || config.device_id || null;
40
+ return {
41
+ ...base,
42
+ status: authenticated ? "idle" : "unauthenticated",
43
+ phase: authenticated ? "idle" : "unauthenticated",
44
+ authenticated,
45
+ session_verified: true,
46
+ auth_source: "core",
47
+ account_id: accountID,
48
+ buyer_account_id: accountID,
49
+ device_id: deviceID,
50
+ agent_device_id: deviceID,
51
+ linked_providers: Array.isArray(me.linked_providers) ? me.linked_providers : [],
52
+ order_count: Number.isFinite(Number(me.order_count)) ? Number(me.order_count) : 0,
53
+ created_at: me.created_at || null,
54
+ device: me.device || (deviceID ? { agent_device_id: deviceID } : null),
55
+ next: authenticated
56
+ ? { type: "buyer_ready", command: cliCommand("buyer", "auth", "status", "--json"), safe_for_agent: true }
57
+ : base.next,
58
+ agent_next_actions: authenticated
59
+ ? ["search_catalog", "view_orders", "create_refund_if_needed", "list_agent_read_grants"]
60
+ : ["start_human_auth_if_unauthenticated"],
61
+ backend: {
62
+ environment: me.environment || null,
63
+ backend_version: me.backend_version || null
64
+ }
65
+ };
66
+ } catch (error) {
67
+ return {
68
+ ...base,
69
+ error: safeErrorMessage(error)
70
+ };
71
+ }
72
+ }
73
+
74
+ function outputAccountStatus(status, flags = {}) {
75
+ if (flags.json) {
76
+ output(status);
77
+ return;
78
+ }
79
+ console.log(formatStatusText(status));
80
+ }
81
+
82
+ function formatStatusText(status = {}) {
83
+ if (!status.authenticated) {
84
+ const lines = [
85
+ `Account: ${status.account_id ? `${status.account_id} (unverified)` : "not signed in"}`,
86
+ "Linked: -",
87
+ "Orders: -",
88
+ `Device: ${formatDeviceLine(status.device, status.device_id) || "-"}`
89
+ ];
90
+ if (status.error) lines.push(`Error: ${status.error}`);
91
+ return lines.join("\n");
92
+ }
93
+ const linked = Array.isArray(status.linked_providers) && status.linked_providers.length
94
+ ? status.linked_providers.join(", ")
95
+ : "-";
96
+ return [
97
+ `Account: ${status.buyer_account_id || status.account_id || "-"}`,
98
+ `Linked: ${linked}`,
99
+ `Orders: ${Number.isFinite(Number(status.order_count)) ? Number(status.order_count) : 0}`,
100
+ `Device: ${formatDeviceLine(status.device, status.device_id) || "-"}`
101
+ ].join("\n");
102
+ }
103
+
104
+ function formatDeviceLine(device, fallbackID = "") {
105
+ const displayName = String(device?.display_name || "").trim();
106
+ const deviceID = String(device?.agent_device_id || fallbackID || "").trim();
107
+ const label = displayName || deviceID;
108
+ if (!label) return "";
109
+ const status = String(device?.status || "").trim();
110
+ return status ? `${label} (${status})` : label;
111
+ }
112
+
113
+ async function refreshBuyerSessionForStatus(flags = {}) {
114
+ const config = readConfig();
115
+ const credentials = readCredentials();
116
+ if (!readSessionToken(credentials)) {
117
+ return {
118
+ authenticated: false,
119
+ session_verified: true,
120
+ account_id: config.account_id || null,
121
+ device_id: config.device_id || null,
122
+ auth_source: "core_no_session"
123
+ };
124
+ }
125
+ try {
126
+ const status = await coreApi("/v1/me/auth/status", { method: "GET" }, flags);
127
+ return {
128
+ authenticated: true,
129
+ session_verified: true,
130
+ account_id: status.buyer_account_id || config.account_id || null,
131
+ device_id: config.device_id || null,
132
+ account_status: status.account_status || null,
133
+ auth_source: "core"
134
+ };
135
+ } catch (error) {
136
+ return {
137
+ authenticated: false,
138
+ session_verified: true,
139
+ account_id: config.account_id || null,
140
+ device_id: config.device_id || null,
141
+ auth_source: "core",
142
+ error: safeErrorMessage(error)
143
+ };
144
+ }
145
+ }
146
+
147
+ function nextActionForAuthStatus(auth = {}, flags = {}) {
148
+ if (auth.authenticated && auth.session_verified === false) {
149
+ return { type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true };
150
+ }
151
+ if (auth.authenticated) {
152
+ return { type: "buyer_ready", command: cliCommand("buyer", "auth", "status", "--json"), safe_for_agent: true };
153
+ }
154
+ return { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true };
155
+ }
156
+
157
+ export { accountOverviewStatus, outputAccountStatus, refreshBuyerSessionForStatus, nextActionForAuthStatus };
package/lib/buyer.js CHANGED
@@ -4,9 +4,10 @@ import {
4
4
  writeCredentials, writeSessionCredentials, writeState
5
5
  } from "./env.js";
6
6
  import { coreApi, coreApiBase } from "./http.js";
7
- import { renderHumanAction, renderItPayPaymentAction, writeWaitHeartbeat } from "./render-human.js";
7
+ import { renderHumanAction, renderItPayPaymentAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
8
8
 
9
9
  async function buyerBuy(flags) {
10
+ const showThenWait = shouldReturnAfterAgentTextQR(flags);
10
11
  const selectionID = flags.selection || flags.variant || flags.catalog_variant_id || flags.item || flags.catalog_item_id;
11
12
  if (!selectionID) throw new Error("catalog variant id is required, for example: itp buy var_pubg_couple_skin_cny20 --sandbox --email buyer@example.com --phone +8613800000000 --json");
12
13
  const selection = await resolveBuyerCatalogSelection(selectionID, flags);
@@ -14,7 +15,7 @@ async function buyerBuy(flags) {
14
15
  let checkout = await createBuyerCheckoutFromCart(cart, selection, flags);
15
16
  if (checkout.next_required_action === "auth_qr" || checkout.identity_status === "waiting_human_auth") {
16
17
  await renderHumanAction(checkout.human_action, flags);
17
- if (flags.no_wait || flags.no_wait_auth) {
18
+ if (flags.no_wait || flags.no_wait_auth || showThenWait) {
18
19
  output(buyerRunOutput({
19
20
  status: "waiting_human_auth",
20
21
  selection,
@@ -41,15 +42,20 @@ async function buyerBuy(flags) {
41
42
  : await createBuyerPaymentIntent(checkout.checkout_id, flags);
42
43
  await renderItPayPaymentAction(intent, flags);
43
44
 
44
- if (flags.no_wait || flags.no_wait_payment) {
45
+ if (flags.no_wait || flags.no_wait_payment || showThenWait) {
45
46
  output(buyerRunOutput({
46
47
  status: "waiting_user_payment",
47
48
  selection,
48
49
  cart,
49
50
  checkout,
50
51
  payment_intent: intent,
52
+ payment_handoff: paymentHandoff(intent),
51
53
  agent_next_actions: intent.agent_next_actions || ["wait_payment"],
52
- next: { command: cliCommand("buyer", "payment", "wait", intent.payment_intent_id, "--json") }
54
+ next: {
55
+ command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
56
+ safe_for_agent: true,
57
+ instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
58
+ }
53
59
  }));
54
60
  return;
55
61
  }
@@ -312,8 +318,16 @@ async function buyer(command, rest, flags) {
312
318
  status: intent.status === "verified" ? "payment_verified" : "waiting_user_payment",
313
319
  checkout,
314
320
  payment_intent: intent,
321
+ payment_handoff: intent.status === "verified" ? undefined : paymentHandoff(intent),
315
322
  buyer_session: buyerSessionClaimStatus(claimedSession),
316
- agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"]
323
+ agent_next_actions: intent.agent_next_actions || checkout.agent_next_actions || ["wait_payment"],
324
+ next: intent.status === "verified"
325
+ ? { command: cliCommand("buyer", "checkout", "status", checkout.checkout_id, "--json"), safe_for_agent: true }
326
+ : {
327
+ command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
328
+ safe_for_agent: true,
329
+ instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
330
+ }
317
331
  }));
318
332
  return;
319
333
  }
@@ -326,7 +340,18 @@ async function buyer(command, rest, flags) {
326
340
  }
327
341
  const intent = await createBuyerPaymentIntent(checkout.checkout_id, flags);
328
342
  await renderItPayPaymentAction(intent, flags);
329
- output(buyerRunOutput({ status: "waiting_user_payment", checkout, payment_intent: intent, agent_next_actions: intent.agent_next_actions || ["wait_payment"] }));
343
+ output(buyerRunOutput({
344
+ status: "waiting_user_payment",
345
+ checkout,
346
+ payment_intent: intent,
347
+ payment_handoff: paymentHandoff(intent),
348
+ agent_next_actions: intent.agent_next_actions || ["wait_payment"],
349
+ next: {
350
+ command: paymentWaitCommand(intent.payment_intent_id, { short: true }),
351
+ safe_for_agent: true,
352
+ instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
353
+ }
354
+ }));
330
355
  return;
331
356
  }
332
357
  output(buyerRunOutput({
@@ -367,11 +392,16 @@ async function buyer(command, rest, flags) {
367
392
  output(buyerRunOutput({
368
393
  status: refreshed.status === "verified" ? "payment_verified" : "waiting_user_payment",
369
394
  payment_intent: refreshed,
395
+ payment_handoff: refreshed.status === "verified" ? undefined : paymentHandoff(refreshed),
370
396
  payment_guidance: paymentRecoveryGuidance(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
371
397
  agent_next_actions: refreshed.agent_next_actions || paymentAgentNextActions(refreshed, { event_type: refreshed.status === "verified" ? "payment_intent.verified" : "qr_refreshed" }),
372
398
  next: refreshed.status === "verified"
373
399
  ? { command: cliCommand("buyer", "checkout", "status", refreshed.checkout_id, "--json"), safe_for_agent: true }
374
- : { command: cliCommand("buyer", "payment", "wait", refreshed.payment_intent_id, "--json"), safe_for_agent: true }
400
+ : {
401
+ command: paymentWaitCommand(refreshed.payment_intent_id, { short: true }),
402
+ safe_for_agent: true,
403
+ instruction: "Send payment_handoff.markdown to the human first, then run this wait command unless the human explicitly asked you to pause."
404
+ }
375
405
  }));
376
406
  return;
377
407
  }
@@ -997,6 +1027,49 @@ function paymentAgentNextActions(intent = {}, event = {}) {
997
1027
  return ["show_payment_qr_if_needed", "wait_same_payment_intent"];
998
1028
  }
999
1029
 
1030
+ function paymentHandoff(intent = {}) {
1031
+ const action = intent.human_action || {};
1032
+ const paymentIntentID = intent.payment_intent_id || action.id || "";
1033
+ const entryURL = intent.payment_entry_url || intent.payment_url || action.url || "";
1034
+ const qrPNGURL = action.qr_png_url || intent.qr_png_url || intent.qr?.png_url || "";
1035
+ const preferredQRURL = action.preferred_qr_url || qrPNGURL || action.qr_image_url || intent.qr_image_url || intent.qr?.image_url || "";
1036
+ const localQRPath = action.local_qr_path || intent.local_qr_path || "";
1037
+ const mobileWalletURL = action.mobile_wallet_url || intent.mobile_wallet_url || "";
1038
+ const waitCommand = paymentWaitCommand(paymentIntentID, { short: true });
1039
+ return {
1040
+ type: "payment_qr_handoff",
1041
+ payment_intent_id: paymentIntentID || null,
1042
+ checkout_id: intent.checkout_id || null,
1043
+ primary: localQRPath ? "local_qr_path" : (qrPNGURL ? "qr_png_url" : (preferredQRURL ? "preferred_qr_url" : "payment_entry_url")),
1044
+ local_qr_path: localQRPath || null,
1045
+ qr_png_url: qrPNGURL || null,
1046
+ preferred_qr_url: preferredQRURL || null,
1047
+ payment_entry_url: entryURL || null,
1048
+ mobile_wallet_url: mobileWalletURL || null,
1049
+ markdown: paymentHandoffMarkdown({ localQRPath, qrPNGURL, preferredQRURL, entryURL, mobileWalletURL }),
1050
+ wait_command: waitCommand || null,
1051
+ safe_for_agent: true,
1052
+ instruction: "Show markdown to the human first. Then run wait_command and wait for payment_intent.verified unless the human asked you to pause."
1053
+ };
1054
+ }
1055
+
1056
+ function paymentHandoffMarkdown({ localQRPath = "", qrPNGURL = "", preferredQRURL = "", entryURL = "", mobileWalletURL = "" } = {}) {
1057
+ const imageURL = localQRPath || qrPNGURL || preferredQRURL;
1058
+ const lines = ["请扫码付款:"];
1059
+ if (imageURL) lines.push(`![ItPay payment QR](${imageURL})`);
1060
+ if (entryURL) lines.push(`[打开付款页面](${entryURL})`);
1061
+ if (mobileWalletURL) lines.push(`[手机钱包打开](${mobileWalletURL})`);
1062
+ lines.push("我会继续等待 ItPay 返回 payment_intent.verified。");
1063
+ return lines.join("\n\n");
1064
+ }
1065
+
1066
+ function paymentWaitCommand(paymentIntentID, { short = false } = {}) {
1067
+ if (!paymentIntentID) return "";
1068
+ return short
1069
+ ? cliCommand("buyer", "payment", "wait", paymentIntentID, "--timeout", "60", "--json")
1070
+ : cliCommand("buyer", "payment", "wait", paymentIntentID, "--json");
1071
+ }
1072
+
1000
1073
  function paymentRecoveryGuidance(intent = {}, event = {}) {
1001
1074
  const paymentIntentID = intent.payment_intent_id || event.payment_intent_id || null;
1002
1075
  return {
@@ -8,7 +8,7 @@ import { apiTimeoutMs, cliCommand, commandExists, mergeRun, readRun, readState,
8
8
  async function renderItPayPaymentAction(intent, flags = {}) {
9
9
  const action = intent?.human_action ? { ...intent.human_action } : (intent?.payment_url ? {
10
10
  id: intent.payment_intent_id,
11
- title: "Scan with Alipay",
11
+ title: "Scan payment QR",
12
12
  url: intent.payment_url,
13
13
  expires_at: intent.qr?.expires_at
14
14
  } : null);
@@ -23,7 +23,7 @@ async function renderItPayPaymentAction(intent, flags = {}) {
23
23
  if (action && (intent?.qr_image_url || intent?.qr?.image_url)) {
24
24
  action.qr_image_url = intent.qr_image_url || intent.qr.image_url;
25
25
  action.display_mode = intent.qr?.scan_mode || "itpay_entry_qr";
26
- action.description = action.description || "Scan the ItPay payment entry QR; ItPay will safely hand off to Alipay.";
26
+ action.description = action.description || "Scan the ItPay payment entry QR; ItPay will safely hand off to the payment provider.";
27
27
  }
28
28
  const result = await renderHumanAction(action, flags);
29
29
  if (intent && action) {
@@ -39,7 +39,7 @@ function humanActionSummaryLines(action) {
39
39
  if (!action?.url) return [];
40
40
  const lines = [
41
41
  "ITP HUMAN ACTION REQUIRED",
42
- `Title: ${action.title || "Scan with Alipay"}`,
42
+ `Title: ${humanActionTitle(action)}`,
43
43
  `URL: ${action.url}`
44
44
  ];
45
45
  if (action.local_qr_path) {
@@ -55,7 +55,7 @@ function humanActionSummaryLines(action) {
55
55
  lines.push(`Mobile wallet link: ${action.mobile_wallet_url}`);
56
56
  }
57
57
  if (action.oauth_start_url) {
58
- lines.push(`Alipay auth fallback: ${action.oauth_start_url}`);
58
+ lines.push(`${humanActionProviderLabel(action)} auth fallback: ${action.oauth_start_url}`);
59
59
  }
60
60
  if (action.fallback_text && !action.fallback_text.includes(action.url)) {
61
61
  lines.push(`Fallback: ${action.fallback_text}`);
@@ -89,7 +89,7 @@ function writeWaitHeartbeat({ kind, idName, idValue, status, action, lastHeartbe
89
89
  action?.qr_png_url ? `QR PNG: ${action.qr_png_url}` : null,
90
90
  action?.qr_image_url ? `QR image: ${action.qr_image_url}` : null,
91
91
  action?.mobile_wallet_url ? `Mobile wallet link: ${action.mobile_wallet_url}` : null,
92
- action?.oauth_start_url ? `Alipay auth fallback: ${action.oauth_start_url}` : null,
92
+ action?.oauth_start_url ? `${humanActionProviderLabel(action)} auth fallback: ${action.oauth_start_url}` : null,
93
93
  command ? `Resume command: ${command}` : null
94
94
  ].filter(Boolean);
95
95
  process.stderr.write(`\n${lines.join("\n")}\n\n`);
@@ -121,7 +121,7 @@ async function renderHumanAction(action, flags = {}) {
121
121
  }
122
122
  return { rendered: false, mode: flags.json ? "json" : mode };
123
123
  }
124
- const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
124
+ const host = agentHost(flags);
125
125
  if (["discord", "telegram", "whatsapp"].includes(host)) {
126
126
  return { rendered: false, mode: "chat-json", host };
127
127
  }
@@ -147,9 +147,29 @@ async function renderHumanAction(action, flags = {}) {
147
147
  }
148
148
 
149
149
  const renderResult = { rendered: false, mode, outputs: [] };
150
+ const terminalQRAllowed = shouldRenderTerminalQR(host, mode);
151
+ const providerLabel = humanActionProviderLabel(action);
152
+ const actionTitle = humanActionTitle(action);
150
153
  writeHumanActionSummary(action);
154
+
155
+ if ((mode === "auto" || mode === "terminal") && terminalQRAllowed) {
156
+ const qr = await QRCode.toString(action.url, {
157
+ type: terminalQRType(flags, host),
158
+ small: true,
159
+ errorCorrectionLevel: "M"
160
+ });
161
+ process.stderr.write(`\n${actionTitle}\n`);
162
+ if (action.description) process.stderr.write(`${action.description}\n`);
163
+ process.stderr.write(`${qr}\n`);
164
+ process.stderr.write(`${providerLabel} action URL: ${action.url}\n`);
165
+ if (action.expires_at) process.stderr.write(`Expires at: ${formatActionTime(action.expires_at)}\n`);
166
+ renderResult.rendered = true;
167
+ renderResult.outputs.push("terminal");
168
+ return renderResult;
169
+ }
170
+
151
171
  if ((mode === "auto" || mode === "browser") && shouldOpenBrowser(flags)) {
152
- if (openBrowser(action.mobile_wallet_url || qrImageURL || action.url)) {
172
+ if (openBrowser(qrImageURL || action.mobile_wallet_url || action.url)) {
153
173
  renderResult.rendered = true;
154
174
  renderResult.outputs.push("browser");
155
175
  }
@@ -162,7 +182,7 @@ async function renderHumanAction(action, flags = {}) {
162
182
  await downloadQRImage(qrImageURL, file, flags);
163
183
  action.local_qr_path = file;
164
184
  action.local_qr_mime = qrMimeType(qrImageURL, action);
165
- process.stderr.write(`Alipay QR image: ${file}\n`);
185
+ process.stderr.write(`${providerLabel} QR image: ${file}\n`);
166
186
  renderResult.rendered = true;
167
187
  renderResult.outputs.push(file);
168
188
  if (mode === "file") return renderResult;
@@ -173,44 +193,42 @@ async function renderHumanAction(action, flags = {}) {
173
193
  if (localPath) {
174
194
  attachAgentLocalQR(action, localPath);
175
195
  annotateHumanActionPresentation(action, "");
176
- process.stderr.write(`Alipay action QR image: ${localPath}\n`);
196
+ process.stderr.write(`${providerLabel} action QR image: ${localPath}\n`);
177
197
  renderResult.rendered = true;
178
198
  renderResult.outputs.push(localPath);
179
199
  if (mode === "file") return renderResult;
180
200
  } else {
181
- process.stderr.write(`No QR image available. Open action URL: ${action.url}\n`);
201
+ process.stderr.write(`No branded QR image available. Open action URL: ${action.url}\n`);
182
202
  }
183
203
  }
184
204
  }
185
205
 
186
206
  if (qrImageURL) {
187
- process.stderr.write(`Alipay QR image URL: ${qrImageURL}\n`);
207
+ process.stderr.write(`${providerLabel} QR image URL: ${qrImageURL}\n`);
188
208
  if (action.mobile_wallet_url) process.stderr.write(`Mobile wallet link: ${action.mobile_wallet_url}\n`);
189
209
  renderResult.outputs.push("preferred_qr_url");
190
210
  return renderResult;
191
211
  }
192
212
 
193
- if ((mode === "auto" || mode === "terminal") && shouldRenderTerminalQR(host)) {
194
- const qr = await QRCode.toString(action.url, {
195
- type: terminalQRType(flags, host),
196
- small: true,
197
- errorCorrectionLevel: "M"
198
- });
199
- process.stderr.write(`\n${action.title || "Scan with Alipay"}\n`);
200
- if (action.description) process.stderr.write(`${action.description}\n`);
201
- process.stderr.write(`${qr}\n`);
202
- process.stderr.write(`Alipay action URL: ${action.url}\n`);
203
- if (action.expires_at) process.stderr.write(`Expires at: ${formatActionTime(action.expires_at)}\n`);
204
- renderResult.rendered = true;
205
- renderResult.outputs.push("terminal");
206
- return renderResult;
207
- }
208
-
209
- process.stderr.write(`${action.fallback_text || `Open Alipay URL: ${action.url}`}\n`);
213
+ process.stderr.write(`${action.fallback_text || `Open ${providerLabel} URL: ${action.url}`}\n`);
210
214
  renderResult.outputs.push("url");
211
215
  return renderResult;
212
216
  }
213
217
 
218
+ function humanActionTitle(action = {}) {
219
+ if (action.title) return action.title;
220
+ if (action.kind === "auth_qr") return `${humanActionProviderLabel(action)} authentication`;
221
+ return "Scan payment QR";
222
+ }
223
+
224
+ function humanActionProviderLabel(action = {}) {
225
+ const raw = String(action.provider || action.channel || action.display_mode || "").toLowerCase();
226
+ if (raw.includes("alipay")) return "Alipay";
227
+ if (raw.includes("wechat")) return "WeChat Pay";
228
+ if (raw.includes("fake") || raw.includes("local")) return "ItPay local";
229
+ return "Alipay or WeChat Pay";
230
+ }
231
+
214
232
  function preferredHumanActionQRURL(action) {
215
233
  return action?.qr_png_url ||
216
234
  humanActionPresentationURL(action, "qr_png_url") ||
@@ -338,8 +356,8 @@ function formatActionTime(value) {
338
356
 
339
357
  function shouldUseAgentTextQR(flags = {}) {
340
358
  const mode = String(flags.display || process.env.ITP_DISPLAY || "").toLowerCase() || "auto";
341
- const host = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
342
- return mode === "chat" || mode === "agent" || (["gemini", "gemini-cli"].includes(host) && mode === "auto");
359
+ const host = agentHost(flags);
360
+ return mode === "chat" || mode === "agent" || (agentTextQRHosts().has(host) && mode === "auto");
343
361
  }
344
362
 
345
363
  function shouldReturnAfterAgentTextQR(flags = {}) {
@@ -347,6 +365,18 @@ function shouldReturnAfterAgentTextQR(flags = {}) {
347
365
  return shouldUseAgentTextQR(flags);
348
366
  }
349
367
 
368
+ function agentHost(flags = {}) {
369
+ const explicit = String(process.env.ITP_HOST || flags.host || "").toLowerCase();
370
+ if (explicit) return explicit;
371
+ if (process.env.CODEX_THREAD_ID || process.env.CODEX_SHELL || process.env.CODEX_CI) return "codex";
372
+ if (process.env.CLAUDECODE || process.env.CLAUDE_CODE || process.env.CLAUDECODE_SESSION_ID) return "claude-code";
373
+ return "";
374
+ }
375
+
376
+ function agentTextQRHosts() {
377
+ return new Set(["codex", "codex-cli", "claude", "claude-code", "gemini", "gemini-cli"]);
378
+ }
379
+
350
380
  function attachAgentQRImage(action, qrImageURL, localPath = "") {
351
381
  if (!action || !qrImageURL) return action;
352
382
  action.preferred_qr_url = qrImageURL;
@@ -416,16 +446,22 @@ function persistHumanAction(action, flags = {}) {
416
446
  writeRun(mergeRun(run, { human_action: action }));
417
447
  }
418
448
 
419
- function shouldRenderTerminalQR(host) {
449
+ function shouldRenderTerminalQR(host, mode = "auto") {
450
+ if (["terminal", "tty"].includes(mode)) return true;
451
+ if (mode && !["auto", ""].includes(mode)) return false;
420
452
  if (process.stderr.isTTY) return true;
421
- return ["gemini", "gemini-cli"].includes(host);
453
+ return terminalQRHostAliases().has(String(host || "").toLowerCase());
454
+ }
455
+
456
+ function terminalQRHostAliases() {
457
+ return new Set(["terminal", "tty", "bash", "zsh", "sh", "shell", "iterm", "iterm2"]);
422
458
  }
423
459
 
424
460
  function terminalQRType(flags = {}, host = "") {
425
461
  const requested = String(flags.qr_format || process.env.ITP_QR_FORMAT || "").toLowerCase();
426
462
  if (requested === "unicode" || requested === "utf8") return "utf8";
427
463
  if (requested === "ansi" || requested === "terminal") return "terminal";
428
- if (["gemini", "gemini-cli"].includes(host)) return "utf8";
464
+ if (terminalQRHostAliases().has(String(host || "").toLowerCase())) return "utf8";
429
465
  return "terminal";
430
466
  }
431
467
 
package/lib/runtime.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  recoverableBuyerContextForStatus, recoverableIntentCheckGuidance
8
8
  } from "./buyer.js";
9
9
  import { renderHumanAction, shouldReturnAfterAgentTextQR, writeWaitHeartbeat } from "./render-human.js";
10
+ import { accountOverviewStatus, nextActionForAuthStatus, outputAccountStatus, refreshBuyerSessionForStatus } from "./account-status.js";
10
11
  import {
11
12
  CREDENTIALS_PATH, CONFIG_DIR, RUNS_DIR, VERSION, apiBase, apiTimeoutMs, appendURLQuery, booleanFlag, cliCommand, commandExists,
12
13
  cryptoRandom, csvValues, currentExecutable, deleteGrantCredential, deleteSessionCredential, detectNativeCredentialStore,
@@ -131,14 +132,14 @@ async function setup(flags) {
131
132
  grant_id: checkout.grant_id || run.grant?.grant_id || null
132
133
  },
133
134
  human_action: checkout.human_action || null,
134
- safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
135
+ safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay or WeChat Pay payment scan."
135
136
  });
136
137
  writeRun(run);
137
138
 
138
139
  if (checkout.human_action) {
139
140
  await renderHumanAction(checkout.human_action, setupFlags);
140
141
  } else if (checkout.payment?.cashier_url) {
141
- process.stderr.write(`Open Alipay payment URL: ${checkout.payment.cashier_url}\n`);
142
+ process.stderr.write(`Open Alipay or WeChat Pay payment URL: ${checkout.payment.cashier_url}\n`);
142
143
  }
143
144
 
144
145
  if (!checkout.grant_id && (setupFlags.no_wait_payment || setupFlags.no_wait)) {
@@ -263,40 +264,7 @@ async function agentStatus(flags) {
263
264
  const runId = flags.run_id || readState().current_run_id;
264
265
  const run = readRun(runId);
265
266
  if (!run) {
266
- const config = readConfig();
267
- const credentials = readCredentials();
268
- const hasLocalSession = Boolean(readSessionToken(credentials));
269
- const auth = flags.refresh
270
- ? await refreshBuyerSessionForStatus(flags)
271
- : {
272
- authenticated: hasLocalSession,
273
- session_verified: false,
274
- account_id: config.account_id || null,
275
- device_id: config.device_id || null,
276
- auth_source: hasLocalSession ? "local_files_unverified" : "local_files"
277
- };
278
- output({
279
- schema_version: "itp.agent.v1",
280
- status: auth.authenticated
281
- ? (auth.session_verified === false ? "local_session_unverified" : "idle")
282
- : "unauthenticated",
283
- phase: auth.authenticated ? "idle" : "unauthenticated",
284
- authenticated: Boolean(auth.authenticated),
285
- session_verified: auth.session_verified !== false,
286
- auth_source: auth.auth_source,
287
- account_id: auth.account_id || null,
288
- device_id: auth.device_id || null,
289
- next: nextActionForAuthStatus(auth, flags),
290
- recoverable_context: noRecoverableContext(),
291
- agent_next_actions: auth.authenticated && auth.session_verified !== false
292
- ? ["search_catalog", "view_orders", "create_refund_if_needed", "list_agent_read_grants"]
293
- : ["run_status_refresh", "start_human_auth_if_unauthenticated"],
294
- note: auth.authenticated && auth.session_verified === false
295
- ? "Local buyer session exists but the server has not verified it. Run the next.command before order/refund/account operations."
296
- : undefined,
297
- error: auth.error || undefined,
298
- secrets: { raw_key_included: false, session_token_included: false }
299
- });
267
+ outputAccountStatus(await accountOverviewStatus(flags), flags);
300
268
  return;
301
269
  }
302
270
  const refreshed = flags.refresh ? await refreshRun(run, flags) : run;
@@ -306,50 +274,6 @@ async function agentStatus(flags) {
306
274
  }));
307
275
  }
308
276
 
309
- async function refreshBuyerSessionForStatus(flags = {}) {
310
- const config = readConfig();
311
- const credentials = readCredentials();
312
- if (!readSessionToken(credentials)) {
313
- return {
314
- authenticated: false,
315
- session_verified: true,
316
- account_id: config.account_id || null,
317
- device_id: config.device_id || null,
318
- auth_source: "core_no_session"
319
- };
320
- }
321
- try {
322
- const status = await coreApi("/v1/me/auth/status", { method: "GET" }, flags);
323
- return {
324
- authenticated: true,
325
- session_verified: true,
326
- account_id: status.buyer_account_id || config.account_id || null,
327
- device_id: config.device_id || null,
328
- account_status: status.account_status || null,
329
- auth_source: "core"
330
- };
331
- } catch (error) {
332
- return {
333
- authenticated: false,
334
- session_verified: true,
335
- account_id: config.account_id || null,
336
- device_id: config.device_id || null,
337
- auth_source: "core",
338
- error: safeErrorMessage(error)
339
- };
340
- }
341
- }
342
-
343
- function nextActionForAuthStatus(auth = {}, flags = {}) {
344
- if (auth.authenticated && auth.session_verified === false) {
345
- return { type: "verify_buyer_session", command: cliCommand("status", "--refresh", "--json"), safe_for_agent: true };
346
- }
347
- if (auth.authenticated) {
348
- return { type: "buyer_ready", command: cliCommand("buyer", "auth", "status", "--json"), safe_for_agent: true };
349
- }
350
- return { type: "start_auth", command: cliCommand("setup", "--plan", "credit-300", "--method", "alipay", "--json"), safe_for_agent: true };
351
- }
352
-
353
277
  async function resume(flags) {
354
278
  const runId = flags.run_id || readState().current_run_id;
355
279
  const run = readRun(runId);
@@ -761,7 +685,7 @@ async function createCheckoutResult(flags) {
761
685
  grant_id: response.grant_id || currentRun?.grant?.grant_id || null
762
686
  },
763
687
  human_action: response.human_action || null,
764
- safe_summary: response.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay payment scan."
688
+ safe_summary: response.grant_id ? "Payment verified and grant is ready." : "Waiting for Alipay or WeChat Pay payment scan."
765
689
  }, flags);
766
690
  return response;
767
691
  }
@@ -827,7 +751,7 @@ async function paymentWaitResult(checkoutId, flags) {
827
751
  }
828
752
  }
829
753
  lastHeartbeatAt = writeWaitHeartbeat({
830
- kind: "Alipay payment",
754
+ kind: "Alipay or WeChat Pay payment",
831
755
  idName: "checkout_id",
832
756
  idValue: checkoutId,
833
757
  status: response.status || "waiting",
@@ -1376,7 +1300,7 @@ async function refreshRun(run, flags = {}) {
1376
1300
  payment: { provider: next.payment_method, status: checkout.status },
1377
1301
  grant: { ...(next.grant || {}), grant_id: checkout.grant_id || next.grant?.grant_id || null },
1378
1302
  human_action: checkout.human_action || null,
1379
- safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : checkout.status === "waiting_user_payment" ? "Waiting for Alipay payment scan." : `Checkout status: ${checkout.status}`
1303
+ safe_summary: checkout.grant_id ? "Payment verified and grant is ready." : checkout.status === "waiting_user_payment" ? "Waiting for Alipay or WeChat Pay payment scan." : `Checkout status: ${checkout.status}`
1380
1304
  });
1381
1305
  } catch (error) {
1382
1306
  next = mergeRun(next, { last_error: safeErrorMessage(error), safe_summary: "Could not refresh checkout status." });
@@ -1498,7 +1422,7 @@ function appendPassthroughFlag(args, flags, key, flagName = key) {
1498
1422
 
1499
1423
  function safeUserMessageForRun(run) {
1500
1424
  if (run.phase === "waiting_human_auth") return "Please scan the Alipay authentication QR. I will continue automatically after approval.";
1501
- if (run.phase === "waiting_human_payment") return "Please scan the Alipay payment QR. I will continue automatically after payment is verified.";
1425
+ if (run.phase === "waiting_human_payment") return "Please scan the Alipay or WeChat Pay payment QR. I will continue automatically after payment is verified.";
1502
1426
  if (run.grant?.installed) return "Payment verified. The API credential is stored locally.";
1503
1427
  return run.safe_summary || "ITPay setup is in progress.";
1504
1428
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itpay/cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "ItPay CLI, buyer skill, and agent-readable docs for agent-native commerce.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,7 +59,7 @@ read this skill
59
59
  The high-level command can wrap this flow:
60
60
 
61
61
  ```bash
62
- itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --json
62
+ itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
63
63
  ```
64
64
 
65
65
  For step-by-step testing:
@@ -93,7 +93,7 @@ itp buyer cart create --variant var_itpay_enterprise_fuzzy_search_cny01 --input
93
93
  itp buyer cart show <cart_id> --json
94
94
  itp buyer cart add <cart_id> --variant var_itpay_enterprise_fuzzy_search_cny01 --input company_name=美团 --json
95
95
  itp buyer cart create --variant var_itpay_enterprise_precise_lookup_cny05 --input company_name_or_credit_no=北京京东世纪贸易有限公司 --json
96
- itp buy var_itpay_enterprise_fuzzy_search_cny01 --sandbox --email <buyer_email> --input company_name=京东 --json
96
+ itp buy var_itpay_enterprise_fuzzy_search_cny01 --sandbox --email <buyer_email> --input company_name=京东 --display agent --no-wait-payment --json
97
97
  ```
98
98
 
99
99
  For cart edits, always read the server cart first with `buyer cart show`.
@@ -206,13 +206,16 @@ Each docs page includes `next_docs`. Follow those links as the state changes.
206
206
  For payment creation in an agent/chat client, prefer:
207
207
 
208
208
  ```bash
209
- itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --json
209
+ itp buy <variant_id> --sandbox --email <buyer_email> --phone <buyer_phone> --display agent --no-wait-payment --json
210
210
  ```
211
211
 
212
212
  This keeps JSON output machine-readable while allowing the CLI to prepare a
213
- local QR image path for clients that cannot render remote SVG reliably. If the
214
- human is on mobile, present `mobile_wallet_url` as a clickable human-only
215
- fallback; do not convert it into a QR.
213
+ local QR image path for clients that cannot render remote SVG reliably. In
214
+ Codex or Claude Code app clients, prefer `--no-wait-payment`: send
215
+ `payment_handoff.markdown` to the human first, then run
216
+ `payment_handoff.wait_command`. If the human is on mobile, present
217
+ `mobile_wallet_url` as a clickable human-only fallback; do not convert it into a
218
+ QR.
216
219
 
217
220
  For first-purchase auth, treat the returned ItPay authorization entry as a
218
221
  single human orchestration entry. It may open Alipay login/registration first