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,51 @@
import { createAction, Property } from "@activepieces/pieces-framework";
import { uscreenAuth } from "../common/auth";
import { uscreenProps } from "../common/props";
import { UscreenClient } from "../common/client";
import { HttpMethod } from "@activepieces/pieces-common";
export const assignUserAccess = createAction({
auth: uscreenAuth,
name: 'assign_user_access',
displayName: 'Assign User Access',
description: "Assigns a bundle or subscription to a customer. Creates a new customer if one doesn't exist.",
props: {
customer_id: uscreenProps.customerId(),
productType: uscreenProps.productType(),
product_id: uscreenProps.productId(),
perform_action_at: Property.ShortText({
displayName: 'Schedule Access (Optional)',
description: 'Schedule a time for the action to be performed (ISO 8601 format, e.g., 2024-07-05T13:47:52Z). Leave blank to assign immediately.',
required: false,
}),
with_manual_billing: Property.Checkbox({
displayName: 'Manual Billing (for Offers)',
description: 'Only check this if the Product Type is "Offer" and this is a manual billing scenario.',
required: false,
defaultValue: false,
}),
},
async run(context) {
const { customer_id, product_id, productType, perform_action_at, with_manual_billing } = context.propsValue;
const client = new UscreenClient(context.auth.secret_text);
const body: Record<string, unknown> = {
product_id: product_id,
product_type: productType,
};
if (productType === 'offer' && with_manual_billing) {
body['with_manual_billing'] = true;
} else if (perform_action_at) {
body['perform_action_at'] = perform_action_at;
}
return await client.makeRequest(
HttpMethod.POST,
`/customers/${customer_id}/accesses`,
body
);
},
});

View File

@@ -0,0 +1,101 @@
import { createAction, Property, DynamicPropsValue, PieceAuth } from "@activepieces/pieces-framework";
import { httpClient, HttpMethod } from "@activepieces/pieces-common";
import { uscreenAuth } from "../common/auth";
import { uscreenApiUrl, UscreenClient } from "../common/client";
interface CreateUserProps {
email: string;
first_name: string;
last_name: string;
password?: string;
opted_in_for_news_and_updates?: boolean;
custom_fields?: DynamicPropsValue;
}
export const createUser = createAction({
auth: uscreenAuth,
name: 'create_user',
displayName: 'Create User',
description: "Creates a new user and optionally sends them a welcome email to your storefront.",
props: {
email: Property.ShortText({
displayName: 'Email Address',
description: "The new user's email address.",
required: true,
}),
first_name: Property.ShortText({
displayName: 'First Name',
description: "The new user's first name.",
required: true,
}),
last_name: Property.ShortText({
displayName: 'Last Name',
description: "The new user's last name.",
required: true,
}),
password: Property.ShortText({
displayName: 'Password',
description: "The new user's password. Leave blank if sending an invite email.",
required: false,
}),
opted_in_for_news_and_updates: Property.Checkbox({
displayName: 'Opted in for News and Updates',
description: 'If set to true, the new user will receive news and updates.',
required: false,
defaultValue: true,
}),
custom_fields: Property.DynamicProperties({
auth: uscreenAuth,
displayName: 'Custom Fields',
description: 'Add custom user fields defined in your storefront (e.g., "favorite_genre").',
required: false,
refreshers: [],
props: async () => {
const fields: DynamicPropsValue = {};
fields['add_field_helper'] = Property.ShortText({
displayName: "Custom Field Instructions",
description: "Click 'Add Property' to add a custom field. Use the 'Property Name' as the API key (e.g., 'favorite_genre') and enter the value below.",
required: false,
});
return fields;
}
})
},
async run(context) {
const {
email,
first_name,
last_name,
password,
opted_in_for_news_and_updates,
custom_fields
} = context.propsValue as CreateUserProps;
const client = new UscreenClient(context.auth.secret_text);
const body: Record<string, unknown> = { ...custom_fields };
delete body['add_field_helper'];
body['email'] = email;
body['name'] = first_name + ' ' + last_name;
if (password) {
body['password'] = password;
}
if (opted_in_for_news_and_updates) {
body['opted_in_for_news_and_updates'] = opted_in_for_news_and_updates;
}
const response = await client.makeRequest(
HttpMethod.POST,
`/customers`,
body
);
return response;
},
});

View File

@@ -0,0 +1,35 @@
import { PieceAuth } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { uscreenPublisherApiUrl } from './client';
export const uscreenAuth = PieceAuth.SecretText({
displayName: 'API Key',
description: `
To get your API key:
1. Log in to your Uscreen account
2. Contact your Customer Success Manager to have an API key (X-Store-Token) issued.
`,
required: true,
validate: async ({ auth }) => {
try {
await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${uscreenPublisherApiUrl}/offers`,
headers: {
'X-Store-Token': auth,
'Accept': 'application/json'
}
});
return {
valid: true,
};
} catch (e) {
return {
valid: false,
error: 'Invalid API key or insufficient permissions.',
};
}
},
});

View File

@@ -0,0 +1,125 @@
import { httpClient, HttpMethod, HttpRequest, QueryParams } from "@activepieces/pieces-common";
export const uscreenApiUrl = 'https://api.uscreen.io/v1';
export const uscreenPublisherApiUrl = 'https://uscreen.io/publisher_api/v1';
export interface UscreenProduct {
id: number;
name: string;
}
export interface UscreenInvoice {
id: string;
title: string;
total: string;
amount: string;
discount: string;
offer_id: string;
customer_name: string;
customer_email: string;
status: string;
paid_at: string;
created_at: string;
[key: string]: unknown;
}
export class UscreenClient {
constructor(private apiKey: string) {}
/**
* Makes an authenticated request to the Uscreen PUBLISHER API.
*/
async makeRequest<T>(method: HttpMethod, url: string, body?: object, query?: QueryParams): Promise<T> {
const request: HttpRequest<object> = {
method,
url: `${uscreenPublisherApiUrl}${url}`,
body: body,
queryParams: query,
headers: {
"X-Store-Token": this.apiKey,
"Accept": "application/json",
"Content-Type": "application/json",
},
};
const { body: responseBody } = await httpClient.sendRequest<T>(request);
return responseBody;
}
/**
* Fetches a paginated list of invoices, sorted by payment date.
*/
async getInvoices(params: {
sort_by: string,
status: string,
per_page?: number
}): Promise<{ items: UscreenInvoice[] }> {
const query: QueryParams = {
sort_by: params.sort_by,
status: params.status,
};
if (params.per_page !== undefined) {
query['per_page'] = params.per_page.toString();
}
try {
return await this.makeRequest<{ items: UscreenInvoice[] }>(
HttpMethod.GET,
'/invoices',
undefined,
query
);
} catch (e) {
console.error("Failed to fetch Uscreen invoices", e);
return { items: [] };
}
}
/**
* Fetches a list of offers (subscriptions).
*/
async getOffers(): Promise<UscreenProduct[]> {
try {
return await this.makeRequest<UscreenProduct[]>(
HttpMethod.GET,
'/offers'
);
} catch (e) {
console.error("Failed to fetch Uscreen offers", e);
return [];
}
}
/**
* Fetches a list of programs (bundles).
*/
async getPrograms(): Promise<UscreenProduct[]> {
try {
return await this.makeRequest<UscreenProduct[]>(
HttpMethod.GET,
'/programs'
);
} catch (e) {
console.error("Failed to fetch Uscreen programs", e);
return [];
}
}
async getCustomers() {
try {
return await this.makeRequest<{ id: number; email: string }[]>(
HttpMethod.GET,
'/customers'
);
} catch (e) {
console.error("Failed to fetch Uscreen customers", e);
return [];
}
}
}

View File

@@ -0,0 +1,116 @@
import { Property } from '@activepieces/pieces-framework';
import { UscreenClient, UscreenProduct } from './client';
import { uscreenAuth } from './auth';
export const uscreenProps = {
customerId: (required = true) =>
Property.Dropdown({
auth: uscreenAuth,
displayName: 'Customer ID or Email',
description: 'The unique ID or email address of the customer.',
required: required,
refreshers: ['productType'],
options: async (context) => {
const auth = context['auth'];
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
const client = new UscreenClient(auth.secret_text);
const customers = await client.getCustomers();
return {
disabled: false,
options: customers.map((customer) => ({
label: customer.email,
value: customer.id,
})),
};
},
}),
productType: (required = true) =>
Property.StaticDropdown({
displayName: 'Product Type',
description: 'The type of product to assign.',
required: required,
options: {
options: [
{ label: 'Program (Bundle)', value: 'program' },
{ label: 'Offer (Subscription)', value: 'offer' },
],
},
}),
productId: (required = true) =>
Property.Dropdown({
auth: uscreenAuth,
displayName: 'Product',
description: 'The bundle (program) or subscription (offer) to assign.',
required: required,
refreshers: ['productType'],
options: async (context) => {
const auth = context['auth'];
const propsValue = context['propsValue'] as Record<string, unknown>;
const productType = propsValue['productType'] as
| 'program'
| 'offer'
| undefined;
if (!auth) {
return {
disabled: true,
placeholder: 'Connect your account first',
options: [],
};
}
if (!productType) {
return {
disabled: true,
placeholder: 'Select a Product Type first',
options: [],
};
}
const client = new UscreenClient(auth.secret_text);
let products: UscreenProduct[] = [];
if (productType === 'program') {
products = await client.getPrograms();
} else if (productType === 'offer') {
products = await client.getOffers();
}
return {
disabled: false,
options: products.map((product) => ({
label: product.name,
value: product.id,
})),
};
},
}),
webhookInstructions: (required = true) =>
Property.MarkDown({
value: `## Setup Instructions
### 1. Access Uscreen Webhook Settings
- Log into your **Uscreen Admin Panel**
- Navigate to **Settings** > **Webhooks**
### 2. Create New Webhook
- Click **"New Webhook"**
- **Callback URL**:
\`\`\`text
{{webhookUrl}}
\`\`\`
- Select the appropriate event
- Click **Save**`,
}),
};

View File

@@ -0,0 +1,50 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { uscreenAuth } from '../common/auth';
import { uscreenProps } from '../common/props';
const sampleData = {
event: 'video_play',
title: 'How to Build an API',
id: 987654,
chapter_id: 123456,
name: 'John Doe',
email: 'john.doe@example.com',
};
export const beganToPlayVideo = createTrigger({
auth: uscreenAuth,
name: 'began_to_play_video',
displayName: 'Began to Play Video',
description: 'Triggers when a user plays a video for the first time.',
props: {
setupInstructions: uscreenProps.webhookInstructions(),
},
sampleData: sampleData,
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
//Empty
},
async onDisable(context) {
//Empty
},
async run(context) {
const payload = context.payload as unknown as typeof sampleData;
if (payload.event !== 'video_play') {
return [];
}
return [payload];
},
async test(context) {
return [sampleData];
},
});

View File

@@ -0,0 +1,49 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { uscreenAuth } from '../common/auth';
import { uscreenProps } from '../common/props';
const sampleData = {
event: 'subscription_canceled',
user_id: 123456,
user_email: 'user@example.com',
subscription_id: 'sub_789xyz',
subscription_title: 'Premium Plan',
canceled_at: '2025-10-27T13:32:10Z',
custom_fields: {
favorite_genre: 'Comedy',
referral_source: 'Google Ads',
},
};
export const canceledSubscription = createTrigger({
auth: uscreenAuth,
name: 'canceled_subscription',
displayName: 'Canceled Subscription',
description:
'Triggers when a subscription is canceled for a user.',
props: { setupInstructions: uscreenProps.webhookInstructions() },
sampleData: sampleData,
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
//Empty
},
async onDisable(context) {
//Empty
},
async run(context) {
const payload = context.payload as unknown as typeof sampleData;
if (payload.event !== 'subscription_canceled') {
return [];
}
return [payload];
},
async test(context) {
return [sampleData];
},
});

View File

@@ -0,0 +1,51 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { uscreenAuth } from '../common/auth';
import { uscreenProps } from '../common/props';
const sampleData = {
event: 'user.created',
data: {
id: 123456,
email: 'newuser@example.com',
first_name: 'John',
last_name: 'Doe',
created_at: '2025-10-27T13:00:00Z',
custom_fields: {
favorite_genre: 'Documentary',
how_heard_about_us: 'Referral',
},
},
};
export const newUser = createTrigger({
auth: uscreenAuth,
name: 'new_user',
displayName: 'New User',
description:
'Triggers when a new user is added to your storefront.',
props: { setupInstructions: uscreenProps.webhookInstructions() },
sampleData: sampleData.data,
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
//Empty
},
async onDisable(context) {
//Empty
},
async run(context) {
const payload = context.payload as unknown as typeof sampleData;
if (payload.event !== 'user_created') {
return [];
}
return [payload.data];
},
async test(context) {
return [sampleData.data];
},
});

View File

@@ -0,0 +1,57 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import {
DedupeStrategy,
Polling,
pollingHelper,
} from '@activepieces/pieces-common';
import { uscreenAuth } from '../common/auth';
import { UscreenClient, UscreenInvoice } from '../common/client';
const sampleData = {
id: '789012',
title: 'Masterclass Bundle',
total: '49.99 USD',
amount: '39.99 USD',
discount: '10.00 USD',
offer_id: '98765',
customer_name: 'Jane Smith',
customer_email: 'customer@example.com',
country_code: 'US',
transaction_id: 'tr_1001abcd',
ip_address: '192.168.1.101',
origin: 'Stripe',
coupon: 'WELCOME10',
event: 'order_paid',
};
export const paidOrder = createTrigger({
auth: uscreenAuth,
name: 'paid_order',
displayName: 'Paid Order',
description:
'Triggers when a payment is processed for subscriptions, bundles, or content.',
props: {},
sampleData: sampleData,
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
//Empty
},
async onDisable(context) {
//Empty
},
async run(context) {
const payload = context.payload as unknown as typeof sampleData;
if (payload.event !== 'order_paid') {
return [];
}
return [payload];
},
async test(context) {
return [sampleData];
},
});

View File

@@ -0,0 +1,63 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { uscreenAuth } from '../common/auth';
import { uscreenProps } from '../common/props';
const sampleData = {
id: ' 123456',
email: 'john.doe@example.com',
changes: {
name: 'John Doe',
email: 'john.doe@example.com',
subscription_status: 'active',
lifetime_spent: '100',
bounced_email: false,
status: 'active',
field_1: 'value_1',
field_2: 'value_2',
field_3: 'value_3',
},
attributes: {
name: 'John Doe',
email: 'john.doe@example.com',
subscription_status: 'active',
lifetime_spent: '100',
bounced_email: false,
segments: ['segment_1', 'segment_2'],
status: 'active',
origin: 'user.creation_source',
},
event: 'user_updated',
};
export const userUpdated = createTrigger({
auth: uscreenAuth,
name: 'user_updated',
displayName: 'User Updated',
description:
'Triggers when a users profile or information is updated.',
props: { setupInstructions: uscreenProps.webhookInstructions() },
sampleData: sampleData,
type: TriggerStrategy.WEBHOOK,
async onEnable(context) {
//Empty
},
async onDisable(context) {
//Empty
},
async run(context) {
const payload = context.payload as unknown as typeof sampleData;
if (payload.event !== 'user_updated') {
return [];
}
return [payload];
},
async test(context) {
return [sampleData];
},
});