Add data-backed store catalog and selected-offer quote flow
This commit is contained in:
parent
df0788fb43
commit
6158460b8c
@ -8,6 +8,7 @@ Public-facing EDUT web surfaces and deployment specs.
|
|||||||
public/
|
public/
|
||||||
index.html
|
index.html
|
||||||
store/index.html
|
store/index.html
|
||||||
|
store/offers.json
|
||||||
trust/index.html
|
trust/index.html
|
||||||
privacy/index.html
|
privacy/index.html
|
||||||
terms/index.html
|
terms/index.html
|
||||||
|
|||||||
@ -161,10 +161,13 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<p class="label">Offer Skeleton</p>
|
<p class="label">Offer Catalog</p>
|
||||||
<p class="title">EDUT CRM Pro</p>
|
<p class="line">Offer:</p>
|
||||||
<p class="line">Price: 199.00 USDC</p>
|
<select id="offer-select" aria-label="Select offer"></select>
|
||||||
<p class="line">Policy: member-only, workspace-bound, non-transferable</p>
|
<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>
|
<p class="line">Action chain: membership check -> 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>
|
||||||
@ -193,6 +196,8 @@
|
|||||||
membership: 'unknown',
|
membership: 'unknown',
|
||||||
gate: false,
|
gate: false,
|
||||||
source: 'live',
|
source: 'live',
|
||||||
|
offers: [],
|
||||||
|
selectedOfferId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const walletLabel = document.getElementById('wallet-label');
|
const walletLabel = document.getElementById('wallet-label');
|
||||||
@ -205,6 +210,11 @@
|
|||||||
const refreshBtn = document.getElementById('refresh-btn');
|
const refreshBtn = document.getElementById('refresh-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 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) {
|
function abbreviateWallet(wallet) {
|
||||||
if (!wallet || wallet.length < 10) return wallet || 'not connected';
|
if (!wallet || wallet.length < 10) return wallet || 'not connected';
|
||||||
@ -219,6 +229,43 @@
|
|||||||
checkoutLog.textContent = message;
|
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) {
|
function normalizeMembership(raw) {
|
||||||
const value = String(raw || '').toLowerCase();
|
const value = String(raw || '').toLowerCase();
|
||||||
if (value === 'active') return 'active';
|
if (value === 'active') return 'active';
|
||||||
@ -246,7 +293,7 @@
|
|||||||
gatePill.textContent = 'membership required';
|
gatePill.textContent = 'membership required';
|
||||||
}
|
}
|
||||||
|
|
||||||
checkoutBtn.disabled = !state.gate;
|
checkoutBtn.disabled = !state.gate || !state.selectedOfferId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMockFromQuery() {
|
function getMockFromQuery() {
|
||||||
@ -273,6 +320,33 @@
|
|||||||
return response.json();
|
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() {
|
async function fetchLiveMembershipStatus() {
|
||||||
if (!state.wallet) {
|
if (!state.wallet) {
|
||||||
throw new Error('Connect wallet first.');
|
throw new Error('Connect wallet first.');
|
||||||
@ -356,7 +430,7 @@
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
wallet: state.wallet,
|
wallet: state.wallet,
|
||||||
offer_id: 'edut.crm.pro.annual',
|
offer_id: state.selectedOfferId,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -368,7 +442,7 @@
|
|||||||
const quoteId = payload.quote_id || 'unknown';
|
const quoteId = payload.quote_id || 'unknown';
|
||||||
const amount = payload.amount || payload.amount_atomic || 'unknown';
|
const amount = payload.amount || payload.amount_atomic || 'unknown';
|
||||||
const currency = payload.currency || 'unknown';
|
const currency = payload.currency || 'unknown';
|
||||||
setCheckoutLog('Quote ready: ' + 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.');
|
||||||
}
|
}
|
||||||
@ -378,8 +452,14 @@
|
|||||||
refreshBtn.addEventListener('click', refreshMembershipState);
|
refreshBtn.addEventListener('click', refreshMembershipState);
|
||||||
checkoutBtn.addEventListener('click', requestCheckoutQuote);
|
checkoutBtn.addEventListener('click', requestCheckoutQuote);
|
||||||
mockSelect.addEventListener('change', refreshMembershipState);
|
mockSelect.addEventListener('change', refreshMembershipState);
|
||||||
|
offerSelect.addEventListener('change', function () {
|
||||||
|
state.selectedOfferId = offerSelect.value;
|
||||||
|
renderSelectedOffer();
|
||||||
|
applyGateState();
|
||||||
|
});
|
||||||
|
|
||||||
applyGateState();
|
applyGateState();
|
||||||
|
loadOffers();
|
||||||
const initialMock = getMockFromQuery();
|
const initialMock = getMockFromQuery();
|
||||||
if (initialMock !== 'unknown') {
|
if (initialMock !== 'unknown') {
|
||||||
mockSelect.value = initialMock;
|
mockSelect.value = initialMock;
|
||||||
|
|||||||
25
public/store/offers.json
Normal file
25
public/store/offers.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user