Add data-backed store catalog and selected-offer quote flow

This commit is contained in:
Joshua 2026-02-17 12:04:45 -08:00
parent df0788fb43
commit 6158460b8c
3 changed files with 113 additions and 7 deletions

View File

@ -8,6 +8,7 @@ Public-facing EDUT web surfaces and deployment specs.
public/
index.html
store/index.html
store/offers.json
trust/index.html
privacy/index.html
terms/index.html

View File

@ -161,10 +161,13 @@
</section>
<section class="card">
<p class="label">Offer Skeleton</p>
<p class="title">EDUT CRM Pro</p>
<p class="line">Price: 199.00 USDC</p>
<p class="line">Policy: member-only, workspace-bound, non-transferable</p>
<p class="label">Offer Catalog</p>
<p class="line">Offer:</p>
<select id="offer-select" aria-label="Select offer"></select>
<p class="title" id="offer-title">Loading offers...</p>
<p class="line" id="offer-summary">Catalog data pending.</p>
<p class="line" id="offer-price">Price: --</p>
<p class="line" id="offer-policy">Policy: --</p>
<p class="line">Action chain: membership check -> quote -> wallet confirm -> entitlement receipt</p>
<div class="actions">
<button id="checkout-btn" type="button" disabled>request checkout quote</button>
@ -193,6 +196,8 @@
membership: 'unknown',
gate: false,
source: 'live',
offers: [],
selectedOfferId: null,
};
const walletLabel = document.getElementById('wallet-label');
@ -205,6 +210,11 @@
const refreshBtn = document.getElementById('refresh-btn');
const checkoutBtn = document.getElementById('checkout-btn');
const mockSelect = document.getElementById('mock-select');
const offerSelect = document.getElementById('offer-select');
const offerTitle = document.getElementById('offer-title');
const offerSummary = document.getElementById('offer-summary');
const offerPrice = document.getElementById('offer-price');
const offerPolicy = document.getElementById('offer-policy');
function abbreviateWallet(wallet) {
if (!wallet || wallet.length < 10) return wallet || 'not connected';
@ -219,6 +229,43 @@
checkoutLog.textContent = message;
}
function getSelectedOffer() {
if (!state.selectedOfferId) return null;
return state.offers.find((offer) => offer.offer_id === state.selectedOfferId) || null;
}
function renderSelectedOffer() {
const selected = getSelectedOffer();
if (!selected) {
offerTitle.textContent = 'No offer selected';
offerSummary.textContent = 'Catalog data is unavailable.';
offerPrice.textContent = 'Price: --';
offerPolicy.textContent = 'Policy: --';
return;
}
offerTitle.textContent = selected.title || selected.offer_id;
offerSummary.textContent = selected.summary || 'No summary provided.';
offerPrice.textContent = 'Price: ' + (selected.price || '--') + ' ' + (selected.currency || '');
offerPolicy.textContent = 'Policy: member-only=' + Boolean(selected.member_only) +
', workspace-bound=' + Boolean(selected.workspace_bound) +
', transferable=' + Boolean(selected.transferable);
}
function populateOfferSelect() {
offerSelect.innerHTML = '';
for (const offer of state.offers) {
const option = document.createElement('option');
option.value = offer.offer_id;
option.textContent = offer.offer_id;
offerSelect.appendChild(option);
}
if (state.selectedOfferId) {
offerSelect.value = state.selectedOfferId;
}
renderSelectedOffer();
}
function normalizeMembership(raw) {
const value = String(raw || '').toLowerCase();
if (value === 'active') return 'active';
@ -246,7 +293,7 @@
gatePill.textContent = 'membership required';
}
checkoutBtn.disabled = !state.gate;
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
}
function getMockFromQuery() {
@ -273,6 +320,33 @@
return response.json();
}
async function loadOffers() {
try {
const payload = await fetchJson('/store/offers.json');
if (!payload || !Array.isArray(payload.offers) || payload.offers.length === 0) {
throw new Error('offers catalog is empty');
}
state.offers = payload.offers;
state.selectedOfferId = payload.offers[0].offer_id;
populateOfferSelect();
setCheckoutLog('Offer catalog loaded: ' + payload.offers.length + ' offers.');
} catch (err) {
state.offers = [{
offer_id: 'edut.crm.pro.annual',
title: 'EDUT CRM Pro (fallback)',
summary: 'Fallback offer loaded because catalog fetch failed.',
price: '199.00',
currency: 'USDC',
member_only: true,
workspace_bound: true,
transferable: false,
}];
state.selectedOfferId = state.offers[0].offer_id;
populateOfferSelect();
setCheckoutLog('Catalog load failed: ' + err.message + '. Using fallback offer.');
}
}
async function fetchLiveMembershipStatus() {
if (!state.wallet) {
throw new Error('Connect wallet first.');
@ -356,7 +430,7 @@
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
wallet: state.wallet,
offer_id: 'edut.crm.pro.annual',
offer_id: state.selectedOfferId,
}),
});
@ -368,7 +442,7 @@
const quoteId = payload.quote_id || 'unknown';
const amount = payload.amount || payload.amount_atomic || 'unknown';
const currency = payload.currency || 'unknown';
setCheckoutLog('Quote ready: ' + quoteId + ' (' + amount + ' ' + currency + ').');
setCheckoutLog('Quote ready for ' + state.selectedOfferId + ': ' + quoteId + ' (' + amount + ' ' + currency + ').');
} catch (err) {
setCheckoutLog('Quote request failed: ' + err.message + '. API wiring pending.');
}
@ -378,8 +452,14 @@
refreshBtn.addEventListener('click', refreshMembershipState);
checkoutBtn.addEventListener('click', requestCheckoutQuote);
mockSelect.addEventListener('change', refreshMembershipState);
offerSelect.addEventListener('change', function () {
state.selectedOfferId = offerSelect.value;
renderSelectedOffer();
applyGateState();
});
applyGateState();
loadOffers();
const initialMock = getMockFromQuery();
if (initialMock !== 'unknown') {
mockSelect.value = initialMock;

25
public/store/offers.json Normal file
View File

@ -0,0 +1,25 @@
{
"catalog_id": "launch-2026-operator",
"offers": [
{
"offer_id": "edut.crm.pro.annual",
"title": "EDUT CRM Pro",
"summary": "Workspace-bound CRM module with governance and evidence integration.",
"price": "199.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true,
"transferable": false
},
{
"offer_id": "edut.invoicing.core.annual",
"title": "EDUT Invoicing Core",
"summary": "Invoicing workflow module for member workspaces.",
"price": "99.00",
"currency": "USDC",
"member_only": true,
"workspace_bound": true,
"transferable": false
}
]
}