Improve invitation acceptance flow

- Add GET /api/invitations/info/:code endpoint to fetch role before accepting
- Show role and permissions on accept page BEFORE clicking Accept
- Simplify success page: remove permissions list, add link to wellnuo.com
- Minimalist design: light header background, logo only
This commit is contained in:
Sergei 2026-01-04 09:04:43 -08:00
parent d0c4930d38
commit d9fcdf1751
2 changed files with 122 additions and 37 deletions

View File

@ -23,6 +23,56 @@ function authMiddleware(req, res, next) {
} }
} }
/**
* PUBLIC: Get invitation info without auth
* GET /api/invitations/info/:code
* Used to show role info before accepting
*/
router.get('/info/:code', async (req, res) => {
try {
const code = req.params.code;
if (!code) {
return res.status(400).json({ error: 'Code is required' });
}
// Find invitation by code
const formattedCode = code.toUpperCase().replace(/-/g, '').replace(/(.{3})/g, '$1-').slice(0, 11);
let { data: invitation } = await supabase
.from('invitations')
.select('role, accepted_at')
.eq('token', formattedCode)
.single();
// Try without formatting
if (!invitation) {
const { data: inv2 } = await supabase
.from('invitations')
.select('role, accepted_at')
.eq('token', code.toUpperCase())
.single();
invitation = inv2;
}
if (!invitation) {
return res.status(404).json({ error: 'Invitation not found' });
}
if (invitation.accepted_at) {
return res.status(400).json({ error: 'This invitation has already been accepted' });
}
res.json({
role: invitation.role
});
} catch (error) {
console.error('[INVITE] Get info error:', error);
res.status(500).json({ error: error.message });
}
});
/** /**
* PUBLIC: Accept invitation without auth * PUBLIC: Accept invitation without auth
* POST /api/invitations/accept-public * POST /api/invitations/accept-public

View File

@ -153,8 +153,24 @@
</div> </div>
<div class="content"> <div class="content">
<!-- Loading Section -->
<div id="loadingSection">
<p style="color: #666;">Loading invitation...</p>
</div>
<!-- Accept Section --> <!-- Accept Section -->
<div id="acceptSection"> <div id="acceptSection" style="display: none;">
<div id="roleBadgeAccept" class="role-badge"></div>
<p class="success-text">
You've been invited to monitor a family member through WellNuo.
</p>
<div id="permissionsBlockAccept">
<div class="permissions-title">With this role you will be able to:</div>
<ul class="permissions-list" id="permissionsListAccept"></ul>
</div>
<div id="errorMessage" class="error-message"></div> <div id="errorMessage" class="error-message"></div>
<button class="btn btn-primary" id="acceptBtn" onclick="acceptInvitation()"> <button class="btn btn-primary" id="acceptBtn" onclick="acceptInvitation()">
@ -171,23 +187,17 @@
</div> </div>
<h2 class="success-title">You're all set!</h2> <h2 class="success-title">You're all set!</h2>
<div id="roleBadge" class="role-badge"></div>
<p class="success-text"> <p class="success-text">
You have been granted access to monitor a family member through WellNuo. Your invitation has been accepted. Download the WellNuo app to get started.
</p> </p>
<div id="permissionsBlock"> <a href="https://wellnuo.com" target="_blank" class="btn btn-primary" style="display: block; text-decoration: none; margin-top: 20px;">
<div class="permissions-title">With your role you can:</div> Go to WellNuo.com
<ul class="permissions-list" id="permissionsList"></ul> </a>
</div>
<div class="next-step"> <p style="margin-top: 16px; font-size: 13px; color: #666;">
<div class="next-step-title">What's next?</div> Sign in with the email address this invitation was sent to.
<div class="next-step-text"> </p>
Download the WellNuo app and sign in with the email address this invitation was sent to. You'll see the beneficiary in your dashboard.
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -199,38 +209,29 @@
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const inviteCode = params.get('code') || ''; const inviteCode = params.get('code') || '';
// Show error if no code // Load invitation info on page load
if (!inviteCode) { async function loadInvitationInfo() {
document.getElementById('errorMessage').textContent = 'Invalid invitation link.'; const loadingSection = document.getElementById('loadingSection');
document.getElementById('errorMessage').style.display = 'block'; const acceptSection = document.getElementById('acceptSection');
document.getElementById('acceptBtn').disabled = true; const errorMessage = document.getElementById('errorMessage');
}
async function acceptInvitation() { if (!inviteCode) {
const btn = document.getElementById('acceptBtn'); loadingSection.innerHTML = '<p style="color: #c00;">Invalid invitation link.</p>';
const errorEl = document.getElementById('errorMessage'); return;
}
btn.disabled = true;
btn.innerHTML = '<span class="loading"></span>Accepting...';
errorEl.style.display = 'none';
try { try {
const response = await fetch(`${API_BASE}/invitations/accept-public`, { const response = await fetch(`${API_BASE}/invitations/info/${inviteCode}`);
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: inviteCode })
});
const data = await response.json(); const data = await response.json();
if (!response.ok) { if (!response.ok) {
throw new Error(data.error || 'Failed to accept invitation'); throw new Error(data.error || 'Invalid invitation');
} }
// Show success with role-specific permissions // Show role info before accepting
const isGuardian = data.role === 'guardian'; const isGuardian = data.role === 'guardian';
const roleBadge = document.getElementById('roleBadge'); const roleBadge = document.getElementById('roleBadgeAccept');
const permissionsList = document.getElementById('permissionsList'); const permissionsList = document.getElementById('permissionsListAccept');
if (isGuardian) { if (isGuardian) {
roleBadge.textContent = 'Guardian'; roleBadge.textContent = 'Guardian';
@ -254,6 +255,40 @@
`; `;
} }
// Show accept section
loadingSection.style.display = 'none';
acceptSection.style.display = 'block';
} catch (error) {
loadingSection.innerHTML = `<p style="color: #c00;">${error.message}</p>`;
}
}
// Load info on page load
loadInvitationInfo();
async function acceptInvitation() {
const btn = document.getElementById('acceptBtn');
const errorEl = document.getElementById('errorMessage');
btn.disabled = true;
btn.innerHTML = '<span class="loading"></span>Accepting...';
errorEl.style.display = 'none';
try {
const response = await fetch(`${API_BASE}/invitations/accept-public`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: inviteCode })
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to accept invitation');
}
// Show success
document.getElementById('acceptSection').style.display = 'none'; document.getElementById('acceptSection').style.display = 'none';
document.getElementById('successSection').style.display = 'block'; document.getElementById('successSection').style.display = 'block';