@seasonkoh/webaz 0.1.27 → 0.1.29
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 +4 -3
- package/README.zh-CN.md +7 -6
- package/dist/currency.js +16 -0
- package/dist/deposit-rails.js +52 -0
- package/dist/direct-pay-aml-monitor.js +67 -0
- package/dist/direct-pay-aml-review.js +40 -0
- package/dist/direct-pay-base-bond-entry.js +5 -0
- package/dist/direct-pay-bond-rail-clearance.js +94 -0
- package/dist/direct-pay-compliance-ingress.js +145 -0
- package/dist/direct-pay-controls.js +136 -0
- package/dist/direct-pay-create.js +162 -0
- package/dist/direct-pay-deferral-quota.js +43 -0
- package/dist/direct-pay-disclosures.js +44 -0
- package/dist/direct-pay-eligibility.js +46 -0
- package/dist/direct-pay-fee-ar.js +241 -0
- package/dist/direct-pay-launch-readiness.js +108 -0
- package/dist/direct-pay-launch-summary.js +68 -0
- package/dist/direct-pay-ledger.js +94 -0
- package/dist/direct-receive-account-qr.js +77 -0
- package/dist/direct-receive-accounts.js +82 -0
- package/dist/direct-receive-deferral.js +142 -0
- package/dist/direct-receive-deposits.js +260 -0
- package/dist/direct-receive-payment-instruction.js +26 -0
- package/dist/fx-rates.js +71 -0
- package/dist/layer0-foundation/L0-1-database/schema.js +531 -1
- package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +15 -5
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +41 -0
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +1 -1
- package/dist/layer1-agent/L1-1-mcp-server/cli.js +64 -0
- package/dist/layer1-agent/L1-1-mcp-server/network-mode.js +80 -0
- package/dist/layer1-agent/L1-1-mcp-server/server.js +251 -69
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +4 -4
- package/dist/ledger.js +12 -3
- package/dist/mcp.js +23 -4
- package/dist/merchant-bond-domain.js +64 -0
- package/dist/merchant-bond-exposure.js +78 -0
- package/dist/merchant-bond-watcher.js +26 -0
- package/dist/payment-rails.js +29 -0
- package/dist/product-verification.js +93 -0
- package/dist/pwa/acp-feed.js +3 -2
- package/dist/pwa/contract-fingerprint.js +3 -0
- package/dist/pwa/direct-pay-guards.js +20 -0
- package/dist/pwa/direct-pay-order-redaction.js +24 -0
- package/dist/pwa/endpoint-actions.js +3 -0
- package/dist/pwa/public/app-account.js +977 -0
- package/dist/pwa/public/app-admin.js +608 -0
- package/dist/pwa/public/app-agents.js +63 -0
- package/dist/pwa/public/app-ai.js +2162 -0
- package/dist/pwa/public/app-chat-poll.js +29 -0
- package/dist/pwa/public/app-contribution.js +836 -0
- package/dist/pwa/public/app-create-kinds.js +17 -0
- package/dist/pwa/public/app-direct-pay-accounts.js +141 -0
- package/dist/pwa/public/app-direct-pay-buyer.js +72 -0
- package/dist/pwa/public/app-direct-pay-compliance.js +67 -0
- package/dist/pwa/public/app-direct-pay-deferral-admin.js +72 -0
- package/dist/pwa/public/app-direct-pay-deferral.js +61 -0
- package/dist/pwa/public/app-direct-pay-fee-center.js +33 -0
- package/dist/pwa/public/app-direct-pay-fee-ops.js +112 -0
- package/dist/pwa/public/app-direct-pay-product-verify.js +103 -0
- package/dist/pwa/public/app-direct-pay-readiness.js +38 -0
- package/dist/pwa/public/app-direct-pay-store-verify.js +100 -0
- package/dist/pwa/public/app-direct-pay.js +227 -0
- package/dist/pwa/public/app-discover.js +1296 -0
- package/dist/pwa/public/app-external-links.js +32 -0
- package/dist/pwa/public/app-listings.js +226 -0
- package/dist/pwa/public/app-prelaunch-waz.js +39 -0
- package/dist/pwa/public/app-price.js +55 -0
- package/dist/pwa/public/app-product-media.js +15 -0
- package/dist/pwa/public/app-profile.js +1692 -0
- package/dist/pwa/public/app-seller.js +199 -0
- package/dist/pwa/public/app-shop.js +1145 -0
- package/dist/pwa/public/app.js +13388 -22277
- package/dist/pwa/public/i18n.js +413 -196
- package/dist/pwa/public/index.html +27 -0
- package/dist/pwa/public/openapi.json +579 -1
- package/dist/pwa/routes/admin-analytics.js +4 -2
- package/dist/pwa/routes/admin-direct-receive-deposits.js +321 -0
- package/dist/pwa/routes/agent-grants.js +255 -0
- package/dist/pwa/routes/buyer-feeds.js +2 -1
- package/dist/pwa/routes/chat.js +6 -1
- package/dist/pwa/routes/dashboards.js +2 -1
- package/dist/pwa/routes/direct-pay-availability.js +196 -0
- package/dist/pwa/routes/direct-pay-disclosure-acks.js +74 -0
- package/dist/pwa/routes/direct-pay-timeouts.js +71 -0
- package/dist/pwa/routes/direct-receive-accounts.js +155 -0
- package/dist/pwa/routes/direct-receive-payment-instructions.js +45 -0
- package/dist/pwa/routes/fx.js +12 -0
- package/dist/pwa/routes/leaderboard.js +47 -9
- package/dist/pwa/routes/listings.js +2 -2
- package/dist/pwa/routes/logistics.js +4 -0
- package/dist/pwa/routes/manifests.js +38 -0
- package/dist/pwa/routes/me-data.js +5 -2
- package/dist/pwa/routes/orders-action.js +117 -9
- package/dist/pwa/routes/orders-create.js +4 -0
- package/dist/pwa/routes/orders-read.js +36 -0
- package/dist/pwa/routes/p2p-products.js +2 -2
- package/dist/pwa/routes/products-create.js +5 -3
- package/dist/pwa/routes/products-links.js +34 -0
- package/dist/pwa/routes/products-list.js +2 -1
- package/dist/pwa/routes/products-update.js +22 -2
- package/dist/pwa/routes/promoter.js +3 -0
- package/dist/pwa/routes/referral.js +4 -0
- package/dist/pwa/routes/returns.js +26 -1
- package/dist/pwa/routes/rewards-apply.js +10 -5
- package/dist/pwa/routes/rewards-clearing-mature.js +96 -0
- package/dist/pwa/routes/rewards-escrow-expire.js +6 -2
- package/dist/pwa/routes/shops.js +2 -1
- package/dist/pwa/routes/url-claim.js +2 -2
- package/dist/pwa/routes/users-public.js +8 -0
- package/dist/pwa/routes/wallet-read.js +3 -0
- package/dist/pwa/routes/webauthn.js +7 -2
- package/dist/pwa/server-schema.js +9 -0
- package/dist/pwa/server.js +261 -1706
- package/dist/runtime/agent-grant-scopes.js +128 -0
- package/dist/runtime/agent-grant-verifier.js +67 -0
- package/dist/runtime/agent-pairing.js +60 -0
- package/dist/runtime/apply-webaz-runtime-schema.js +15 -0
- package/dist/runtime/webaz-schema-helpers.js +1952 -0
- package/dist/store-verification.js +77 -0
- package/dist/version.js +1 -1
- package/package.json +80 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — 按产品认证 UI (PR-④b)。UI ONLY。两个面:
|
|
2
|
+
// ① SELLER 自助(seller workbench settings):逐产品申领验证码 → 贴到外部商品页 → 提交该产品外链 → 看逐产品状态。
|
|
3
|
+
// ② ADMIN 审核队列(#admin/product-verifications):打开卖家提交的外链手动核对验证码,verify/reject(ROOT + 真人 Passkey)。
|
|
4
|
+
// 硬门:每个产品都须【单独】被 admin 核验才可直付;一次验证绝不放行所有产品。诚实:WebAZ 不抓取链接,admin 手动核。
|
|
5
|
+
// 不碰 wallet/escrow/settlement/refund;只调既有受门控端点。
|
|
6
|
+
|
|
7
|
+
// ── 状态 → 双语文案 ──
|
|
8
|
+
window.dpPvCopy = () => ({
|
|
9
|
+
issued: { icon: '①', text: t('已签发验证码,请贴到该商品的外部页面后提交链接') },
|
|
10
|
+
submitted: { icon: '⏳', text: t('已提交,等待管理员核验') },
|
|
11
|
+
verified: { icon: '✅', text: t('已通过验证,可直付') },
|
|
12
|
+
rejected: { icon: '🚫', text: t('未通过,请修正后重新申请') },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// ══════ SELLER 自助面板 ══════
|
|
16
|
+
window.dpSellerProductVerifySection = () => `
|
|
17
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
18
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🔖 ${t('逐产品直付验证(仅你可见)')}</div>
|
|
19
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('每个走直付收款的商品都需【单独】通过平台人工验证(防作弊):申领验证码 → 贴到该商品的外部平台页面 → 回来提交该商品链接 → 管理员手动核对。未验证的商品只能用托管交易。')}</div>
|
|
20
|
+
<div id="dp-seller-pv">${loading$()}</div>
|
|
21
|
+
</div>`
|
|
22
|
+
|
|
23
|
+
window.dpHydrateSellerProductVerify = async () => {
|
|
24
|
+
const box = document.getElementById('dp-seller-pv')
|
|
25
|
+
if (!box) return
|
|
26
|
+
const [products, vres] = await Promise.all([GET('/my-products'), GET('/direct-receive/product-verifications')])
|
|
27
|
+
if (!Array.isArray(products) || !vres || vres.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(vres && vres.error_code, vres && vres.error) : t('操作失败,请重试')}</div>`; return }
|
|
28
|
+
const byProduct = {}; for (const v of (vres.verifications || [])) { if (!byProduct[v.product_id]) byProduct[v.product_id] = v } // 最新在前
|
|
29
|
+
const copy = window.dpPvCopy()
|
|
30
|
+
if (!products.length) { box.innerHTML = `<div style="font-size:12px;color:#9ca3af">${t('你还没有商品')}</div>`; return }
|
|
31
|
+
box.innerHTML = products.map(p => {
|
|
32
|
+
const v = byProduct[p.id]
|
|
33
|
+
const meta = v ? (copy[v.status] || { icon: 'ℹ️', text: v.status }) : null
|
|
34
|
+
let action = ''
|
|
35
|
+
if (!v || v.status === 'rejected') {
|
|
36
|
+
action = `<button onclick="window.dpRequestProductVerify('${p.id}')" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('申请验证')}</button>`
|
|
37
|
+
} else if (v.status === 'issued') {
|
|
38
|
+
action = `
|
|
39
|
+
<div style="font-size:12px;color:#374151;margin-bottom:6px">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code> · ${t('把它展示在该商品的外部页面')}</div>
|
|
40
|
+
<input id="dp-pv-url-${p.id}" type="url" placeholder="${t('该商品的外部链接(http/https)')}" style="width:100%;padding:6px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;margin-bottom:6px">
|
|
41
|
+
<button onclick="window.dpSubmitProductVerify('${p.id}')" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('提交链接')}</button>`
|
|
42
|
+
}
|
|
43
|
+
return `<div style="padding:8px 0;border-top:1px solid #f3f4f6">
|
|
44
|
+
<div style="font-size:13px;font-weight:600;color:#374151">${escHtml(p.title || p.id)}</div>
|
|
45
|
+
${meta ? `<div style="font-size:12px;color:#6b7280;margin:4px 0">${meta.icon} ${escHtml(meta.text)}</div>` : ''}
|
|
46
|
+
${action}
|
|
47
|
+
</div>`
|
|
48
|
+
}).join('')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
window.dpRequestProductVerify = async (productId) => {
|
|
52
|
+
const r = await POST('/direct-receive/product-verification', { product_id: productId })
|
|
53
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
54
|
+
if (typeof toast$ === 'function') toast$(t('验证码已签发'))
|
|
55
|
+
window.dpHydrateSellerProductVerify()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.dpSubmitProductVerify = async (productId) => {
|
|
59
|
+
const el = document.getElementById('dp-pv-url-' + productId)
|
|
60
|
+
const url = el && el.value ? el.value.trim() : ''
|
|
61
|
+
if (!url) { if (typeof toast$ === 'function') toast$(t('请填写该商品的外部链接'), 'error'); return }
|
|
62
|
+
const r = await PUT('/direct-receive/product-verification', { product_id: productId, external_url: url })
|
|
63
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
64
|
+
if (typeof toast$ === 'function') toast$(t('已提交,等待管理员核验'))
|
|
65
|
+
window.dpHydrateSellerProductVerify()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ══════ ADMIN 审核队列 ══════
|
|
69
|
+
window.renderAdminProductVerifications = async function (app) {
|
|
70
|
+
if (!state.user) { renderLogin(); return }
|
|
71
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
72
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
73
|
+
const data = await GET('/admin/direct-receive/product-verifications?status=submitted')
|
|
74
|
+
if (!data || data.error) { app.innerHTML = shell(alert$('error', (data && data.error) || t('加载失败')), 'admin'); return }
|
|
75
|
+
const rows = data.verifications || []
|
|
76
|
+
const list = rows.length ? rows.map(v => `
|
|
77
|
+
<div class="card" style="margin-bottom:10px">
|
|
78
|
+
<div style="font-size:11px;color:#9ca3af">${escHtml(v.seller_id)} · ${escHtml(v.product_id)} · ${fmtTime(v.created_at)}</div>
|
|
79
|
+
<div style="font-size:13px;margin-top:6px">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code>${v.platform ? ` · ${escHtml(v.platform)}` : ''}</div>
|
|
80
|
+
<div style="font-size:12px;margin-top:4px">${t('外部链接')}: ${v.external_url ? `<a href="${encodeURI(v.external_url)}" target="_blank" rel="noopener noreferrer nofollow" style="color:#2563eb;text-decoration:underline;word-break:break-all">${escHtml(v.external_url)}</a>` : '—'}</div>
|
|
81
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('请手动打开链接,确认该商品页面上展示了上面的验证码,再决定通过/拒绝。')}</div>
|
|
82
|
+
<div style="display:flex;gap:6px;margin-top:10px">
|
|
83
|
+
<button class="btn btn-primary btn-sm" style="font-size:12px" onclick="window.doReviewProductVerify('${v.id}','verified')">✓ ${t('通过(真人 Passkey)')}</button>
|
|
84
|
+
<button class="btn btn-sm" style="font-size:12px;background:#dc2626;border-color:#dc2626;color:#fff" onclick="window.doReviewProductVerify('${v.id}','rejected')">✗ ${t('拒绝(真人 Passkey)')}</button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>`).join('') : `<div class="empty"><div class="empty-icon">📥</div><div class="empty-text">${t('暂无待核验商品')}</div></div>`
|
|
87
|
+
app.innerHTML = shell(`
|
|
88
|
+
<h1 class="page-title">🔖 ${t('逐产品直付验证审核')}</h1>
|
|
89
|
+
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
90
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('每个商品需单独核验:打开卖家提交的外部链接,确认页面展示了验证码再通过。通过=该商品可直付(逐品,绝不放行全部)。通过/拒绝均需真人 Passkey。')}</div>
|
|
91
|
+
${list}
|
|
92
|
+
`, 'admin')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// purpose_data 绑 verification_id + decision(签 A 用 B / 改结论一律拒)。
|
|
96
|
+
window.doReviewProductVerify = async function (id, decision) {
|
|
97
|
+
const token = await requestPasskeyGate('direct_pay_product_verify', { verification_id: id, decision })
|
|
98
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
99
|
+
const r = await POST(`/admin/direct-receive/product-verifications/${id}/review`, { decision, webauthn_token: token })
|
|
100
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
101
|
+
if (typeof toast$ === 'function') toast$(decision === 'verified' ? t('该商品已通过验证') : t('该商品已拒绝'))
|
|
102
|
+
window.renderAdminProductVerifications(document.getElementById('app'))
|
|
103
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — SELLER de-identified readiness panel (PR readiness-seller-ui)。UI ONLY。
|
|
2
|
+
// 只渲染【卖家可行动 / 脱敏状态】项(收款说明 / Passkey / 履约保证金 / 商户审核 / 暂停 / 平台开放)。
|
|
3
|
+
// 数据来自 GET /api/direct-receive/readiness(后端已脱敏:绝不下发 raw blocker、也绝不暴露 KYB/制裁/AML 分项)。
|
|
4
|
+
// 前端只做 code→卖家可读双语文案 的映射,绝不展示原始内部判定码。Direct Pay 仍 non-launchable / fail-closed。
|
|
5
|
+
// 不碰 wallet/escrow/settlement/refund/钱路;不开真实 rail;不声称已上线。买家侧不展示本面板。
|
|
6
|
+
|
|
7
|
+
// 内部 code → 卖家安全双语文案(ok / 未达成 两态 + 可行动入口)。绝不出现 KYB/sanctions/AML 字样。
|
|
8
|
+
window.dpSellerReadinessCopy = () => ({
|
|
9
|
+
PLATFORM_OPEN: { ok: t('平台侧直付已开放'), no: t('直付平台侧暂未开放(无需你操作)') },
|
|
10
|
+
PAYMENT_INSTRUCTION: { ok: t('已设置收款说明'), no: t('未设置收款说明'), action: t('去设置收款说明'), href: '#seller/settings' },
|
|
11
|
+
PASSKEY: { ok: t('已注册 Passkey'), no: t('未注册 Passkey'), action: t('去注册 Passkey'), href: '#me' },
|
|
12
|
+
BASE_BOND: { ok: t('履约保证金已完成'), no: t('履约保证金未完成') },
|
|
13
|
+
COMPLIANCE_REVIEW: { ok: t('商户审核已通过'), no: t('商户审核进行中或未通过') },
|
|
14
|
+
NOT_SUSPENDED: { ok: t('直付资格正常'), no: t('直付资格已被暂停') },
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
window.dpSellerReadinessSection = () => `
|
|
18
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
19
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🧭 ${t('直付开通进度(仅你可见)')}</div>
|
|
20
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('以上为你的直付开通进度;直付按轨道分阶段开放,完成可行动项不代表立即可用。')}</div>
|
|
21
|
+
<div id="dp-seller-readiness">${loading$()}</div>
|
|
22
|
+
</div>`
|
|
23
|
+
|
|
24
|
+
window.dpHydrateSellerReadiness = async () => {
|
|
25
|
+
const box = document.getElementById('dp-seller-readiness')
|
|
26
|
+
if (!box) return
|
|
27
|
+
const r = await GET('/direct-receive/readiness')
|
|
28
|
+
if (!r || r.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试')}</div>`; return }
|
|
29
|
+
const copy = window.dpSellerReadinessCopy()
|
|
30
|
+
const rows = (r.items || []).map(it => {
|
|
31
|
+
const c = copy[it.code]; if (!c) return ''
|
|
32
|
+
const icon = it.ok ? '✅' : (it.actionable ? '⚠️' : '⏳')
|
|
33
|
+
const text = it.ok ? c.ok : c.no
|
|
34
|
+
const act = (!it.ok && it.actionable && c.action) ? ` · <a href="${c.href}" style="color:#2563eb;font-weight:600;text-decoration:underline">${c.action}</a>` : ''
|
|
35
|
+
return `<li style="display:flex;gap:8px;align-items:flex-start;font-size:13px;line-height:1.7;color:#374151"><span>${icon}</span><span>${escHtml(text)}${act}</span></li>`
|
|
36
|
+
}).join('')
|
|
37
|
+
box.innerHTML = `<ul style="margin:0;padding-left:2px;list-style:none">${rows}</ul>`
|
|
38
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — 店铺认证 UI (PR-⑤b) = 逐品验证的【豁免】路径。UI ONLY。两个面:
|
|
2
|
+
// ① SELLER 自助(seller workbench settings):申领店铺验证码 → 贴到外部店铺页 → 提交店铺链接 → 看状态(含是否已豁免)。
|
|
3
|
+
// ② ADMIN 审核队列(#admin/store-verifications):打开卖家提交的店铺链接手动核对验证码,verify/reject;verify 时可【勾选
|
|
4
|
+
// “免逐品验证”】(per_product_exempt)→ 该卖家所有商品免逐品、可直付。ROOT + 真人 Passkey。
|
|
5
|
+
// 诚实:WebAZ 不抓取链接,admin 手动核。不碰 wallet/escrow/settlement/refund;只调既有受门控端点。
|
|
6
|
+
|
|
7
|
+
window.dpSvCopy = () => ({
|
|
8
|
+
issued: { icon: '①', text: t('已签发验证码,请贴到你的外部店铺页后提交店铺链接') },
|
|
9
|
+
submitted: { icon: '⏳', text: t('已提交,等待管理员核验') },
|
|
10
|
+
verified: { icon: '✅', text: t('店铺已通过验证') },
|
|
11
|
+
rejected: { icon: '🚫', text: t('未通过,请修正后重新申请') },
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// ══════ SELLER 自助面板 ══════
|
|
15
|
+
window.dpSellerStoreVerifySection = () => `
|
|
16
|
+
<div class="card" style="margin-bottom:12px;border:1px solid #e5e7eb">
|
|
17
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:4px">🏬 ${t('店铺认证(可申请免逐品验证)')}</div>
|
|
18
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('默认每个商品需单独验证才能直付。你也可以申请【店铺认证】:申领验证码 → 贴到你的外部店铺主页 → 提交店铺链接 → 管理员手动核对;通过且管理员勾选豁免后,你【所有商品】免逐品验证即可直付。')}</div>
|
|
19
|
+
<div id="dp-seller-sv">${loading$()}</div>
|
|
20
|
+
</div>`
|
|
21
|
+
|
|
22
|
+
window.dpHydrateSellerStoreVerify = async () => {
|
|
23
|
+
const box = document.getElementById('dp-seller-sv')
|
|
24
|
+
if (!box) return
|
|
25
|
+
const r = await GET('/direct-receive/store-verification')
|
|
26
|
+
if (!r || r.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试')}</div>`; return }
|
|
27
|
+
const v = r.verification, copy = window.dpSvCopy()
|
|
28
|
+
if (!v || v.status === 'rejected') {
|
|
29
|
+
const prior = v ? `<div style="font-size:12px;color:#6b7280;margin-bottom:8px">${copy.rejected.icon} ${escHtml(copy.rejected.text)}</div>` : ''
|
|
30
|
+
box.innerHTML = prior + `<button onclick="window.dpRequestStoreVerify()" style="padding:8px 16px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer">${t('申请店铺认证')}</button>`
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
const meta = copy[v.status] || { icon: 'ℹ️', text: v.status }
|
|
34
|
+
const lines = [`<div style="font-size:13px;font-weight:600;color:#374151">${meta.icon} ${escHtml(meta.text)}</div>`]
|
|
35
|
+
if (v.status === 'issued') {
|
|
36
|
+
lines.push(`<div style="font-size:12px;color:#374151;margin:6px 0">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code> · ${t('把它展示在你的外部店铺主页')}</div>`)
|
|
37
|
+
lines.push(`<input id="dp-sv-url" type="url" placeholder="${t('你的外部店铺链接(http/https)')}" style="width:100%;padding:6px;border:1px solid #d1d5db;border-radius:4px;font-size:12px;margin-bottom:6px">`)
|
|
38
|
+
lines.push(`<button onclick="window.dpSubmitStoreVerify()" style="padding:6px 12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:12px;font-weight:600;cursor:pointer">${t('提交店铺链接')}</button>`)
|
|
39
|
+
} else if (v.status === 'verified') {
|
|
40
|
+
lines.push(`<div style="font-size:12px;color:${r.exempt ? '#166534' : '#6b7280'};margin-top:4px">${r.exempt ? '✅ ' + t('已豁免逐品验证:你的所有商品可直付') : t('已通过,但未获逐品豁免:商品仍需逐个验证')}</div>`)
|
|
41
|
+
}
|
|
42
|
+
box.innerHTML = `<div style="display:flex;flex-direction:column;gap:2px">${lines.join('')}</div>`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
window.dpRequestStoreVerify = async () => {
|
|
46
|
+
const r = await POST('/direct-receive/store-verification', {})
|
|
47
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
48
|
+
if (typeof toast$ === 'function') toast$(t('验证码已签发'))
|
|
49
|
+
window.dpHydrateSellerStoreVerify()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
window.dpSubmitStoreVerify = async () => {
|
|
53
|
+
const el = document.getElementById('dp-sv-url')
|
|
54
|
+
const url = el && el.value ? el.value.trim() : ''
|
|
55
|
+
if (!url) { if (typeof toast$ === 'function') toast$(t('请填写你的外部店铺链接'), 'error'); return }
|
|
56
|
+
const r = await PUT('/direct-receive/store-verification', { external_url: url })
|
|
57
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
58
|
+
if (typeof toast$ === 'function') toast$(t('已提交,等待管理员核验'))
|
|
59
|
+
window.dpHydrateSellerStoreVerify()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ══════ ADMIN 审核队列 ══════
|
|
63
|
+
window.renderAdminStoreVerifications = async function (app) {
|
|
64
|
+
if (!state.user) { renderLogin(); return }
|
|
65
|
+
if (!isAdmin()) { app.innerHTML = shell(`<div class="alert alert-info">${t('仅限管理员')}</div>`, 'admin'); return }
|
|
66
|
+
app.innerHTML = shell(loading$(), 'admin')
|
|
67
|
+
const data = await GET('/admin/direct-receive/store-verifications?status=submitted')
|
|
68
|
+
if (!data || data.error) { app.innerHTML = shell(alert$('error', (data && data.error) || t('加载失败')), 'admin'); return }
|
|
69
|
+
const rows = data.verifications || []
|
|
70
|
+
const list = rows.length ? rows.map(v => `
|
|
71
|
+
<div class="card" style="margin-bottom:10px">
|
|
72
|
+
<div style="font-size:11px;color:#9ca3af">${escHtml(v.user_id)} · ${fmtTime(v.created_at)}</div>
|
|
73
|
+
<div style="font-size:13px;margin-top:6px">${t('验证码')}: <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-weight:700">${escHtml(v.code)}</code>${v.platform ? ` · ${escHtml(v.platform)}` : ''}</div>
|
|
74
|
+
<div style="font-size:12px;margin-top:4px">${t('店铺链接')}: ${v.external_url ? `<a href="${encodeURI(v.external_url)}" target="_blank" rel="noopener noreferrer nofollow" style="color:#2563eb;text-decoration:underline;word-break:break-all">${escHtml(v.external_url)}</a>` : '—'}</div>
|
|
75
|
+
<div style="font-size:11px;color:#9ca3af;margin-top:4px">${t('请手动打开链接,确认店铺主页展示了上面的验证码,再决定通过/拒绝。')}</div>
|
|
76
|
+
<label style="display:flex;align-items:center;gap:6px;font-size:12px;color:#374151;margin-top:8px"><input type="checkbox" id="sv-exempt-${v.id}"> ${t('免逐品验证(通过后该卖家所有商品可直付)')}</label>
|
|
77
|
+
<div style="display:flex;gap:6px;margin-top:8px">
|
|
78
|
+
<button class="btn btn-primary btn-sm" style="font-size:12px" onclick="window.doReviewStoreVerify('${v.id}','verified')">✓ ${t('通过(真人 Passkey)')}</button>
|
|
79
|
+
<button class="btn btn-sm" style="font-size:12px;background:#dc2626;border-color:#dc2626;color:#fff" onclick="window.doReviewStoreVerify('${v.id}','rejected')">✗ ${t('拒绝(真人 Passkey)')}</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`).join('') : `<div class="empty"><div class="empty-icon">📥</div><div class="empty-text">${t('暂无待核验店铺')}</div></div>`
|
|
82
|
+
app.innerHTML = shell(`
|
|
83
|
+
<h1 class="page-title">🏬 ${t('店铺认证审核')}</h1>
|
|
84
|
+
<div style="margin-bottom:12px"><button class="btn btn-outline btn-sm" style="width:auto" onclick="navigate('#admin')">${t('返回概览')}</button></div>
|
|
85
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('打开卖家提交的店铺链接,确认店铺页展示了验证码再通过。勾选“免逐品验证”=该卖家所有商品免逐个验证即可直付(请仅对可信商户勾选)。通过/拒绝均需真人 Passkey。')}</div>
|
|
86
|
+
${list}
|
|
87
|
+
`, 'admin')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// purpose_data 绑 verification_id + decision + per_product_exempt(改任一项一律拒)。
|
|
91
|
+
window.doReviewStoreVerify = async function (id, decision) {
|
|
92
|
+
const exempt = decision === 'verified' && !!(document.getElementById('sv-exempt-' + id) || {}).checked
|
|
93
|
+
const body = { verification_id: id, decision, per_product_exempt: exempt }
|
|
94
|
+
const token = await requestPasskeyGate('direct_pay_store_verify', body)
|
|
95
|
+
if (!token) { if (typeof toast$ === 'function') toast$(t('需要真人 Passkey 确认'), 'error'); return }
|
|
96
|
+
const r = await POST(`/admin/direct-receive/store-verifications/${id}/review`, { decision, per_product_exempt: exempt, webauthn_token: token })
|
|
97
|
+
if (!r || r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText ? window.dpErrorText(r && r.error_code, r && r.error) : t('操作失败,请重试'), 'error'); return }
|
|
98
|
+
if (typeof toast$ === 'function') toast$(decision === 'verified' ? (r.per_product_exempt ? t('店铺已通过,已豁免逐品验证') : t('店铺已通过')) : t('店铺申请已拒绝'))
|
|
99
|
+
window.renderAdminStoreVerifications(document.getElementById('app'))
|
|
100
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// Direct Pay (Rail 1) — bilingual UI wiring (PR-4f-b / 5c)。UI ONLY;直付(direct_p2p)可用性 per-seller/product 实时数据驱动(见 GET /api/direct-pay/availability);注:escrow 轨仍模拟 WAZ(独立开关 _wazSimulated,非直付)。
|
|
2
|
+
// WebAZ【不托管本金】、不担保、不退款/维权,也不验证/路由卖家付款方式或币种;不做 USDC/fiat/PSP/链上、不碰
|
|
3
|
+
// wallet/escrow/settlement/refund/订单钱路。真实强制全在后端(#92/#93 两次披露 + 现场真人 Passkey 硬门控;前端只带输入)。
|
|
4
|
+
// 只把已合并后端能力(#96 收款说明、#94 建单、#92/#93 ack+Passkey、#98+ 控制面/可用性)接到 PWA。
|
|
5
|
+
// 面向用户中文文案走 t(),英文在 i18n.js _EN(双语 parity 由 test-direct-pay-ui.ts 守)。
|
|
6
|
+
|
|
7
|
+
// ── 错误码 → 双语文案(后端返回的 error_code → 清楚的中英文提示)──────────────────────────────
|
|
8
|
+
window.dpErrorText = (code, fallback) => {
|
|
9
|
+
const M = {
|
|
10
|
+
DIRECT_PAY_DISABLED: t('直付当前未开放'),
|
|
11
|
+
DIRECT_PAY_RAIL_BREAKER: t('直付暂停受理(运营维护中),请稍后再试'),
|
|
12
|
+
DIRECT_PAY_REGION_UNSUPPORTED: t('直付在你所在地区暂未开放'),
|
|
13
|
+
DIRECT_PAY_CAP_EXCEEDED: t('超出直付单笔上限(按 WebAZ 记录的订单金额计;不涉及你与卖家场外实际付款金额)'),
|
|
14
|
+
DIRECT_PAY_SELLER_NOT_ELIGIBLE: t('该卖家暂不支持直付'),
|
|
15
|
+
DIRECT_PAY_SELLER_SUSPENDED: t('该卖家直付已被暂停'),
|
|
16
|
+
DISCLOSURE_NOT_ACKED: t('需先完成两次风险披露确认(D1 + D2)'),
|
|
17
|
+
HUMAN_PRESENCE_REQUIRED: t('需现场真人 Passkey 确认'),
|
|
18
|
+
PASSKEY_REQUIRED_FOR_DIRECT_PAY: t('直付需要先注册 Passkey'),
|
|
19
|
+
NO_PAYMENT_INSTRUCTION: t('卖家尚未设置收款说明,暂不可直付'),
|
|
20
|
+
DIRECT_PAY_NOT_AVAILABLE: t('该卖家暂不支持直付'),
|
|
21
|
+
DIRECT_PAY_KYC_REQUIRED: t('该卖家暂不支持直付'), // 对买家脱敏:不暴露卖家 KYC/制裁具体状态
|
|
22
|
+
ORDER_NOT_DELIVERED: t('订单尚未送达,暂不可确认收货'),
|
|
23
|
+
DIRECT_PAY_SIMPLE_PRODUCT_ONLY: t('直付当前仅支持简单商品(无规格)'),
|
|
24
|
+
DIRECT_PAY_UNSUPPORTED_OPTION: t('直付当前不支持该下单选项'),
|
|
25
|
+
}
|
|
26
|
+
return M[code] || fallback || t('操作失败,请重试')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── 买家结算:支付方式(rail)选择。escrow 默认;direct_p2p 可选,选中先查可用性(见 dpOnRailChange);D1/D2 ack 在建单后用 Passkey 完成。
|
|
30
|
+
window.dpRailSelectorHtml = (productId, priceUsdc) => `
|
|
31
|
+
<details style="margin-top:10px" id="dp-rail-block" data-product="${productId || ''}" data-amt="${priceUsdc != null && isFinite(Number(priceUsdc)) ? Number(priceUsdc) : ''}">
|
|
32
|
+
<summary style="font-size:13px;font-weight:600;color:#374151;cursor:pointer">${t('支付方式')}</summary>
|
|
33
|
+
<div style="padding:8px 2px 2px">
|
|
34
|
+
<label style="display:flex;gap:8px;align-items:flex-start;font-size:13px;margin-bottom:6px;cursor:pointer">
|
|
35
|
+
<input type="radio" name="dp-rail" value="escrow" ${window._wazSimulated ? '' : 'checked'} onchange="dpOnRailChange('${productId || ''}')">
|
|
36
|
+
<span><b>${window._wazSimulated ? t('托管(Escrow,模拟测试币)') : t('托管(Escrow,默认)')}</b><br><span style="font-size:11px;color:#6b7280">${t('本金由协议托管,确认收货后释放给卖家')}</span></span>
|
|
37
|
+
</label>
|
|
38
|
+
<label style="display:flex;gap:8px;align-items:flex-start;font-size:13px;cursor:pointer">
|
|
39
|
+
<input type="radio" name="dp-rail" value="direct_p2p" onchange="dpOnRailChange('${productId || ''}')">
|
|
40
|
+
<span><b>${t('直付(Direct Pay · 非托管)')}</b><br><span style="font-size:11px;color:#6b7280">${t('你直接付款给卖家(场外),本金不经 WebAZ')}</span></span>
|
|
41
|
+
</label>
|
|
42
|
+
<div id="dp-rail-note" style="display:none;margin-top:8px;font-size:11px;line-height:1.6;color:#92400e;background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
|
|
43
|
+
⚠️ ${t('直付风险提醒:WebAZ 不托管本金、不担保、不退款、不代维权,也不验证卖家的付款方式或币种。下单后需用 Passkey 完成两次风险确认,再标记付款。')}
|
|
44
|
+
<div style="margin-top:6px">${t('需要 Passkey。')}<a href="#me" style="color:#854d0e;font-weight:600;text-decoration:underline">${t('前往「我的 → 安全与存储」注册 →')}</a></div>
|
|
45
|
+
</div>
|
|
46
|
+
<div id="dp-rail-unavailable" style="display:none;margin-top:8px;font-size:11px;line-height:1.6;color:#991b1b;background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:8px 10px"></div><div id="dp-account-picker"></div>
|
|
47
|
+
</div>
|
|
48
|
+
</details>`
|
|
49
|
+
|
|
50
|
+
// 选直付先查 /direct-pay/availability(与建单门同源):available=true 才展示风险提醒+允许继续;否则用 dpErrorText 显示明确不可用原因(非系统错误/不裸露 JSON)并退回托管轨,阻止进入直付建单。
|
|
51
|
+
window.dpOnRailChange = async (productId) => {
|
|
52
|
+
const note = document.getElementById('dp-rail-note'); window._dpDirectAvailable = false // 进入即 pending:确认前 dpSelectedRail 只输出 escrow(防"选直付后立刻点确认"竞态)
|
|
53
|
+
const un = document.getElementById('dp-rail-unavailable')
|
|
54
|
+
if (note) note.style.display = 'none'
|
|
55
|
+
if (un) un.style.display = 'none'; { const _pk = document.getElementById('dp-account-picker'); if (_pk) _pk.innerHTML = '' } // D3:切轨/不可用即清空账号选择器
|
|
56
|
+
const _sel = document.querySelector('input[name="dp-rail"]:checked')?.value; if (window.wazEscrowRailNote) window.wazEscrowRailNote(_sel); if (_sel !== 'direct_p2p') return // [PRELAUNCH-WAZ-SIM] 选 escrow→测试币提醒
|
|
57
|
+
let av = null
|
|
58
|
+
try { av = await GET('/direct-pay/availability?product_id=' + encodeURIComponent(productId || '')) } catch { av = null }
|
|
59
|
+
if (av && av.available === true) { window._dpDirectAvailable = true; if (note) note.style.display = ''; if (window.dpLoadBuyerAccounts) window.dpLoadBuyerAccounts(productId); return } // D3:可用时加载卖家可选收款账号
|
|
60
|
+
if (un) { un.textContent = '⚠️ ' + window.dpErrorText(av && av.error_code, av && av.reason) ; un.style.display = '' } // 不可用:明确原因 + 退回 escrow
|
|
61
|
+
const esc = document.querySelector('input[name="dp-rail"][value="escrow"]')
|
|
62
|
+
if (esc) { esc.checked = true; if (window.wazEscrowRailNote) window.wazEscrowRailNote('escrow') } // [PRELAUNCH-WAZ-SIM] 直付不可用退回 escrow 时也提醒测试币
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 只有 availability 已确认 available:true(window._dpDirectAvailable)才输出 direct_p2p;pending/unavailable → escrow(竞态下确认只会下 escrow 单,绝不发 direct_p2p create)。
|
|
66
|
+
window.dpSelectedRail = () => { const c = document.querySelector('input[name="dp-rail"]:checked')?.value; if (c === 'direct_p2p') return window._dpDirectAvailable === true ? 'direct_p2p' : ''; return c === 'escrow' ? 'escrow' : (window._wazSimulated ? '' : 'escrow') } // #28 直付选中但未确认→''(永不静默回退 escrow,不分模拟);[PRELAUNCH-WAZ-SIM] 模拟期未选→''
|
|
67
|
+
|
|
68
|
+
// ── 建单成功后(direct_p2p):D1/D2 披露 ack(各一次 Passkey)→ ack 后才展示快照收款说明 → 跳订单页。res=POST /orders 返回;有错则双语提示并停。
|
|
69
|
+
window.dpAfterCreate = async (res) => {
|
|
70
|
+
if (!res || res.error || res.error_code) {
|
|
71
|
+
const m = window.dpErrorText(res?.error_code, res?.error)
|
|
72
|
+
if (typeof toast$ === 'function') toast$(m, 'error'); else alert(m)
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
const orderId = res.order_id
|
|
76
|
+
// 边界:必须【先】完成 D1/D2 + Passkey,之后【才】展示卖家收款说明(快照)。未完成则不展示,留到订单页继续。
|
|
77
|
+
const acked = await window.dpEnsureAcks(orderId)
|
|
78
|
+
if (!acked) {
|
|
79
|
+
if (typeof toast$ === 'function') toast$(t('需完成 D1/D2 Passkey 风险确认后才显示收款说明 · 可在订单页继续'), 'info')
|
|
80
|
+
setTimeout(() => navigate(`#order/${orderId}`), 600)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
// 两次披露已 ack —— 收款说明【不】来自 create 响应(后端在 both-acked 前不下发);现在才 GET 订单读取 redaction-gated 快照。
|
|
84
|
+
const o = await GET(`/orders/${orderId}`)
|
|
85
|
+
const instr = o && o.order ? (o.order.direct_pay_instruction_snapshot || '') : ''
|
|
86
|
+
await confirmModal(
|
|
87
|
+
`${t('风险确认完成 · 这是卖家的收款说明(下单时快照,WebAZ 不验证)')}\n\n${instr}\n\n${t('请按此付款,然后在订单页标记"我已付款"')}`,
|
|
88
|
+
t('我知道了'), {})
|
|
89
|
+
setTimeout(() => navigate(`#order/${orderId}`), 300)
|
|
90
|
+
}
|
|
91
|
+
// ── 确保两次披露都已 ack(缺哪个补哪个;每次都需现场真人 Passkey)。返回 both-acked 与否。 ──────────
|
|
92
|
+
window.dpEnsureAcks = async (orderId) => {
|
|
93
|
+
const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
|
|
94
|
+
if (st.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(st.error_code, st.error), 'error'); return false }
|
|
95
|
+
const disc = st.disclosures || {}
|
|
96
|
+
const stages = [
|
|
97
|
+
{ key: 'pre_select', acked: st.acked?.pre_select, text: disc.pre_select },
|
|
98
|
+
{ key: 'pre_confirm', acked: st.acked?.pre_confirm, text: disc.pre_confirm },
|
|
99
|
+
]
|
|
100
|
+
for (const s of stages) {
|
|
101
|
+
if (s.acked) continue
|
|
102
|
+
const body = s.text ? (window._lang === 'en' ? s.text.en : s.text.zh) : t('风险披露')
|
|
103
|
+
const go = await confirmModal(`${body}\n\n${t('我已阅读并理解上述风险')}`, t('确认(需 Passkey)'), { danger: true })
|
|
104
|
+
if (!go) return false
|
|
105
|
+
const ok = await window.dpDoAck(orderId, s.key)
|
|
106
|
+
if (!ok) return false
|
|
107
|
+
}
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
// Passkey 门失败(无 Passkey / 设备不支持 / 用户取消)→ 给出明确的【注册 Passkey 入口】(前往「我的 → 安全与存储」)。
|
|
111
|
+
window.dpPromptRegisterPasskey = async (reason) => {
|
|
112
|
+
const go = await confirmModal(
|
|
113
|
+
`${reason ? reason + '\n\n' : ''}${t('直付的风险确认与付款标记需要 Passkey。前往「我的 → 安全与存储」注册一个?')}`,
|
|
114
|
+
t('前往注册 Passkey'), {})
|
|
115
|
+
if (go) navigate('#me')
|
|
116
|
+
}
|
|
117
|
+
// 单次披露 ack:Passkey ceremony(purpose=direct_pay_disclosure_ack,order+stage 绑 purpose_data)→ POST ack。
|
|
118
|
+
window.dpDoAck = async (orderId, stage) => {
|
|
119
|
+
let token
|
|
120
|
+
try { token = await requestPasskeyGate('direct_pay_disclosure_ack', { order_id: orderId, stage }) }
|
|
121
|
+
catch (e) { await window.dpPromptRegisterPasskey(e && e.message); return false }
|
|
122
|
+
const r = await POST('/direct-pay/disclosure-acks', { order_id: orderId, stage, webauthn_token: token })
|
|
123
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(r.error_code, r.error), 'error'); return false }
|
|
124
|
+
return true
|
|
125
|
+
}
|
|
126
|
+
// ── 订单详情:direct_p2p 诚实边界始终显示;卖家收款说明快照【不】内联进 HTML,由 dpHydrateOrderDisclosure 在 both-acked 后才另取渲染(未 ack 时 DOM 里也没有快照)。
|
|
127
|
+
window.dpOrderDisclosureHtml = (_order) => `
|
|
128
|
+
<div class="card" style="border:1px solid #fde68a;background:linear-gradient(135deg,#fffbeb,#fef3c7)">
|
|
129
|
+
<div style="font-size:13px;font-weight:700;color:#92400e;margin-bottom:8px">💸 ${t('直付订单(非托管)')}</div>
|
|
130
|
+
<ul style="margin:0 0 8px;padding-left:18px;font-size:12px;line-height:1.7;color:#374151">
|
|
131
|
+
<li>${t('本金不经 WebAZ —— 你直接付款给卖家(场外)')}</li>
|
|
132
|
+
<li>${t('WebAZ 不托管、不担保、不退款、不代维权')}</li>
|
|
133
|
+
<li>${t('卖家收款说明来自卖家自填,WebAZ 不验证付款方式或币种')}</li>
|
|
134
|
+
</ul>
|
|
135
|
+
<div id="dp-order-instr">${loading$()}</div>
|
|
136
|
+
</div>`
|
|
137
|
+
// ack-gated 收款说明:both-acked → 另取订单快照并展示;否则只显示"先完成 D1/D2 Passkey"的门(快照不入 DOM)。
|
|
138
|
+
window.dpHydrateOrderDisclosure = async (orderId) => {
|
|
139
|
+
const box = document.getElementById('dp-order-instr')
|
|
140
|
+
if (!box) return
|
|
141
|
+
const st = await GET(`/direct-pay/disclosure-acks/${orderId}`)
|
|
142
|
+
if (st.error) { box.innerHTML = `<div style="font-size:12px;color:#dc2626">${window.dpErrorText(st.error_code, st.error)}</div>`; return }
|
|
143
|
+
if (!st.both) {
|
|
144
|
+
box.innerHTML = `<div style="font-size:12px;color:#92400e;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
|
|
145
|
+
${t('需先完成 D1/D2 Passkey 风险确认,确认后才显示卖家收款说明。')}
|
|
146
|
+
<div style="margin-top:6px"><button class="btn btn-outline btn-sm" onclick="dpCompleteAcksThenReveal('${orderId}')">${t('完成风险确认')}</button></div></div>`
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
const o = await GET(`/orders/${orderId}`)
|
|
150
|
+
const snap = o && o.order ? (o.order.direct_pay_instruction_snapshot || '') : ''
|
|
151
|
+
box.innerHTML = snap ? `<div style="font-size:12px;color:#374151;background:#fff;border:1px solid #fde68a;border-radius:8px;padding:8px 10px">
|
|
152
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t('卖家收款说明(下单时快照)')}</div>${escHtml(snap)}<div id="dp-order-qr"></div></div>`
|
|
153
|
+
: `<div style="font-size:12px;color:#9ca3af">${t('卖家尚未设置收款说明,暂不可直付')}</div>`; if (snap && window.dpLoadOrderQr) window.dpLoadOrderQr(orderId) // D3:ack 门后取所选账号的收款二维码(若有)
|
|
154
|
+
}
|
|
155
|
+
window.dpCompleteAcksThenReveal = async (orderId) => {
|
|
156
|
+
const ok = await window.dpEnsureAcks(orderId)
|
|
157
|
+
if (ok) window.dpHydrateOrderDisclosure(orderId)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── 订单动作(mark_paid/confirm/confirm_in_person):前端先确保披露 ack,再取一次性 Passkey token 带进 order-action 端点;后端(#93)仍强制两次披露+Passkey。
|
|
161
|
+
window.dpHandleAction = async (orderId, action) => {
|
|
162
|
+
const msgEl = document.getElementById('action-msg')
|
|
163
|
+
const show = (type, m) => { if (msgEl) msgEl.innerHTML = alert$(type, m); else if (typeof toast$ === 'function') toast$(m, type) }
|
|
164
|
+
// mark_paid 前必须两次披露都 ack(confirm/confirm_in_person 通常已在 mark_paid 阶段 ack 过,这里兜底再查)。
|
|
165
|
+
const acked = await window.dpEnsureAcks(orderId)
|
|
166
|
+
if (!acked) { show('error', window.dpErrorText('DISCLOSURE_NOT_ACKED')); return }
|
|
167
|
+
let token
|
|
168
|
+
try { token = await requestPasskeyGate('direct_pay_order_action', { order_id: orderId, action }) }
|
|
169
|
+
catch (e) { await window.dpPromptRegisterPasskey(e && e.message); return }
|
|
170
|
+
show('info', `<span class="spinner"></span>${t('处理中...')}`)
|
|
171
|
+
const path = action === 'confirm_in_person' ? `/orders/${orderId}/confirm-in-person` : `/orders/${orderId}/action`
|
|
172
|
+
const body = action === 'confirm_in_person' ? { webauthn_token: token } : { action, webauthn_token: token }
|
|
173
|
+
const r = await POST(path, body)
|
|
174
|
+
if (r.error) { show('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
175
|
+
show('success', t('操作成功'))
|
|
176
|
+
setTimeout(() => renderOrderDetail(document.getElementById('app'), orderId), 1000)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── 卖家:收款说明设置(GET / PUT / DELETE,仅展示文本)─────────────────────────────────────────
|
|
180
|
+
window.dpSellerInstructionSection = () => `
|
|
181
|
+
<div class="card" style="margin-bottom:12px">
|
|
182
|
+
<div style="font-size:14px;font-weight:700;margin-bottom:6px">💳 ${t('直付收款说明')}</div>
|
|
183
|
+
<div style="font-size:12px;color:#6b7280;line-height:1.6;margin-bottom:10px">${t('这是你自填的收款展示文本(场外结算用);WebAZ 不验证付款方式或币种,不路由支付,不托管资金。')}</div>
|
|
184
|
+
<div id="dp-instr-box">${loading$()}</div>
|
|
185
|
+
</div>`
|
|
186
|
+
|
|
187
|
+
window.dpHydrateInstruction = async () => {
|
|
188
|
+
const box = document.getElementById('dp-instr-box')
|
|
189
|
+
if (!box) return
|
|
190
|
+
const r = await GET('/direct-receive/payment-instruction')
|
|
191
|
+
if (r.error) { box.innerHTML = alert$('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
192
|
+
const cur = r.instruction
|
|
193
|
+
box.innerHTML = `
|
|
194
|
+
${cur ? `<div style="font-size:12px;color:#374151;background:#f9fafb;border:1px solid #e5e7eb;border-radius:8px;padding:8px 10px;margin-bottom:10px">
|
|
195
|
+
<div style="font-size:11px;color:#9ca3af;margin-bottom:2px">${t('当前生效')}${cur.label ? ` · ${escHtml(cur.label)}` : ''}</div>${escHtml(cur.instruction)}</div>` :
|
|
196
|
+
`<div style="font-size:12px;color:#9ca3af;margin-bottom:10px">${t('尚未设置收款说明')}</div>`}
|
|
197
|
+
<div id="dp-instr-msg"></div>
|
|
198
|
+
<div class="form-group"><label class="form-label">${t('收款说明')} <span style="font-size:11px;color:#9ca3af">${t('(展示给买家,如 PayNow / 银行转账 等)')}</span></label>
|
|
199
|
+
<textarea class="form-control" id="dp-instr-text" rows="3" maxlength="500" placeholder="${t('例:PayNow +65 9xxx(场外结算)')}">${cur ? escHtml(cur.instruction) : ''}</textarea></div>
|
|
200
|
+
<div class="form-group"><label class="form-label">${t('标签(可选)')}</label>
|
|
201
|
+
<input class="form-control" id="dp-instr-label" maxlength="40" value="${cur && cur.label ? escHtml(cur.label) : ''}" placeholder="${t('如 PayNow')}"></div>
|
|
202
|
+
<div style="display:flex;gap:8px">
|
|
203
|
+
<button class="btn btn-primary btn-sm" onclick="dpSaveInstruction()">${t('保存')}</button>
|
|
204
|
+
${cur ? `<button class="btn btn-outline btn-sm" style="color:#dc2626;border-color:#dc2626" onclick="dpDeleteInstruction()">${t('停用')}</button>` : ''}
|
|
205
|
+
</div>`
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
window.dpSaveInstruction = async () => {
|
|
209
|
+
const msg = document.getElementById('dp-instr-msg')
|
|
210
|
+
const instruction = document.getElementById('dp-instr-text')?.value?.trim() || ''
|
|
211
|
+
const label = document.getElementById('dp-instr-label')?.value?.trim() || ''
|
|
212
|
+
if (!instruction) { if (msg) msg.innerHTML = alert$('error', t('收款说明不能为空')); return }
|
|
213
|
+
if (msg) msg.innerHTML = `<div class="alert alert-info"><span class="spinner"></span>${t('保存中...')}</div>`
|
|
214
|
+
const r = await PUT('/direct-receive/payment-instruction', { instruction, label })
|
|
215
|
+
if (r.error) { if (msg) msg.innerHTML = alert$('error', window.dpErrorText(r.error_code, r.error)); return }
|
|
216
|
+
if (typeof toast$ === 'function') toast$(t('已保存'), 'success')
|
|
217
|
+
window.dpHydrateInstruction()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
window.dpDeleteInstruction = async () => {
|
|
221
|
+
const go = await confirmModal(t('停用后买家将无法对你发起直付订单,确定停用?'), t('停用'), { danger: true })
|
|
222
|
+
if (!go) return
|
|
223
|
+
const r = await api('DELETE', '/direct-receive/payment-instruction')
|
|
224
|
+
if (r.error) { if (typeof toast$ === 'function') toast$(window.dpErrorText(r.error_code, r.error), 'error'); return }
|
|
225
|
+
if (typeof toast$ === 'function') toast$(t('已停用'), 'success')
|
|
226
|
+
window.dpHydrateInstruction()
|
|
227
|
+
}
|