diff --git a/README.md b/README.md index c4b68c0..f83de57 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/public/store/index.html b/public/store/index.html index d3efead..c9c7c67 100644 --- a/public/store/index.html +++ b/public/store/index.html @@ -161,10 +161,13 @@
-

Offer Skeleton

-

EDUT CRM Pro

-

Price: 199.00 USDC

-

Policy: member-only, workspace-bound, non-transferable

+

Offer Catalog

+

Offer:

+ +

Loading offers...

+

Catalog data pending.

+

Price: --

+

Policy: --

Action chain: membership check -> quote -> wallet confirm -> entitlement receipt

@@ -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; diff --git a/public/store/offers.json b/public/store/offers.json new file mode 100644 index 0000000..11bfc2b --- /dev/null +++ b/public/store/offers.json @@ -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 + } + ] +}