Improve invitation UI and fix access removal

- Move role selector (Caretaker/Guardian) above email input in Access screen
- Remove "(view only)" suffix from Caretaker role in email templates
- Remove "expires in 7 days" text from invitation emails
- Remove expires_at field from invitation creation (invitations never expire)
- Fix deletion of accepted invitations (now also removes user_access record)
- Add favicon to accept-invite.html page
This commit is contained in:
Sergei 2026-01-04 10:28:36 -08:00
parent d9fcdf1751
commit f4ff281bcc
4 changed files with 37 additions and 39 deletions

View File

@ -324,32 +324,7 @@ export default function ShareAccessScreen() {
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Invite Someone</Text> <Text style={styles.sectionTitle}>Invite Someone</Text>
<View style={styles.inputRow}> {/* Role Toggle - above email */}
<TextInput
style={styles.input}
placeholder="Email address"
placeholderTextColor={AppColors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
editable={!isLoading}
/>
<TouchableOpacity
style={[styles.sendButton, isLoading && styles.sendButtonDisabled]}
onPress={handleSendInvite}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator size="small" color={AppColors.white} />
) : (
<Ionicons name="send" size={18} color={AppColors.white} />
)}
</TouchableOpacity>
</View>
{/* Role Toggle */}
<View style={styles.roleToggle}> <View style={styles.roleToggle}>
<TouchableOpacity <TouchableOpacity
style={[styles.roleButton, role === 'caretaker' && styles.roleButtonActive]} style={[styles.roleButton, role === 'caretaker' && styles.roleButtonActive]}
@ -374,6 +349,31 @@ export default function ShareAccessScreen() {
? 'Can view activity and chat with Julia' ? 'Can view activity and chat with Julia'
: 'Full access: edit info, manage subscription'} : 'Full access: edit info, manage subscription'}
</Text> </Text>
<View style={[styles.inputRow, { marginTop: Spacing.md }]}>
<TextInput
style={styles.input}
placeholder="Email address"
placeholderTextColor={AppColors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
autoCorrect={false}
keyboardType="email-address"
editable={!isLoading}
/>
<TouchableOpacity
style={[styles.sendButton, isLoading && styles.sendButtonDisabled]}
onPress={handleSendInvite}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator size="small" color={AppColors.white} />
) : (
<Ionicons name="send" size={18} color={AppColors.white} />
)}
</TouchableOpacity>
</View>
</View> </View>
{/* People with Access */} {/* People with Access */}

View File

@ -310,9 +310,7 @@ router.post('/', async (req, res) => {
attempts++; attempts++;
} }
// Create invitation (expires in 7 days) // Create invitation (no expiry)
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const { data: invitation, error } = await supabase const { data: invitation, error } = await supabase
.from('invitations') .from('invitations')
.insert({ .insert({
@ -321,8 +319,7 @@ router.post('/', async (req, res) => {
token: inviteToken, token: inviteToken,
role: role, role: role,
email: email?.toLowerCase() || null, email: email?.toLowerCase() || null,
label: label || null, label: label || null
expires_at: expiresAt.toISOString()
}) })
.select() .select()
.single(); .single();
@ -690,8 +687,13 @@ router.delete('/:id', async (req, res) => {
return res.status(404).json({ error: 'Invitation not found' }); return res.status(404).json({ error: 'Invitation not found' });
} }
if (invitation.accepted_at) { // If invitation was accepted, also remove user_access
return res.status(400).json({ error: 'Cannot revoke accepted invitation' }); if (invitation.accepted_by) {
await supabase
.from('user_access')
.delete()
.eq('beneficiary_id', invitation.beneficiary_id)
.eq('accessor_id', invitation.accepted_by);
} }
// Delete invitation // Delete invitation

View File

@ -222,7 +222,7 @@ If you didn't request this code, you can safely ignore this email.
async function sendInvitationEmail({ email, inviterName, beneficiaryName, role, inviteCode }) { async function sendInvitationEmail({ email, inviterName, beneficiaryName, role, inviteCode }) {
const frontendUrl = process.env.FRONTEND_URL || 'https://wellnuo.smartlaunchhub.com'; const frontendUrl = process.env.FRONTEND_URL || 'https://wellnuo.smartlaunchhub.com';
const acceptLink = `${frontendUrl}/accept-invite.html?code=${inviteCode}`; const acceptLink = `${frontendUrl}/accept-invite.html?code=${inviteCode}`;
const roleText = role === 'guardian' ? 'Guardian (full access)' : 'Caretaker (view only)'; const roleText = role === 'guardian' ? 'Guardian' : 'Caretaker';
const htmlContent = ` const htmlContent = `
<!DOCTYPE html> <!DOCTYPE html>
@ -276,9 +276,6 @@ async function sendInvitationEmail({ email, inviterName, beneficiaryName, role,
</tr> </tr>
</table> </table>
<p style="margin: 24px 0 0 0; font-size: 13px; color: #999999; text-align: center;">
This invitation expires in 7 days.
</p>
</td> </td>
</tr> </tr>
@ -310,8 +307,6 @@ Your invitation code: ${inviteCode}
Accept your invitation: ${acceptLink} Accept your invitation: ${acceptLink}
This invitation expires in 7 days.
If you didn't expect this invitation, you can safely ignore this email. If you didn't expect this invitation, you can safely ignore this email.
WellNuo - Elderly Care Monitoring WellNuo - Elderly Care Monitoring

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accept Invitation - WellNuo</title> <title>Accept Invitation - WellNuo</title>
<link rel="icon" type="image/png" href="/favicon.png">
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
body { body {