From d2675962ba362d4d36a6d64686dcde04e1803277 Mon Sep 17 00:00:00 2001 From: David Anyatonwu <51977119+onyedikachi-david@users.noreply.github.com> Date: Mon, 2 Mar 2026 10:39:05 +0100 Subject: [PATCH 1/3] feat(piece): add SignNow integration (#11456) --- bun.lock | 13 ++ .../pieces/community/sign-now/.eslintrc.json | 33 +++ .../pieces/community/sign-now/package.json | 18 ++ .../pieces/community/sign-now/src/index.ts | 44 ++++ .../sign-now/src/lib/actions/cancel-invite.ts | 39 ++++ ...-document-from-template-and-send-invite.ts | 168 ++++++++++++++ ...rom-template-and-send-role-based-invite.ts | 206 +++++++++++++++++ ...ent-group-from-template-and-send-invite.ts | 214 ++++++++++++++++++ .../src/lib/actions/custom-api-call.ts | 14 ++ .../sign-now/src/lib/actions/send-invite.ts | 165 ++++++++++++++ .../upload-document-and-extract-fields.ts | 40 ++++ .../src/lib/actions/upload-document.ts | 49 ++++ .../community/sign-now/src/lib/common/auth.ts | 65 ++++++ .../sign-now/src/lib/common/webhook.ts | 57 +++++ .../src/lib/triggers/document-completed.ts | 49 ++++ .../src/lib/triggers/document-deleted.ts | 48 ++++ .../lib/triggers/document-group-completed.ts | 48 ++++ .../src/lib/triggers/document-updated.ts | 48 ++++ .../sign-now/src/lib/triggers/new-document.ts | 48 ++++ .../pieces/community/sign-now/tsconfig.json | 19 ++ .../community/sign-now/tsconfig.lib.json | 21 ++ 21 files changed, 1406 insertions(+) create mode 100644 packages/pieces/community/sign-now/.eslintrc.json create mode 100644 packages/pieces/community/sign-now/package.json create mode 100644 packages/pieces/community/sign-now/src/index.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/cancel-invite.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-invite.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-role-based-invite.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/create-document-group-from-template-and-send-invite.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/custom-api-call.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/send-invite.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/upload-document-and-extract-fields.ts create mode 100644 packages/pieces/community/sign-now/src/lib/actions/upload-document.ts create mode 100644 packages/pieces/community/sign-now/src/lib/common/auth.ts create mode 100644 packages/pieces/community/sign-now/src/lib/common/webhook.ts create mode 100644 packages/pieces/community/sign-now/src/lib/triggers/document-completed.ts create mode 100644 packages/pieces/community/sign-now/src/lib/triggers/document-deleted.ts create mode 100644 packages/pieces/community/sign-now/src/lib/triggers/document-group-completed.ts create mode 100644 packages/pieces/community/sign-now/src/lib/triggers/document-updated.ts create mode 100644 packages/pieces/community/sign-now/src/lib/triggers/new-document.ts create mode 100644 packages/pieces/community/sign-now/tsconfig.json create mode 100644 packages/pieces/community/sign-now/tsconfig.lib.json diff --git a/bun.lock b/bun.lock index b39f75c16e0..d9d3e51aa43 100644 --- a/bun.lock +++ b/bun.lock @@ -5623,6 +5623,17 @@ "tslib": "^2.3.0", }, }, + "packages/pieces/community/sign-now": { + "name": "@activepieces/piece-sign-now", + "version": "0.0.1", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "form-data": "4.0.4", + "tslib": "2.6.2", + }, + }, "packages/pieces/community/signrequest": { "name": "@activepieces/piece-signrequest", "version": "0.1.4", @@ -8650,6 +8661,8 @@ "@activepieces/piece-short-io": ["@activepieces/piece-short-io@workspace:packages/pieces/community/short-io"], + "@activepieces/piece-sign-now": ["@activepieces/piece-sign-now@workspace:packages/pieces/community/sign-now"], + "@activepieces/piece-signrequest": ["@activepieces/piece-signrequest@workspace:packages/pieces/community/signrequest"], "@activepieces/piece-simplepdf": ["@activepieces/piece-simplepdf@workspace:packages/pieces/community/simplepdf"], diff --git a/packages/pieces/community/sign-now/.eslintrc.json b/packages/pieces/community/sign-now/.eslintrc.json new file mode 100644 index 00000000000..359ff63d51d --- /dev/null +++ b/packages/pieces/community/sign-now/.eslintrc.json @@ -0,0 +1,33 @@ +{ + "extends": [ + "../../../../.eslintrc.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/sign-now/package.json b/packages/pieces/community/sign-now/package.json new file mode 100644 index 00000000000..fad564362a6 --- /dev/null +++ b/packages/pieces/community/sign-now/package.json @@ -0,0 +1,18 @@ +{ + "name": "@activepieces/piece-sign-now", + "version": "0.0.1", + "type": "commonjs", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", + "dependencies": { + "@activepieces/pieces-common": "workspace:*", + "@activepieces/pieces-framework": "workspace:*", + "@activepieces/shared": "workspace:*", + "form-data": "4.0.4", + "tslib": "2.6.2" + }, + "scripts": { + "build": "tsc -p tsconfig.lib.json && cp package.json dist/", + "lint": "eslint 'src/**/*.ts'" + } +} \ No newline at end of file diff --git a/packages/pieces/community/sign-now/src/index.ts b/packages/pieces/community/sign-now/src/index.ts new file mode 100644 index 00000000000..46aa86e8033 --- /dev/null +++ b/packages/pieces/community/sign-now/src/index.ts @@ -0,0 +1,44 @@ +import { createPiece } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; +import { signNowAuth } from './lib/common/auth'; +import { cancelInviteAction } from './lib/actions/cancel-invite'; +import { createDocumentFromTemplateAndSendInviteAction } from './lib/actions/create-document-from-template-and-send-invite'; +import { createDocumentFromTemplateAndSendRoleBasedInviteAction } from './lib/actions/create-document-from-template-and-send-role-based-invite'; +import { createDocumentGroupFromTemplateAndSendInviteAction } from './lib/actions/create-document-group-from-template-and-send-invite'; +import { sendInviteAction } from './lib/actions/send-invite'; +import { uploadDocumentAction } from './lib/actions/upload-document'; +import { uploadDocumentAndExtractFieldsAction } from './lib/actions/upload-document-and-extract-fields'; +import { customApiCallAction } from './lib/actions/custom-api-call'; +import { documentCompletedTrigger } from './lib/triggers/document-completed'; +import { documentDeletedTrigger } from './lib/triggers/document-deleted'; +import { newDocumentTrigger } from './lib/triggers/new-document'; +import { documentGroupCompletedTrigger } from './lib/triggers/document-group-completed'; +import { documentUpdatedTrigger } from './lib/triggers/document-updated'; + +export const signNow = createPiece({ + displayName: 'SignNow', + description: + 'eSignature platform for sending, signing, and managing documents.', + auth: signNowAuth, + minimumSupportedRelease: '0.36.1', + logoUrl: 'https://cdn.activepieces.com/pieces/sign-now.png', + categories: [PieceCategory.PRODUCTIVITY], + authors: [], + actions: [ + cancelInviteAction, + uploadDocumentAction, + uploadDocumentAndExtractFieldsAction, + sendInviteAction, + createDocumentFromTemplateAndSendInviteAction, + createDocumentFromTemplateAndSendRoleBasedInviteAction, + createDocumentGroupFromTemplateAndSendInviteAction, + customApiCallAction, + ], + triggers: [ + newDocumentTrigger, + documentUpdatedTrigger, + documentCompletedTrigger, + documentDeletedTrigger, + documentGroupCompletedTrigger, + ], +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/cancel-invite.ts b/packages/pieces/community/sign-now/src/lib/actions/cancel-invite.ts new file mode 100644 index 00000000000..47546177a11 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/cancel-invite.ts @@ -0,0 +1,39 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const cancelInviteAction = createAction({ + auth: signNowAuth, + name: 'cancel_invite', + displayName: 'Cancel Invite to Sign', + description: 'Cancels an invite to sign a document.', + props: { + document_id: Property.ShortText({ + displayName: 'Document ID', + description: 'The ID of the document whose invite you want to cancel.', + required: true, + }), + reason: Property.ShortText({ + displayName: 'Cancellation Reason', + description: 'The reason for cancelling the invite.', + required: true, + }), + }, + async run(context) { + const { document_id, reason } = context.propsValue; + const token = getSignNowBearerToken(context.auth); + + const response = await httpClient.sendRequest({ + method: HttpMethod.PUT, + url: `https://api.signnow.com/document/${document_id}/fieldinvitecancel`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { reason }, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-invite.ts b/packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-invite.ts new file mode 100644 index 00000000000..e0697aa9a6e --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-invite.ts @@ -0,0 +1,168 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const createDocumentFromTemplateAndSendInviteAction = createAction({ + auth: signNowAuth, + name: 'create_document_from_template_and_send_invite', + displayName: 'Create Document From Template & Send Free Form Invite', + description: + 'Creates a new document from a template and sends a free form invite to a signer.', + props: { + template_id: Property.ShortText({ + displayName: 'Template ID', + description: 'The ID of the template to create the document from.', + required: true, + }), + document_name: Property.ShortText({ + displayName: 'Document Name', + description: 'Name for the new document. Defaults to the template name if not provided.', + required: false, + }), + to: Property.ShortText({ + displayName: "Signer's Email", + description: "Email address of the signer. A free form invite can be sent to the sender's own email address.", + required: true, + }), + from: Property.ShortText({ + displayName: "Sender's Email", + description: 'Must be the email address associated with your SignNow account.', + required: true, + }), + subject: Property.ShortText({ + displayName: 'Email Subject', + description: 'Subject line of the invite email sent to the signer.', + required: false, + }), + message: Property.LongText({ + displayName: 'Email Message', + description: 'Body message of the invite email sent to the signer.', + required: false, + }), + cc: Property.Array({ + displayName: 'CC Recipients', + description: 'Email addresses to CC on the invite.', + required: false, + }), + cc_subject: Property.ShortText({ + displayName: 'CC Email Subject', + description: 'Subject line of the email sent to CC recipients.', + required: false, + }), + cc_message: Property.LongText({ + displayName: 'CC Email Message', + description: 'Body message of the email sent to CC recipients.', + required: false, + }), + language: Property.StaticDropdown({ + displayName: 'Language', + description: 'Language for the signing session and notification emails.', + required: false, + options: { + options: [ + { label: 'English', value: 'en' }, + { label: 'Spanish', value: 'es' }, + { label: 'French', value: 'fr' }, + ], + }, + }), + redirect_uri: Property.ShortText({ + displayName: 'Redirect URL', + description: 'URL the signer is redirected to after signing.', + required: false, + }), + close_redirect_uri: Property.ShortText({ + displayName: 'Close Redirect URL', + description: 'URL that opens when the signer clicks the Close button.', + required: false, + }), + redirect_target: Property.StaticDropdown({ + displayName: 'Redirect Target', + description: 'Whether to open the redirect URL in a new tab or the same tab.', + required: false, + options: { + options: [ + { label: 'New tab', value: 'blank' }, + { label: 'Same tab', value: 'self' }, + ], + }, + }), + }, + async run(context) { + const { + template_id, + document_name, + to, + from, + subject, + message, + cc, + cc_subject, + cc_message, + language, + redirect_uri, + close_redirect_uri, + redirect_target, + } = context.propsValue; + + const token = getSignNowBearerToken(context.auth); + + // Step 1: Create document from template + let document_id: string; + let createdDocumentName: string; + + try { + const createResponse = await httpClient.sendRequest<{ id: string; document_name: string }>({ + method: HttpMethod.POST, + url: `https://api.signnow.com/template/${template_id}/copy`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + ...(document_name ? { document_name } : {}), + }, + }); + document_id = createResponse.body.id; + createdDocumentName = createResponse.body.document_name; + } catch (e) { + throw new Error(`Failed to create document from template: ${(e as Error).message}`); + } + + // Step 2: Send free form invite + const inviteBody: Record = { to, from }; + if (subject) inviteBody['subject'] = subject; + if (message) inviteBody['message'] = message; + if (cc && (cc as string[]).length > 0) inviteBody['cc'] = cc; + if (cc_subject) inviteBody['cc_subject'] = cc_subject; + if (cc_message) inviteBody['cc_message'] = cc_message; + if (language) inviteBody['language'] = language; + if (redirect_uri) inviteBody['redirect_uri'] = redirect_uri; + if (close_redirect_uri) inviteBody['close_redirect_uri'] = close_redirect_uri; + if (redirect_target) inviteBody['redirect_target'] = redirect_target; + + try { + const inviteResponse = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://api.signnow.com/document/${document_id}/invite`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: inviteBody, + }); + + return { + document_id, + document_name: createdDocumentName, + invite: inviteResponse.body, + }; + } catch (e) { + throw new Error( + `Document was created (ID: ${document_id}, Name: "${createdDocumentName}") but sending the invite failed: ${(e as Error).message}` + ); + } + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-role-based-invite.ts b/packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-role-based-invite.ts new file mode 100644 index 00000000000..25a84b48d35 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/create-document-from-template-and-send-role-based-invite.ts @@ -0,0 +1,206 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const createDocumentFromTemplateAndSendRoleBasedInviteAction = createAction({ + auth: signNowAuth, + name: 'create_document_from_template_and_send_role_based_invite', + displayName: 'Create Document From Template & Send Role-Based Invite', + description: + 'Creates a new document from a template with fillable fields and sends an invite to one or more signers by role.', + props: { + template_id: Property.ShortText({ + displayName: 'Template ID', + description: 'The ID of the template to create the document from.', + required: true, + }), + document_name: Property.ShortText({ + displayName: 'Document Name', + description: 'Name for the new document. Defaults to the template name if not provided.', + required: false, + }), + from: Property.ShortText({ + displayName: "Sender's Email", + description: 'Must be the email address associated with your SignNow account.', + required: true, + }), + signers: Property.Array({ + displayName: 'Signers', + description: + 'One entry per signer role defined in the template. Each signer must have an email and a role name (e.g. "Signer 1").', + required: true, + properties: { + email: Property.ShortText({ + displayName: 'Email', + description: "Signer's email address.", + required: true, + }), + role: Property.ShortText({ + displayName: 'Role Name', + description: 'Role name as defined in the template (e.g. "Signer 1").', + required: true, + }), + order: Property.Number({ + displayName: 'Signing Order', + description: + 'Order in which this signer receives the invite. Multiple signers can share the same order to sign in parallel.', + required: true, + defaultValue: 1, + }), + subject: Property.ShortText({ + displayName: 'Email Subject', + description: 'Custom email subject for this signer.', + required: false, + }), + message: Property.LongText({ + displayName: 'Email Message', + description: 'Custom email body for this signer.', + required: false, + }), + redirect_uri: Property.ShortText({ + displayName: 'Redirect URL', + description: 'URL the signer is redirected to after signing.', + required: false, + }), + }, + }), + subject: Property.ShortText({ + displayName: 'Email Subject (All Signers)', + description: 'Default email subject sent to all signers. Overridden by per-signer subject if set.', + required: false, + }), + message: Property.LongText({ + displayName: 'Email Message (All Signers)', + description: 'Default email body sent to all signers. Overridden by per-signer message if set.', + required: false, + }), + cc: Property.Array({ + displayName: 'CC Recipients', + description: 'Email addresses to CC on the invite.', + required: false, + }), + cc_subject: Property.ShortText({ + displayName: 'CC Email Subject', + description: 'Subject line for CC recipient emails.', + required: false, + }), + cc_message: Property.LongText({ + displayName: 'CC Email Message', + description: 'Body message for CC recipient emails.', + required: false, + }), + expiration_days: Property.Number({ + displayName: 'Expiration Days', + description: 'Number of days before the invite expires (3–180). Defaults to 30.', + required: false, + }), + language: Property.StaticDropdown({ + displayName: 'Language', + description: 'Language for the signing session and notification emails.', + required: false, + options: { + options: [ + { label: 'English', value: 'en' }, + { label: 'Spanish', value: 'es' }, + { label: 'French', value: 'fr' }, + ], + }, + }), + }, + async run(context) { + const { + template_id, + document_name, + from, + signers, + subject, + message, + cc, + cc_subject, + cc_message, + expiration_days, + language, + } = context.propsValue; + + const token = getSignNowBearerToken(context.auth); + + // Step 1: Create document from template + let document_id: string; + let createdDocumentName: string; + + try { + const createResponse = await httpClient.sendRequest<{ id: string; document_name: string }>({ + method: HttpMethod.POST, + url: `https://api.signnow.com/template/${template_id}/copy`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + ...(document_name ? { document_name } : {}), + }, + }); + document_id = createResponse.body.id; + createdDocumentName = createResponse.body.document_name; + } catch (e) { + throw new Error(`Failed to create document from template: ${(e as Error).message}`); + } + + // Step 2: Send role-based field invite + type SignerEntry = { + email: string; + role: string; + order: number; + subject?: string; + message?: string; + redirect_uri?: string; + }; + + const toArray = (signers as SignerEntry[]).map((signer) => { + const entry: Record = { + email: signer.email, + role: signer.role, + order: signer.order, + }; + if (signer.subject) entry['subject'] = signer.subject; + if (signer.message) entry['message'] = signer.message; + if (signer.redirect_uri) entry['redirect_uri'] = signer.redirect_uri; + return entry; + }); + + const inviteBody: Record = { to: toArray, from }; + if (subject) inviteBody['subject'] = subject; + if (message) inviteBody['message'] = message; + if (cc && (cc as string[]).length > 0) { + inviteBody['cc'] = (cc as string[]).map((email) => ({ email })); + } + if (cc_subject) inviteBody['cc_subject'] = cc_subject; + if (cc_message) inviteBody['cc_message'] = cc_message; + if (expiration_days) inviteBody['expiration_days'] = expiration_days; + if (language) inviteBody['language'] = language; + + try { + const inviteResponse = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://api.signnow.com/document/${document_id}/invite`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: inviteBody, + }); + + return { + document_id, + document_name: createdDocumentName, + invite: inviteResponse.body, + }; + } catch (e) { + throw new Error( + `Document was created (ID: ${document_id}, Name: "${createdDocumentName}") but sending the invite failed: ${(e as Error).message}` + ); + } + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/create-document-group-from-template-and-send-invite.ts b/packages/pieces/community/sign-now/src/lib/actions/create-document-group-from-template-and-send-invite.ts new file mode 100644 index 00000000000..6d08af443d8 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/create-document-group-from-template-and-send-invite.ts @@ -0,0 +1,214 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const createDocumentGroupFromTemplateAndSendInviteAction = createAction({ + auth: signNowAuth, + name: 'create_document_group_from_template_and_send_invite', + displayName: 'Create Document Group From Template Group & Send Invite', + description: + 'Creates a new document group from a template group and sends an invite to one or more signers.', + props: { + template_id: Property.ShortText({ + displayName: 'Document Group Template ID', + description: 'The unique ID of the document group template.', + required: true, + }), + group_name: Property.ShortText({ + displayName: 'Document Group Name', + description: 'Name for the new document group.', + required: true, + }), + signers: Property.Array({ + displayName: 'Signers', + description: + 'One entry per signer. Each signer needs an email, role name, and the document they are signing within the group. Signing order determines the step sequence.', + required: true, + properties: { + email: Property.ShortText({ + displayName: 'Email', + description: "Signer's email address.", + required: true, + }), + role_name: Property.ShortText({ + displayName: 'Role Name', + description: 'Role name as defined in the template (e.g. "Signer 1").', + required: true, + }), + document_id: Property.ShortText({ + displayName: 'Document ID', + description: + 'ID of the specific document within the group this signer is assigned to. Leave blank to assign to all documents.', + required: false, + }), + action: Property.StaticDropdown({ + displayName: 'Action', + description: 'What this recipient is allowed to do with the document.', + required: true, + options: { + options: [ + { label: 'Sign', value: 'sign' }, + { label: 'Approve', value: 'approve' }, + { label: 'View', value: 'view' }, + ], + }, + }), + order: Property.Number({ + displayName: 'Signing Step', + description: + 'Step order for this signer (1 = first). Signers with the same step number act in parallel.', + required: true, + defaultValue: 1, + }), + subject: Property.ShortText({ + displayName: 'Email Subject', + description: 'Custom email subject for this signer.', + required: false, + }), + message: Property.LongText({ + displayName: 'Email Message', + description: 'Custom email body for this signer.', + required: false, + }), + language: Property.StaticDropdown({ + displayName: 'Language', + description: 'Language for this signer\'s signing session and emails.', + required: false, + options: { + options: [ + { label: 'English', value: 'en' }, + { label: 'Spanish', value: 'es' }, + { label: 'French', value: 'fr' }, + ], + }, + }), + redirect_uri: Property.ShortText({ + displayName: 'Redirect URL', + description: 'URL the signer is redirected to after completing the document.', + required: false, + }), + }, + }), + cc: Property.Array({ + displayName: 'CC Recipients', + description: 'Email addresses to CC on the invite.', + required: false, + }), + }, + async run(context) { + const { template_id, group_name, signers, cc } = context.propsValue; + const token = getSignNowBearerToken(context.auth); + + // Step 1: Create document group from template + type DocumentGroupResponse = { + data: { + unique_id: string; + name: string; + documents: Array<{ id: string; document_name: string; roles: string[] }>; + }; + }; + + let documentGroupId: string; + let documents: DocumentGroupResponse['data']['documents']; + + try { + const createResponse = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://api.signnow.com/v2/document-group-templates/${template_id}/document-group`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { group_name }, + }); + documentGroupId = createResponse.body.data.unique_id; + documents = createResponse.body.data.documents; + } catch (e) { + throw new Error(`Failed to create document group from template: ${(e as Error).message}`); + } + + // Step 2: Build invite_steps by grouping signers by their order/step + type SignerEntry = { + email: string; + role_name: string; + document_id?: string; + action: string; + order: number; + subject?: string; + message?: string; + language?: string; + redirect_uri?: string; + }; + + // Group signers by step order + const stepMap = new Map(); + for (const signer of signers as SignerEntry[]) { + const step = signer.order ?? 1; + if (!stepMap.has(step)) stepMap.set(step, []); + stepMap.get(step)!.push(signer); + } + + const invite_steps = Array.from(stepMap.entries()) + .sort(([a], [b]) => a - b) + .map(([order, stepSigners]) => { + const invite_actions = stepSigners.map((signer) => { + // If no document_id provided, use the first document in the group + const targetDocId = + signer.document_id || (documents.length > 0 ? documents[0].id : ''); + + const action: Record = { + email: signer.email, + role_name: signer.role_name, + action: signer.action, + document_id: targetDocId, + }; + if (signer.redirect_uri) action['redirect_uri'] = signer.redirect_uri; + if (signer.language) action['language'] = signer.language; + return action; + }); + + const invite_emails = stepSigners + .filter((s) => s.subject || s.message) + .map((signer) => { + const emailEntry: Record = { email: signer.email }; + if (signer.subject) emailEntry['subject'] = signer.subject; + if (signer.message) emailEntry['message'] = signer.message; + return emailEntry; + }); + + const step: Record = { order, invite_actions }; + if (invite_emails.length > 0) step['invite_emails'] = invite_emails; + return step; + }); + + const inviteBody: Record = { invite_steps }; + if (cc && (cc as string[]).length > 0) inviteBody['cc'] = cc; + + // Step 3: Send document group invite + try { + const inviteResponse = await httpClient.sendRequest<{ id: string; pending_invite_link: string | null }>({ + method: HttpMethod.POST, + url: `https://api.signnow.com/documentgroup/${documentGroupId}/groupinvite`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: inviteBody, + }); + + return { + document_group_id: documentGroupId, + document_group_name: group_name, + invite_id: inviteResponse.body.id, + pending_invite_link: inviteResponse.body.pending_invite_link, + documents: documents.map((d) => ({ id: d.id, name: d.document_name })), + }; + } catch (e) { + throw new Error( + `Document group was created (ID: ${documentGroupId}) but sending the invite failed: ${(e as Error).message}` + ); + } + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/custom-api-call.ts b/packages/pieces/community/sign-now/src/lib/actions/custom-api-call.ts new file mode 100644 index 00000000000..9a04b8e6831 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/custom-api-call.ts @@ -0,0 +1,14 @@ +import { createCustomApiCallAction } from '@activepieces/pieces-common'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const customApiCallAction = createCustomApiCallAction({ + auth: signNowAuth, + name: 'custom_api_call', + displayName: 'Custom API Call', + description: 'Make a custom authenticated request to any SignNow API endpoint.', + baseUrl: () => 'https://api.signnow.com', + authMapping: async (auth) => ({ + Authorization: `Bearer ${getSignNowBearerToken(auth)}`, + 'Content-Type': 'application/json', + }), +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/send-invite.ts b/packages/pieces/community/sign-now/src/lib/actions/send-invite.ts new file mode 100644 index 00000000000..1bbba1552ad --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/send-invite.ts @@ -0,0 +1,165 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const sendInviteAction = createAction({ + auth: signNowAuth, + name: 'send_invite', + displayName: 'Invite to Sign', + description: 'Sends an invite to sign an existing document.', + props: { + document_id: Property.ShortText({ + displayName: 'Document ID', + description: 'The ID of the document to send for signing.', + required: true, + }), + from: Property.ShortText({ + displayName: "Sender's Email", + description: 'Must be the email address associated with your SignNow account.', + required: true, + }), + signers: Property.Array({ + displayName: 'Signers', + description: + 'One entry per signer role. Each signer needs an email, role name, and signing order.', + required: true, + properties: { + email: Property.ShortText({ + displayName: 'Email', + description: "Signer's email address.", + required: true, + }), + role: Property.ShortText({ + displayName: 'Role Name', + description: 'Role name as defined in the document (e.g. "Signer 1").', + required: true, + }), + order: Property.Number({ + displayName: 'Signing Order', + description: + 'Order in which this signer receives the invite. Multiple signers can share the same order to sign in parallel.', + required: true, + defaultValue: 1, + }), + subject: Property.ShortText({ + displayName: 'Email Subject', + description: 'Custom email subject for this signer.', + required: false, + }), + message: Property.LongText({ + displayName: 'Email Message', + description: 'Custom email body for this signer.', + required: false, + }), + redirect_uri: Property.ShortText({ + displayName: 'Redirect URL', + description: 'URL the signer is redirected to after signing.', + required: false, + }), + language: Property.StaticDropdown({ + displayName: 'Language', + description: "Language for this signer's signing session and emails.", + required: false, + options: { + options: [ + { label: 'English', value: 'en' }, + { label: 'Spanish', value: 'es' }, + { label: 'French', value: 'fr' }, + ], + }, + }), + expiration_days: Property.Number({ + displayName: 'Expiration Days', + description: 'Number of days before this signer\'s invite expires (3–180).', + required: false, + }), + }, + }), + subject: Property.ShortText({ + displayName: 'Email Subject (All Signers)', + description: 'Default email subject for all signers. Overridden by per-signer subject if set.', + required: false, + }), + message: Property.LongText({ + displayName: 'Email Message (All Signers)', + description: 'Default email body for all signers. Overridden by per-signer message if set.', + required: false, + }), + cc: Property.Array({ + displayName: 'CC Recipients', + description: 'Email addresses to CC on the invite.', + required: false, + }), + cc_subject: Property.ShortText({ + displayName: 'CC Email Subject', + description: 'Subject line for CC recipient emails.', + required: false, + }), + cc_message: Property.LongText({ + displayName: 'CC Email Message', + description: 'Body message for CC recipient emails.', + required: false, + }), + }, + async run(context) { + const { + document_id, + from, + signers, + subject, + message, + cc, + cc_subject, + cc_message, + } = context.propsValue; + + const token = getSignNowBearerToken(context.auth); + + type SignerEntry = { + email: string; + role: string; + order: number; + subject?: string; + message?: string; + redirect_uri?: string; + language?: string; + expiration_days?: number; + }; + + const toArray = (signers as SignerEntry[]).map((signer) => { + const entry: Record = { + email: signer.email, + role: signer.role, + order: signer.order, + }; + if (signer.subject) entry['subject'] = signer.subject; + if (signer.message) entry['message'] = signer.message; + if (signer.redirect_uri) entry['redirect_uri'] = signer.redirect_uri; + if (signer.language) entry['language'] = signer.language; + if (signer.expiration_days) entry['expiration_days'] = signer.expiration_days; + return entry; + }); + + const inviteBody: Record = { to: toArray, from }; + if (subject) inviteBody['subject'] = subject; + if (message) inviteBody['message'] = message; + if (cc && (cc as string[]).length > 0) { + inviteBody['cc'] = (cc as string[]).map((email) => ({ email })); + } + if (cc_subject) inviteBody['cc_subject'] = cc_subject; + if (cc_message) inviteBody['cc_message'] = cc_message; + + const response = await httpClient.sendRequest({ + method: HttpMethod.POST, + url: `https://api.signnow.com/document/${document_id}/invite`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: inviteBody, + }); + + return response.body; + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/upload-document-and-extract-fields.ts b/packages/pieces/community/sign-now/src/lib/actions/upload-document-and-extract-fields.ts new file mode 100644 index 00000000000..7b80412119d --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/upload-document-and-extract-fields.ts @@ -0,0 +1,40 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import FormData from 'form-data'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const uploadDocumentAndExtractFieldsAction = createAction({ + auth: signNowAuth, + name: 'upload_document_and_extract_fields', + displayName: 'Upload Document & Extract Fields', + description: + 'Uploads a document and automatically converts text tags into SignNow fillable fields.', + props: { + file: Property.File({ + displayName: 'File', + description: + 'The file to upload. Text tags in the document (e.g. {{t:s;r:y;o:"Signer 1";}}) will be converted to fillable fields. Accepted formats: .pdf, .doc, .docx, .odt, .rtf, .png, .jpg, .jpeg, .gif, .bmp, .xml, .xls, .xlsx, .ppt, .pptx, .ps, .eps. Maximum size: 50 MB.', + required: true, + }), + }, + async run(context) { + const { file } = context.propsValue; + const token = getSignNowBearerToken(context.auth); + + const formData = new FormData(); + formData.append('file', Buffer.from(file.base64, 'base64'), file.filename); + formData.append('check_fields', 'true'); + + const response = await httpClient.sendRequest<{ id: string }>({ + method: HttpMethod.POST, + url: 'https://api.signnow.com/document', + headers: { + ...formData.getHeaders(), + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + + return { id: response.body.id }; + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/actions/upload-document.ts b/packages/pieces/community/sign-now/src/lib/actions/upload-document.ts new file mode 100644 index 00000000000..4772722bc77 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/actions/upload-document.ts @@ -0,0 +1,49 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import FormData from 'form-data'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; + +export const uploadDocumentAction = createAction({ + auth: signNowAuth, + name: 'upload_document', + displayName: 'Upload Document', + description: + 'Uploads a document to your SignNow account. Supports PDF, Word, PowerPoint, Excel, images, and more.', + props: { + file: Property.File({ + displayName: 'File', + description: + 'The file to upload. Accepted formats: .pdf, .doc, .docx, .odt, .rtf, .png, .jpg, .jpeg, .gif, .bmp, .xml, .xls, .xlsx, .ppt, .pptx, .ps, .eps. Maximum size: 50 MB.', + required: true, + }), + check_fields: Property.Checkbox({ + displayName: 'Parse Text Tags', + description: + 'Enable if your document contains simple text tags that should be converted to fillable fields.', + required: false, + defaultValue: false, + }), + }, + async run(context) { + const { file, check_fields } = context.propsValue; + const token = getSignNowBearerToken(context.auth); + + const formData = new FormData(); + formData.append('file', Buffer.from(file.base64, 'base64'), file.filename); + if (check_fields) { + formData.append('check_fields', 'true'); + } + + const response = await httpClient.sendRequest<{ id: string }>({ + method: HttpMethod.POST, + url: 'https://api.signnow.com/document', + headers: { + ...formData.getHeaders(), + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + + return { id: response.body.id }; + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/common/auth.ts b/packages/pieces/community/sign-now/src/lib/common/auth.ts new file mode 100644 index 00000000000..6cf9908ab07 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/common/auth.ts @@ -0,0 +1,65 @@ +import { PieceAuth, OAuth2AuthorizationMethod, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework'; +import { AppConnectionType } from '@activepieces/shared'; +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; + +export const signNowOAuth2Auth = PieceAuth.OAuth2({ + displayName: 'Sign in with SignNow', + description: 'Connect your SignNow account using OAuth2. You will be redirected to SignNow to authorize access.', + authUrl: 'https://app.signnow.com/authorize', + tokenUrl: 'https://api.signnow.com/oauth2/token', + required: true, + scope: ['*'], + authorizationMethod: OAuth2AuthorizationMethod.HEADER, + // SignNow does not accept access_type or prompt params — suppress both with empty strings + extra: { access_type: '' }, + prompt: 'omit', +}); + +export const signNowApiKeyAuth = PieceAuth.CustomAuth({ + displayName: 'API Key', + description: `Authenticate using an API key from the SignNow dashboard. + +1. Log in to the [SignNow API dashboard](https://app.signnow.com/webapp/developer-experience). +2. Go to **Apps and Keys** and select your application. +3. On the **API Keys** tab, copy your API key.`, + required: true, + props: { + apiKey: PieceAuth.SecretText({ + displayName: 'API Key', + description: 'Your SignNow API key.', + required: true, + }), + }, + validate: async ({ auth }) => { + try { + const response = await httpClient.sendRequest({ + method: HttpMethod.GET, + url: 'https://api.signnow.com/user', + headers: { + Authorization: `Bearer ${auth.apiKey.trim()}`, + Accept: 'application/json', + }, + }); + if (response.status === 200) { + return { valid: true }; + } + return { valid: false, error: 'Invalid API key.' }; + } catch (e) { + return { valid: false, error: (e as Error).message }; + } + }, +}); + +export const signNowAuth = [signNowOAuth2Auth, signNowApiKeyAuth]; + +export type SignNowAuthValue = AppConnectionValueForAuthProperty; + +/** + * Returns the bearer token for API requests regardless of which auth method was used. + */ +export function getSignNowBearerToken(auth: SignNowAuthValue): string { + if (auth.type === AppConnectionType.OAUTH2) { + return auth.access_token; + } + return (auth as Extract).props.apiKey.trim(); +} diff --git a/packages/pieces/community/sign-now/src/lib/common/webhook.ts b/packages/pieces/community/sign-now/src/lib/common/webhook.ts new file mode 100644 index 00000000000..0a507c1bf4a --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/common/webhook.ts @@ -0,0 +1,57 @@ +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { SignNowAuthValue, getSignNowBearerToken } from './auth'; + +export async function getUserId(token: string): Promise { + const response = await httpClient.sendRequest<{ id: string }>({ + method: HttpMethod.GET, + url: 'https://api.signnow.com/user', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + }); + return response.body.id; +} + +export async function registerWebhook( + auth: SignNowAuthValue, + event: string, + entityId: string, + callbackUrl: string +): Promise { + const token = getSignNowBearerToken(auth); + const response = await httpClient.sendRequest<{ data: { id: string } }>({ + method: HttpMethod.POST, + url: 'https://api.signnow.com/v2/event-subscriptions', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: { + event, + entity_id: entityId, + attributes: { + callback: callbackUrl, + delete_access_token: true, + }, + }, + }); + return response.body.data.id; +} + +export async function unregisterWebhook( + auth: SignNowAuthValue, + subscriptionId: string +): Promise { + const token = getSignNowBearerToken(auth); + await httpClient.sendRequest({ + method: HttpMethod.DELETE, + url: `https://api.signnow.com/v2/event-subscriptions/${subscriptionId}`, + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); +} diff --git a/packages/pieces/community/sign-now/src/lib/triggers/document-completed.ts b/packages/pieces/community/sign-now/src/lib/triggers/document-completed.ts new file mode 100644 index 00000000000..0d02da66578 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/triggers/document-completed.ts @@ -0,0 +1,49 @@ +import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework'; +import { signNowAuth } from '../common/auth'; +import { getUserId, registerWebhook, unregisterWebhook } from '../common/webhook'; +import { getSignNowBearerToken } from '../common/auth'; + +const STORE_KEY = 'sign_now_document_completed_subscription_id'; + +export const documentCompletedTrigger = createTrigger({ + auth: signNowAuth, + name: 'document_completed', + displayName: 'Document Completed', + description: 'Triggers when all signers have filled in and signed the document.', + props: {}, + type: TriggerStrategy.WEBHOOK, + async onEnable(context) { + const token = getSignNowBearerToken(context.auth); + const userId = await getUserId(token); + const subscriptionId = await registerWebhook( + context.auth, + 'user.document.complete', + userId, + context.webhookUrl + ); + await context.store.put(STORE_KEY, subscriptionId); + }, + async onDisable(context) { + const subscriptionId = await context.store.get(STORE_KEY); + if (subscriptionId) { + await unregisterWebhook(context.auth, subscriptionId); + await context.store.delete(STORE_KEY); + } + }, + async run(context) { + return [context.payload.body]; + }, + sampleData: { + meta: { + timestamp: 1661430936, + event: 'user.document.complete', + environment: 'https://api.signnow.com', + callback_url: 'https://your-callback-url.com', + }, + content: { + document_id: 'a1b2c3d4e5f67890123456789abcdef012345678', + document_name: 'Contract.pdf', + user_id: 'b2c3d4e5f67890123456789abcdef0123456789a', + }, + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/triggers/document-deleted.ts b/packages/pieces/community/sign-now/src/lib/triggers/document-deleted.ts new file mode 100644 index 00000000000..ac716e886d3 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/triggers/document-deleted.ts @@ -0,0 +1,48 @@ +import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; +import { getUserId, registerWebhook, unregisterWebhook } from '../common/webhook'; + +const STORE_KEY = 'sign_now_document_deleted_subscription_id'; + +export const documentDeletedTrigger = createTrigger({ + auth: signNowAuth, + name: 'document_deleted', + displayName: 'Document Deleted', + description: 'Triggers when a document has been deleted.', + props: {}, + type: TriggerStrategy.WEBHOOK, + async onEnable(context) { + const token = getSignNowBearerToken(context.auth); + const userId = await getUserId(token); + const subscriptionId = await registerWebhook( + context.auth, + 'user.document.delete', + userId, + context.webhookUrl + ); + await context.store.put(STORE_KEY, subscriptionId); + }, + async onDisable(context) { + const subscriptionId = await context.store.get(STORE_KEY); + if (subscriptionId) { + await unregisterWebhook(context.auth, subscriptionId); + await context.store.delete(STORE_KEY); + } + }, + async run(context) { + return [context.payload.body]; + }, + sampleData: { + meta: { + timestamp: 1661430936, + event: 'user.document.delete', + environment: 'https://api.signnow.com', + callback_url: 'https://your-callback-url.com', + }, + content: { + document_id: 'a1b2c3d4e5f67890123456789abcdef012345678', + document_name: 'Contract.pdf', + user_id: 'b2c3d4e5f67890123456789abcdef0123456789a', + }, + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/triggers/document-group-completed.ts b/packages/pieces/community/sign-now/src/lib/triggers/document-group-completed.ts new file mode 100644 index 00000000000..1da753d6127 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/triggers/document-group-completed.ts @@ -0,0 +1,48 @@ +import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; +import { getUserId, registerWebhook, unregisterWebhook } from '../common/webhook'; + +const STORE_KEY = 'sign_now_document_group_completed_subscription_id'; + +export const documentGroupCompletedTrigger = createTrigger({ + auth: signNowAuth, + name: 'document_group_completed', + displayName: 'Document Group Completed', + description: 'Triggers when all signers have filled in and signed the document group.', + props: {}, + type: TriggerStrategy.WEBHOOK, + async onEnable(context) { + const token = getSignNowBearerToken(context.auth); + const userId = await getUserId(token); + const subscriptionId = await registerWebhook( + context.auth, + 'user.document_group.complete', + userId, + context.webhookUrl + ); + await context.store.put(STORE_KEY, subscriptionId); + }, + async onDisable(context) { + const subscriptionId = await context.store.get(STORE_KEY); + if (subscriptionId) { + await unregisterWebhook(context.auth, subscriptionId); + await context.store.delete(STORE_KEY); + } + }, + async run(context) { + return [context.payload.body]; + }, + sampleData: { + meta: { + timestamp: 1661430936, + event: 'user.document_group.complete', + environment: 'https://api.signnow.com', + callback_url: 'https://your-callback-url.com', + }, + content: { + document_group_id: 'a1b2c3d4e5f67890123456789abcdef012345678', + document_group_name: 'Contract Package', + user_id: 'b2c3d4e5f67890123456789abcdef0123456789a', + }, + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/triggers/document-updated.ts b/packages/pieces/community/sign-now/src/lib/triggers/document-updated.ts new file mode 100644 index 00000000000..dbe0a63b755 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/triggers/document-updated.ts @@ -0,0 +1,48 @@ +import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; +import { getUserId, registerWebhook, unregisterWebhook } from '../common/webhook'; + +const STORE_KEY = 'sign_now_document_updated_subscription_id'; + +export const documentUpdatedTrigger = createTrigger({ + auth: signNowAuth, + name: 'document_updated', + displayName: 'Document Updated', + description: 'Triggers when a document has been updated.', + props: {}, + type: TriggerStrategy.WEBHOOK, + async onEnable(context) { + const token = getSignNowBearerToken(context.auth); + const userId = await getUserId(token); + const subscriptionId = await registerWebhook( + context.auth, + 'user.document.update', + userId, + context.webhookUrl + ); + await context.store.put(STORE_KEY, subscriptionId); + }, + async onDisable(context) { + const subscriptionId = await context.store.get(STORE_KEY); + if (subscriptionId) { + await unregisterWebhook(context.auth, subscriptionId); + await context.store.delete(STORE_KEY); + } + }, + async run(context) { + return [context.payload.body]; + }, + sampleData: { + meta: { + timestamp: 1661430936, + event: 'user.document.update', + environment: 'https://api.signnow.com', + callback_url: 'https://your-callback-url.com', + }, + content: { + document_id: 'a1b2c3d4e5f67890123456789abcdef012345678', + document_name: 'Contract.pdf', + user_id: 'b2c3d4e5f67890123456789abcdef0123456789a', + }, + }, +}); diff --git a/packages/pieces/community/sign-now/src/lib/triggers/new-document.ts b/packages/pieces/community/sign-now/src/lib/triggers/new-document.ts new file mode 100644 index 00000000000..0ab148fdea4 --- /dev/null +++ b/packages/pieces/community/sign-now/src/lib/triggers/new-document.ts @@ -0,0 +1,48 @@ +import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework'; +import { signNowAuth, getSignNowBearerToken } from '../common/auth'; +import { getUserId, registerWebhook, unregisterWebhook } from '../common/webhook'; + +const STORE_KEY = 'sign_now_new_document_subscription_id'; + +export const newDocumentTrigger = createTrigger({ + auth: signNowAuth, + name: 'new_document', + displayName: 'New Document', + description: 'Triggers when a document has been uploaded to SignNow.', + props: {}, + type: TriggerStrategy.WEBHOOK, + async onEnable(context) { + const token = getSignNowBearerToken(context.auth); + const userId = await getUserId(token); + const subscriptionId = await registerWebhook( + context.auth, + 'user.document.create', + userId, + context.webhookUrl + ); + await context.store.put(STORE_KEY, subscriptionId); + }, + async onDisable(context) { + const subscriptionId = await context.store.get(STORE_KEY); + if (subscriptionId) { + await unregisterWebhook(context.auth, subscriptionId); + await context.store.delete(STORE_KEY); + } + }, + async run(context) { + return [context.payload.body]; + }, + sampleData: { + meta: { + timestamp: 1661430936, + event: 'user.document.create', + environment: 'https://api.signnow.com', + callback_url: 'https://your-callback-url.com', + }, + content: { + document_id: 'a1b2c3d4e5f67890123456789abcdef012345678', + document_name: 'Contract.pdf', + user_id: 'b2c3d4e5f67890123456789abcdef0123456789a', + }, + }, +}); diff --git a/packages/pieces/community/sign-now/tsconfig.json b/packages/pieces/community/sign-now/tsconfig.json new file mode 100644 index 00000000000..b512ca3708c --- /dev/null +++ b/packages/pieces/community/sign-now/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} \ No newline at end of file diff --git a/packages/pieces/community/sign-now/tsconfig.lib.json b/packages/pieces/community/sign-now/tsconfig.lib.json new file mode 100644 index 00000000000..312dfdeb31f --- /dev/null +++ b/packages/pieces/community/sign-now/tsconfig.lib.json @@ -0,0 +1,21 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "paths": {}, + "outDir": "./dist", + "declaration": true, + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts" + ] +} \ No newline at end of file From 890e11b885dfb18d0fb2e2c734202f3932a7ba80 Mon Sep 17 00:00:00 2001 From: Kishan Parmar <135701940+kishanprmr@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:09:34 +0530 Subject: [PATCH 2/3] feat(slack): new modal interaction trigger (#11454) --- bun.lock | 10 +- packages/pieces/community/slack/package.json | 2 +- packages/pieces/community/slack/src/index.ts | 341 +++++++++--------- .../slack/src/lib/common/request-action.ts | 235 ++++++------ .../community/slack/src/lib/common/types.ts | 9 + .../src/lib/triggers/new-modal-interaction.ts | 51 +++ 6 files changed, 353 insertions(+), 295 deletions(-) create mode 100644 packages/pieces/community/slack/src/lib/common/types.ts create mode 100644 packages/pieces/community/slack/src/lib/triggers/new-modal-interaction.ts diff --git a/bun.lock b/bun.lock index d9d3e51aa43..bc510834ea4 100644 --- a/bun.lock +++ b/bun.lock @@ -190,7 +190,6 @@ "intercom-client": "6.2.0", "intl-messageformat": "10.5.14", "ioredis": "5.4.1", - "is-base64": "1.1.0", "isolated-vm": "5.0.1", "js-yaml": "4.1.1", "jsdom": "23.0.1", @@ -5708,7 +5707,7 @@ }, "packages/pieces/community/slack": { "name": "@activepieces/piece-slack", - "version": "0.12.3", + "version": "0.12.4", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", @@ -7370,7 +7369,6 @@ "@types/content-disposition": "0.5.9", "@types/decompress": "4.2.4", "@types/deep-equal": "1.0.1", - "@types/is-base64": "1.1.1", "@types/jsonwebtoken": "9.0.10", "@types/mime-types": "2.1.1", "@types/mustache": "4.2.4", @@ -7428,7 +7426,6 @@ "dayjs": "1.11.9", "fetch-retry": "6.0.0", "http-status-codes": "2.2.0", - "is-base64": "1.1.0", "isolated-vm": "5.0.1", "mime-types": "2.1.35", "nanoid": "3.3.8", @@ -7439,7 +7436,6 @@ "zod": "4.1.13", }, "devDependencies": { - "@types/is-base64": "1.1.1", "@types/mime-types": "2.1.1", "@types/node": "20.19.9", "vitest": "3.0.8", @@ -10677,8 +10673,6 @@ "@types/imapflow": ["@types/imapflow@1.0.23", "", { "dependencies": { "@types/node": "*" } }, "sha512-/p6I5UBzoYWjLNsd5P4WHaQPFUe9QLTvoDYsfoP8Lp7bDLjhusjwDzZxWT4ukYyhgOcOuXyDrn19h0KrEp7WIA=="], - "@types/is-base64": ["@types/is-base64@1.1.1", "", {}, "sha512-JgnGhP+MeSHEQmvxcobcwPEP4Ew56voiq9/0hmP/41lyQ/3gBw/ZCIRy2v+QkEOdeCl58lRcrf6+Y6WMlJGETA=="], - "@types/is-url": ["@types/is-url@1.2.32", "", {}, "sha512-46VLdbWI8Sc+hPexQ6NLNR2YpoDyDZIpASHkJQ2Yr+Kf9Giw6LdCTkwOdsnHKPQeh7xTjTmSnxbE8qpxYuCiHA=="], "@types/js-cookie": ["@types/js-cookie@2.2.7", "", {}, "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA=="], @@ -12319,8 +12313,6 @@ "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], - "is-base64": ["is-base64@1.1.0", "", { "bin": { "is-base64": "bin/is-base64", "is_base64": "bin/is-base64" } }, "sha512-Nlhg7Z2dVC4/PTvIFkgVVNvPHSO2eR/Yd0XzhGiXCXEvWnptXlXa/clQ8aePPiMuxEGcWfzWbGw2Fe3d+Y3v1g=="], - "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], diff --git a/packages/pieces/community/slack/package.json b/packages/pieces/community/slack/package.json index 10d350f7344..e59b0001542 100644 --- a/packages/pieces/community/slack/package.json +++ b/packages/pieces/community/slack/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-slack", - "version": "0.12.3", + "version": "0.12.4", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/slack/src/index.ts b/packages/pieces/community/slack/src/index.ts index d0896b480e6..3ce9c209d7b 100644 --- a/packages/pieces/community/slack/src/index.ts +++ b/packages/pieces/community/slack/src/index.ts @@ -1,13 +1,9 @@ +import { createCustomApiCallAction, httpClient, HttpMethod } from '@activepieces/pieces-common'; import { - createCustomApiCallAction, - httpClient, - HttpMethod, -} from '@activepieces/pieces-common'; -import { - createPiece, - OAuth2PropertyValue, - PieceAuth, - Property, + createPiece, + OAuth2PropertyValue, + PieceAuth, + Property, } from '@activepieces/pieces-framework'; import { PieceCategory } from '@activepieces/shared'; @@ -50,172 +46,183 @@ import { inviteUserToChannelAction } from './lib/actions/invite-user-to-channel' import { listUsers } from './lib/actions/list-users'; import { deleteMessageAction } from './lib/actions/delete-message'; import { slackAuth } from './lib/auth'; +import { newModalInteractionTrigger } from './lib/triggers/new-modal-interaction'; export const slack = createPiece({ - displayName: 'Slack', - description: 'Channel-based messaging platform', - minimumSupportedRelease: '0.66.7', - logoUrl: 'https://cdn.activepieces.com/pieces/slack.png', - categories: [PieceCategory.COMMUNICATION], - auth: slackAuth, - events: { - parseAndReply: ({ payload, server }) => { - if ( - payload.headers['content-type'] === 'application/x-www-form-urlencoded' - ) { - if ( - payload.body && - typeof payload.body == 'object' && - 'payload' in payload.body - ) { - const interactionPayloadBody = JSON.parse( - (payload.body as { payload: string }).payload - ) as InteractionPayloadBody; - if (interactionPayloadBody.type === 'block_actions') { - const action = interactionPayloadBody.actions?.[0]; - if ( - action && - action.type === 'button' && - action.value?.startsWith(server.publicUrl) - ) { - // We don't await the promise as we don't handle the response anyway - httpClient.sendRequest({ - url: action.value, - method: HttpMethod.POST, - body: interactionPayloadBody, - }); - } - } - } - return { - reply: { - headers: {}, - body: {}, - }, - }; - } else { - const eventPayloadBody = payload.body as EventPayloadBody; - if (eventPayloadBody.challenge) { - return { - reply: { - body: eventPayloadBody['challenge'], - headers: {}, - }, - }; - } + displayName: 'Slack', + description: 'Channel-based messaging platform', + minimumSupportedRelease: '0.66.7', + logoUrl: 'https://cdn.activepieces.com/pieces/slack.png', + categories: [PieceCategory.COMMUNICATION], + auth: slackAuth, + events: { + parseAndReply: ({ payload, server }) => { + if (payload.headers['content-type'] === 'application/x-www-form-urlencoded') { + if (payload.body && typeof payload.body == 'object' && 'payload' in payload.body) { + const interactionPayloadBody = JSON.parse( + (payload.body as { payload: string }).payload, + ) as InteractionPayloadBody; + if (interactionPayloadBody.type === 'block_actions') { + const action = interactionPayloadBody.actions?.[0]; + if ( + action && + action.type === 'button' && + action.value?.startsWith(server.publicUrl) + ) { + // We don't await the promise as we don't handle the response anyway + httpClient.sendRequest({ + url: action.value, + method: HttpMethod.POST, + body: interactionPayloadBody, + }); + } + } else if ( + interactionPayloadBody.type === 'view_submission' || + interactionPayloadBody.type === 'view_closed' + ) { + const viewModalPayload = interactionPayloadBody as unknown as { + type: string; + team: { + id: string; + token: string; + api_app_id: string; + }; + }; - return { - event: eventPayloadBody?.event?.type, - identifierValue: eventPayloadBody.team_id, - }; - } - }, - verify: ({ webhookSecret, payload }) => { - // Construct the signature base string - const timestamp = payload.headers['x-slack-request-timestamp']; - const signature = payload.headers['x-slack-signature']; - const signatureBaseString = `v0:${timestamp}:${payload.rawBody}`; - const hmac = crypto.createHmac('sha256', webhookSecret as string); - hmac.update(signatureBaseString); - const computedSignature = `v0=${hmac.digest('hex')}`; - return signature === computedSignature; + return { + event: viewModalPayload.type, + identifierValue: viewModalPayload.team.id, + }; + } + } + return { + reply: { + headers: {}, + body: {}, + }, + }; + } else { + const eventPayloadBody = payload.body as EventPayloadBody; + if (eventPayloadBody.challenge) { + return { + reply: { + body: eventPayloadBody['challenge'], + headers: {}, + }, + }; + } + + return { + event: eventPayloadBody?.event?.type, + identifierValue: eventPayloadBody.team_id, + }; + } + }, + verify: ({ webhookSecret, payload }) => { + // Construct the signature base string + const timestamp = payload.headers['x-slack-request-timestamp']; + const signature = payload.headers['x-slack-signature']; + const signatureBaseString = `v0:${timestamp}:${payload.rawBody}`; + const hmac = crypto.createHmac('sha256', webhookSecret as string); + hmac.update(signatureBaseString); + const computedSignature = `v0=${hmac.digest('hex')}`; + return signature === computedSignature; + }, }, - }, - authors: [ - 'rita-gorokhod', - 'AdamSelene', - 'Abdallah-Alwarawreh', - 'kishanprmr', - 'MoShizzle', - 'AbdulTheActivePiecer', - 'khaledmashaly', - 'abuaboud', - ], - actions: [ - addRectionToMessageAction, - slackSendDirectMessageAction, - slackSendMessageAction, - requestApprovalDirectMessageAction, - requestSendApprovalMessageAction, - requestActionDirectMessageAction, - requestActionMessageAction, - uploadFile, - getFileAction, - searchMessages, - findUserByEmailAction, - findUserByHandleAction, - findUserByIdAction, - listUsers, - updateMessage, - deleteMessageAction, - createChannelAction, - updateProfileAction, - getChannelHistory, - setUserStatusAction, - markdownToSlackFormat, - retrieveThreadMessages, - setChannelTopicAction, - getMessageAction, - inviteUserToChannelAction, - createCustomApiCallAction({ - baseUrl: () => { - return 'https://slack.com/api'; - }, - auth: slackAuth, - authMapping: async (auth, propsValue) => { - if (propsValue.useUserToken) { - return { - Authorization: `Bearer ${ - (auth as OAuth2PropertyValue).data['authed_user']?.access_token - }`, - }; - } else { - return { - Authorization: `Bearer ${ - (auth as OAuth2PropertyValue).access_token - }`, - }; - } - }, - extraProps: { - useUserToken: Property.Checkbox({ - displayName: 'Use user token', - description: 'Use user token instead of bot token', - required: true, - defaultValue: false, + authors: [ + 'rita-gorokhod', + 'AdamSelene', + 'Abdallah-Alwarawreh', + 'kishanprmr', + 'MoShizzle', + 'AbdulTheActivePiecer', + 'khaledmashaly', + 'abuaboud', + ], + actions: [ + addRectionToMessageAction, + slackSendDirectMessageAction, + slackSendMessageAction, + requestApprovalDirectMessageAction, + requestSendApprovalMessageAction, + requestActionDirectMessageAction, + requestActionMessageAction, + uploadFile, + getFileAction, + searchMessages, + findUserByEmailAction, + findUserByHandleAction, + findUserByIdAction, + listUsers, + updateMessage, + deleteMessageAction, + createChannelAction, + updateProfileAction, + getChannelHistory, + setUserStatusAction, + markdownToSlackFormat, + retrieveThreadMessages, + setChannelTopicAction, + getMessageAction, + inviteUserToChannelAction, + createCustomApiCallAction({ + baseUrl: () => { + return 'https://slack.com/api'; + }, + auth: slackAuth, + authMapping: async (auth, propsValue) => { + if (propsValue.useUserToken) { + return { + Authorization: `Bearer ${ + (auth as OAuth2PropertyValue).data['authed_user']?.access_token + }`, + }; + } else { + return { + Authorization: `Bearer ${(auth as OAuth2PropertyValue).access_token}`, + }; + } + }, + extraProps: { + useUserToken: Property.Checkbox({ + displayName: 'Use user token', + description: 'Use user token instead of bot token', + required: true, + defaultValue: false, + }), + }, }), - }, - }), - ], - triggers: [ - newMessageTrigger, - newMessageInChannelTrigger, - newDirectMessageTrigger, - newMention, - newMentionInDirectMessageTrigger, - newReactionAdded, - channelCreated, - newCommand, - newCommandInDirectMessageTrigger, - newUserTrigger, - newSavedMessageTrigger, - newTeamCustomEmojiTrigger, - ], + ], + triggers: [ + newMessageTrigger, + newMessageInChannelTrigger, + newDirectMessageTrigger, + newMention, + newMentionInDirectMessageTrigger, + newReactionAdded, + channelCreated, + newCommand, + newCommandInDirectMessageTrigger, + newUserTrigger, + newSavedMessageTrigger, + newTeamCustomEmojiTrigger, + newModalInteractionTrigger, + ], }); type EventPayloadBody = { - // Event payload - challenge: string; - event: { - type: string; - }; - team_id: string; + // Event payload + challenge: string; + event: { + type: string; + }; + team_id: string; }; type InteractionPayloadBody = { - // Interaction payload - type?: string; - actions?: { - type: string; - value: string; - }[]; + // Interaction payload + type?: string; + actions?: { + type: string; + value: string; + }[]; }; diff --git a/packages/pieces/community/slack/src/lib/common/request-action.ts b/packages/pieces/community/slack/src/lib/common/request-action.ts index f0cd7726525..5fbb53ec818 100644 --- a/packages/pieces/community/slack/src/lib/common/request-action.ts +++ b/packages/pieces/community/slack/src/lib/common/request-action.ts @@ -1,135 +1,134 @@ -import { buildFlowOriginContextBlock, processMessageTimestamp, slackSendMessage, textToSectionBlocks } from './utils'; import { - assertNotNullOrUndefined, - ExecutionType, - PauseType, -} from '@activepieces/shared'; + buildFlowOriginContextBlock, + processMessageTimestamp, + slackSendMessage, + textToSectionBlocks, +} from './utils'; +import { assertNotNullOrUndefined, ExecutionType, PauseType } from '@activepieces/shared'; import { ChatPostMessageResponse } from '@slack/web-api'; export const requestAction = async (conversationId: string, context: any) => { - const { actions } = context.propsValue; - assertNotNullOrUndefined(actions, 'actions'); + const { actions } = context.propsValue; + assertNotNullOrUndefined(actions, 'actions'); - if (!actions.length) { - throw new Error(`Must have at least one button action`); - } - - const actionTextToIds = actions.map( - ({ label, style }: { label: string; style: string }) => { - if (!label) { - throw new Error(`Button text for the action cannot be empty`); - } - - return { - label, - style, - actionId: encodeURI(label as string), - }; + if (!actions.length) { + throw new Error(`Must have at least one button action`); } - ); - if (context.executionType === ExecutionType.BEGIN) { - context.run.pause({ - pauseMetadata: { - type: PauseType.WEBHOOK, - actions: actionTextToIds.map( - (action: { actionId: string }) => action.actionId - ), - }, + const actionTextToIds = actions.map(({ label, style }: { label: string; style: string }) => { + if (!label) { + throw new Error(`Button text for the action cannot be empty`); + } + + return { + label, + style, + actionId: encodeURI(label as string), + }; }); - const token = context.auth.access_token; - const { text, username, profilePicture } = context.propsValue; + if (context.executionType === ExecutionType.BEGIN) { + context.run.pause({ + pauseMetadata: { + type: PauseType.WEBHOOK, + actions: actionTextToIds.map((action: { actionId: string }) => action.actionId), + }, + }); + + const token = context.auth.access_token; + const { text, username, profilePicture } = context.propsValue; + + assertNotNullOrUndefined(token, 'token'); + assertNotNullOrUndefined(text, 'text'); - assertNotNullOrUndefined(token, 'token'); - assertNotNullOrUndefined(text, 'text'); + const actionElements = actionTextToIds.map( + (action: { label: string; style: string; actionId: string }) => { + const actionLink = context.generateResumeUrl({ + queryParams: { action: action.actionId }, + }); - const actionElements = actionTextToIds.map( - (action: { label: string; style: string; actionId: string }) => { - const actionLink = context.generateResumeUrl({ - queryParams: { action: action.actionId }, + return { + type: 'button', + text: { + type: 'plain_text', + text: action.label, + }, + ...(action.style && { style: action.style }), + value: actionLink, + action_id: action.actionId, + }; + }, + ); + + const messageResponse: ChatPostMessageResponse = await slackSendMessage({ + token, + text: `${context.propsValue.text}`, + username, + profilePicture, + threadTs: context.propsValue.threadTs + ? processMessageTimestamp(context.propsValue.threadTs) + : undefined, + blocks: [ + ...textToSectionBlocks(`${context.propsValue.text}`), + { + type: 'actions', + block_id: 'actions', + elements: actionElements, + }, + ...(context.propsValue.mentionOriginFlow + ? [buildFlowOriginContextBlock(context)] + : []), + ], + conversationId: conversationId, }); return { - type: 'button', - text: { - type: 'plain_text', - text: action.label, - }, - ...(action.style && {style: action.style}), - value: actionLink, - action_id: action.actionId, + action: actionTextToIds.at(0) || 'N/A', + payload: { + type: 'action_blocks', + user: { + id: messageResponse.message?.user, + username: 'user.name', + name: 'john.smith', + team_id: messageResponse.message?.team, + }, + container: { + type: 'message', + message_ts: messageResponse.ts, + channel_id: messageResponse.channel, + is_ephemeral: false, + }, + trigger_id: 'trigger_id', + team: { + id: messageResponse.message?.team, + domain: 'team_name', + }, + channel: { + id: messageResponse.channel, + name: '#channel', + }, + message: messageResponse.message, + state: {}, + actions: [ + { + action_id: 'action_id', + block_id: 'actions', + value: 'resume_url', + style: 'primary', + type: 'button', + action_ts: 'action_ts', + }, + ], + }, + }; + } else { + const payloadQueryParams = context.resumePayload.queryParams as { + action: string; }; - } - ); - - const messageResponse: ChatPostMessageResponse = await slackSendMessage({ - token, - text: `${context.propsValue.text}`, - username, - profilePicture, - threadTs: context.propsValue.threadTs - ? processMessageTimestamp(context.propsValue.threadTs) - : undefined, - blocks: [ - ...textToSectionBlocks(`${context.propsValue.text}`), - { - type: 'actions', - block_id: 'actions', - elements: actionElements, - }, - ...(context.propsValue.mentionOriginFlow ? [buildFlowOriginContextBlock(context)] : []), - ], - conversationId: conversationId, - }); - - return { - action: actionTextToIds.at(0) || 'N/A', - payload: { - type: 'action_blocks', - user: { - id: messageResponse.message?.user, - username: 'user.name', - name: 'john.smith', - team_id: messageResponse.message?.team, - }, - container: { - type: 'message', - message_ts: messageResponse.ts, - channel_id: messageResponse.channel, - is_ephemeral: false, - }, - trigger_id: 'trigger_id', - team: { - id: messageResponse.message?.team, - domain: 'team_name', - }, - channel: { - id: messageResponse.channel, - name: '#channel', - }, - message: messageResponse.message, - state: {}, - actions: [ - { - action_id: 'action_id', - block_id: 'actions', - value: 'resume_url', - style: 'primary', - type: 'button', - action_ts: 'action_ts', - }, - ], - }, - }; - } else { - const payloadQueryParams = context.resumePayload.queryParams as { - action: string; - }; - return { - action: decodeURI(payloadQueryParams.action), - payload: context.resumePayload.body, - }; - } + return { + action: decodeURI(payloadQueryParams.action), + payload: context.resumePayload.body, + }; + } }; diff --git a/packages/pieces/community/slack/src/lib/common/types.ts b/packages/pieces/community/slack/src/lib/common/types.ts new file mode 100644 index 00000000000..87584b2ea53 --- /dev/null +++ b/packages/pieces/community/slack/src/lib/common/types.ts @@ -0,0 +1,9 @@ +export type ViewSubmissionPayload = { + type: string; + team: { + id: string; + }; + api_app_id: string; + token: string; + trigger_id: string; +}; diff --git a/packages/pieces/community/slack/src/lib/triggers/new-modal-interaction.ts b/packages/pieces/community/slack/src/lib/triggers/new-modal-interaction.ts new file mode 100644 index 00000000000..34728d604f7 --- /dev/null +++ b/packages/pieces/community/slack/src/lib/triggers/new-modal-interaction.ts @@ -0,0 +1,51 @@ +import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework'; +import { slackAuth } from '../auth'; +import { ViewSubmissionPayload } from '../common/types'; + +export const newModalInteractionTrigger = createTrigger({ + auth: slackAuth, + name: 'new-modal-interaction', + displayName: 'New Modal Interaction', + description: 'Triggers when a user interacts with a modal.', + props: { + interactionType: Property.StaticDropdown({ + displayName: 'Interaction Type', + description: 'Select the type of modal interaction to trigger on.', + required: true, + defaultValue: 'view_submission', + options: { + disabled: false, + options: [ + { label: 'View Submission', value: 'view_submission' }, + { label: 'View Closed', value: 'view_closed' }, + ], + }, + }), + }, + type: TriggerStrategy.APP_WEBHOOK, + sampleData: undefined, + onEnable: async (context) => { + // Older OAuth2 has team_id, newer has team.id + const teamId = context.auth.data['team_id'] ?? context.auth.data['team']['id']; + context.app.createListeners({ + events: [context.propsValue.interactionType as string], + identifierValue: teamId, + }); + }, + onDisable: async (context) => { + // Ignored + }, + + run: async (context) => { + const body = context.payload.body as { payload: string }; + const payload = JSON.parse(body.payload) as ViewSubmissionPayload; + + const interactionType = context.propsValue.interactionType as string; + if (payload.type !== interactionType) { + return []; + } + + const { token: _token, ...safePayload } = payload; + return [safePayload]; + }, +}); From 18d5dc0aec9727afce08ef85ca2a2a9162ceef23 Mon Sep 17 00:00:00 2001 From: Daniel Poon <17039704+danielpoonwj@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:40:19 +0800 Subject: [PATCH 3/3] fix(snowflake): Connection issue (#11478) --- .../pieces/community/snowflake/package.json | 2 +- .../snowflake/src/lib/actions/run-query.ts | 33 +++++-------------- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/packages/pieces/community/snowflake/package.json b/packages/pieces/community/snowflake/package.json index addcf24dac1..896faadb811 100644 --- a/packages/pieces/community/snowflake/package.json +++ b/packages/pieces/community/snowflake/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-snowflake", - "version": "0.1.3", + "version": "0.1.4", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/snowflake/src/lib/actions/run-query.ts b/packages/pieces/community/snowflake/src/lib/actions/run-query.ts index 12b64f37dad..e46d219e0fa 100644 --- a/packages/pieces/community/snowflake/src/lib/actions/run-query.ts +++ b/packages/pieces/community/snowflake/src/lib/actions/run-query.ts @@ -1,7 +1,7 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import snowflake from 'snowflake-sdk'; import { snowflakeAuth } from '../auth'; -import { configureConnection } from '../common'; +import { configureConnection, connect, execute, destroy } from '../common'; const DEFAULT_APPLICATION_NAME = 'ActivePieces'; const DEFAULT_QUERY_TIMEOUT = 30000; @@ -45,30 +45,13 @@ export const runQuery = createAction({ context.propsValue.timeout ); - return new Promise((resolve, reject) => { - connection.connect(function (err, conn) { - if (err) { - reject(err); - } - }); + const { sqlText, binds } = context.propsValue; - const { sqlText, binds } = context.propsValue; - - connection.execute({ - sqlText, - binds: binds as snowflake.Binds, - complete: (err, stmt, rows) => { - if (err) { - reject(err); - } - connection.destroy((err, conn) => { - if (err) { - reject(err); - } - }); - resolve(rows); - }, - }); - }); + await connect(connection); + try { + return await execute(connection, sqlText, binds as snowflake.Binds); + } finally { + await destroy(connection); + } }, });