diff --git a/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx b/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx index 47719014b..14922a326 100644 --- a/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx +++ b/apps/web/src/app/(recipient)/d/[token]/sign-direct-template.tsx @@ -41,6 +41,7 @@ import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field'; import { DateField } from '~/app/(signing)/sign/[token]/date-field'; import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field'; import { EmailField } from '~/app/(signing)/sign/[token]/email-field'; +import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field'; import { NameField } from '~/app/(signing)/sign/[token]/name-field'; import { NumberField } from '~/app/(signing)/sign/[token]/number-field'; import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider'; @@ -182,6 +183,15 @@ export const SignDirectTemplateForm = ({ onUnsignField={onUnsignField} /> )) + .with(FieldType.INITIALS, () => ( + + )) .with(FieldType.NAME, () => ( Promise | void; + onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void; +}; + +export const InitialsField = ({ + field, + recipient, + onSignField, + onUnsignField, +}: InitialsFieldProps) => { + const router = useRouter(); + const { toast } = useToast(); + + const { fullName } = useRequiredSigningContext(); + const initials = extractInitials(fullName); + + const [isPending, startTransition] = useTransition(); + + const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } = + trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); + + const { + mutateAsync: removeSignedFieldWithToken, + isLoading: isRemoveSignedFieldWithTokenLoading, + } = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION); + + const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; + + const onSign = async (authOptions?: TRecipientActionAuth) => { + try { + const value = initials ?? ''; + + const payload: TSignFieldWithTokenMutationSchema = { + token: recipient.token, + fieldId: field.id, + value, + isBase64: false, + authOptions, + }; + + if (onSignField) { + await onSignField(payload); + return; + } + + await signFieldWithToken(payload); + + startTransition(() => router.refresh()); + } catch (err) { + const error = AppError.parseError(err); + + if (error.code === AppErrorCode.UNAUTHORIZED) { + throw error; + } + + console.error(err); + + toast({ + title: 'Error', + description: 'An error occurred while signing the document.', + variant: 'destructive', + }); + } + }; + + const onRemove = async () => { + try { + const payload: TRemovedSignedFieldWithTokenMutationSchema = { + token: recipient.token, + fieldId: field.id, + }; + + if (onUnsignField) { + await onUnsignField(payload); + return; + } + + await removeSignedFieldWithToken(payload); + + startTransition(() => router.refresh()); + } catch (err) { + console.error(err); + + toast({ + title: 'Error', + description: 'An error occurred while removing the signature.', + variant: 'destructive', + }); + } + }; + + return ( + + {isLoading && ( +
+ +
+ )} + + {!field.inserted && ( +

+ Initials +

+ )} + + {field.inserted && ( +

+ {field.customText} +

+ )} +
+ ); +}; diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx index c73e35306..103c5f9e5 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx @@ -39,7 +39,16 @@ export type SignatureFieldProps = { */ onSign?: (documentAuthValue?: TRecipientActionAuth) => Promise | void; onRemove?: (fieldType?: string) => Promise | void; - type?: 'Date' | 'Email' | 'Name' | 'Signature' | 'Radio' | 'Dropdown' | 'Number' | 'Checkbox'; + type?: + | 'Date' + | 'Initials' + | 'Email' + | 'Name' + | 'Signature' + | 'Radio' + | 'Dropdown' + | 'Number' + | 'Checkbox'; tooltipText?: string | null; }; diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx index d1382a278..ced07ef5b 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-page-view.tsx @@ -26,6 +26,7 @@ import { DateField } from './date-field'; import { DropdownField } from './dropdown-field'; import { EmailField } from './email-field'; import { SigningForm } from './form'; +import { InitialsField } from './initials-field'; import { NameField } from './name-field'; import { NumberField } from './number-field'; import { RadioField } from './radio-field'; @@ -101,6 +102,9 @@ export const SigningPageView = ({ .with(FieldType.SIGNATURE, () => ( )) + .with(FieldType.INITIALS, () => ( + + )) .with(FieldType.NAME, () => ( )) diff --git a/apps/web/src/components/document/document-read-only-fields.tsx b/apps/web/src/components/document/document-read-only-fields.tsx index d07c0b26c..cbf566b25 100644 --- a/apps/web/src/components/document/document-read-only-fields.tsx +++ b/apps/web/src/components/document/document-read-only-fields.tsx @@ -98,6 +98,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl { type: P.union( FieldType.NAME, + FieldType.INITIALS, FieldType.EMAIL, FieldType.NUMBER, FieldType.RADIO, diff --git a/packages/lib/server-only/field/sign-field-with-token.ts b/packages/lib/server-only/field/sign-field-with-token.ts index aa596dc37..087de646b 100644 --- a/packages/lib/server-only/field/sign-field-with-token.ts +++ b/packages/lib/server-only/field/sign-field-with-token.ts @@ -231,10 +231,17 @@ export const signFieldWithToken = async ({ type, data: signatureImageAsBase64 || typedSignature || '', })) - .with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, FieldType.TEXT, (type) => ({ - type, - data: updatedField.customText, - })) + .with( + FieldType.DATE, + FieldType.EMAIL, + FieldType.NAME, + FieldType.TEXT, + FieldType.INITIALS, + (type) => ({ + type, + data: updatedField.customText, + }), + ) .with( FieldType.NUMBER, FieldType.RADIO, diff --git a/packages/lib/server-only/template/create-document-from-direct-template.ts b/packages/lib/server-only/template/create-document-from-direct-template.ts index 8ec4e9f1c..5827fb76c 100644 --- a/packages/lib/server-only/template/create-document-from-direct-template.ts +++ b/packages/lib/server-only/template/create-document-from-direct-template.ts @@ -468,6 +468,7 @@ export const createDocumentFromDirectTemplate = async ({ .with( FieldType.DATE, FieldType.EMAIL, + FieldType.INITIALS, FieldType.NAME, FieldType.TEXT, FieldType.NUMBER, diff --git a/packages/lib/types/document-audit-logs.ts b/packages/lib/types/document-audit-logs.ts index 1d2bf7c53..5c00724cc 100644 --- a/packages/lib/types/document-audit-logs.ts +++ b/packages/lib/types/document-audit-logs.ts @@ -233,6 +233,10 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({ // Organised into union to allow us to extend each field if required. field: z.union([ + z.object({ + type: z.literal(FieldType.INITIALS), + data: z.string(), + }), z.object({ type: z.literal(FieldType.EMAIL), data: z.string(), diff --git a/packages/prisma/generated/types.ts b/packages/prisma/generated/types.ts deleted file mode 100644 index 21144bd84..000000000 --- a/packages/prisma/generated/types.ts +++ /dev/null @@ -1,481 +0,0 @@ -import type { ColumnType } from 'kysely'; - -export type Generated = T extends ColumnType - ? ColumnType - : ColumnType; -export type Timestamp = ColumnType; - -export const IdentityProvider = { - DOCUMENSO: 'DOCUMENSO', - GOOGLE: 'GOOGLE', - OIDC: 'OIDC', -} as const; -export type IdentityProvider = (typeof IdentityProvider)[keyof typeof IdentityProvider]; -export const Role = { - ADMIN: 'ADMIN', - USER: 'USER', -} as const; -export type Role = (typeof Role)[keyof typeof Role]; -export const UserSecurityAuditLogType = { - ACCOUNT_PROFILE_UPDATE: 'ACCOUNT_PROFILE_UPDATE', - ACCOUNT_SSO_LINK: 'ACCOUNT_SSO_LINK', - AUTH_2FA_DISABLE: 'AUTH_2FA_DISABLE', - AUTH_2FA_ENABLE: 'AUTH_2FA_ENABLE', - PASSKEY_CREATED: 'PASSKEY_CREATED', - PASSKEY_DELETED: 'PASSKEY_DELETED', - PASSKEY_UPDATED: 'PASSKEY_UPDATED', - PASSWORD_RESET: 'PASSWORD_RESET', - PASSWORD_UPDATE: 'PASSWORD_UPDATE', - SIGN_OUT: 'SIGN_OUT', - SIGN_IN: 'SIGN_IN', - SIGN_IN_FAIL: 'SIGN_IN_FAIL', - SIGN_IN_2FA_FAIL: 'SIGN_IN_2FA_FAIL', - SIGN_IN_PASSKEY_FAIL: 'SIGN_IN_PASSKEY_FAIL', -} as const; -export type UserSecurityAuditLogType = - (typeof UserSecurityAuditLogType)[keyof typeof UserSecurityAuditLogType]; -export const WebhookTriggerEvents = { - DOCUMENT_CREATED: 'DOCUMENT_CREATED', - DOCUMENT_SENT: 'DOCUMENT_SENT', - DOCUMENT_OPENED: 'DOCUMENT_OPENED', - DOCUMENT_SIGNED: 'DOCUMENT_SIGNED', - DOCUMENT_COMPLETED: 'DOCUMENT_COMPLETED', -} as const; -export type WebhookTriggerEvents = (typeof WebhookTriggerEvents)[keyof typeof WebhookTriggerEvents]; -export const WebhookCallStatus = { - SUCCESS: 'SUCCESS', - FAILED: 'FAILED', -} as const; -export type WebhookCallStatus = (typeof WebhookCallStatus)[keyof typeof WebhookCallStatus]; -export const ApiTokenAlgorithm = { - SHA512: 'SHA512', -} as const; -export type ApiTokenAlgorithm = (typeof ApiTokenAlgorithm)[keyof typeof ApiTokenAlgorithm]; -export const SubscriptionStatus = { - ACTIVE: 'ACTIVE', - PAST_DUE: 'PAST_DUE', - INACTIVE: 'INACTIVE', -} as const; -export type SubscriptionStatus = (typeof SubscriptionStatus)[keyof typeof SubscriptionStatus]; -export const DocumentStatus = { - DRAFT: 'DRAFT', - PENDING: 'PENDING', - COMPLETED: 'COMPLETED', -} as const; -export type DocumentStatus = (typeof DocumentStatus)[keyof typeof DocumentStatus]; -export const DocumentSource = { - DOCUMENT: 'DOCUMENT', - TEMPLATE: 'TEMPLATE', - TEMPLATE_DIRECT_LINK: 'TEMPLATE_DIRECT_LINK', -} as const; -export type DocumentSource = (typeof DocumentSource)[keyof typeof DocumentSource]; -export const DocumentDataType = { - S3_PATH: 'S3_PATH', - BYTES: 'BYTES', - BYTES_64: 'BYTES_64', -} as const; -export type DocumentDataType = (typeof DocumentDataType)[keyof typeof DocumentDataType]; -export const ReadStatus = { - NOT_OPENED: 'NOT_OPENED', - OPENED: 'OPENED', -} as const; -export type ReadStatus = (typeof ReadStatus)[keyof typeof ReadStatus]; -export const SendStatus = { - NOT_SENT: 'NOT_SENT', - SENT: 'SENT', -} as const; -export type SendStatus = (typeof SendStatus)[keyof typeof SendStatus]; -export const SigningStatus = { - NOT_SIGNED: 'NOT_SIGNED', - SIGNED: 'SIGNED', -} as const; -export type SigningStatus = (typeof SigningStatus)[keyof typeof SigningStatus]; -export const RecipientRole = { - CC: 'CC', - SIGNER: 'SIGNER', - VIEWER: 'VIEWER', - APPROVER: 'APPROVER', -} as const; -export type RecipientRole = (typeof RecipientRole)[keyof typeof RecipientRole]; -export const FieldType = { - SIGNATURE: 'SIGNATURE', - FREE_SIGNATURE: 'FREE_SIGNATURE', - NAME: 'NAME', - EMAIL: 'EMAIL', - DATE: 'DATE', - TEXT: 'TEXT', - NUMBER: 'NUMBER', - RADIO: 'RADIO', - CHECKBOX: 'CHECKBOX', - DROPDOWN: 'DROPDOWN', -} as const; -export type FieldType = (typeof FieldType)[keyof typeof FieldType]; -export const TeamMemberRole = { - ADMIN: 'ADMIN', - MANAGER: 'MANAGER', - MEMBER: 'MEMBER', -} as const; -export type TeamMemberRole = (typeof TeamMemberRole)[keyof typeof TeamMemberRole]; -export const TeamMemberInviteStatus = { - ACCEPTED: 'ACCEPTED', - PENDING: 'PENDING', -} as const; -export type TeamMemberInviteStatus = - (typeof TeamMemberInviteStatus)[keyof typeof TeamMemberInviteStatus]; -export const TemplateType = { - PUBLIC: 'PUBLIC', - PRIVATE: 'PRIVATE', -} as const; -export type TemplateType = (typeof TemplateType)[keyof typeof TemplateType]; -export type Account = { - id: string; - userId: number; - type: string; - provider: string; - providerAccountId: string; - refresh_token: string | null; - access_token: string | null; - expires_at: number | null; - created_at: number | null; - ext_expires_in: number | null; - token_type: string | null; - scope: string | null; - id_token: string | null; - session_state: string | null; -}; -export type AnonymousVerificationToken = { - id: string; - token: string; - expiresAt: Timestamp; - createdAt: Generated; -}; -export type ApiToken = { - id: Generated; - name: string; - token: string; - algorithm: Generated; - expires: Timestamp | null; - createdAt: Generated; - userId: number | null; - teamId: number | null; -}; -export type Document = { - id: Generated; - userId: number; - authOptions: unknown | null; - formValues: unknown | null; - title: string; - status: Generated; - documentDataId: string; - createdAt: Generated; - updatedAt: Generated; - completedAt: Timestamp | null; - deletedAt: Timestamp | null; - teamId: number | null; - templateId: number | null; - source: DocumentSource; -}; -export type DocumentAuditLog = { - id: string; - documentId: number; - createdAt: Generated; - type: string; - data: unknown; - name: string | null; - email: string | null; - userId: number | null; - userAgent: string | null; - ipAddress: string | null; -}; -export type DocumentData = { - id: string; - type: DocumentDataType; - data: string; - initialData: string; -}; -export type DocumentMeta = { - id: string; - subject: string | null; - message: string | null; - timezone: Generated; - password: string | null; - dateFormat: Generated; - documentId: number; - redirectUrl: string | null; -}; -export type DocumentShareLink = { - id: Generated; - email: string; - slug: string; - documentId: number; - createdAt: Generated; - updatedAt: Timestamp; -}; -export type Field = { - id: Generated; - secondaryId: string; - documentId: number | null; - templateId: number | null; - recipientId: number; - type: FieldType; - page: number; - positionX: Generated; - positionY: Generated; - width: Generated; - height: Generated; - customText: string; - inserted: boolean; - fieldMeta: unknown | null; -}; -export type Passkey = { - id: string; - userId: number; - name: string; - createdAt: Generated; - updatedAt: Generated; - lastUsedAt: Timestamp | null; - credentialId: Buffer; - credentialPublicKey: Buffer; - counter: string; - credentialDeviceType: string; - credentialBackedUp: boolean; - transports: string[]; -}; -export type PasswordResetToken = { - id: Generated; - token: string; - createdAt: Generated; - expiry: Timestamp; - userId: number; -}; -export type Recipient = { - id: Generated; - documentId: number | null; - templateId: number | null; - email: string; - name: Generated; - token: string; - documentDeletedAt: Timestamp | null; - expired: Timestamp | null; - signedAt: Timestamp | null; - authOptions: unknown | null; - role: Generated; - readStatus: Generated; - signingStatus: Generated; - sendStatus: Generated; -}; -export type Session = { - id: string; - sessionToken: string; - userId: number; - expires: Timestamp; -}; -export type Signature = { - id: Generated; - created: Generated; - recipientId: number; - fieldId: number; - signatureImageAsBase64: string | null; - typedSignature: string | null; -}; -export type SiteSettings = { - id: string; - enabled: Generated; - data: unknown; - lastModifiedByUserId: number | null; - lastModifiedAt: Generated; -}; -export type Subscription = { - id: Generated; - status: Generated; - planId: string; - priceId: string; - periodEnd: Timestamp | null; - userId: number | null; - teamId: number | null; - createdAt: Generated; - updatedAt: Timestamp; - cancelAtPeriodEnd: Generated; -}; -export type Team = { - id: Generated; - name: string; - url: string; - createdAt: Generated; - customerId: string | null; - ownerUserId: number; -}; -export type TeamEmail = { - teamId: number; - createdAt: Generated; - name: string; - email: string; -}; -export type TeamEmailVerification = { - teamId: number; - name: string; - email: string; - token: string; - expiresAt: Timestamp; - createdAt: Generated; -}; -export type TeamMember = { - id: Generated; - teamId: number; - createdAt: Generated; - role: TeamMemberRole; - userId: number; -}; -export type TeamMemberInvite = { - id: Generated; - teamId: number; - createdAt: Generated; - email: string; - status: Generated; - role: TeamMemberRole; - token: string; -}; -export type TeamPending = { - id: Generated; - name: string; - url: string; - createdAt: Generated; - customerId: string; - ownerUserId: number; -}; -export type TeamTransferVerification = { - teamId: number; - userId: number; - name: string; - email: string; - token: string; - expiresAt: Timestamp; - createdAt: Generated; - clearPaymentMethods: Generated; -}; -export type Template = { - id: Generated; - type: Generated; - title: string; - userId: number; - teamId: number | null; - authOptions: unknown | null; - templateDocumentDataId: string; - createdAt: Generated; - updatedAt: Generated; -}; -export type TemplateDirectLink = { - id: string; - templateId: number; - token: string; - createdAt: Generated; - enabled: boolean; - directTemplateRecipientId: number; -}; -export type TemplateMeta = { - id: string; - subject: string | null; - message: string | null; - timezone: Generated; - password: string | null; - dateFormat: Generated; - templateId: number; - redirectUrl: string | null; -}; -export type User = { - id: Generated; - name: string | null; - customerId: string | null; - email: string; - emailVerified: Timestamp | null; - password: string | null; - source: string | null; - signature: string | null; - createdAt: Generated; - updatedAt: Generated; - lastSignedIn: Generated; - roles: Generated; - identityProvider: Generated; - twoFactorSecret: string | null; - twoFactorEnabled: Generated; - twoFactorBackupCodes: string | null; - url: string | null; -}; -export type UserProfile = { - id: number; - bio: string | null; -}; -export type UserSecurityAuditLog = { - id: Generated; - userId: number; - createdAt: Generated; - type: UserSecurityAuditLogType; - userAgent: string | null; - ipAddress: string | null; -}; -export type VerificationToken = { - id: Generated; - secondaryId: string; - identifier: string; - token: string; - expires: Timestamp; - createdAt: Generated; - userId: number; -}; -export type Webhook = { - id: string; - webhookUrl: string; - eventTriggers: WebhookTriggerEvents[]; - secret: string | null; - enabled: Generated; - createdAt: Generated; - updatedAt: Generated; - userId: number; - teamId: number | null; -}; -export type WebhookCall = { - id: string; - status: WebhookCallStatus; - url: string; - event: WebhookTriggerEvents; - requestBody: unknown; - responseCode: number; - responseHeaders: unknown | null; - responseBody: unknown | null; - createdAt: Generated; - webhookId: string; -}; -export type DB = { - Account: Account; - AnonymousVerificationToken: AnonymousVerificationToken; - ApiToken: ApiToken; - Document: Document; - DocumentAuditLog: DocumentAuditLog; - DocumentData: DocumentData; - DocumentMeta: DocumentMeta; - DocumentShareLink: DocumentShareLink; - Field: Field; - Passkey: Passkey; - PasswordResetToken: PasswordResetToken; - Recipient: Recipient; - Session: Session; - Signature: Signature; - SiteSettings: SiteSettings; - Subscription: Subscription; - Team: Team; - TeamEmail: TeamEmail; - TeamEmailVerification: TeamEmailVerification; - TeamMember: TeamMember; - TeamMemberInvite: TeamMemberInvite; - TeamPending: TeamPending; - TeamTransferVerification: TeamTransferVerification; - Template: Template; - TemplateDirectLink: TemplateDirectLink; - TemplateMeta: TemplateMeta; - User: User; - UserProfile: UserProfile; - UserSecurityAuditLog: UserSecurityAuditLog; - VerificationToken: VerificationToken; - Webhook: Webhook; - WebhookCall: WebhookCall; -}; diff --git a/packages/prisma/migrations/20240812065352_add_initials_field_type/migration.sql b/packages/prisma/migrations/20240812065352_add_initials_field_type/migration.sql new file mode 100644 index 000000000..b4c827ae7 --- /dev/null +++ b/packages/prisma/migrations/20240812065352_add_initials_field_type/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "FieldType" ADD VALUE 'INITIALS'; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 44cf9e157..9d8860f44 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -409,6 +409,7 @@ model Recipient { enum FieldType { SIGNATURE FREE_SIGNATURE + INITIALS NAME EMAIL DATE diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 85c87e3b6..049035f89 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -414,7 +414,7 @@ export const documentRouter = router({ teamId, }).catch(() => null); - if (!document || document.teamId !== teamId) { + if (!document || (teamId && document.teamId !== teamId)) { throw new TRPCError({ code: 'FORBIDDEN', message: 'You do not have access to this document.', diff --git a/packages/ui/primitives/document-flow/add-fields.tsx b/packages/ui/primitives/document-flow/add-fields.tsx index 5ae55b7b3..597948fab 100644 --- a/packages/ui/primitives/document-flow/add-fields.tsx +++ b/packages/ui/primitives/document-flow/add-fields.tsx @@ -10,6 +10,7 @@ import { CheckSquare, ChevronDown, ChevronsUpDown, + Contact, Disc, Hash, Info, @@ -650,6 +651,32 @@ export const AddFieldsFormPartial = ({ + + + +