Remove manual 'existing member' toggle - auto-detect from contact data

IMPROVEMENTS:
- Removed unnecessary manual toggle from invitation page
- System now automatically determines member status from contact data
- Added visual status indicator showing recipient's NAO membership status
- Simplified invitation flow - no manual decisions needed

STATUS DETECTION:
- Checks contact.naoStatus automatically (member/invited/not_invited)
- Shows clear visual feedback with appropriate icons and colors
- Green: Existing member (will choose rCard)
- Orange: Invited but not joined yet
- Gray: New user (will create account)

CLEANER UX:
- Removed confusing toggle that users had to manually set
- System now has single source of truth for membership status
- More reliable and less error-prone invitation process

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
main
Claude Code Assistant 2 months ago
parent d71bc03451
commit 1865362c06
  1. 125
      src/pages/InvitationPage.tsx

@ -14,9 +14,7 @@ import {
TextField, TextField,
InputAdornment, InputAdornment,
Avatar, Avatar,
Chip, Chip
FormControlLabel,
Switch
} from '@mui/material'; } from '@mui/material';
import { import {
Share, Share,
@ -27,7 +25,10 @@ import {
GetApp, GetApp,
Refresh, Refresh,
ArrowBack, ArrowBack,
Groups Groups,
CheckCircle,
Schedule,
PersonOutline
} from '@mui/icons-material'; } from '@mui/icons-material';
import { QRCodeSVG } from 'qrcode.react'; import { QRCodeSVG } from 'qrcode.react';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
@ -41,11 +42,11 @@ const InvitationPage = () => {
inviterName?: string; inviterName?: string;
relationshipType?: string; relationshipType?: string;
}>({}); }>({});
const [isExistingMember, setIsExistingMember] = useState(false);
const [copySuccess, setCopySuccess] = useState(false); const [copySuccess, setCopySuccess] = useState(false);
const [invitationId, setInvitationId] = useState(''); const [invitationId, setInvitationId] = useState('');
const [group, setGroup] = useState<Group | null>(null); const [group, setGroup] = useState<Group | null>(null);
const [isGroupInvite, setIsGroupInvite] = useState(false); const [isGroupInvite, setIsGroupInvite] = useState(false);
const [inviteeNaoStatus, setInviteeNaoStatus] = useState<'member' | 'invited' | 'not_invited' | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
@ -64,17 +65,21 @@ const InvitationPage = () => {
relationshipType: relationshipType || undefined, relationshipType: relationshipType || undefined,
}); });
// Auto-detect if invitee is an existing member by checking contacts // Determine member status from contact data if available
let isExistingMember = false;
if (inviteeName) { if (inviteeName) {
try { try {
const contacts: Contact[] = await dataService.getContacts(); const contacts: Contact[] = await dataService.getContacts();
const isExistingContact = contacts.some(contact => const contact = contacts.find(c =>
contact.name.toLowerCase() === inviteeName.toLowerCase() c.name.toLowerCase() === inviteeName.toLowerCase()
); );
if (isExistingContact) { if (contact) {
console.log(`Auto-detected ${inviteeName} as existing member`); isExistingMember = contact.naoStatus === 'member';
setIsExistingMember(true); setInviteeNaoStatus(contact.naoStatus || 'not_invited');
console.log(`${inviteeName} NAO status:`, contact.naoStatus);
} else {
setInviteeNaoStatus('not_invited');
} }
} catch (error) { } catch (error) {
console.error('Failed to check contacts:', error); console.error('Failed to check contacts:', error);
@ -202,7 +207,7 @@ const InvitationPage = () => {
} }
}; };
const handleNewInvitation = () => { const handleNewInvitation = async () => {
const groupId = searchParams.get('groupId'); const groupId = searchParams.get('groupId');
const inviteeName = searchParams.get('inviteeName'); const inviteeName = searchParams.get('inviteeName');
const inviterName = searchParams.get('inviterName'); const inviterName = searchParams.get('inviterName');
@ -211,6 +216,26 @@ const InvitationPage = () => {
const id = Math.random().toString(36).substring(2, 15); const id = Math.random().toString(36).substring(2, 15);
setInvitationId(id); setInvitationId(id);
// Determine member status from contact data if available
let isExistingMember = false;
if (inviteeName) {
try {
const contacts: Contact[] = await dataService.getContacts();
const contact = contacts.find(c =>
c.name.toLowerCase() === inviteeName.toLowerCase()
);
if (contact) {
isExistingMember = contact.naoStatus === 'member';
setInviteeNaoStatus(contact.naoStatus || 'not_invited');
} else {
setInviteeNaoStatus('not_invited');
}
} catch (error) {
console.error('Failed to check contacts:', error);
}
}
// Build URL with personalized parameters // Build URL with personalized parameters
const urlParams = new URLSearchParams({ const urlParams = new URLSearchParams({
invite: id, invite: id,
@ -364,48 +389,42 @@ const InvitationPage = () => {
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Invitation ID: {invitationId} Invitation ID: {invitationId}
</Typography> </Typography>
{/* NAO Status Indicator */}
{personalizedInvite.inviteeName && inviteeNaoStatus && (
<Box sx={{ mb: 2, p: 1.5, borderRadius: 2, backgroundColor: 'grey.50', border: 1, borderColor: 'divider' }}>
<Typography variant="body2" sx={{ fontWeight: 500, mb: 0.5 }}>
Recipient Status:
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{inviteeNaoStatus === 'member' && (
<>
<CheckCircle sx={{ fontSize: 16, color: 'success.main' }} />
<Typography variant="body2" color="success.main">
<strong>{personalizedInvite.inviteeName}</strong> is already a NAO member - they'll choose how to connect with this group
</Typography>
</>
)}
{inviteeNaoStatus === 'invited' && (
<>
<Schedule sx={{ fontSize: 16, color: 'warning.main' }} />
<Typography variant="body2" color="warning.main">
<strong>{personalizedInvite.inviteeName}</strong> has been invited to NAO but hasn't joined yet
</Typography>
</>
)}
{inviteeNaoStatus === 'not_invited' && (
<>
<PersonOutline sx={{ fontSize: 16, color: 'text.secondary' }} />
<Typography variant="body2" color="text.secondary">
<strong>{personalizedInvite.inviteeName}</strong> will need to create a new NAO account
</Typography>
</>
)}
</Box>
</Box>
)}
{/* Existing Member Toggle */}
<Box sx={{ mb: 2 }}>
<FormControlLabel
control={
<Switch
checked={isExistingMember}
onChange={(e) => {
setIsExistingMember(e.target.checked);
// Regenerate URL when toggle changes
const id = Math.random().toString(36).substring(2, 15);
setInvitationId(id);
const groupId = searchParams.get('groupId');
const inviteeName = searchParams.get('inviteeName');
const inviterName = searchParams.get('inviterName');
const relationshipType = searchParams.get('relationshipType');
const urlParams = new URLSearchParams({
invite: id,
...(groupId && { groupId }),
...(inviteeName && { inviteeName }),
...(inviterName && { inviterName }),
...(relationshipType && { relationshipType }),
...(e.target.checked && { existingMember: 'true' }),
});
const url = `${window.location.origin}/onboarding?${urlParams.toString()}`;
setInvitationUrl(url);
}}
color="primary"
/>
}
label="Recipient is already a NAO member"
sx={{ alignItems: 'flex-start' }}
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.5 }}>
{isExistingMember
? "They'll choose how to connect with this group using their existing profile"
: "They'll need to create a new NAO account first"
}
</Typography>
</Box>
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />

Loading…
Cancel
Save