Add payer-wallet override flow with ownership proof in store
This commit is contained in:
parent
8f349a899a
commit
a215a7d0f0
@ -44,6 +44,7 @@ Implemented now:
|
|||||||
17. Download endpoints now validate wallet membership status before authorizing channel messaging.
|
17. Download endpoints now validate wallet membership status before authorizing channel messaging.
|
||||||
18. Governance install API contract, examples, backend handoff checklist, and conformance vectors.
|
18. Governance install API contract, examples, backend handoff checklist, and conformance vectors.
|
||||||
19. Repo boundary blueprint and free launcher specification aligned with first paid governance model.
|
19. Repo boundary blueprint and free launcher specification aligned with first paid governance model.
|
||||||
|
20. Store UI now supports distinct payer wallet overrides with ownership-proof signing before quote requests.
|
||||||
|
|
||||||
Remaining in this repo:
|
Remaining in this repo:
|
||||||
|
|
||||||
|
|||||||
@ -111,6 +111,16 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: #3d434a;
|
color: #3d434a;
|
||||||
}
|
}
|
||||||
|
input {
|
||||||
|
border: 1px solid #c2c8d0;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-family: 'IBM Plex Mono', 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: #3d434a;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
.status-log {
|
.status-log {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
border-top: 1px solid #d0d5db;
|
border-top: 1px solid #d0d5db;
|
||||||
@ -144,10 +154,14 @@
|
|||||||
<p class="line">Wallet: <span class="mono" id="wallet-label">not connected</span></p>
|
<p class="line">Wallet: <span class="mono" id="wallet-label">not connected</span></p>
|
||||||
<p class="line">Membership status: <span class="mono" id="membership-label">unknown</span></p>
|
<p class="line">Membership status: <span class="mono" id="membership-label">unknown</span></p>
|
||||||
<p class="line">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
|
<p class="line">Gate decision: <span class="mono" id="gate-label">blocked</span></p>
|
||||||
|
<p class="line">Payer wallet override (optional):</p>
|
||||||
|
<input id="payer-wallet-input" type="text" placeholder="0x... (leave blank to use ownership wallet)">
|
||||||
|
<p class="line">Ownership proof: <span class="mono" id="proof-label">not required</span></p>
|
||||||
<span class="state block" id="gate-pill">membership required</span>
|
<span class="state block" id="gate-pill">membership required</span>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="connect-btn" type="button">connect wallet</button>
|
<button id="connect-btn" type="button">connect wallet</button>
|
||||||
<button id="refresh-btn" type="button">refresh state</button>
|
<button id="refresh-btn" type="button">refresh state</button>
|
||||||
|
<button id="sign-proof-btn" type="button">sign ownership proof</button>
|
||||||
<label class="line" for="mock-select">mock mode:</label>
|
<label class="line" for="mock-select">mock mode:</label>
|
||||||
<select id="mock-select" aria-label="Mock state override">
|
<select id="mock-select" aria-label="Mock state override">
|
||||||
<option value="">live</option>
|
<option value="">live</option>
|
||||||
@ -168,7 +182,7 @@
|
|||||||
<p class="line" id="offer-summary">Catalog data pending.</p>
|
<p class="line" id="offer-summary">Catalog data pending.</p>
|
||||||
<p class="line" id="offer-price">Price: --</p>
|
<p class="line" id="offer-price">Price: --</p>
|
||||||
<p class="line" id="offer-policy">Policy: --</p>
|
<p class="line" id="offer-policy">Policy: --</p>
|
||||||
<p class="line">Action chain: membership check -> quote -> wallet confirm -> entitlement receipt</p>
|
<p class="line">Action chain: membership check -> ownership proof (if needed) -> quote -> wallet confirm -> entitlement receipt</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="checkout-btn" type="button" disabled>request checkout quote</button>
|
<button id="checkout-btn" type="button" disabled>request checkout quote</button>
|
||||||
</div>
|
</div>
|
||||||
@ -193,6 +207,7 @@
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
wallet: null,
|
wallet: null,
|
||||||
|
ownershipProof: null,
|
||||||
membership: 'unknown',
|
membership: 'unknown',
|
||||||
gate: false,
|
gate: false,
|
||||||
source: 'live',
|
source: 'live',
|
||||||
@ -208,8 +223,11 @@
|
|||||||
const checkoutLog = document.getElementById('checkout-log');
|
const checkoutLog = document.getElementById('checkout-log');
|
||||||
const connectBtn = document.getElementById('connect-btn');
|
const connectBtn = document.getElementById('connect-btn');
|
||||||
const refreshBtn = document.getElementById('refresh-btn');
|
const refreshBtn = document.getElementById('refresh-btn');
|
||||||
|
const signProofBtn = document.getElementById('sign-proof-btn');
|
||||||
const checkoutBtn = document.getElementById('checkout-btn');
|
const checkoutBtn = document.getElementById('checkout-btn');
|
||||||
const mockSelect = document.getElementById('mock-select');
|
const mockSelect = document.getElementById('mock-select');
|
||||||
|
const payerWalletInput = document.getElementById('payer-wallet-input');
|
||||||
|
const proofLabel = document.getElementById('proof-label');
|
||||||
const offerSelect = document.getElementById('offer-select');
|
const offerSelect = document.getElementById('offer-select');
|
||||||
const offerTitle = document.getElementById('offer-title');
|
const offerTitle = document.getElementById('offer-title');
|
||||||
const offerSummary = document.getElementById('offer-summary');
|
const offerSummary = document.getElementById('offer-summary');
|
||||||
@ -221,6 +239,33 @@
|
|||||||
return wallet.slice(0, 6) + '...' + wallet.slice(-4);
|
return wallet.slice(0, 6) + '...' + wallet.slice(-4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeWallet(value) {
|
||||||
|
return String(value || '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidWallet(wallet) {
|
||||||
|
return /^0x[a-fA-F0-9]{40}$/.test(wallet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPayerWallet() {
|
||||||
|
const override = normalizeWallet(payerWalletInput.value);
|
||||||
|
return override || state.wallet;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProofLabel() {
|
||||||
|
const payer = getPayerWallet();
|
||||||
|
if (!state.wallet || !payer || payer.toLowerCase() === state.wallet.toLowerCase()) {
|
||||||
|
proofLabel.textContent = 'not required';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.ownershipProof && state.ownershipProof.payer_wallet &&
|
||||||
|
state.ownershipProof.payer_wallet.toLowerCase() === payer.toLowerCase()) {
|
||||||
|
proofLabel.textContent = 'signed';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
proofLabel.textContent = 'required';
|
||||||
|
}
|
||||||
|
|
||||||
function setLog(message) {
|
function setLog(message) {
|
||||||
statusLog.textContent = message;
|
statusLog.textContent = message;
|
||||||
}
|
}
|
||||||
@ -294,6 +339,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
|
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
|
||||||
|
updateProofLabel();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMockFromQuery() {
|
function getMockFromQuery() {
|
||||||
@ -404,6 +450,7 @@
|
|||||||
throw new Error('Wallet connection not approved.');
|
throw new Error('Wallet connection not approved.');
|
||||||
}
|
}
|
||||||
state.wallet = accounts[0];
|
state.wallet = accounts[0];
|
||||||
|
state.ownershipProof = null;
|
||||||
setLog('Wallet connected: ' + abbreviateWallet(state.wallet) + '.');
|
setLog('Wallet connected: ' + abbreviateWallet(state.wallet) + '.');
|
||||||
await refreshMembershipState();
|
await refreshMembershipState();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -411,6 +458,62 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function signOwnershipProof() {
|
||||||
|
if (!state.wallet) {
|
||||||
|
setLog('Connect ownership wallet first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.ethereum) {
|
||||||
|
setLog('No wallet provider detected for signing.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payer = getPayerWallet();
|
||||||
|
if (!payer) {
|
||||||
|
setLog('Payer wallet is empty.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isValidWallet(payer)) {
|
||||||
|
setLog('Payer wallet format is invalid.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payer.toLowerCase() === state.wallet.toLowerCase()) {
|
||||||
|
state.ownershipProof = null;
|
||||||
|
updateProofLabel();
|
||||||
|
setLog('Ownership proof is not required when payer equals ownership wallet.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selectedOffer = state.selectedOfferId || 'none';
|
||||||
|
const issuedAt = new Date().toISOString();
|
||||||
|
const message = [
|
||||||
|
'EDUT OWNERSHIP PROOF',
|
||||||
|
'Ownership wallet: ' + state.wallet,
|
||||||
|
'Payer wallet: ' + payer,
|
||||||
|
'Offer: ' + selectedOffer,
|
||||||
|
'Issued at: ' + issuedAt,
|
||||||
|
].join('\\n');
|
||||||
|
|
||||||
|
const signature = await window.ethereum.request({
|
||||||
|
method: 'personal_sign',
|
||||||
|
params: [message, state.wallet],
|
||||||
|
});
|
||||||
|
|
||||||
|
state.ownershipProof = {
|
||||||
|
payer_wallet: payer,
|
||||||
|
signature: signature,
|
||||||
|
message: message,
|
||||||
|
issued_at: issuedAt,
|
||||||
|
};
|
||||||
|
updateProofLabel();
|
||||||
|
setLog('Ownership proof signed for payer wallet override.');
|
||||||
|
} catch (err) {
|
||||||
|
setLog('Ownership proof signing failed: ' + err.message + '.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function requestCheckoutQuote() {
|
async function requestCheckoutQuote() {
|
||||||
if (!state.gate) {
|
if (!state.gate) {
|
||||||
setCheckoutLog('Checkout blocked: membership is not active.');
|
setCheckoutLog('Checkout blocked: membership is not active.');
|
||||||
@ -422,26 +525,49 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payerWallet = getPayerWallet();
|
||||||
|
if (!payerWallet || !isValidWallet(payerWallet)) {
|
||||||
|
setCheckoutLog('Checkout blocked: payer wallet is invalid.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usingDistinctPayer = payerWallet.toLowerCase() !== state.wallet.toLowerCase();
|
||||||
|
if (usingDistinctPayer) {
|
||||||
|
if (!state.ownershipProof ||
|
||||||
|
!state.ownershipProof.signature ||
|
||||||
|
!state.ownershipProof.payer_wallet ||
|
||||||
|
state.ownershipProof.payer_wallet.toLowerCase() !== payerWallet.toLowerCase()) {
|
||||||
|
setCheckoutLog('Checkout blocked: sign ownership proof for the payer wallet override.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setCheckoutLog('Requesting quote...');
|
setCheckoutLog('Requesting quote...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const payload = {
|
||||||
|
wallet: state.wallet,
|
||||||
|
offer_id: state.selectedOfferId,
|
||||||
|
};
|
||||||
|
if (usingDistinctPayer) {
|
||||||
|
payload.payer_wallet = payerWallet;
|
||||||
|
payload.ownership_proof = state.ownershipProof.signature;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch('/marketplace/checkout/quote', {
|
const response = await fetch('/marketplace/checkout/quote', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload),
|
||||||
wallet: state.wallet,
|
|
||||||
offer_id: state.selectedOfferId,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('HTTP ' + response.status);
|
throw new Error('HTTP ' + response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = await response.json();
|
const quotePayload = await response.json();
|
||||||
const quoteId = payload.quote_id || 'unknown';
|
const quoteId = quotePayload.quote_id || 'unknown';
|
||||||
const amount = payload.amount || payload.amount_atomic || 'unknown';
|
const amount = quotePayload.amount || quotePayload.amount_atomic || 'unknown';
|
||||||
const currency = payload.currency || 'unknown';
|
const currency = quotePayload.currency || 'unknown';
|
||||||
setCheckoutLog('Quote ready for ' + state.selectedOfferId + ': ' + quoteId + ' (' + amount + ' ' + currency + ').');
|
setCheckoutLog('Quote ready for ' + state.selectedOfferId + ': ' + quoteId + ' (' + amount + ' ' + currency + ').');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
|
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
|
||||||
@ -450,10 +576,16 @@
|
|||||||
|
|
||||||
connectBtn.addEventListener('click', connectWallet);
|
connectBtn.addEventListener('click', connectWallet);
|
||||||
refreshBtn.addEventListener('click', refreshMembershipState);
|
refreshBtn.addEventListener('click', refreshMembershipState);
|
||||||
|
signProofBtn.addEventListener('click', signOwnershipProof);
|
||||||
checkoutBtn.addEventListener('click', requestCheckoutQuote);
|
checkoutBtn.addEventListener('click', requestCheckoutQuote);
|
||||||
mockSelect.addEventListener('change', refreshMembershipState);
|
mockSelect.addEventListener('change', refreshMembershipState);
|
||||||
|
payerWalletInput.addEventListener('input', function () {
|
||||||
|
state.ownershipProof = null;
|
||||||
|
updateProofLabel();
|
||||||
|
});
|
||||||
offerSelect.addEventListener('change', function () {
|
offerSelect.addEventListener('change', function () {
|
||||||
state.selectedOfferId = offerSelect.value;
|
state.selectedOfferId = offerSelect.value;
|
||||||
|
state.ownershipProof = null;
|
||||||
renderSelectedOffer();
|
renderSelectedOffer();
|
||||||
applyGateState();
|
applyGateState();
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user