fix(signing): branded embed URL for developer + Copy link
Two bugs in the marketing-site embed-URL rewrite, both surfaced by testing through the actual invitation emails: 1. Developer invite linked to `…/sign/undefined/<token>`. document_signers persists Documenso's normalized role, so the order-2 EOI developer arrives as 'signer', which ROLE_TO_URL_SEGMENT didn't have — the lookup returned undefined. Add a 'signer' → 'developer' alias and a 'cc' fallback so an unknown role can never emit `/sign/undefined/`. 2. "Copy link" copied the bare Documenso URL, not the branded embed URL. listDocumentSigners (feeds the EOI tab signers + Copy link) now runs each signing_url through transformSigningUrl, so the copied link matches what the invitation email sends. Send-invitation/automation read the raw rows directly, so they're unaffected. Regression tests pin 'signer' → /sign/developer and the unknown-role fallback. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,9 +131,14 @@ export interface SigningCompletedArgs {
|
|||||||
* Risk #5 - fixing this mapping prevents an `approver` invite from
|
* Risk #5 - fixing this mapping prevents an `approver` invite from
|
||||||
* landing on `/sign/error`.
|
* landing on `/sign/error`.
|
||||||
*/
|
*/
|
||||||
const ROLE_TO_URL_SEGMENT: Record<SignerRole, 'client' | 'cc' | 'developer' | 'witness'> = {
|
const ROLE_TO_URL_SEGMENT: Record<string, 'client' | 'cc' | 'developer' | 'witness'> = {
|
||||||
client: 'client',
|
client: 'client',
|
||||||
developer: 'developer',
|
developer: 'developer',
|
||||||
|
// `document_signers.signer_role` persists Documenso's normalized role, so
|
||||||
|
// the order-2 EOI developer arrives here as 'signer' (not 'developer').
|
||||||
|
// Without this alias the lookup returned `undefined` and the branded link
|
||||||
|
// became `…/sign/undefined/<token>` (dead). Map it to the developer page.
|
||||||
|
signer: 'developer',
|
||||||
approver: 'cc',
|
approver: 'cc',
|
||||||
witness: 'witness',
|
witness: 'witness',
|
||||||
other: 'cc',
|
other: 'cc',
|
||||||
@@ -155,7 +160,9 @@ export function transformSigningUrl(
|
|||||||
// Trim trailing slashes off the host so we always produce a clean
|
// Trim trailing slashes off the host so we always produce a clean
|
||||||
// single `/` between segments.
|
// single `/` between segments.
|
||||||
const host = embeddedSigningHost.replace(/\/+$/, '');
|
const host = embeddedSigningHost.replace(/\/+$/, '');
|
||||||
const urlRole = ROLE_TO_URL_SEGMENT[signerRole];
|
// Fall back to the passive `cc` page for any unrecognised role rather than
|
||||||
|
// ever emitting `…/sign/undefined/<token>`.
|
||||||
|
const urlRole = ROLE_TO_URL_SEGMENT[signerRole] ?? 'cc';
|
||||||
return `${host}/sign/${urlRole}/${token}`;
|
return `${host}/sign/${urlRole}/${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ import {
|
|||||||
sendSigningInvitation,
|
sendSigningInvitation,
|
||||||
sendSigningCompleted,
|
sendSigningCompleted,
|
||||||
sendSigningStatusNotification,
|
sendSigningStatusNotification,
|
||||||
|
transformSigningUrl,
|
||||||
type SignerRole,
|
type SignerRole,
|
||||||
} from '@/lib/services/document-signing-emails.service';
|
} from '@/lib/services/document-signing-emails.service';
|
||||||
import {
|
import {
|
||||||
@@ -1063,10 +1064,23 @@ export async function uploadSignedManually(
|
|||||||
export async function listDocumentSigners(documentId: string, portId: string) {
|
export async function listDocumentSigners(documentId: string, portId: string) {
|
||||||
await getDocumentById(documentId, portId); // verify access
|
await getDocumentById(documentId, portId); // verify access
|
||||||
|
|
||||||
return db.query.documentSigners.findMany({
|
const rows = await db.query.documentSigners.findMany({
|
||||||
where: eq(documentSigners.documentId, documentId),
|
where: eq(documentSigners.documentId, documentId),
|
||||||
orderBy: (ds, { asc }) => [asc(ds.signingOrder)],
|
orderBy: (ds, { asc }) => [asc(ds.signingOrder)],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Surface the BRANDED marketing-site embed URL (the same wrap the
|
||||||
|
// invitation email applies) rather than the bare Documenso link, so the
|
||||||
|
// EOI tab's "Copy link" shares the on-brand signing page. No-op when the
|
||||||
|
// port has no embeddedSigningHost configured (transformSigningUrl returns
|
||||||
|
// the raw URL unchanged).
|
||||||
|
const { embeddedSigningHost } = await getPortDocumensoConfig(portId);
|
||||||
|
return rows.map((r) => ({
|
||||||
|
...r,
|
||||||
|
signingUrl: r.signingUrl
|
||||||
|
? transformSigningUrl(r.signingUrl, embeddedSigningHost, r.signerRole as SignerRole)
|
||||||
|
: r.signingUrl,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── List Events ──────────────────────────────────────────────────────────────
|
// ─── List Events ──────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -44,6 +44,21 @@ describe('transformSigningUrl', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps 'signer' (Documenso's persisted order-2 role) → /sign/developer/<token>", () => {
|
||||||
|
// document_signers.signer_role stores Documenso's normalized role, so the
|
||||||
|
// EOI developer arrives as 'signer'. Regression: this used to fall through
|
||||||
|
// to `undefined` → dead `…/sign/undefined/<token>` invitation links.
|
||||||
|
expect(transformSigningUrl(RAW, HOST, 'signer' as never)).toBe(
|
||||||
|
'https://portnimara.com/sign/developer/vbT8hi3jKQmrFP_LN1WcS',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to /sign/cc/<token> for any unrecognised role (never undefined)', () => {
|
||||||
|
expect(transformSigningUrl(RAW, HOST, 'mystery-role' as never)).toBe(
|
||||||
|
'https://portnimara.com/sign/cc/vbT8hi3jKQmrFP_LN1WcS',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('maps witness → /sign/witness/<token>', () => {
|
it('maps witness → /sign/witness/<token>', () => {
|
||||||
expect(transformSigningUrl(RAW, HOST, 'witness')).toBe(
|
expect(transformSigningUrl(RAW, HOST, 'witness')).toBe(
|
||||||
'https://portnimara.com/sign/witness/vbT8hi3jKQmrFP_LN1WcS',
|
'https://portnimara.com/sign/witness/vbT8hi3jKQmrFP_LN1WcS',
|
||||||
|
|||||||
Reference in New Issue
Block a user