diff --git a/app/(tabs)/beneficiaries/[id]/index.tsx b/app/(tabs)/beneficiaries/[id]/index.tsx
index c6a12c5..c19b9d0 100644
--- a/app/(tabs)/beneficiaries/[id]/index.tsx
+++ b/app/(tabs)/beneficiaries/[id]/index.tsx
@@ -73,7 +73,12 @@ export default function BeneficiaryDetailScreen() {
// Edit modal state
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
- const [editForm, setEditForm] = useState({ name: '', address: '', avatar: undefined as string | undefined });
+ const [editForm, setEditForm] = useState({
+ name: '',
+ address: '',
+ avatar: undefined as string | undefined,
+ customName: '' // For non-custodian users
+ });
const [isSavingEdit, setIsSavingEdit] = useState(false);
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
@@ -215,11 +220,15 @@ export default function BeneficiaryDetailScreen() {
name: beneficiary.name || '',
address: beneficiary.address || '',
avatar: beneficiary.avatar,
+ customName: beneficiary.customName || '',
});
setIsEditModalVisible(true);
}
};
+ // Check if user is custodian (can edit all fields)
+ const isCustodian = beneficiary?.role === 'custodian';
+
const handlePickAvatar = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
@@ -240,8 +249,8 @@ export default function BeneficiaryDetailScreen() {
};
const handleSaveEdit = async () => {
- if (!editForm.name.trim() || !id) {
- toast.error('Error', 'Name is required');
+ if (!id) {
+ toast.error('Error', 'Invalid beneficiary');
return;
}
@@ -249,32 +258,51 @@ export default function BeneficiaryDetailScreen() {
setIsSavingEdit(true);
try {
- // Update basic info
- const response = await api.updateWellNuoBeneficiary(beneficiaryId, {
- name: editForm.name.trim(),
- address: editForm.address.trim() || undefined,
- });
+ if (isCustodian) {
+ // Custodian: update name, address in beneficiaries table
+ if (!editForm.name.trim()) {
+ toast.error('Error', 'Name is required');
+ setIsSavingEdit(false);
+ return;
+ }
- if (!response.ok) {
- toast.error('Error', response.error?.message || 'Failed to save changes.');
- setIsSavingEdit(false);
- return;
- }
+ const response = await api.updateWellNuoBeneficiary(beneficiaryId, {
+ name: editForm.name.trim(),
+ address: editForm.address.trim() || undefined,
+ });
- // Upload avatar if changed (new local file URI)
- if (editForm.avatar && editForm.avatar.startsWith('file://')) {
- setIsUploadingAvatar(true);
- const avatarResult = await api.updateBeneficiaryAvatar(beneficiaryId, editForm.avatar);
- setIsUploadingAvatar(false);
- if (!avatarResult.ok) {
- console.warn('[BeneficiaryDetail] Failed to upload avatar:', avatarResult.error?.message);
- // Show info but don't fail the whole operation
- toast.info('Note', 'Profile saved but avatar upload failed');
+ if (!response.ok) {
+ toast.error('Error', response.error?.message || 'Failed to save changes.');
+ setIsSavingEdit(false);
+ return;
+ }
+
+ // Upload avatar if changed (new local file URI)
+ if (editForm.avatar && editForm.avatar.startsWith('file://')) {
+ setIsUploadingAvatar(true);
+ const avatarResult = await api.updateBeneficiaryAvatar(beneficiaryId, editForm.avatar);
+ setIsUploadingAvatar(false);
+ if (!avatarResult.ok) {
+ console.warn('[BeneficiaryDetail] Failed to upload avatar:', avatarResult.error?.message);
+ toast.info('Note', 'Profile saved but avatar upload failed');
+ }
+ }
+ } else {
+ // Guardian/Caretaker: update only customName in user_access table
+ const response = await api.updateBeneficiaryCustomName(
+ beneficiaryId,
+ editForm.customName.trim() || null
+ );
+
+ if (!response.ok) {
+ toast.error('Error', response.error?.message || 'Failed to save nickname.');
+ setIsSavingEdit(false);
+ return;
}
}
setIsEditModalVisible(false);
- toast.success('Saved', 'Profile updated successfully');
+ toast.success('Saved', isCustodian ? 'Profile updated successfully' : 'Nickname saved');
loadBeneficiary(false);
} catch (err) {
toast.error('Error', 'Failed to save changes.');
@@ -375,7 +403,7 @@ export default function BeneficiaryDetailScreen() {
)}
- {beneficiary.name}
+ {beneficiary.customName || beneficiary.name}
}
>
-
+
)}
@@ -485,64 +513,94 @@ export default function BeneficiaryDetailScreen() {
>
- Edit Profile
+
+ {isCustodian ? 'Edit Profile' : 'Edit Nickname'}
+
setIsEditModalVisible(false)}>
- {/* Avatar */}
-
- {editForm.avatar ? (
-
- ) : (
-
-
-
- )}
- {isUploadingAvatar && (
-
-
- Uploading...
-
- )}
- {!isUploadingAvatar && (
-
-
-
- )}
-
+ {isCustodian ? (
+ <>
+ {/* Custodian: Avatar, Name, Address */}
+
+ {editForm.avatar ? (
+
+ ) : (
+
+
+
+ )}
+ {isUploadingAvatar && (
+
+
+ Uploading...
+
+ )}
+ {!isUploadingAvatar && (
+
+
+
+ )}
+
- {/* Name */}
-
- Name
- setEditForm(prev => ({ ...prev, name: text }))}
- placeholder="Full name"
- placeholderTextColor={AppColors.textMuted}
- />
-
+
+ Name
+ setEditForm(prev => ({ ...prev, name: text }))}
+ placeholder="Full name"
+ placeholderTextColor={AppColors.textMuted}
+ />
+
- {/* Address */}
-
- Address
- setEditForm(prev => ({ ...prev, address: text }))}
- placeholder="Street address"
- placeholderTextColor={AppColors.textMuted}
- multiline
- numberOfLines={3}
- />
-
+
+ Address
+ setEditForm(prev => ({ ...prev, address: text }))}
+ placeholder="Street address"
+ placeholderTextColor={AppColors.textMuted}
+ multiline
+ numberOfLines={3}
+ />
+
+ >
+ ) : (
+ <>
+ {/* Guardian/Caretaker: Only custom nickname */}
+
+
+ Set a personal nickname for {beneficiary?.name}. This is only visible to you.
+
+
+
+
+ Nickname
+ setEditForm(prev => ({ ...prev, customName: text }))}
+ placeholder={`e.g., "Mom", "Dad", "Grandma"`}
+ placeholderTextColor={AppColors.textMuted}
+ maxLength={100}
+ />
+
+
+
+ Original name:
+ {beneficiary?.name}
+
+ >
+ )}
@@ -819,4 +877,34 @@ const styles = StyleSheet.create({
buttonDisabled: {
opacity: 0.6,
},
+ // Non-custodian edit modal styles
+ nicknameInfo: {
+ backgroundColor: AppColors.surfaceSecondary,
+ padding: Spacing.md,
+ borderRadius: BorderRadius.md,
+ marginBottom: Spacing.lg,
+ },
+ nicknameInfoText: {
+ fontSize: FontSizes.sm,
+ color: AppColors.textSecondary,
+ lineHeight: 20,
+ },
+ originalNameContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ marginTop: Spacing.sm,
+ paddingTop: Spacing.md,
+ borderTopWidth: 1,
+ borderTopColor: AppColors.border,
+ },
+ originalNameLabel: {
+ fontSize: FontSizes.sm,
+ color: AppColors.textMuted,
+ marginRight: Spacing.xs,
+ },
+ originalNameValue: {
+ fontSize: FontSizes.sm,
+ color: AppColors.textSecondary,
+ fontWeight: FontWeights.medium,
+ },
});
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 9712bbe..29a6216 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -89,6 +89,9 @@ function BeneficiaryCard({ beneficiary, onPress, onActivate }: BeneficiaryCardPr
const statusConfig = equipmentStatus ? equipmentStatusConfig[equipmentStatus as keyof typeof equipmentStatusConfig] : equipmentStatusConfig.none;
+ // Display name: customName (e.g., "Mom") if set, otherwise full name
+ const displayName = beneficiary.customName || beneficiary.name;
+
// Check if avatar is valid (not empty, null, or placeholder)
const hasValidAvatar = beneficiary.avatar &&
beneficiary.avatar.trim() !== '' &&
@@ -116,7 +119,7 @@ function BeneficiaryCard({ beneficiary, onPress, onActivate }: BeneficiaryCardPr
) : (
- {beneficiary.name.charAt(0).toUpperCase()}
+ {displayName.charAt(0).toUpperCase()}
)}
@@ -124,7 +127,7 @@ function BeneficiaryCard({ beneficiary, onPress, onActivate }: BeneficiaryCardPr
{/* Name and Status */}
- {beneficiary.name}
+ {displayName}
{/* User's role for this beneficiary */}
{beneficiary.role && (
@@ -260,9 +263,10 @@ export default function HomeScreen() {
const handleActivate = (beneficiary: Beneficiary) => {
setCurrentBeneficiary(beneficiary);
+ const lovedOneName = beneficiary.customName || beneficiary.name;
router.push({
pathname: '/(auth)/activate',
- params: { lovedOneName: beneficiary.name, beneficiaryId: beneficiary.id.toString() },
+ params: { lovedOneName, beneficiaryId: beneficiary.id.toString() },
});
};
diff --git a/backend/src/routes/beneficiaries.js b/backend/src/routes/beneficiaries.js
index 8444ad2..0481324 100644
--- a/backend/src/routes/beneficiaries.js
+++ b/backend/src/routes/beneficiaries.js
@@ -508,8 +508,9 @@ router.post('/', async (req, res) => {
/**
* PATCH /api/me/beneficiaries/:id
- * Updates beneficiary info (requires custodian or guardian role)
- * Now uses the proper beneficiaries table (not users)
+ * Updates beneficiary info
+ * - Custodian: can update name, phone, address in beneficiaries table
+ * - Guardian/Caretaker: can only update customName in user_access table
*/
router.patch('/:id', async (req, res) => {
try {
@@ -518,53 +519,110 @@ router.patch('/:id', async (req, res) => {
console.log('[BENEFICIARY PATCH] Request:', { userId, beneficiaryId, body: req.body });
- // Check user has custodian or guardian access - using beneficiary_id
+ // Check user has access - using beneficiary_id
const { data: access, error: accessError } = await supabase
.from('user_access')
- .select('role')
+ .select('id, role')
.eq('accessor_id', userId)
.eq('beneficiary_id', beneficiaryId)
.single();
- if (accessError || !access || !['custodian', 'guardian'].includes(access.role)) {
- return res.status(403).json({ error: 'Only custodian or guardian can update beneficiary info' });
+ if (accessError || !access) {
+ return res.status(403).json({ error: 'Access denied to this beneficiary' });
}
- const { name, phone, address } = req.body;
+ const { name, phone, address, customName } = req.body;
+ const isCustodian = access.role === 'custodian';
- const updateData = {
- updated_at: new Date().toISOString()
- };
+ // Custodian can update beneficiary data (name, phone, address)
+ if (isCustodian) {
+ const updateData = {
+ updated_at: new Date().toISOString()
+ };
- if (name !== undefined) updateData.name = name;
- if (phone !== undefined) updateData.phone = phone;
- if (address !== undefined) updateData.address = address;
+ if (name !== undefined) updateData.name = name;
+ if (phone !== undefined) updateData.phone = phone;
+ if (address !== undefined) updateData.address = address;
- // Update in beneficiaries table
- const { data: beneficiary, error } = await supabase
- .from('beneficiaries')
- .update(updateData)
- .eq('id', beneficiaryId)
- .select()
- .single();
+ // Update in beneficiaries table
+ const { data: beneficiary, error } = await supabase
+ .from('beneficiaries')
+ .update(updateData)
+ .eq('id', beneficiaryId)
+ .select()
+ .single();
- if (error) {
- console.error('[BENEFICIARY PATCH] Supabase error:', error);
- return res.status(500).json({ error: 'Failed to update beneficiary' });
- }
-
- console.log('[BENEFICIARY PATCH] Success:', { id: beneficiary.id, name: beneficiary.name, address: beneficiary.address });
-
- res.json({
- success: true,
- beneficiary: {
- id: beneficiary.id,
- name: beneficiary.name,
- phone: beneficiary.phone,
- address: beneficiary.address || null,
- avatarUrl: beneficiary.avatar_url
+ if (error) {
+ console.error('[BENEFICIARY PATCH] Supabase error:', error);
+ return res.status(500).json({ error: 'Failed to update beneficiary' });
}
- });
+
+ console.log('[BENEFICIARY PATCH] Custodian updated:', { id: beneficiary.id, name: beneficiary.name, address: beneficiary.address });
+
+ res.json({
+ success: true,
+ beneficiary: {
+ id: beneficiary.id,
+ name: beneficiary.name,
+ displayName: beneficiary.name, // For custodian, displayName = name
+ originalName: beneficiary.name,
+ phone: beneficiary.phone,
+ address: beneficiary.address || null,
+ avatarUrl: beneficiary.avatar_url
+ }
+ });
+ } else {
+ // Guardian/Caretaker can only update their custom_name
+ if (customName === undefined) {
+ return res.status(400).json({ error: 'customName is required for non-custodian roles' });
+ }
+
+ // Validate custom name
+ if (customName !== null && typeof customName !== 'string') {
+ return res.status(400).json({ error: 'customName must be a string or null' });
+ }
+ if (customName && customName.length > 100) {
+ return res.status(400).json({ error: 'customName must be 100 characters or less' });
+ }
+
+ // Update custom_name in user_access
+ const { error: updateError } = await supabase
+ .from('user_access')
+ .update({
+ custom_name: customName || null // Empty string becomes null
+ })
+ .eq('id', access.id);
+
+ if (updateError) {
+ console.error('[BENEFICIARY PATCH] Custom name update error:', updateError);
+ return res.status(500).json({ error: 'Failed to update custom name' });
+ }
+
+ // Get beneficiary data for response
+ const { data: beneficiary } = await supabase
+ .from('beneficiaries')
+ .select('id, name, phone, address, avatar_url')
+ .eq('id', beneficiaryId)
+ .single();
+
+ const displayName = customName || beneficiary?.name || null;
+
+ console.log('[BENEFICIARY PATCH] Custom name updated:', { beneficiaryId, customName, displayName });
+
+ res.json({
+ success: true,
+ beneficiary: {
+ id: beneficiaryId,
+ name: beneficiary?.name || null,
+ displayName: displayName,
+ originalName: beneficiary?.name || null,
+ customName: customName || null,
+ phone: beneficiary?.phone || null,
+ address: beneficiary?.address || null,
+ avatarUrl: beneficiary?.avatar_url || null
+ }
+ });
+ }
} catch (error) {
console.error('[BENEFICIARY PATCH] Error:', error);