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,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);
},
});