Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

222 lines
9.5 KiB
TypeScript

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<string, string> = {
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<string, string> = {
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 <strong>Project settings > API keys</strong> and create a <strong>v2 Secret API key</strong> with the <code>project_configuration:integrations:read_write</code> permission.',
'Paste the Secret API key into the <strong>API Key</strong> field below.',
'Enter your <strong>Project ID</strong> (the <code>proj...</code> identifier shown in your project settings or the dashboard URL) in the <strong>Project ID</strong> field below.',
'Optionally restrict the trigger to a single <strong>Environment</strong> (Production or Sandbox). Leave it on <strong>All</strong> to receive both.',
`Sim creates the webhook integration in RevenueCat automatically when you deploy and removes it when you undeploy. This trigger fires on <strong>${eventLabel}</strong> 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) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.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<string, TriggerOutput> {
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<string, unknown>): boolean {
const expectedType = REVENUECAT_TRIGGER_TO_EVENT_TYPE[triggerId]
if (!expectedType) {
return true
}
const event = body.event as Record<string, unknown> | undefined
const actualType = event?.type as string | undefined
return actualType === expectedType
}