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
* POST /api/invitations/accept-public

View File

@ -153,8 +153,24 @@
</div>
<div class="content">
<!-- Loading Section -->
<div id="loadingSection">
<p style="color: #666;">Loading invitation...</p>
</div>
<!-- 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>
<button class="btn btn-primary" id="acceptBtn" onclick="acceptInvitation()">
@ -171,23 +187,17 @@
</div>
<h2 class="success-title">You're all set!</h2>
<div id="roleBadge" class="role-badge"></div>
<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>
<div id="permissionsBlock">
<div class="permissions-title">With your role you can:</div>
<ul class="permissions-list" id="permissionsList"></ul>
</div>
<a href="https://wellnuo.com" target="_blank" class="btn btn-primary" style="display: block; text-decoration: none; margin-top: 20px;">
Go to WellNuo.com
</a>
<div class="next-step">
<div class="next-step-title">What's next?</div>
<div class="next-step-text">
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>
<p style="margin-top: 16px; font-size: 13px; color: #666;">
Sign in with the email address this invitation was sent to.
</p>
</div>
</div>
</div>
@ -199,38 +209,29 @@
const params = new URLSearchParams(window.location.search);
const inviteCode = params.get('code') || '';
// Show error if no code
// Load invitation info on page load
async function loadInvitationInfo() {
const loadingSection = document.getElementById('loadingSection');
const acceptSection = document.getElementById('acceptSection');
const errorMessage = document.getElementById('errorMessage');
if (!inviteCode) {
document.getElementById('errorMessage').textContent = 'Invalid invitation link.';
document.getElementById('errorMessage').style.display = 'block';
document.getElementById('acceptBtn').disabled = true;
loadingSection.innerHTML = '<p style="color: #c00;">Invalid invitation link.</p>';
return;
}
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 response = await fetch(`${API_BASE}/invitations/info/${inviteCode}`);
const data = await response.json();
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 roleBadge = document.getElementById('roleBadge');
const permissionsList = document.getElementById('permissionsList');
const roleBadge = document.getElementById('roleBadgeAccept');
const permissionsList = document.getElementById('permissionsListAccept');
if (isGuardian) {
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('successSection').style.display = 'block';