Polish milestone dialog and one-time delivery

This commit is contained in:
2026-08-14 23:43:20 +02:00
parent 13879e3d3a
commit 55c04761ce
5 changed files with 77 additions and 63 deletions

View File

@@ -16,8 +16,9 @@ async function ownedMilestone(id: string, userId: string) {
});
}
function clientState(milestone: { brandStatus: string; brandPostUrl: string | null; brandPostError: string | null; selfSharedAt: Date | null }) {
function clientState(milestone: { status: string; brandStatus: string; brandPostUrl: string | null; brandPostError: string | null; selfSharedAt: Date | null }) {
return {
promptStatus: milestone.status,
brandStatus: milestone.brandStatus,
brandPostUrl: milestone.brandPostUrl,
brandPostError: milestone.brandPostError,
@@ -86,7 +87,10 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st
// much less disruptive in an X compose window than a full UUID.
const updated = await db.socialMilestone.update({
where: { id: milestone.id },
data: { selfSharedAt: now, publicShareApprovedAt: now, shareToken: token, cardData: card, language },
data: {
status: 'self_shared', respondedAt: milestone.respondedAt || now,
selfSharedAt: now, publicShareApprovedAt: now, shareToken: token, cardData: card, language,
},
});
return NextResponse.json({ ok: true, shareToken: token, shareUrl, shareVersion: now.getTime(), milestone: clientState(updated) });
}

View File

@@ -19,28 +19,18 @@ export async function GET(request: NextRequest) {
if (!user || user.socialPromptOptOut) return NextResponse.json({ milestone: null });
const milestone = await db.socialMilestone.findFirst({
where: {
userId,
OR: [
{ status: { in: ['detected', 'shown'] } },
{ status: 'approved', brandStatus: { in: ['approved', 'processing', 'failed'] } },
],
},
// `shown` is a durable delivery receipt: one milestone may auto-open only
// once, even across refreshes, tabs and later dashboard visits.
where: { userId, status: 'detected' },
orderBy: { detectedAt: 'asc' },
include: { qr: { select: { id: true, title: true, createdAt: true } } },
});
if (!milestone) return NextResponse.json({ milestone: null });
if (milestone.status === 'detected') {
await db.socialMilestone.update({ where: { id: milestone.id }, data: { status: 'shown', shownAt: new Date() } });
}
const threshold = milestoneThreshold(milestone.kind);
if (!threshold) return NextResponse.json({ milestone: null });
const locale = socialLocale(request.nextUrl.searchParams.get('locale'));
const shareToken = milestone.shareToken || randomBytes(9).toString('base64url');
if (!milestone.shareToken) {
await db.socialMilestone.update({ where: { id: milestone.id }, data: { shareToken } });
}
const shareUrl = `${getWwwOrigin()}/s/m/${shareToken}`;
const card = await ensureSocialMilestoneCard({
milestoneId: milestone.id,
@@ -50,15 +40,21 @@ export async function GET(request: NextRequest) {
language: locale,
qr: milestone.qr,
primaryUseCase: user.primaryUseCase,
refresh: ['detected', 'shown'].includes(milestone.status),
refresh: true,
snapshotAt: new Date(),
});
const delivered = await db.socialMilestone.updateMany({
where: { id: milestone.id, status: 'detected' },
data: { status: 'shown', shownAt: new Date(), shareToken },
});
if (!delivered.count) return NextResponse.json({ milestone: null });
return NextResponse.json({
milestone: {
id: milestone.id, qrTitle: milestone.qr.title, threshold,
defaultXHandle: user.xHandle,
brandStatus: milestone.brandStatus,
promptStatus: 'shown',
brandPostUrl: milestone.brandPostUrl,
brandPostError: milestone.brandPostError,
language: locale,