Skip to content

Commit e999b64

Browse files
committed
fix(revenuecat): align tools with v1 docs
- Unwrap {value:{subscriber}} envelope across post-receipts, attributes, entitlements, and Google sub endpoints - Trim entitlement output to documented fields (expires_date, grace_period_expires_date, product_identifier, purchase_date) - Add subscriber output fields: last_seen, original_application_version, other_purchases, subscriber_attributes - create_purchase: productId optional (Google-only required), add introductoryPrice, attributes, updated_at_ms; surface customer + subscriber - update_subscriber_attributes: read response and surface subscriber - defer_google_subscription: enforce XOR(extendByDays, expiryTimeMs) and 1-365 range - get_customer: count active subs by expiry/refund instead of object-key length
1 parent 873f4ca commit e999b64

11 files changed

Lines changed: 384 additions & 223 deletions

apps/docs/content/docs/en/tools/revenuecat.mdx

Lines changed: 91 additions & 63 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/revenuecat.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@ Return ONLY the numeric timestamp, no text.`,
163163
required: {
164164
field: 'operation',
165165
value: [
166-
'create_purchase',
167166
'defer_google_subscription',
168167
'refund_google_subscription',
169168
'revoke_google_subscription',
@@ -218,6 +217,35 @@ Return ONLY the numeric timestamp, no text.`,
218217
},
219218
mode: 'advanced',
220219
},
220+
{
221+
id: 'introductoryPrice',
222+
title: 'Introductory Price',
223+
type: 'short-input',
224+
placeholder: 'e.g., 0.99',
225+
condition: {
226+
field: 'operation',
227+
value: 'create_purchase',
228+
},
229+
mode: 'advanced',
230+
},
231+
{
232+
id: 'updatedAtMs',
233+
title: 'Updated At (ms)',
234+
type: 'short-input',
235+
placeholder: 'Unix epoch ms used to resolve attribute conflicts',
236+
condition: {
237+
field: 'operation',
238+
value: 'create_purchase',
239+
},
240+
mode: 'advanced',
241+
wandConfig: {
242+
enabled: true,
243+
prompt: `Generate a Unix epoch timestamp in milliseconds based on the user's description.
244+
Used by RevenueCat to resolve attribute conflicts on a posted purchase.
245+
246+
Return ONLY the numeric timestamp, no text.`,
247+
},
248+
},
221249
{
222250
id: 'isRestore',
223251
title: 'Is Restore',
@@ -263,7 +291,7 @@ Return ONLY the numeric timestamp, no text.`,
263291
placeholder: '{"$email": {"value": "user@example.com"}}',
264292
condition: {
265293
field: 'operation',
266-
value: 'update_subscriber_attributes',
294+
value: ['update_subscriber_attributes', 'create_purchase'],
267295
},
268296
required: {
269297
field: 'operation',
@@ -375,6 +403,12 @@ Return ONLY the numeric timestamp, no text.`,
375403
if (params.expiryTimeMs !== undefined && params.expiryTimeMs !== '') {
376404
next.expiryTimeMs = Number(params.expiryTimeMs)
377405
}
406+
if (params.introductoryPrice !== undefined && params.introductoryPrice !== '') {
407+
next.introductoryPrice = Number(params.introductoryPrice)
408+
}
409+
if (params.updatedAtMs !== undefined && params.updatedAtMs !== '') {
410+
next.updatedAtMs = Number(params.updatedAtMs)
411+
}
378412
return next
379413
},
380414
},
@@ -402,7 +436,16 @@ Return ONLY the numeric timestamp, no text.`,
402436
type: 'string',
403437
description: 'Payment mode (pay_as_you_go, pay_up_front, free_trial)',
404438
},
405-
attributes: { type: 'string', description: 'JSON object of subscriber attributes' },
439+
attributes: {
440+
type: 'string',
441+
description:
442+
'JSON object of subscriber attributes (used by update_subscriber_attributes and create_purchase)',
443+
},
444+
introductoryPrice: { type: 'number', description: 'Introductory price for the purchase' },
445+
updatedAtMs: {
446+
type: 'number',
447+
description: 'Unix epoch ms used by RevenueCat to resolve attribute conflicts',
448+
},
406449
extendByDays: { type: 'number', description: 'Number of days to extend (1-365)' },
407450
expiryTimeMs: { type: 'number', description: 'Absolute new expiry time in ms since epoch' },
408451
endTimeMs: {
@@ -430,5 +473,9 @@ Return ONLY the numeric timestamp, no text.`,
430473
deleted: { type: 'boolean', description: 'Whether the subscriber was deleted' },
431474
app_user_id: { type: 'string', description: 'The app user ID' },
432475
updated: { type: 'boolean', description: 'Whether the attributes were updated' },
476+
customer: {
477+
type: 'json',
478+
description: 'Customer object returned by create_purchase (when present in the response)',
479+
},
433480
},
434481
}

apps/sim/tools/revenuecat/create_purchase.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import type { CreatePurchaseParams, CreatePurchaseResponse } from '@/tools/revenuecat/types'
2-
import { SUBSCRIBER_OUTPUT, throwIfRevenueCatError } from '@/tools/revenuecat/types'
2+
import {
3+
extractCustomer,
4+
extractSubscriber,
5+
SUBSCRIBER_OUTPUT,
6+
shapeSubscriber,
7+
throwIfRevenueCatError,
8+
} from '@/tools/revenuecat/types'
39
import type { ToolConfig } from '@/tools/types'
410

511
export const revenuecatCreatePurchaseTool: ToolConfig<
@@ -29,45 +35,66 @@ export const revenuecatCreatePurchaseTool: ToolConfig<
2935
required: true,
3036
visibility: 'user-or-llm',
3137
description:
32-
'The receipt token or purchase token from the store (App Store receipt, Google Play purchase token, or Stripe subscription ID)',
38+
'For iOS, the base64-encoded receipt (or JWSTransaction for StoreKit2); for Android the purchase token; for Amazon the receipt; for Stripe the subscription ID or Checkout Session ID; for Roku the transaction ID; for Paddle the subscription ID or transaction ID',
3339
},
3440
productId: {
3541
type: 'string',
36-
required: true,
42+
required: false,
3743
visibility: 'user-or-llm',
38-
description: 'The product identifier for the purchase',
44+
description:
45+
'Apple, Google, Amazon, Roku, or Paddle product identifier or SKU. Required for Google.',
3946
},
4047
price: {
4148
type: 'number',
4249
required: false,
4350
visibility: 'user-or-llm',
44-
description: 'The price of the product in the currency specified',
51+
description: 'Price of the product. Required if you provide a currency.',
4552
},
4653
currency: {
4754
type: 'string',
4855
required: false,
4956
visibility: 'user-or-llm',
50-
description: 'ISO 4217 currency code (e.g., USD, EUR)',
57+
description: 'ISO 4217 currency code (e.g., USD, EUR). Required if you provide a price.',
5158
},
5259
isRestore: {
5360
type: 'boolean',
5461
required: false,
5562
visibility: 'user-or-llm',
56-
description: 'Whether this is a restore of a previous purchase (deprecated by RevenueCat)',
63+
description: 'Deprecated. Triggers configured restore behavior for shared fetch tokens.',
5764
},
5865
presentedOfferingIdentifier: {
5966
type: 'string',
6067
required: false,
6168
visibility: 'user-or-llm',
6269
description:
63-
'Identifier of the offering that was presented to the user when they made this purchase. Used by RevenueCat for offering-level analytics.',
70+
'Identifier of the offering presented to the customer at the time of purchase. Attached to new transactions in this fetch token and exposed in ETL exports and webhooks.',
6471
},
6572
paymentMode: {
6673
type: 'string',
6774
required: false,
6875
visibility: 'user-or-llm',
6976
description:
70-
'Payment mode for the purchase. One of: pay_as_you_go, pay_up_front, free_trial. Only applies to introductory pricing periods.',
77+
'Payment mode for the introductory period. One of: pay_as_you_go, pay_up_front, free_trial. Defaults to free_trial when an introductory period is detected and no value is provided.',
78+
},
79+
introductoryPrice: {
80+
type: 'number',
81+
required: false,
82+
visibility: 'user-or-llm',
83+
description: 'Introductory price paid (if any).',
84+
},
85+
attributes: {
86+
type: 'json',
87+
required: false,
88+
visibility: 'user-or-llm',
89+
description:
90+
'JSON object of subscriber attributes to set alongside the purchase. Each key maps to {"value": string, "updated_at_ms": number}.',
91+
},
92+
updatedAtMs: {
93+
type: 'number',
94+
required: false,
95+
visibility: 'user-or-llm',
96+
description:
97+
'UNIX epoch in milliseconds used to resolve attribute conflicts at the request level.',
7198
},
7299
platform: {
73100
type: 'string',
@@ -95,39 +122,46 @@ export const revenuecatCreatePurchaseTool: ToolConfig<
95122
const body: Record<string, unknown> = {
96123
app_user_id: params.appUserId,
97124
fetch_token: params.fetchToken,
98-
product_id: params.productId,
99125
}
126+
if (params.productId) body.product_id = params.productId
100127
if (params.price !== undefined) body.price = params.price
101128
if (params.currency) body.currency = params.currency
102129
if (params.isRestore !== undefined) body.is_restore = params.isRestore
103130
if (params.presentedOfferingIdentifier) {
104131
body.presented_offering_identifier = params.presentedOfferingIdentifier
105132
}
106133
if (params.paymentMode) body.payment_mode = params.paymentMode
134+
if (params.introductoryPrice !== undefined) {
135+
body.introductory_price = params.introductoryPrice
136+
}
137+
if (params.attributes !== undefined && params.attributes !== '') {
138+
body.attributes =
139+
typeof params.attributes === 'string' ? JSON.parse(params.attributes) : params.attributes
140+
}
141+
if (params.updatedAtMs !== undefined) body.updated_at_ms = params.updatedAtMs
107142
return body
108143
},
109144
},
110145

111146
transformResponse: async (response) => {
112147
await throwIfRevenueCatError(response)
113148
const data = await response.json()
114-
const subscriber = data.subscriber ?? {}
115-
116149
return {
117150
success: true,
118151
output: {
119-
subscriber: {
120-
first_seen: subscriber.first_seen ?? '',
121-
original_app_user_id: subscriber.original_app_user_id ?? '',
122-
subscriptions: subscriber.subscriptions ?? {},
123-
entitlements: subscriber.entitlements ?? {},
124-
non_subscriptions: subscriber.non_subscriptions ?? {},
125-
},
152+
customer: extractCustomer(data),
153+
subscriber: shapeSubscriber(extractSubscriber(data)),
126154
},
127155
}
128156
},
129157

130158
outputs: {
159+
customer: {
160+
type: 'object',
161+
description:
162+
'Customer object returned at the top level of POST /v1/receipts (first_seen, last_seen, original_app_user_id, original_application_version, original_sdk_version, management_url, entitlements, original_purchase_date, request_date). Null when the response uses the `value`-wrapped envelope.',
163+
optional: true,
164+
},
131165
subscriber: {
132166
...SUBSCRIBER_OUTPUT,
133167
description: 'The updated subscriber object after recording the purchase',

apps/sim/tools/revenuecat/defer_google_subscription.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import type {
22
DeferGoogleSubscriptionParams,
33
DeferGoogleSubscriptionResponse,
44
} from '@/tools/revenuecat/types'
5-
import { SUBSCRIBER_OUTPUT, throwIfRevenueCatError } from '@/tools/revenuecat/types'
5+
import {
6+
extractSubscriber,
7+
SUBSCRIBER_OUTPUT,
8+
shapeSubscriber,
9+
throwIfRevenueCatError,
10+
} from '@/tools/revenuecat/types'
611
import type { ToolConfig } from '@/tools/types'
712

813
export const revenuecatDeferGoogleSubscriptionTool: ToolConfig<
@@ -60,30 +65,36 @@ export const revenuecatDeferGoogleSubscriptionTool: ToolConfig<
6065
'Content-Type': 'application/json',
6166
}),
6267
body: (params) => {
63-
if (params.extendByDays === undefined && params.expiryTimeMs === undefined) {
68+
const hasExtend = params.extendByDays !== undefined
69+
const hasExpiry = params.expiryTimeMs !== undefined
70+
if (!hasExtend && !hasExpiry) {
6471
throw new Error('Provide either extendByDays or expiryTimeMs to defer a subscription')
6572
}
73+
if (hasExtend && hasExpiry) {
74+
throw new Error(
75+
'Provide only one of extendByDays or expiryTimeMs — they cannot be used together'
76+
)
77+
}
6678
const body: Record<string, unknown> = {}
67-
if (params.expiryTimeMs !== undefined) body.expiry_time_ms = params.expiryTimeMs
68-
else if (params.extendByDays !== undefined) body.extend_by_days = params.extendByDays
79+
if (hasExpiry) body.expiry_time_ms = params.expiryTimeMs
80+
else if (hasExtend) {
81+
const days = params.extendByDays as number
82+
if (!Number.isFinite(days) || days < 1 || days > 365) {
83+
throw new Error('extendByDays must be an integer between 1 and 365')
84+
}
85+
body.extend_by_days = days
86+
}
6987
return body
7088
},
7189
},
7290

7391
transformResponse: async (response) => {
7492
await throwIfRevenueCatError(response)
7593
const data = await response.json()
76-
const subscriber = data.subscriber ?? {}
77-
7894
return {
7995
success: true,
8096
output: {
81-
subscriber: {
82-
first_seen: subscriber.first_seen ?? '',
83-
original_app_user_id: subscriber.original_app_user_id ?? '',
84-
subscriptions: subscriber.subscriptions ?? {},
85-
entitlements: subscriber.entitlements ?? {},
86-
},
97+
subscriber: shapeSubscriber(extractSubscriber(data)),
8798
},
8899
}
89100
},

apps/sim/tools/revenuecat/get_customer.ts

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { CustomerResponse, GetCustomerParams } from '@/tools/revenuecat/types'
22
import {
3+
extractSubscriber,
34
METADATA_OUTPUT_PROPERTIES,
45
SUBSCRIBER_OUTPUT,
6+
shapeSubscriber,
57
throwIfRevenueCatError,
68
} from '@/tools/revenuecat/types'
79
import type { ToolConfig } from '@/tools/types'
@@ -40,39 +42,47 @@ export const revenuecatGetCustomerTool: ToolConfig<GetCustomerParams, CustomerRe
4042
transformResponse: async (response) => {
4143
await throwIfRevenueCatError(response)
4244
const data = await response.json()
43-
const subscriber = data.subscriber ?? {}
44-
const entitlements = subscriber.entitlements ?? {}
45-
const subscriptions = subscriber.subscriptions ?? {}
46-
const requestDate: string | undefined = data.request_date
47-
45+
const subscriberRaw = extractSubscriber(data)
46+
const subscriber = shapeSubscriber(subscriberRaw)
47+
const requestDate = (data?.value?.request_date ?? data?.request_date) as string | undefined
4848
const now = requestDate ? new Date(requestDate).getTime() : Date.now()
49-
const activeEntitlements = Object.values(entitlements).filter((e: unknown) => {
50-
const ent = e as Record<string, unknown>
51-
if (typeof ent.is_active === 'boolean') return ent.is_active
52-
const expires = ent.expires_date as string | null | undefined
53-
const grace = ent.grace_period_expires_date as string | null | undefined
49+
50+
const isActiveByDates = (
51+
expires: string | null | undefined,
52+
grace: string | null | undefined,
53+
refundedAt?: string | null | undefined
54+
) => {
55+
if (refundedAt) return false
5456
if (!expires) return true
5557
if (new Date(expires).getTime() > now) return true
5658
if (grace && new Date(grace).getTime() > now) return true
5759
return false
60+
}
61+
62+
const activeEntitlements = Object.values(subscriber.entitlements).filter((e) => {
63+
const ent = e as Record<string, unknown>
64+
return isActiveByDates(
65+
ent.expires_date as string | null | undefined,
66+
ent.grace_period_expires_date as string | null | undefined
67+
)
68+
}).length
69+
70+
const activeSubscriptions = Object.values(subscriber.subscriptions).filter((s) => {
71+
const sub = s as Record<string, unknown>
72+
return isActiveByDates(
73+
sub.expires_date as string | null | undefined,
74+
sub.grace_period_expires_date as string | null | undefined,
75+
sub.refunded_at as string | null | undefined
76+
)
5877
}).length
59-
const activeSubscriptions = Object.keys(subscriptions).length
6078

6179
return {
6280
success: true,
6381
output: {
64-
subscriber: {
65-
first_seen: subscriber.first_seen ?? '',
66-
original_app_user_id: subscriber.original_app_user_id ?? '',
67-
original_purchase_date: subscriber.original_purchase_date ?? null,
68-
management_url: subscriber.management_url ?? null,
69-
subscriptions: subscriptions,
70-
entitlements: entitlements,
71-
non_subscriptions: subscriber.non_subscriptions ?? {},
72-
},
82+
subscriber,
7383
metadata: {
74-
app_user_id: subscriber.original_app_user_id ?? '',
75-
first_seen: subscriber.first_seen ?? '',
84+
app_user_id: subscriber.original_app_user_id,
85+
first_seen: subscriber.first_seen,
7686
active_entitlements: activeEntitlements,
7787
active_subscriptions: activeSubscriptions,
7888
},

0 commit comments

Comments
 (0)