Gate download channels by wallet membership status
This commit is contained in:
parent
afe14c33d6
commit
9d5e531fe8
@ -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:
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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>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>
|
||||
|
||||
@ -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>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>
|
||||
|
||||
@ -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>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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user