Add account sharing, Get Paid schedule, recurring manager, overspend warnings, mobile nav

Account sharing:
- fileStorage: add shares.json with createShare/listShares/getShareData/revokeShare
- /api/share/account: real implementation using session token
- /api/share/list: GET endpoint returning owned + received shares
- /api/share/data: GET endpoint to fetch a shared account's data
- SharedAccountManager: use session token, load existing shares, revoke support
- AccountManagement: render SharedAccountManager per-account
- page.tsx: load received shares on login, show "Shared with me" cards in Accounts view

Get Paid schedule:
- PaycheckSchedule type: weekly/biweekly/semimonthly/monthly with day config
- computeNextPayday(): calculates next payday from today
- Schedule UI in GetPaid modal with live "Next payday: ..." preview
- Schedule persisted to localStorage template

Recurring transaction manager:
- Fix biweekly support (was missing)
- Show "due now" vs "upcoming" with amber badge for overdue
- Render in Transactions view; onApplyRecurring creates new transactions + updates lastAppliedDate

Envelope overspending:
- Red warning banner at top of Envelopes view listing overspent envelopes
- Red ring on individual overspent envelope cards

Mobile layout:
- Fixed bottom tab bar (Home/Accounts/Txns/Envelopes/More) on sm: and below
- Active tab highlighted in blue
- Main content padded to avoid being hidden by bottom nav

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-07-14 09:10:10 -04:00
1 parent 18fe6b5141
commit 5339e9818a
9 files changed
+518 -161

No files matched your search

+5 -1
View File
@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
import SharedAccountManager from './SharedAccountManager';
interface Account {
id: string;
@@ -165,7 +166,8 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
) : (
<div className="divide-y divide-gray-200">
{(accounts || []).map((account) => (
<div key={account.id} className="px-6 py-4 flex items-center justify-between hover:bg-gray-50" role="article" aria-label={`Account: ${account.name}`}>
<div key={account.id} className="px-6 py-4 hover:bg-gray-50" role="article" aria-label={`Account: ${account.name}`}>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className={`w-4 h-4 rounded-full ${account.color}`} role="img" aria-label={`Color indicator: ${account.color}`}></div>
<div>
@@ -210,6 +212,8 @@ export default function AccountManagement({ accounts, onAccountAdd, onAccountUpd
</button>
</div>
</div>
</div>
<SharedAccountManager accountId={account.id} accountName={account.name} />
</div>
))}
</div>
+97 -11
View File
@@ -35,12 +35,20 @@ interface Transaction {
type DistributionMethod = 'proportional' | 'equal' | 'custom';
type AllocationMap = Record<string, number>;
interface PaycheckSchedule {
frequency: 'weekly' | 'biweekly' | 'semimonthly' | 'monthly';
dayOfWeek?: number; // 0-6, for weekly/biweekly
dayOfMonth?: number; // 1-31, for monthly/semimonthly
dayOfMonth2?: number; // for semimonthly second date
}
interface PaycheckTemplate {
amount: number;
selectedAccountId: string;
description: string;
distributionMethod: DistributionMethod;
customAllocations: AllocationMap;
schedule?: PaycheckSchedule;
}
interface GetPaidProps {
@@ -70,6 +78,8 @@ export default function GetPaid({
const [defaultAmountInput, setDefaultAmountInput] = useState('');
const [saveTemplate, setSaveTemplate] = useState(false);
const [savedTemplate, setSavedTemplate] = useState<PaycheckTemplate | null>(null);
const [showSchedule, setShowSchedule] = useState(false);
const [schedule, setSchedule] = useState<PaycheckSchedule>({ frequency: 'biweekly', dayOfWeek: 5 });
useEffect(() => {
if (typeof window === 'undefined') return;
@@ -122,12 +132,37 @@ export default function GetPaid({
const applySavedTemplate = () => {
if (!savedTemplate) return;
setAmount(savedTemplate.amount?.toString() || '');
setSelectedAccountId(savedTemplate.selectedAccountId || '');
setDescription(savedTemplate.description || 'Paycheck');
setDistributionMethod(savedTemplate.distributionMethod || 'proportional');
setCustomAllocations(savedTemplate.customAllocations || {});
if (savedTemplate.schedule) setSchedule(savedTemplate.schedule);
};
const computeNextPayday = (sched: PaycheckSchedule): Date => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const next = new Date(today);
if (sched.frequency === 'weekly' || sched.frequency === 'biweekly') {
const dow = sched.dayOfWeek ?? 5;
const diff = (dow - today.getDay() + 7) % 7 || (sched.frequency === 'biweekly' ? 14 : 7);
next.setDate(today.getDate() + diff);
} else if (sched.frequency === 'semimonthly') {
const d1 = sched.dayOfMonth ?? 1;
const d2 = sched.dayOfMonth2 ?? 15;
const thisMonthD1 = new Date(today.getFullYear(), today.getMonth(), d1);
const thisMonthD2 = new Date(today.getFullYear(), today.getMonth(), d2);
const nextMonthD1 = new Date(today.getFullYear(), today.getMonth() + 1, d1);
const candidates = [thisMonthD1, thisMonthD2, nextMonthD1].filter(d => d > today);
return candidates.sort((a, b) => a.getTime() - b.getTime())[0] ?? nextMonthD1;
} else {
const dom = sched.dayOfMonth ?? 1;
next.setDate(dom);
if (next <= today) next.setMonth(next.getMonth() + 1);
}
return next;
};
const handleSubmit = (event: React.FormEvent) => {
@@ -183,6 +218,7 @@ export default function GetPaid({
description,
distributionMethod,
customAllocations: previewAllocations,
schedule: showSchedule ? schedule : undefined,
};
localStorage.setItem('paycheckTemplate', JSON.stringify(template));
setSavedTemplate(template);
@@ -384,17 +420,67 @@ export default function GetPaid({
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="saveTemplate"
checked={saveTemplate}
onChange={(event) => setSaveTemplate(event.target.checked)}
className="mr-2"
/>
<label htmlFor="saveTemplate" className="text-sm text-gray-700">
Save this information to use next time
<div className="space-y-3">
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="saveTemplate" checked={saveTemplate} onChange={e => setSaveTemplate(e.target.checked)} className="rounded" />
<span className="text-sm text-gray-700">Save this information to use next time</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={showSchedule} onChange={e => setShowSchedule(e.target.checked)} className="rounded" />
<span className="text-sm text-gray-700">Set a paycheck schedule (shows next payday)</span>
</label>
{showSchedule && (
<div className="ml-6 p-3 bg-green-50 border border-green-200 rounded-lg space-y-3">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Frequency</label>
<select value={schedule.frequency} onChange={e => setSchedule(s => ({ ...s, frequency: e.target.value as PaycheckSchedule['frequency'] }))}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded bg-white text-gray-900">
<option value="weekly">Weekly</option>
<option value="biweekly">Bi-weekly (every 2 weeks)</option>
<option value="semimonthly">Semi-monthly (twice a month)</option>
<option value="monthly">Monthly</option>
</select>
</div>
{(schedule.frequency === 'weekly' || schedule.frequency === 'biweekly') && (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Payday</label>
<select value={schedule.dayOfWeek ?? 5} onChange={e => setSchedule(s => ({ ...s, dayOfWeek: parseInt(e.target.value) }))}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded bg-white text-gray-900">
{['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'].map((d, i) => (
<option key={i} value={i}>{d}</option>
))}
</select>
</div>
)}
{schedule.frequency === 'semimonthly' && (
<div className="grid grid-cols-2 gap-2">
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">First payday</label>
<input type="number" min={1} max={31} value={schedule.dayOfMonth ?? 1}
onChange={e => setSchedule(s => ({ ...s, dayOfMonth: parseInt(e.target.value) }))}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded bg-white text-gray-900" />
</div>
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Second payday</label>
<input type="number" min={1} max={31} value={schedule.dayOfMonth2 ?? 15}
onChange={e => setSchedule(s => ({ ...s, dayOfMonth2: parseInt(e.target.value) }))}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded bg-white text-gray-900" />
</div>
</div>
)}
{schedule.frequency === 'monthly' && (
<div>
<label className="block text-xs font-medium text-gray-700 mb-1">Day of month</label>
<input type="number" min={1} max={31} value={schedule.dayOfMonth ?? 1}
onChange={e => setSchedule(s => ({ ...s, dayOfMonth: parseInt(e.target.value) }))}
className="w-full px-2 py-1.5 text-sm border border-gray-300 rounded bg-white text-gray-900" />
</div>
)}
<p className="text-xs text-green-800 font-medium">
Next payday: {computeNextPayday(schedule).toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
</p>
</div>
)}
</div>
{selectedAccountId && accountEnvelopes.length > 0 && (
+91 -71
View File
@@ -2,97 +2,117 @@
import { useState } from 'react';
interface RecurringTx {
id: string;
description: string;
amount: number;
accountId: string;
envelopeId?: string;
date: Date;
isRecurring?: boolean;
recurringFrequency?: 'weekly' | 'biweekly' | 'monthly' | 'yearly';
lastAppliedDate?: string;
}
interface RecurringTransactionManagerProps {
transactions: Array<{
id: string;
description: string;
amount: number;
isRecurring?: boolean;
recurringFrequency?: 'weekly' | 'monthly' | 'yearly';
lastAppliedDate?: string;
}>;
onApplyRecurring: (transactions: any[]) => void;
transactions: RecurringTx[];
onApplyRecurring: (due: RecurringTx[]) => void;
}
function nextDate(tx: RecurringTx): Date | null {
const last = tx.lastAppliedDate ? new Date(tx.lastAppliedDate) : (tx.date instanceof Date ? tx.date : new Date(tx.date));
const next = new Date(last);
switch (tx.recurringFrequency) {
case 'weekly': next.setDate(next.getDate() + 7); break;
case 'biweekly': next.setDate(next.getDate() + 14); break;
case 'monthly': next.setMonth(next.getMonth() + 1); break;
case 'yearly': next.setFullYear(next.getFullYear() + 1); break;
default: return null;
}
return next;
}
export default function RecurringTransactionManager({ transactions, onApplyRecurring }: RecurringTransactionManagerProps) {
const [expanded, setExpanded] = useState(false);
const getNextRecurringDate = (lastDate: string | undefined, frequency: string | undefined): Date | null => {
if (!lastDate || !frequency) return null;
const last = new Date(lastDate);
const now = new Date();
const recurring = transactions.filter(t => t.isRecurring && t.recurringFrequency);
const now = new Date();
switch (frequency) {
case 'weekly':
const nextWeek = new Date(last);
nextWeek.setDate(nextWeek.getDate() + 7);
return nextWeek <= now ? nextWeek : null;
case 'monthly':
const nextMonth = new Date(last);
nextMonth.setMonth(nextMonth.getMonth() + 1);
return nextMonth <= now ? nextMonth : null;
case 'yearly':
const nextYear = new Date(last);
nextYear.setFullYear(nextYear.getFullYear() + 1);
return nextYear <= now ? nextYear : null;
default:
return null;
}
};
const due = recurring.filter(t => {
const n = nextDate(t);
return n && n <= now;
});
const recurringTransactions = transactions.filter(t => t.isRecurring);
const upcomingTransactions = recurringTransactions.filter(t => getNextRecurringDate(t.lastAppliedDate, t.recurringFrequency));
const upcoming = recurring.filter(t => {
const n = nextDate(t);
return n && n > now;
}).sort((a, b) => {
const na = nextDate(a)!.getTime();
const nb = nextDate(b)!.getTime();
return na - nb;
});
if (recurringTransactions.length === 0) {
return null;
}
if (recurring.length === 0) return null;
return (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className={`border rounded-lg p-4 mb-6 ${due.length > 0 ? 'bg-amber-50 border-amber-300' : 'bg-blue-50 border-blue-200'}`}>
<button
onClick={() => setExpanded(!expanded)}
className="w-full flex justify-between items-center font-semibold text-blue-900 hover:text-blue-700"
className="w-full flex justify-between items-center font-semibold text-gray-900 hover:text-gray-700"
>
<span>🔄 Recurring Transactions ({recurringTransactions.length})</span>
<span>{expanded ? '▲' : '▼'}</span>
<span className="flex items-center gap-2">
<span>↻ Recurring Transactions</span>
{due.length > 0 && (
<span className="px-2 py-0.5 bg-amber-500 text-white text-xs rounded-full">{due.length} due</span>
)}
</span>
<span className="text-gray-500 text-sm">{expanded ? '▲' : '▼'}</span>
</button>
{expanded && (
<div className="mt-4 space-y-3">
<p className="text-sm text-blue-800">
You have <strong>{upcomingTransactions.length}</strong> recurring transaction(s) that are due to be applied.
</p>
<div className="mt-4 space-y-4">
{due.length > 0 && (
<div>
<p className="text-sm font-medium text-amber-900 mb-2">Due now ({due.length})</p>
<div className="space-y-2">
{due.map(tx => (
<div key={tx.id} className="bg-white border border-amber-200 rounded-lg p-3 flex justify-between items-center">
<div>
<p className="font-medium text-gray-900 text-sm">{tx.description}</p>
<p className="text-xs text-gray-500 capitalize">{tx.recurringFrequency} · was due {nextDate(tx)?.toLocaleDateString()}</p>
</div>
<p className={`font-semibold text-sm ${tx.amount >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{tx.amount >= 0 ? '+' : ''}${Math.abs(tx.amount).toFixed(2)}
</p>
</div>
))}
</div>
<button
onClick={() => onApplyRecurring(due)}
className="w-full mt-3 bg-amber-600 text-white py-2 px-4 rounded-lg hover:bg-amber-700 transition-colors text-sm font-medium"
>
Apply {due.length} overdue transaction{due.length > 1 ? 's' : ''} now
</button>
</div>
)}
{upcomingTransactions.map(tx => (
<div key={tx.id} className="bg-white p-3 rounded border border-blue-100">
<div className="flex justify-between items-center">
<div>
<p className="font-medium text-gray-900">{tx.description}</p>
<p className="text-xs text-gray-500">
Frequency: <span className="capitalize font-semibold">{tx.recurringFrequency}</span>
</p>
</div>
<p className={`font-semibold ${tx.amount >= 0 ? 'text-green-600' : 'text-red-600'}`}>
${Math.abs(tx.amount).toFixed(2)}
</p>
{upcoming.length > 0 && (
<div>
<p className="text-sm font-medium text-blue-900 mb-2">Upcoming</p>
<div className="space-y-2">
{upcoming.slice(0, 5).map(tx => (
<div key={tx.id} className="bg-white border border-blue-100 rounded-lg p-3 flex justify-between items-center">
<div>
<p className="font-medium text-gray-900 text-sm">{tx.description}</p>
<p className="text-xs text-gray-500 capitalize">{tx.recurringFrequency} · next: {nextDate(tx)?.toLocaleDateString()}</p>
</div>
<p className={`font-semibold text-sm ${tx.amount >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{tx.amount >= 0 ? '+' : ''}${Math.abs(tx.amount).toFixed(2)}
</p>
</div>
))}
</div>
</div>
))}
{upcomingTransactions.length > 0 && (
<button
onClick={() => {
const newTransactions = upcomingTransactions.map(tx => ({
...tx,
lastAppliedDate: new Date().toISOString().split('T')[0],
}));
onApplyRecurring(newTransactions);
setExpanded(false);
}}
className="w-full mt-3 bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700 transition-colors"
>
Apply Now
</button>
)}
</div>
)}
+75 -51
View File
@@ -1,61 +1,91 @@
'use client';
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { AuthService } from '../lib/authService';
interface SharedUser {
shareId: string;
email: string;
role: string;
}
interface SharedAccountManagerProps {
userId: string;
accountId: string;
accountName: string;
}
export default function SharedAccountManager({ userId, accountId, accountName }: SharedAccountManagerProps) {
export default function SharedAccountManager({ accountId, accountName }: SharedAccountManagerProps) {
const [expanded, setExpanded] = useState(false);
const [shareEmail, setShareEmail] = useState('');
const [shareRole, setShareRole] = useState<'view' | 'edit'>('view');
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [sharedUsers, setSharedUsers] = useState<Array<{ email: string; role: string }>>([]);
const [sharedUsers, setSharedUsers] = useState<SharedUser[]>([]);
const [loading, setLoading] = useState(false);
const [loadingShares, setLoadingShares] = useState(false);
useEffect(() => {
if (!expanded) return;
setLoadingShares(true);
const token = AuthService.getSessionToken();
fetch('/api/share/list', { headers: { 'x-session-token': token ?? '' } })
.then(r => r.json())
.then(data => {
const forThisAccount: SharedUser[] = (data.owned ?? [])
.filter((s: any) => s.accountId === accountId)
.map((s: any) => ({ shareId: s.shareId, email: s.targetEmail, role: s.role }));
setSharedUsers(forThisAccount);
})
.catch(() => {})
.finally(() => setLoadingShares(false));
}, [expanded, accountId]);
const handleShareAccount = async (e: React.FormEvent) => {
e.preventDefault();
setMessage(null);
setLoading(true);
try {
const token = AuthService.getSessionToken();
const response = await fetch('/api/share/account', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': userId,
},
headers: { 'Content-Type': 'application/json', 'x-session-token': token ?? '' },
body: JSON.stringify({ accountId, email: shareEmail, role: shareRole }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Failed to share account');
}
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Failed to share account');
setMessage({ type: 'success', text: `Account shared with ${shareEmail}` });
setSharedUsers(prev => [...prev, { shareId: result.shareId, email: shareEmail, role: shareRole }]);
setShareEmail('');
setSharedUsers([...sharedUsers, { email: shareEmail, role: shareRole }]);
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Failed to share account',
});
setMessage({ type: 'error', text: error instanceof Error ? error.message : 'Failed to share account' });
} finally {
setLoading(false);
}
};
const handleRevoke = async (shareId: string) => {
if (!confirm('Remove this person\'s access?')) return;
try {
const token = AuthService.getSessionToken();
const response = await fetch('/api/share/account', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json', 'x-session-token': token ?? '' },
body: JSON.stringify({ shareId }),
});
if (!response.ok) throw new Error('Failed to revoke');
setSharedUsers(prev => prev.filter(u => u.shareId !== shareId));
} catch {
setMessage({ type: 'error', text: 'Failed to remove access' });
}
};
return (
<div className="mt-4 pt-4 border-t border-gray-200">
<button
onClick={() => setExpanded(!expanded)}
className="flex justify-between items-center w-full font-semibold text-gray-900 hover:text-gray-700"
className="flex justify-between items-center w-full font-semibold text-gray-900 hover:text-gray-700 text-sm"
>
<span>👥 Share Account</span>
<span>👥 Share "{accountName}"</span>
<span>{expanded ? '▲' : '▼'}</span>
</button>
@@ -63,71 +93,65 @@ export default function SharedAccountManager({ userId, accountId, accountName }:
<div className="mt-4 space-y-4">
<form onSubmit={handleShareAccount} className="space-y-3">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email Address
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Email Address</label>
<input
type="email"
value={shareEmail}
onChange={(e) => setShareEmail(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900 text-sm"
placeholder="user@example.com"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Permission Level
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">Permission</label>
<select
value={shareRole}
onChange={(e) => setShareRole(e.target.value as 'view' | 'edit')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white text-gray-900 text-sm"
>
<option value="view">View Only</option>
<option value="edit">Can Edit</option>
</select>
</div>
<button
type="submit"
disabled={loading || !shareEmail}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors"
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 transition-colors text-sm"
>
{loading ? 'Sharing...' : 'Share Account'}
{loading ? 'Sharing…' : 'Share Account'}
</button>
</form>
{message && (
<div
className={`p-3 rounded-md text-sm ${
message.type === 'success'
? 'bg-green-50 text-green-800 border border-green-200'
: 'bg-red-50 text-red-800 border border-red-200'
}`}
>
<div className={`p-3 rounded-md text-sm ${message.type === 'success' ? 'bg-green-50 text-green-800 border border-green-200' : 'bg-red-50 text-red-800 border border-red-200'}`}>
{message.text}
</div>
)}
{sharedUsers.length > 0 && (
<div className="mt-4 pt-4 border-t border-gray-200">
<p className="text-sm font-medium text-gray-900 mb-2">Shared With:</p>
{loadingShares ? (
<p className="text-sm text-gray-400">Loading…</p>
) : sharedUsers.length > 0 ? (
<div>
<p className="text-sm font-medium text-gray-900 mb-2">Shared with:</p>
<ul className="space-y-2">
{sharedUsers.map((user) => (
<li key={user.email} className="flex justify-between items-center text-sm">
<span className="text-gray-700">{user.email}</span>
<span className="text-xs bg-gray-100 px-2 py-1 rounded capitalize">
{user.role}
</span>
{sharedUsers.map((u) => (
<li key={u.shareId} className="flex justify-between items-center text-sm bg-gray-50 px-3 py-2 rounded">
<div>
<span className="text-gray-800">{u.email}</span>
<span className="ml-2 text-xs bg-gray-200 px-1.5 py-0.5 rounded capitalize">{u.role}</span>
</div>
<button onClick={() => handleRevoke(u.shareId)} className="text-xs text-red-600 hover:text-red-800">Remove</button>
</li>
))}
</ul>
</div>
) : (
<p className="text-xs text-gray-500">Not shared with anyone yet.</p>
)}
<p className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
💡 <strong>Note:</strong> Shared accounts will appear in the recipient's Capsule app. They can view or edit based on the permissions you set.
<p className="text-xs text-gray-500 bg-gray-50 p-2 rounded">
The recipient must already have a Capsule account. They'll see this account in their "Shared with me" section.
</p>
</div>
)}