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.
|
14. Public `/trust` page scaffold aligned with trust-page spec.
|
||||||
15. Dedicated marketplace OpenAPI contract and examples.
|
15. Dedicated marketplace OpenAPI contract and examples.
|
||||||
16. Member app channel contract, examples, backend handoff checklist, and conformance vectors.
|
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:
|
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.
|
12. Page confirms via `POST /secret/membership/confirm` and/or status poll.
|
||||||
13. UI shows `acknowledged · {token}` when membership is active.
|
13. UI shows `acknowledged · {token}` when membership is active.
|
||||||
14. Post-mint success state presents `download your platform` links (Desktop/iOS/Android).
|
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.
|
Privacy and Terms links bypass flow and navigate normally.
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,34 @@
|
|||||||
border: 1px solid #d6dce4;
|
border: 1px solid #d6dce4;
|
||||||
background: #f7fafc;
|
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; }
|
a { color: #2c2c2c; text-decoration: underline; text-underline-offset: 2px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -43,12 +71,79 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p><a href="/">back</a></p>
|
<p><a href="/">back</a></p>
|
||||||
<h1>EDUT Platform Android</h1>
|
<h1>EDUT Platform Android</h1>
|
||||||
<p class="muted">membership channel acknowledged</p>
|
<p class="muted">membership channel verification</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p>This endpoint is bound to wallet-authenticated membership access.</p>
|
<p>Android delivery is tied to wallet-authenticated membership state.</p>
|
||||||
<p>When Android distribution is staged for your designation era, this channel delivers current install instructions.</p>
|
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
|
||||||
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
|
<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>
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -36,6 +36,34 @@
|
|||||||
border: 1px solid #d6dce4;
|
border: 1px solid #d6dce4;
|
||||||
background: #f7fafc;
|
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; }
|
a { color: #2c2c2c; text-decoration: underline; text-underline-offset: 2px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -43,12 +71,79 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p><a href="/">back</a></p>
|
<p><a href="/">back</a></p>
|
||||||
<h1>EDUT Platform Desktop</h1>
|
<h1>EDUT Platform Desktop</h1>
|
||||||
<p class="muted">membership channel acknowledged</p>
|
<p class="muted">membership channel verification</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p>This endpoint is bound to wallet-authenticated membership access.</p>
|
<p>Desktop delivery is tied to wallet-authenticated membership state.</p>
|
||||||
<p>When desktop installers are staged for your designation era, this channel delivers them directly.</p>
|
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
|
||||||
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
|
<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>
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -36,6 +36,34 @@
|
|||||||
border: 1px solid #d6dce4;
|
border: 1px solid #d6dce4;
|
||||||
background: #f7fafc;
|
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; }
|
a { color: #2c2c2c; text-decoration: underline; text-underline-offset: 2px; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -43,12 +71,79 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<p><a href="/">back</a></p>
|
<p><a href="/">back</a></p>
|
||||||
<h1>EDUT Platform iOS</h1>
|
<h1>EDUT Platform iOS</h1>
|
||||||
<p class="muted">membership channel acknowledged</p>
|
<p class="muted">membership channel verification</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<p>This endpoint is bound to wallet-authenticated membership access.</p>
|
<p>iOS delivery is tied to wallet-authenticated membership state.</p>
|
||||||
<p>When iOS distribution is staged for your designation era, this channel delivers current install instructions.</p>
|
<button id="connect-wallet" class="cta" type="button">connect wallet</button>
|
||||||
<p>Member updates and entitlement notices are delivered inside the EDUT app after wallet sign-in.</p>
|
<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>
|
||||||
</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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user