import type { SubBlockConfig } from '@/blocks/types' import type { TriggerOutput } from '@/triggers/types' /** * Shared trigger dropdown options for all RevenueCat triggers. * All RevenueCat events POST to the same webhook endpoint; the active trigger is * differentiated by `event.type` at match time. */ export const revenueCatTriggerOptions = [ { label: 'Initial Purchase', id: 'revenuecat_initial_purchase' }, { label: 'Renewal', id: 'revenuecat_renewal' }, { label: 'Cancellation', id: 'revenuecat_cancellation' }, { label: 'Expiration', id: 'revenuecat_expiration' }, { label: 'Non-Renewing Purchase', id: 'revenuecat_non_renewing_purchase' }, { label: 'Product Change', id: 'revenuecat_product_change' }, ] /** * Maps each RevenueCat trigger ID to the exact uppercase `event.type` string * RevenueCat sends in the webhook payload. */ export const REVENUECAT_TRIGGER_TO_EVENT_TYPE: Record = { revenuecat_initial_purchase: 'INITIAL_PURCHASE', revenuecat_renewal: 'RENEWAL', revenuecat_cancellation: 'CANCELLATION', revenuecat_expiration: 'EXPIRATION', revenuecat_non_renewing_purchase: 'NON_RENEWING_PURCHASE', revenuecat_product_change: 'PRODUCT_CHANGE', } /** * Maps each RevenueCat trigger ID to the lowercase `WebhookEventType` enum value * the REST API v2 expects when creating a webhook integration. * * @see https://www.revenuecat.com/docs/api-v2 (Integration > Create a webhook integration) */ export const REVENUECAT_TRIGGER_TO_API_EVENT_TYPE: Record = { revenuecat_initial_purchase: 'initial_purchase', revenuecat_renewal: 'renewal', revenuecat_cancellation: 'cancellation', revenuecat_expiration: 'expiration', revenuecat_non_renewing_purchase: 'non_renewing_purchase', revenuecat_product_change: 'product_change', } /** * Generate setup instructions for a specific RevenueCat event type. * * Sim creates and tears down the webhook integration automatically through the * RevenueCat REST API v2, so the user only supplies a Secret API key and the * project ID — no manual dashboard webhook configuration is required. */ export function revenueCatSetupInstructions(eventLabel: string): string { const instructions = [ 'In the RevenueCat dashboard, go to Project settings > API keys and create a v2 Secret API key with the project_configuration:integrations:read_write permission.', 'Paste the Secret API key into the API Key field below.', 'Enter your Project ID (the proj... identifier shown in your project settings or the dashboard URL) in the Project ID field below.', 'Optionally restrict the trigger to a single Environment (Production or Sandbox). Leave it on All to receive both.', `Sim creates the webhook integration in RevenueCat automatically when you deploy and removes it when you undeploy. This trigger fires on ${eventLabel} events.`, 'Sim generates the Authorization header secret for you and verifies every incoming request against it — you do not need to set it manually.', 'Click "Save" above to deploy and activate your trigger.', ] return instructions .map( (instruction, index) => `
${index + 1}. ${instruction}
` ) .join('') } /** * Extra trigger fields shared by all RevenueCat triggers. * * `apiKey` (a RevenueCat v2 Secret API key) and `projectId` let Sim create and * delete the webhook integration through the REST API v2 during deploy/undeploy. * `environment` optionally scopes the integration to production or sandbox. * The Authorization header secret is generated by Sim during registration and * stored in `providerConfig.authHeaderSecret`; the provider handler verifies * every incoming `Authorization` header against it. */ export function buildRevenueCatExtraFields(triggerId: string): SubBlockConfig[] { const condition = { field: 'selectedTriggerId', value: triggerId } return [ { id: 'apiKey', title: 'API Key', type: 'short-input', password: true, paramVisibility: 'user-only', placeholder: 'RevenueCat v2 Secret API key (sk_...)', description: 'Secret API key with the project_configuration:integrations:read_write permission. Sim uses it to create and remove the webhook in RevenueCat.', required: true, mode: 'trigger', condition, }, { id: 'projectId', title: 'Project ID', type: 'short-input', placeholder: 'proj1ab2c3d4', description: 'RevenueCat project identifier the webhook integration is created in.', required: true, mode: 'trigger', condition, }, { id: 'environment', title: 'Environment', type: 'dropdown', options: [ { label: 'All', id: 'all' }, { label: 'Production', id: 'production' }, { label: 'Sandbox', id: 'sandbox' }, ], value: () => 'all', description: 'Restrict events to a single environment, or receive all of them.', mode: 'trigger', condition, }, ] } /** * Build the output schema for RevenueCat events. * * Fields are flattened from the `event` object of the webhook payload * (RevenueCat wraps the event under `event` and the wrapper carries `api_version`). * Event-specific fields (`new_product_id`, `cancel_reason`, `expiration_reason`) are * present for all triggers and are `null` when not applicable to the received event type. * * @see https://www.revenuecat.com/docs/integrations/webhooks/event-types-and-fields */ export function buildRevenueCatOutputs(): Record { return { type: { type: 'string', description: 'Event type (e.g., INITIAL_PURCHASE, RENEWAL)' }, id: { type: 'string', description: 'Unique event identifier' }, app_id: { type: 'string', description: 'RevenueCat app public identifier' }, event_timestamp_ms: { type: 'number', description: 'Timestamp (ms since epoch) when the event was generated', }, app_user_id: { type: 'string', description: 'Current App User ID' }, original_app_user_id: { type: 'string', description: 'First App User ID ever used' }, aliases: { type: 'json', description: 'All App User IDs ever used by the subscriber' }, product_id: { type: 'string', description: 'Product identifier' }, new_product_id: { type: 'string', description: 'Product identifier changed to (PRODUCT_CHANGE events only)', }, period_type: { type: 'string', description: 'Period type (TRIAL, INTRO, NORMAL, PROMOTIONAL, or PREPAID)', }, purchased_at_ms: { type: 'number', description: 'Purchase timestamp (ms since epoch)' }, expiration_at_ms: { type: 'number', description: 'Expiration timestamp (ms since epoch), nullable', }, environment: { type: 'string', description: 'Environment (SANDBOX or PRODUCTION)' }, entitlement_id: { type: 'string', description: 'Deprecated single entitlement identifier' }, entitlement_ids: { type: 'json', description: 'Associated entitlement identifiers' }, presented_offering_id: { type: 'string', description: 'Identifier of the offering presented to the user', }, transaction_id: { type: 'string', description: 'Store transaction ID' }, original_transaction_id: { type: 'string', description: 'Original subscription transaction ID', }, is_family_share: { type: 'boolean', description: 'Whether the purchase was family shared' }, country_code: { type: 'string', description: 'ISO country code of the subscriber' }, currency: { type: 'string', description: 'ISO 4217 currency code' }, price: { type: 'number', description: 'Price in USD' }, price_in_purchased_currency: { type: 'number', description: 'Price in the currency the purchase was made in', }, store: { type: 'string', description: 'Store the purchase was made on (e.g., APP_STORE)' }, takehome_percentage: { type: 'number', description: 'Estimated percentage of the price taken home after store commission', }, tax_percentage: { type: 'number', description: 'Estimated percentage taken as tax' }, commission_percentage: { type: 'number', description: 'Estimated percentage taken by the store as commission', }, offer_code: { type: 'string', description: 'Offer code applied to the purchase, if any' }, subscriber_attributes: { type: 'json', description: 'Subscriber attributes at the time of the event', }, experiments: { type: 'json', description: 'Experiments the subscriber was enrolled in' }, cancel_reason: { type: 'string', description: 'Reason for cancellation (CANCELLATION events only)', }, expiration_reason: { type: 'string', description: 'Reason for expiration (EXPIRATION events only)', }, api_version: { type: 'string', description: 'RevenueCat webhook API version' }, event: { type: 'json', description: 'Full RevenueCat event object' }, } } /** * Check whether an incoming RevenueCat event matches the configured trigger. * Returns true for unknown trigger IDs so unexpected configs fall through rather than drop. */ export function isRevenueCatEventMatch(triggerId: string, body: Record): boolean { const expectedType = REVENUECAT_TRIGGER_TO_EVENT_TYPE[triggerId] if (!expectedType) { return true } const event = body.event as Record | undefined const actualType = event?.type as string | undefined return actualType === expectedType }