Gate download channels by wallet membership status

This commit is contained in:
Joshua 2026-02-17 12:19:54 -08:00
parent afe14c33d6
commit 9d5e531fe8
5 changed files with 300 additions and 13 deletions

View File

@ -41,6 +41,7 @@ Implemented now:
14. Public `/trust` page scaffold aligned with trust-page spec.
15. Dedicated marketplace OpenAPI contract and examples.
16. Member app channel contract, examples, backend handoff checklist, and conformance vectors.
17. Download endpoints now validate wallet membership status before authorizing channel messaging.
Remaining in this repo:

View File

@ -30,7 +30,8 @@ This flow is the pre-launch identity and commerce envelope. It is not a throwawa
12. Page confirms via `POST /secret/membership/confirm` and/or status poll.
13. UI shows `acknowledged · {token}` when membership is active.
14. Post-mint success state presents `download your platform` links (Desktop/iOS/Android).
15. Member opens the app, signs in with the same wallet, and receives platform updates through app notifications.
15. Download endpoints perform wallet membership status checks before channel authorization messaging.
16. Member opens the app, signs in with the same wallet, and receives platform updates through app notifications.
Privacy and Terms links bypass flow and navigate normally.

View File

@ -36,6 +36,34 @@
border: 1px solid #d6dce4;
background: #f7fafc;
}
.status {
margin-top: 14px;
color: #4c535c;
min-height: 18px;
}
.status.error {
color: #7a3a3a;
}
.cta {
margin-top: 8px;
border: 1px solid #c8d0da;
background: #ffffff;
color: #2c2c2c;
padding: 8px 14px;
font: inherit;
letter-spacing: 0.1em;
text-transform: lowercase;
cursor: pointer;
}
.download-instructions {
margin-top: 12px;
border-top: 1px solid #d6dce4;
padding-top: 12px;
display: none;
}
.download-instructions.visible {
display: block;
}
a { color: #2c2c2c; text-decoration: underline; text-underline-offset: 2px; }
</style>
</head>
@ -43,12 +71,79 @@
<div class="container">
<p><a href="/">back</a></p>
<h1>EDUT Platform Android</h1>
<p class="muted">membership channel acknowledged</p>
<p class="muted">membership channel verification</p>
<div class="card">
<p>This endpoint is bound to wallet-authenticated membership access.</p>
<p>When Android distribution is staged for your designation era, this channel delivers current install instructions.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
<p>Android delivery is tied to wallet-authenticated membership state.</p>
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
<p id="status" class="status" aria-live="polite"></p>
<div id="download-instructions" class="download-instructions">
<p>Membership verified. Android distribution remains staged by designation era.</p>
<p>When your channel opens, this endpoint delivers current install instructions for Android onboarding.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
</div>
</div>
</div>
<script>
const connectWalletButton = document.getElementById('connect-wallet');
const statusNode = document.getElementById('status');
const instructionsNode = document.getElementById('download-instructions');
function setStatus(message, isError) {
statusNode.textContent = message;
statusNode.classList.toggle('error', !!isError);
}
async function fetchMembershipStatus(wallet, chainId) {
const params = new URLSearchParams({ wallet });
if (chainId !== null && chainId !== undefined) {
params.set('chain_id', String(chainId));
}
const res = await fetch('/secret/membership/status?' + params.toString(), {
headers: { Accept: 'application/json' },
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || ('HTTP ' + res.status));
}
return res.json();
}
async function handleConnectWallet() {
instructionsNode.classList.remove('visible');
try {
if (!window.ethereum) {
setStatus('No wallet detected. Install a wallet and retry.', true);
return;
}
setStatus('Connecting wallet...', false);
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
if (!Array.isArray(accounts) || accounts.length === 0) {
setStatus('Wallet connection was not approved.', true);
return;
}
const wallet = accounts[0];
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
const chainId = Number.parseInt(chainHex, 16);
setStatus('Checking membership status...', false);
const status = await fetchMembershipStatus(wallet, chainId);
if (status.status === 'active') {
setStatus('Membership active. Android channel is authorized.', false);
instructionsNode.classList.add('visible');
return;
}
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
} catch (err) {
const message = err && err.message ? err.message : 'Membership check failed.';
setStatus(message, true);
}
}
connectWalletButton.addEventListener('click', handleConnectWallet);
</script>
</body>
</html>

View File

@ -36,6 +36,34 @@
border: 1px solid #d6dce4;
background: #f7fafc;
}
.status {
margin-top: 14px;
color: #4c535c;
min-height: 18px;
}
.status.error {
color: #7a3a3a;
}
.cta {
margin-top: 8px;
border: 1px solid #c8d0da;
background: #ffffff;
color: #2c2c2c;
padding: 8px 14px;
font: inherit;
letter-spacing: 0.1em;
text-transform: lowercase;
cursor: pointer;
}
.download-instructions {
margin-top: 12px;
border-top: 1px solid #d6dce4;
padding-top: 12px;
display: none;
}
.download-instructions.visible {
display: block;
}
a { color: #2c2c2c; text-decoration: underline; text-underline-offset: 2px; }
</style>
</head>
@ -43,12 +71,79 @@
<div class="container">
<p><a href="/">back</a></p>
<h1>EDUT Platform Desktop</h1>
<p class="muted">membership channel acknowledged</p>
<p class="muted">membership channel verification</p>
<div class="card">
<p>This endpoint is bound to wallet-authenticated membership access.</p>
<p>When desktop installers are staged for your designation era, this channel delivers them directly.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
<p>Desktop delivery is tied to wallet-authenticated membership state.</p>
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
<p id="status" class="status" aria-live="polite"></p>
<div id="download-instructions" class="download-instructions">
<p>Membership verified. Desktop distribution remains staged by designation era.</p>
<p>When your channel opens, this endpoint delivers the current installer package and checksum manifest.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
</div>
</div>
</div>
<script>
const connectWalletButton = document.getElementById('connect-wallet');
const statusNode = document.getElementById('status');
const instructionsNode = document.getElementById('download-instructions');
function setStatus(message, isError) {
statusNode.textContent = message;
statusNode.classList.toggle('error', !!isError);
}
async function fetchMembershipStatus(wallet, chainId) {
const params = new URLSearchParams({ wallet });
if (chainId !== null && chainId !== undefined) {
params.set('chain_id', String(chainId));
}
const res = await fetch('/secret/membership/status?' + params.toString(), {
headers: { Accept: 'application/json' },
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || ('HTTP ' + res.status));
}
return res.json();
}
async function handleConnectWallet() {
instructionsNode.classList.remove('visible');
try {
if (!window.ethereum) {
setStatus('No wallet detected. Install a wallet and retry.', true);
return;
}
setStatus('Connecting wallet...', false);
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
if (!Array.isArray(accounts) || accounts.length === 0) {
setStatus('Wallet connection was not approved.', true);
return;
}
const wallet = accounts[0];
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
const chainId = Number.parseInt(chainHex, 16);
setStatus('Checking membership status...', false);
const status = await fetchMembershipStatus(wallet, chainId);
if (status.status === 'active') {
setStatus('Membership active. Desktop channel is authorized.', false);
instructionsNode.classList.add('visible');
return;
}
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
} catch (err) {
const message = err && err.message ? err.message : 'Membership check failed.';
setStatus(message, true);
}
}
connectWalletButton.addEventListener('click', handleConnectWallet);
</script>
</body>
</html>

View File

@ -36,6 +36,34 @@
border: 1px solid #d6dce4;
background: #f7fafc;
}
.status {
margin-top: 14px;
color: #4c535c;
min-height: 18px;
}
.status.error {
color: #7a3a3a;
}
.cta {
margin-top: 8px;
border: 1px solid #c8d0da;
background: #ffffff;
color: #2c2c2c;
padding: 8px 14px;
font: inherit;
letter-spacing: 0.1em;
text-transform: lowercase;
cursor: pointer;
}
.download-instructions {
margin-top: 12px;
border-top: 1px solid #d6dce4;
padding-top: 12px;
display: none;
}
.download-instructions.visible {
display: block;
}
a { color: #2c2c2c; text-decoration: underline; text-underline-offset: 2px; }
</style>
</head>
@ -43,12 +71,79 @@
<div class="container">
<p><a href="/">back</a></p>
<h1>EDUT Platform iOS</h1>
<p class="muted">membership channel acknowledged</p>
<p class="muted">membership channel verification</p>
<div class="card">
<p>This endpoint is bound to wallet-authenticated membership access.</p>
<p>When iOS distribution is staged for your designation era, this channel delivers current install instructions.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
<p>iOS delivery is tied to wallet-authenticated membership state.</p>
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
<p id="status" class="status" aria-live="polite"></p>
<div id="download-instructions" class="download-instructions">
<p>Membership verified. iOS distribution remains staged by designation era.</p>
<p>When your channel opens, this endpoint delivers current install instructions for iOS onboarding.</p>
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
</div>
</div>
</div>
<script>
const connectWalletButton = document.getElementById('connect-wallet');
const statusNode = document.getElementById('status');
const instructionsNode = document.getElementById('download-instructions');
function setStatus(message, isError) {
statusNode.textContent = message;
statusNode.classList.toggle('error', !!isError);
}
async function fetchMembershipStatus(wallet, chainId) {
const params = new URLSearchParams({ wallet });
if (chainId !== null && chainId !== undefined) {
params.set('chain_id', String(chainId));
}
const res = await fetch('/secret/membership/status?' + params.toString(), {
headers: { Accept: 'application/json' },
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || ('HTTP ' + res.status));
}
return res.json();
}
async function handleConnectWallet() {
instructionsNode.classList.remove('visible');
try {
if (!window.ethereum) {
setStatus('No wallet detected. Install a wallet and retry.', true);
return;
}
setStatus('Connecting wallet...', false);
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
if (!Array.isArray(accounts) || accounts.length === 0) {
setStatus('Wallet connection was not approved.', true);
return;
}
const wallet = accounts[0];
const chainHex = await window.ethereum.request({ method: 'eth_chainId' });
const chainId = Number.parseInt(chainHex, 16);
setStatus('Checking membership status...', false);
const status = await fetchMembershipStatus(wallet, chainId);
if (status.status === 'active') {
setStatus('Membership active. iOS channel is authorized.', false);
instructionsNode.classList.add('visible');
return;
}
setStatus('Membership is not active for this wallet (' + status.status + ').', true);
} catch (err) {
const message = err && err.message ? err.message : 'Membership check failed.';
setStatus(message, true);
}
}
connectWalletButton.addEventListener('click', handleConnectWallet);
</script>
</body>
</html>