Skip to content

Commit edddb87

Browse files
feat: paid privacy opt-out, email thread entitlements, and settings cleanup
Backend: - Add paid opt-out of training data (schema, enforcement in conversationLogs, Polar webhooks) - Add email thread metering: 10 free/month, auto-paywall email on 11th via preflightOutboundEmail - Integrate preflight checks into emailReply.ts and notes.ts - Add Polar webhook handlers for subscription lifecycle (create/update/cancel) - Auto-populate email from mentraUserId on user creation - Add backfillEmailsFromMentraId internal mutation for existing users - New email templates: OptOutCheckout, EmailThreadPaywall Frontend: - Settings: add Training Data card (sends opt-out email, no in-app checkout) - Remove all payment/billing/usage language from web app - Remove Email Threads card entirely (entitlement handled via email) - Type safety fixes across ChatPage, FollowupsPage, MemoryPage, QueuePage Screenshots: - Add iPhone 15 mobile screenshots for Home, Memory, Follow-ups, Queue, Settings Amp-Thread-ID: https://ampcode.com/threads/T-019c9d33-f47d-73fa-b739-2873a25a4d6f Co-authored-by: Amp <amp@ampcode.com>
1 parent 89de0f5 commit edddb87

32 files changed

Lines changed: 1827 additions & 925 deletions

apps/api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"dependencies": {
2222
"@elysiajs/cors": "^1.4.1",
2323
"@t3-oss/env-core": "^0.13.10",
24-
"convex": "^1.31.7",
24+
"convex": "^1.32.0",
2525
"jose": "^6.1.3",
2626
"jsonwebtoken": "^9.0.3",
2727
"zod": "^3.25.76"

apps/application/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
"@mentra/sdk": "^2.1.29",
1919
"@t3-oss/env-core": "^0.13.10",
2020
"@tavily/core": "^0.5.14",
21-
"convex": "^1.31.7",
21+
"convex": "^1.32.0",
2222
"exa-js": "^1.10.2",
2323
"openai": "^5.23.2",
2424
"zod": "^3.25.76"

apps/web/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,30 @@
2727
"@vercel/analytics": "^1.6.1",
2828
"class-variance-authority": "^0.7.1",
2929
"clsx": "^2.1.1",
30-
"convex": "^1.31.7",
30+
"convex": "^1.32.0",
3131
"date-fns": "^4.1.0",
32-
"framer-motion": "^12.34.0",
32+
"framer-motion": "^12.34.3",
3333
"lucide-react": "^0.553.0",
34-
"mapbox-gl": "^3.18.1",
35-
"motion": "^12.34.0",
34+
"mapbox-gl": "^3.19.0",
35+
"motion": "^12.34.3",
3636
"react": "^19.2.4",
37-
"react-day-picker": "^9.13.2",
37+
"react-day-picker": "^9.14.0",
3838
"react-dom": "^19.2.4",
3939
"react-map-gl": "^8.1.0",
40-
"react-router-dom": "^7.13.0",
40+
"react-router-dom": "^7.13.1",
4141
"recharts": "^3.7.0",
42-
"tailwind-merge": "^3.4.0",
42+
"tailwind-merge": "^3.5.0",
4343
"zod": "^3.25.76"
4444
},
4545
"devDependencies": {
46-
"@tailwindcss/vite": "^4.1.18",
47-
"@types/node": "^24.10.13",
46+
"@tailwindcss/vite": "^4.2.1",
47+
"@types/node": "^24.10.15",
4848
"@types/react": "^19.2.14",
4949
"@types/react-dom": "^19.2.3",
5050
"@vitejs/plugin-react": "^4.7.0",
51-
"autoprefixer": "^10.4.24",
51+
"autoprefixer": "^10.4.27",
5252
"postcss": "^8.5.6",
53-
"tailwindcss": "^4.1.18",
53+
"tailwindcss": "^4.2.1",
5454
"tailwindcss-animate": "^1.0.7",
5555
"tw-animate-css": "^1.4.0",
5656
"typescript": "^5.9.3",

apps/web/src/components/ChatPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function ChatPage({ mentraUserId }: ChatPageProps) {
7676
useEffect(() => {
7777
if (existingMessages && pendingMessages.length > 0) {
7878
const existingContents = new Set(
79-
existingMessages.map((m) => `${m.role}:${m.content}`),
79+
existingMessages.map((m: Message) => `${m.role}:${m.content}`),
8080
);
8181
const remaining = pendingMessages.filter(
8282
(m) => !existingContents.has(`${m.role}:${m.content}`),

apps/web/src/components/FollowupChatPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function FollowupChatPage({ mentraUserId }: FollowupChatPageProps) {
4646
useEffect(() => {
4747
if (existingMessages && pendingMessages.length > 0) {
4848
const existingContents = new Set(
49-
existingMessages.map((m) => `${m.role}:${m.content}`),
49+
existingMessages.map((m: Message) => `${m.role}:${m.content}`),
5050
);
5151
const remaining = pendingMessages.filter(
5252
(m) => !existingContents.has(`${m.role}:${m.content}`),

apps/web/src/components/FollowupsPage.tsx

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -125,52 +125,60 @@ export function FollowupsPage({ userId }: FollowupsPageProps) {
125125
<h2 className="text-xl font-semibold">Follow-ups</h2>
126126

127127
<div className="space-y-4">
128-
{followups.map((followup) => (
129-
<Card
130-
key={followup._id}
131-
className={followup.status === "pending" ? "cursor-pointer" : ""}
132-
onClick={() => {
133-
if (followup.status === "pending") {
134-
navigate(`/followups/chat/${followup._id}`);
135-
}
136-
}}
137-
>
138-
<CardHeader className="pb-2">
139-
<div className="flex items-center justify-between gap-2">
140-
<CardTitle className="text-lg">{followup.topic}</CardTitle>
141-
<StatusBadge status={followup.status} />
142-
</div>
143-
</CardHeader>
144-
<CardContent className="space-y-3">
145-
<p className="text-foreground text-sm">{followup.summary}</p>
128+
{followups.map(
129+
(followup: {
130+
_id: Id<"followups">;
131+
topic: string;
132+
summary: string;
133+
status: "pending" | "completed" | "dismissed";
134+
createdAt: string;
135+
}) => (
136+
<Card
137+
key={followup._id}
138+
className={followup.status === "pending" ? "cursor-pointer" : ""}
139+
onClick={() => {
140+
if (followup.status === "pending") {
141+
navigate(`/followups/chat/${followup._id}`);
142+
}
143+
}}
144+
>
145+
<CardHeader className="pb-2">
146+
<div className="flex items-center justify-between gap-2">
147+
<CardTitle className="text-lg">{followup.topic}</CardTitle>
148+
<StatusBadge status={followup.status} />
149+
</div>
150+
</CardHeader>
151+
<CardContent className="space-y-3">
152+
<p className="text-foreground text-sm">{followup.summary}</p>
146153

147-
<div className="flex items-center justify-between">
148-
<p className="text-xs text-foreground/50">
149-
{formatRelativeTime(followup.createdAt)}
150-
</p>
154+
<div className="flex items-center justify-between">
155+
<p className="text-xs text-foreground/50">
156+
{formatRelativeTime(followup.createdAt)}
157+
</p>
151158

152-
{followup.status === "pending" && (
153-
<div className="flex gap-2">
154-
<Button
155-
variant="neutral"
156-
size="sm"
157-
onClick={(e) => handleComplete(followup._id, e)}
158-
>
159-
Complete
160-
</Button>
161-
<Button
162-
variant="neutral"
163-
size="sm"
164-
onClick={(e) => handleDismiss(followup._id, e)}
165-
>
166-
Dismiss
167-
</Button>
168-
</div>
169-
)}
170-
</div>
171-
</CardContent>
172-
</Card>
173-
))}
159+
{followup.status === "pending" && (
160+
<div className="flex gap-2">
161+
<Button
162+
variant="neutral"
163+
size="sm"
164+
onClick={(e) => handleComplete(followup._id, e)}
165+
>
166+
Complete
167+
</Button>
168+
<Button
169+
variant="neutral"
170+
size="sm"
171+
onClick={(e) => handleDismiss(followup._id, e)}
172+
>
173+
Dismiss
174+
</Button>
175+
</div>
176+
)}
177+
</div>
178+
</CardContent>
179+
</Card>
180+
),
181+
)}
174182
</div>
175183
</div>
176184
);

apps/web/src/components/MemoryPage.tsx

Lines changed: 39 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -82,40 +82,47 @@ export function MemoryPage({ mentraUserId }: MemoryPageProps) {
8282
<h2 className="text-xl font-semibold">Memory</h2>
8383

8484
<div className="space-y-4">
85-
{result.summaries.map((day) => (
86-
<Card
87-
key={day.date}
88-
className="cursor-pointer"
89-
onClick={() => navigate(`/memory/chat/${day.date}`)}
90-
>
91-
<CardHeader className="pb-2">
92-
<CardTitle className="text-lg flex items-center gap-2">
93-
<span>📅</span>
94-
{formatDate(day.date)}
95-
</CardTitle>
96-
</CardHeader>
97-
<CardContent className="space-y-3">
98-
<p className="text-foreground">{day.summary}</p>
85+
{result.summaries.map(
86+
(day: {
87+
date: string;
88+
summary: string;
89+
topics: string[];
90+
sessionCount: number;
91+
}) => (
92+
<Card
93+
key={day.date}
94+
className="cursor-pointer"
95+
onClick={() => navigate(`/memory/chat/${day.date}`)}
96+
>
97+
<CardHeader className="pb-2">
98+
<CardTitle className="text-lg flex items-center gap-2">
99+
<span>📅</span>
100+
{formatDate(day.date)}
101+
</CardTitle>
102+
</CardHeader>
103+
<CardContent className="space-y-3">
104+
<p className="text-foreground">{day.summary}</p>
99105

100-
{day.topics.length > 0 && (
101-
<div className="flex flex-wrap gap-2">
102-
{day.topics.map((topic) => (
103-
<span
104-
key={topic}
105-
className="px-2 py-1 bg-main/10 text-main rounded-base text-sm border border-main/20"
106-
>
107-
{topic}
108-
</span>
109-
))}
110-
</div>
111-
)}
106+
{day.topics.length > 0 && (
107+
<div className="flex flex-wrap gap-2">
108+
{day.topics.map((topic: string) => (
109+
<span
110+
key={topic}
111+
className="px-2 py-1 bg-main/10 text-main rounded-base text-sm border border-main/20"
112+
>
113+
{topic}
114+
</span>
115+
))}
116+
</div>
117+
)}
112118

113-
<p className="text-xs text-foreground/50">
114-
{day.sessionCount} session{day.sessionCount > 1 ? "s" : ""}
115-
</p>
116-
</CardContent>
117-
</Card>
118-
))}
119+
<p className="text-xs text-foreground/50">
120+
{day.sessionCount} session{day.sessionCount > 1 ? "s" : ""}
121+
</p>
122+
</CardContent>
123+
</Card>
124+
),
125+
)}
119126
</div>
120127
</div>
121128
);

0 commit comments

Comments
 (0)