Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,33 @@
{
"extends": [
"../../../../.eslintrc.base.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View File

@@ -0,0 +1,7 @@
# pieces-appfollow
This library was generated with [Nx](https://nx.dev).
## Building
Run `nx build pieces-appfollow` to build the library.

View File

@@ -0,0 +1,10 @@
{
"name": "@activepieces/piece-appfollow",
"version": "0.0.1",
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,65 @@
{
"name": "pieces-appfollow",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/pieces/community/appfollow/src",
"projectType": "library",
"release": {
"version": {
"manifestRootsToUpdate": [
"dist/{projectRoot}"
],
"currentVersionResolver": "git-tag",
"fallbackCurrentVersionResolver": "disk"
}
},
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": [
"{options.outputPath}"
],
"options": {
"outputPath": "dist/packages/pieces/community/appfollow",
"tsConfig": "packages/pieces/community/appfollow/tsconfig.lib.json",
"packageJson": "packages/pieces/community/appfollow/package.json",
"main": "packages/pieces/community/appfollow/src/index.ts",
"assets": [
"packages/pieces/community/appfollow/*.md",
{
"input": "packages/pieces/community/appfollow/src/i18n",
"output": "./src/i18n",
"glob": "**/!(i18n.json)"
}
],
"buildableProjectDepsInPackageJsonType": "dependencies",
"updateBuildableProjectDepsInPackageJson": true
},
"dependsOn": [
"prebuild",
"^build"
]
},
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"prebuild": {
"dependsOn": [
"^build"
],
"executor": "nx:run-commands",
"options": {
"cwd": "packages/pieces/community/appfollow",
"command": "bun install --no-save --silent"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": [
"{options.outputFile}"
]
}
}
}

View File

@@ -0,0 +1,49 @@
{
"Appfollow helps to manage and improve app reviews and ratings.": "Appfollow helps to manage and improve app reviews and ratings.",
"\nTo get your API Key:\n\n1. Go to [Appfollow platform](https://watch.appfollow.io/apps/)\n2. Sign up or log in to your account\n3. Click on the \"Integrations\" from the left sidebar\n4. Navigate to the \"API Dashboard\" section\n5. Create a new API token or the use existing one\n6. Copy and save your API token\n": "\nTo get your API Key:\n\n1. Go to [Appfollow platform](https://watch.appfollow.io/apps/)\n2. Sign up or log in to your account\n3. Click on the \"Integrations\" from the left sidebar\n4. Navigate to the \"API Dashboard\" section\n5. Create a new API token or the use existing one\n6. Copy and save your API token\n",
"Add user": "Add user",
"Reply to Review": "Reply to Review",
"Custom API Call": "Custom API Call",
"Adds a new user to the Appfollow account": "Adds a new user to the Appfollow account",
"Reply to a specific review within a date range for a selected application and collection": "Reply to a specific review within a date range for a selected application and collection",
"Make a custom API call to a specific endpoint": "Make a custom API call to a specific endpoint",
"Name": "Name",
"Email": "Email",
"Role": "Role",
"Collection Name": "Collection Name",
"Application": "Application",
"From Date": "From Date",
"To Date": "To Date",
"Review ID": "Review ID",
"Answer Text": "Answer Text",
"Method": "Method",
"Headers": "Headers",
"Query Parameters": "Query Parameters",
"Body": "Body",
"Response is Binary ?": "Response is Binary ?",
"No Error on Failure": "No Error on Failure",
"Timeout (in seconds)": "Timeout (in seconds)",
"Name of the user to be added": "Name of the user to be added",
"Email of the user to be added": "Email of the user to be added",
"Role of the user to be added": "Role of the user to be added",
"Select the collection name": "Select the collection name",
"Select the application": "Select the application",
"Start date for the reviews to reply to (eg. YYYY-MM-DD)": "Start date for the reviews to reply to (eg. YYYY-MM-DD)",
"End date for the reviews to reply to (eg. YYYY-MM-DD)": "End date for the reviews to reply to (eg. YYYY-MM-DD)",
"Select the Review ID": "Select the Review ID",
"Text of the reply to the review": "Text of the reply to the review",
"Authorization headers are injected automatically from your connection.": "Authorization headers are injected automatically from your connection.",
"Enable for files like PDFs, images, etc..": "Enable for files like PDFs, images, etc..",
"GET": "GET",
"POST": "POST",
"PATCH": "PATCH",
"PUT": "PUT",
"DELETE": "DELETE",
"HEAD": "HEAD",
"New Review": "New Review",
"New Tag": "New Tag",
"Triggered when a new review is added": "Triggered when a new review is added",
"Triggered when a new tag is added": "Triggered when a new tag is added",
"Collection ID": "Collection ID",
"ID of the collection": "ID of the collection"
}

View File

@@ -0,0 +1,33 @@
import { createPiece } from '@activepieces/pieces-framework';
import { appfollowAuth } from './lib/common/auth';
import { PieceCategory } from '@activepieces/shared';
import { newReview } from './lib/triggers/new-review';
import { newTag } from './lib/triggers/new-tag';
import { createCustomApiCallAction } from '@activepieces/pieces-common';
import { BASE_URL } from './lib/common/client';
import { replyToReview } from './lib/actions/reply-to-review';
import { addUser } from './lib/actions/add-user';
export const appfollow = createPiece({
displayName: 'AppFollow',
auth: appfollowAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: 'https://cdn.activepieces.com/pieces/appfollow.png',
description: 'Appfollow helps to manage and improve app reviews and ratings.',
categories: [PieceCategory.BUSINESS_INTELLIGENCE],
authors: ['sanket-a11y'],
actions: [
addUser,
replyToReview,
createCustomApiCallAction({
auth: appfollowAuth,
baseUrl: () => BASE_URL,
authMapping: async (auth) => ({
'X-AppFollow-API-Token': auth.secret_text,
}),
}),
],
triggers: [newReview, newTag],
});

View File

@@ -0,0 +1,42 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { appfollowAuth } from '../common/auth';
import { makeRequest } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
export const addUser = createAction({
auth: appfollowAuth,
name: 'addUser',
displayName: 'Add user',
description: 'Adds a new user to the Appfollow account',
props: {
name: Property.ShortText({
displayName: 'Name',
description: 'Name of the user to be added',
required: true,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Email of the user to be added',
required: true,
}),
role: Property.ShortText({
displayName: 'Role',
description: 'Role of the user to be added',
required: true,
}),
},
async run(context) {
const { name, email, role } = context.propsValue;
const response = await makeRequest(
context.auth.secret_text,
HttpMethod.POST,
`/users/add`,
{
name,
email,
role,
}
);
return response;
},
});

View File

@@ -0,0 +1,53 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { appfollowAuth } from '../common/auth';
import {
application_ext_idDropdown,
collection_idDropdown,
review_ID_Dropdown,
} from '../common/props';
import { makeRequest } from '../common/client';
import { HttpMethod } from '@activepieces/pieces-common';
export const replyToReview = createAction({
auth: appfollowAuth,
name: 'replyToReview',
displayName: 'Reply to Review',
description:
'Reply to a specific review within a date range for a selected application and collection',
props: {
collection_id: collection_idDropdown,
app_ext_id: application_ext_idDropdown,
fromDate: Property.DateTime({
displayName: 'From Date',
description: 'Start date for the reviews to reply to (eg. YYYY-MM-DD)',
required: true,
}),
toDate: Property.DateTime({
displayName: 'To Date',
description: 'End date for the reviews to reply to (eg. YYYY-MM-DD)',
required: false,
}),
review_id: review_ID_Dropdown,
answer_text: Property.LongText({
displayName: 'Answer Text',
description: 'Text of the reply to the review',
required: true,
}),
},
async run(context) {
const { app_ext_id, review_id, answer_text } = context.propsValue;
const response = await makeRequest(
context.auth.secret_text,
HttpMethod.POST,
`/reviews/reply`,
{
ext_id: app_ext_id,
review_id,
answer_text,
}
);
return response;
},
});

View File

@@ -0,0 +1,37 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { PieceAuth } from '@activepieces/pieces-framework';
import { makeRequest } from './client';
export const appfollowAuth = PieceAuth.SecretText({
displayName: 'Appfollow API Key',
description: `
To get your API Key:
1. Go to [Appfollow platform](https://watch.appfollow.io/apps/)
2. Sign up or log in to your account
3. Click on the "Integrations" from the left sidebar
4. Navigate to the "API Dashboard" section
5. Create a new API token or the use existing one
6. Copy and save your API token
`,
required: true,
validate: async ({ auth }) => {
if (auth) {
try {
await makeRequest(auth, HttpMethod.GET, '/account/users');
return {
valid: true,
};
} catch (error) {
return {
valid: false,
error: 'Invalid Api Key',
};
}
}
return {
valid: false,
error: 'Invalid Api Key',
};
},
});

View File

@@ -0,0 +1,25 @@
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
export const BASE_URL = `https://api.appfollow.io/api/v2`;
export async function makeRequest(
api_key: string,
method: HttpMethod,
path: string,
body?: unknown
) {
try {
const response = await httpClient.sendRequest({
method,
url: `${BASE_URL}${path}`,
headers: {
'X-AppFollow-API-Token': `${api_key}`,
'Content-Type': 'application/json',
},
body,
});
return response.body;
} catch (error: any) {
throw new Error(`Unexpected error: ${error.message || String(error)}`);
}
}

View File

@@ -0,0 +1,146 @@
import { Property } from '@activepieces/pieces-framework';
import { makeRequest } from './client';
import { HttpMethod } from '@activepieces/pieces-common';
import { appfollowAuth } from './auth';
export function formatDate(epochMS: any) {
const d = new Date(epochMS);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
export const collection_idDropdown = Property.Dropdown({
auth: appfollowAuth,
displayName: 'Collection Name',
description: 'Select the collection name',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled:true,
options: [],
placeholder: 'Please configure the auth first',
};
}
try {
const response = await makeRequest(
auth.secret_text,
HttpMethod.GET,
`/account/apps`
);
const apps = response.apps;
return {
disabled:false,
options: apps.map((app: any) => ({
label: app.title,
value: app.id,
})),
};
} catch (error) {
return {
disabled:true,
options: [],
placeholder: 'Error fetching collections',
};
}
},
});
export const application_ext_idDropdown = Property.Dropdown({
auth: appfollowAuth,
displayName: 'Application',
description: 'Select the application',
required: true,
refreshers: ['collection_id'],
options: async ({ auth, collection_id }) => {
if (!auth) {
return {
disabled:true,
options: [],
placeholder: 'Please configure the auth first',
};
}
if (!collection_id) {
return {
disabled:true,
options: [],
placeholder: 'Please select application first',
};
}
try {
const response = await makeRequest(
auth.secret_text,
HttpMethod.GET,
`/account/apps/app?apps_id=${collection_id}`
);
console.debug("dsdsdsdsdsdsdssdss",response);
const apps = response.apps_app;
return {
disabled:false,
options: apps.map((app: any) => ({
label: app.app.title,
value: app.app.ext_id,
})),
};
} catch (error) {
return {
disabled:true,
options: [],
placeholder: 'Error fetching applications',
};
}
},
});
export const review_ID_Dropdown = Property.Dropdown({
auth: appfollowAuth,
displayName: 'Review ID',
description: 'Select the Review ID',
required: true,
refreshers: ['app_ext_id', 'toDate', 'fromDate'],
options: async ({ auth, app_ext_id, toDate, fromDate }) => {
if (!auth) {
return {
disabled:true,
options: [],
placeholder: 'Please configure the auth first',
};
}
if (!app_ext_id || !toDate || !fromDate) {
return {
disabled:true,
options: [],
placeholder: 'Please select application and date range first',
};
}
try {
const response = await makeRequest(
auth.secret_text,
HttpMethod.GET,
`/reviews?ext_id=${app_ext_id}&from=${fromDate}&to=${toDate}`
);
const reviews = response.reviews.list;
return {
disabled:false,
options: reviews.map((review: any) => ({
label: `ID: ${review.review_id} - ${review.content.substring(
0,
30
)}...`,
value: review.review_id,
})),
};
} catch (error) {
return {
disabled:true,
options: [],
placeholder: 'Error fetching Review IDs',
};
}
},
});

View File

@@ -0,0 +1,93 @@
import {
createTrigger,
TriggerStrategy,
PiecePropValueSchema,
StaticPropsValue,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import dayjs from 'dayjs';
import { appfollowAuth } from '../common/auth';
import {
application_ext_idDropdown,
collection_idDropdown,
formatDate,
} from '../common/props';
import { makeRequest } from '../common/client';
const props = {
collection_id: collection_idDropdown,
app_ext_id: application_ext_idDropdown,
};
const polling: Polling<AppConnectionValueForAuthProperty<typeof appfollowAuth>, StaticPropsValue<typeof props>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const { app_ext_id } = propsValue;
const fromDate = formatDate(lastFetchEpochMS);
const toDate = formatDate(Date.now());
const url = `/reviews?ext_id=${app_ext_id}&from=${fromDate}&to=${toDate}`;
const response = await makeRequest(auth.secret_text, HttpMethod.GET, url);
return response
.filter((item: any) => {
const itemEpochMS = dayjs(item.created).valueOf();
return itemEpochMS > lastFetchEpochMS;
})
.map((item: any) => ({
epochMilliSeconds: dayjs(item.created).valueOf(),
data: item,
}));
},
};
export const newReview = createTrigger({
auth: appfollowAuth,
name: 'newReview',
displayName: 'New Review',
description: 'Triggered when a new review is added',
props,
sampleData: {
content: '. :)',
user_id: 18426652,
is_answer: 0,
app_version: '1.0',
created: '2025-11-21 14:25:56',
store: 'as',
id: 400558347,
rating_prev: 0,
locale: 'us',
rating: 5,
app_id: 1881629,
dt: '2025-10-30 01:48:11',
title: 'Great App!',
date: '2025-10-30',
time: '01:48:11',
was_changed: 0,
updated: '2025-11-21 14:25:56',
author: 'JohnD',
order_num: null,
review_id: 13331753657,
ext_id: 6746340356,
},
type: TriggerStrategy.POLLING,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
await pollingHelper.onEnable(polling, context);
},
async onDisable(context) {
await pollingHelper.onDisable(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});

View File

@@ -0,0 +1,68 @@
import {
createTrigger,
TriggerStrategy,
PiecePropValueSchema,
Property,
StaticPropsValue,
AppConnectionValueForAuthProperty,
} from '@activepieces/pieces-framework';
import {
DedupeStrategy,
HttpMethod,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import { appfollowAuth } from '../common/auth';
import { makeRequest } from '../common/client';
const props = {
collection_id: Property.ShortText({
displayName: 'Collection ID',
description: 'ID of the collection',
required: false,
}),
};
const polling: Polling<AppConnectionValueForAuthProperty<typeof appfollowAuth>, StaticPropsValue<typeof props>> = {
strategy: DedupeStrategy.LAST_ITEM,
items: async ({ auth, propsValue, lastItemId }) => {
if (lastItemId === undefined || lastItemId === null) {
lastItemId = '0';
}
const response = await makeRequest(auth.secret_text, HttpMethod.GET, `/account/apps`);
const tags = response.apps.tags;
return tags.reverse().map((item: any) => ({
id: item.id,
data: item,
}));
},
};
export const newTag = createTrigger({
auth: appfollowAuth,
name: 'newTag',
displayName: 'New Tag',
description: 'Triggered when a new tag is added',
props,
sampleData: {
tag: 't0021',
tag_name: 'Satisfied user',
category: 'User Feedback',
id: 735517,
apps_id: 149112,
tag_color: '#98D304',
},
type: TriggerStrategy.POLLING,
async test(context) {
return await pollingHelper.test(polling, context);
},
async onEnable(context) {
await pollingHelper.onEnable(polling, context);
},
async onDisable(context) {
await pollingHelper.onDisable(polling, context);
},
async run(context) {
return await pollingHelper.poll(polling, context);
},
});

View File

@@ -0,0 +1,20 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}