feat(06-05): confirmation page + router.push redirect after signing

- Rewrite confirmed/page.tsx: verifies signing token, shows document name + signed timestamp + download button
- Generate 15-min download token server-side; pass as dt= query param to /api/sign/[token]/download
- Success checkmark (navy circle + gold checkmark), document name, formatted signed date
- Download link valid for 15 minutes note shown below button
- Update SigningPageClient.tsx: replace window.location.href with router.push for SPA navigation
This commit is contained in:
Chandler Copeland
2026-03-20 11:42:24 -06:00
parent a276da0da1
commit 4cdd9eea80
2 changed files with 134 additions and 51 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useState, useRef, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import { Document, Page, pdfjs } from 'react-pdf';
import 'react-pdf/dist/Page/AnnotationLayer.css';
import 'react-pdf/dist/Page/TextLayer.css';
@@ -40,6 +41,7 @@ export function SigningPageClient({
documentName,
signatureFields,
}: SigningPageClientProps) {
const router = useRouter();
const [numPages, setNumPages] = useState(0);
// Map from page number (1-indexed) to rendered dimensions
const [pageDimensions, setPageDimensions] = useState<Record<number, PageDimensions>>({});
@@ -133,10 +135,10 @@ export function SigningPageClient({
body: JSON.stringify({ signatures: signaturesRef.current }),
});
if (res.ok) {
window.location.href = `/sign/${token}/confirmed`;
router.push(`/sign/${token}/confirmed`);
} else if (res.status === 409) {
// Already signed — navigate to confirmed anyway
window.location.href = `/sign/${token}/confirmed`;
router.push(`/sign/${token}/confirmed`);
} else {
const data = await res.json().catch(() => ({}));
alert(`Submission failed: ${(data as { error?: string }).error ?? res.status}. Please try again.`);

View File

@@ -1,7 +1,56 @@
// /sign/[token]/confirmed — shown after a document is successfully signed
// Static success screen; no token re-validation needed (submission already committed)
import { verifySigningToken, createDownloadToken } from '@/lib/signing/token';
import { db } from '@/lib/db';
import { signingTokens, documents } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export default function ConfirmedPage() {
interface Props {
params: Promise<{ token: string }>;
}
export default async function ConfirmedPage({ params }: Props) {
const { token } = await params;
// Verify signing token to get documentId (allow expired — confirmation page visited after signing)
let documentId: string | null = null;
let jti: string | null = null;
try {
const payload = await verifySigningToken(token);
documentId = payload.documentId;
jti = payload.jti;
} catch {
// Token expired is OK here — signing may have happened right before expiry
// documentId/jti remain null — handled below
}
if (!documentId) {
return (
<div
style={{
padding: '40px',
textAlign: 'center',
fontFamily: 'Georgia, serif',
color: '#1B2B4B',
}}
>
Document not found.
</div>
);
}
const doc = await db.query.documents.findFirst({
where: eq(documents.id, documentId),
});
// Get signed timestamp from signingTokens.usedAt
let signedAt: Date | null = null;
if (jti) {
const tokenRow = await db.query.signingTokens.findFirst({
where: eq(signingTokens.jti, jti),
});
signedAt = tokenRow?.usedAt ?? doc?.signedAt ?? null;
}
if (!doc || !doc.signedFilePath) {
return (
<div
style={{
@@ -13,59 +62,91 @@ export default function ConfirmedPage() {
fontFamily: 'Georgia, serif',
}}
>
<div style={{ textAlign: 'center', maxWidth: '480px', padding: '40px' }}>
{/* Branded header strip */}
<div
style={{
backgroundColor: '#1B2B4B',
color: '#fff',
padding: '16px 24px',
borderRadius: '8px 8px 0 0',
marginBottom: 0,
}}
>
<div style={{ fontSize: '18px', fontWeight: 'bold', letterSpacing: '0.02em' }}>
Teressa Copeland Homes
</div>
</div>
{/* Success card */}
<div
style={{
backgroundColor: '#fff',
border: '1px solid #e0e0e0',
borderTop: 'none',
borderRadius: '0 0 8px 8px',
padding: '40px 32px',
boxShadow: '0 2px 12px rgba(0,0,0,0.08)',
}}
>
<div style={{ fontSize: '56px', marginBottom: '16px', color: '#22c55e' }}>&#10003;</div>
<h1 style={{ color: '#1B2B4B', fontSize: '24px', marginBottom: '12px', margin: '0 0 12px' }}>
Document Signed
</h1>
<p
style={{
color: '#555',
fontSize: '16px',
lineHeight: '1.6',
margin: '0 0 24px',
}}
>
Your signature has been recorded and the document has been securely submitted.
Teressa Copeland will be in touch shortly.
</p>
<div
style={{
fontSize: '13px',
color: '#999',
borderTop: '1px solid #f0f0f0',
paddingTop: '16px',
}}
>
You may close this window.
</div>
</div>
<div style={{ textAlign: 'center', padding: '40px' }}>
<h1 style={{ color: '#1B2B4B' }}>Document not yet available</h1>
<p style={{ color: '#555' }}>Please check back shortly.</p>
</div>
</div>
);
}
// Generate 15-minute download token for client copy
const downloadToken = await createDownloadToken(doc.id);
const downloadUrl = `/api/sign/${token}/download?dt=${downloadToken}`;
const formattedDate = signedAt
? signedAt.toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit',
})
: 'Just now';
return (
<div
style={{
minHeight: '100vh',
backgroundColor: '#FAF9F7',
fontFamily: 'Georgia, serif',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<div style={{ textAlign: 'center', maxWidth: '480px', padding: '48px 32px' }}>
{/* Success checkmark */}
<div
style={{
width: '72px',
height: '72px',
borderRadius: '50%',
backgroundColor: '#1B2B4B',
color: '#C9A84C',
fontSize: '36px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 24px',
}}
>
&#10003;
</div>
<h1 style={{ color: '#1B2B4B', fontSize: '26px', marginBottom: '8px' }}>
You&apos;ve signed
</h1>
<p
style={{
color: '#1B2B4B',
fontSize: '20px',
fontWeight: 'bold',
marginBottom: '8px',
}}
>
{doc.name}
</p>
<p style={{ color: '#888', fontSize: '14px', marginBottom: '32px' }}>
Signed on {formattedDate}
</p>
<a
href={downloadUrl}
style={{
display: 'inline-block',
backgroundColor: '#C9A84C',
color: '#fff',
padding: '12px 28px',
borderRadius: '4px',
textDecoration: 'none',
fontWeight: 'bold',
fontSize: '15px',
}}
>
Download your copy
</a>
<p style={{ color: '#aaa', fontSize: '12px', marginTop: '16px' }}>
Download link valid for 15 minutes.
</p>
</div>
</div>
);