Successfully implemented LP position notifications for funded invoice state changes and due date monitoring. The system now sends both in-app toast notifications and persistent notifications stored in a notification center.
- Location:
src/context/NotificationContext.tsx - Manages a list of notifications with full read/unread state
- Stores up to 20 most recent notifications in localStorage
- Provides methods:
addNotification(),markAsRead(),markAllAsRead() - Notification types:
"funded" | "settled" | "expired" | "disputed" | "info" | "warning"
- Location:
src/hooks/usePositionPolling.ts - Triggers notifications on invoice state transitions:
Funded → Paid: Success toast + notification "Invoice #X paid - You earned Y USDC"Funded → Defaulted: Error toast + notification "Invoice #X expired"Funded → Cancelled: Error toast + notification "Invoice #X disputed"- Due date expiry: Detects when funded invoice's due date has passed
- Polls every 60 seconds
- Only tracks invoices funded by the connected wallet address
- Yields formatted as USDC in notifications
- Location:
src/components/NotificationBell.tsx - Fixed position bell icon in top navbar
- Displays unread badge with count
- Polls external API (
/api/notifications/[address]) every 60 seconds - Merges external and local notifications
- Sorted by most recent first
- Opens notification drawer when clicked
- Location:
src/components/NotificationDrawer.tsx - Displays last 20 notifications with timestamps
- Click individual notification to mark as read
- "Mark all as read" button
- Color-coded by notification type (red for expired/disputed, blue for settled, etc.)
- Empty state when no notifications
- Dark mode support
- Location:
src/components/LPDashboard.tsx - Integrated
usePositionPollinghook to monitor funded invoices - Passes required callbacks (
addToast,addNotification) - Continuously monitors for state changes while LP is viewing dashboard
- Location:
__tests__/usePositionPolling.test.ts - Tests all notification triggers and state transitions
- Validates address filtering
- Tests null/undefined handling
- Verifies one-time expiry notifications
LP Views Dashboard
↓
LPDashboard Component Loads
↓
usePositionPolling Hook Starts
├─ Monitors all funded invoices by current address
├─ Polls every 60 seconds
└─ Compares previous states with current states
├─ Funded → Paid? → addToast("success") + addNotification("settled")
├─ Funded → Defaulted? → addToast("error") + addNotification("expired")
├─ Funded → Cancelled? → addToast("error") + addNotification("disputed")
└─ Due date passed? → addToast("error") + addNotification("expired")
↓
Toast Displays (auto-dismisses in 6s)
↓
Notification Saved to Context
├─ Stored in memory
└─ Persisted to localStorage
↓
Bell Icon Updates
├─ Unread count badge
└─ User can click to view notification center
External API Notifications
├─ Polled every 60 seconds
├─ Merged with local notifications
└─ Deduped by ID
| File | Type | Changes |
|---|---|---|
src/context/NotificationContext.tsx |
Modified | Enhanced with notification items, read state, localStorage persistence |
src/components/NotificationBell.tsx |
Modified | Refactored to use context, merge external notifications |
src/components/NotificationDrawer.tsx |
Modified | Refactored to use context, improved UI |
src/components/LPDashboard.tsx |
Modified | Added usePositionPolling hook integration |
src/hooks/usePositionPolling.ts |
New | Position polling and notification logic |
__tests__/usePositionPolling.test.ts |
New | Test coverage for polling hook |
- Settled (Blue): Invoice paid successfully
- Expired (Red): Invoice defaulted or due date passed
- Disputed (Orange): Invoice cancelled/disputed
- Success/Error Toast: Immediate feedback on state changes
- LP funds an invoice → Added to monitoring list
- Payer pays the invoice → Toast appears "Invoice #X paid" + Notification
- LP clicks bell icon → Notification drawer opens showing last 20 events
- LP clicks notification → Marked as read (visual change)
- Due date passes → One-time expiry notification sent
- Page refresh → Notifications persist from localStorage
Run tests with:
npm test usePositionPolling.test.tsTests verify:
- ✅ Paid notification triggers correctly
- ✅ Defaulted notification triggers correctly
- ✅ Disputed notification triggers correctly
- ✅ Due date expiry notification fires once
- ✅ Only notifies for LP's own funded invoices
- ✅ No notifications when address is null
Implemented an opt-in email reminder system for payers using Resend and Supabase. Payers can now subscribe to receive email notifications 72 hours and 24 hours before their outstanding invoices are due.
- Location:
src/emails/PaymentReminder.tsx - Built with
react-email - Includes invoice ID, amount, token, due date, and deep links to the payer dashboard and "Pay Now" page.
- Location:
app/api/reminders/route.ts POST: Saves or updates payer email preferences in Supabase.GET: Background job logic (secured byCRON_SECRET) that:- Fetches all active reminder preferences.
- Identifies funded invoices due in 72h or 24h.
- Sends emails via Resend SDK for each milestone once.
- Tracks sent emails in a
sent_reminderstable to prevent duplicates.
- Location:
app/api/reminders/unsubscribe/route.ts - Simple GET handler to disable reminders for a given address.
- Location:
src/components/payer/PayerReminderOptIn.tsx - A modern opt-in form integrated into the Payer Dashboard.
- Allows users to enter their email and toggle reminders on/off.
- Location:
src/lib/supabase.ts - Configured Supabase client for persistence.
- Requires two tables:
reminder_preferencesandsent_reminders.
- Location:
__tests__/reminders.test.ts - Full test coverage for the API route with mocked Resend SDK and Supabase client.
- Verifies opt-in saving, milestone detection, and duplicate prevention.
-
Environment Variables:
RESEND_API_KEY: API key from resend.comNEXT_PUBLIC_SUPABASE_URL&NEXT_PUBLIC_SUPABASE_ANON_KEY: Supabase credentials.SUPABASE_SERVICE_ROLE_KEY: Required for the background cron job to bypass RLS.CRON_SECRET: Secret token for securing the trigger endpoint.
-
Cron Job:
- Set up a periodic job (e.g., via Vercel Cron or GitHub Actions) to call
GET /api/reminderswith theAuthorization: Bearer <CRON_SECRET>header.
- Set up a periodic job (e.g., via Vercel Cron or GitHub Actions) to call
- All existing functionality preserved
- Providers already in place (
app/layout.tsx) - Backward compatible with existing notification API
- No modifications to invoice contract/types