From 2f15b26761fafd2df1e47216252a20391c075402 Mon Sep 17 00:00:00 2001 From: Jamie Holwill Date: Thu, 2 Apr 2026 16:23:43 +0100 Subject: [PATCH 1/3] Pass attribution metadata through purchase results --- api-report/purchases-js.api.json | 34 +++++++++++- api-report/purchases-js.api.md | 1 + src/entities/purchase-result.ts | 5 ++ src/helpers/purchase-operation-helper.ts | 3 ++ src/main.ts | 2 + .../responses/checkout-status-response.ts | 1 + src/paddle/paddle-service.ts | 2 + .../helpers/purchase-operation-helper.test.ts | 16 ++++++ src/tests/main.test.ts | 53 +++++++++++++++++++ 9 files changed, 116 insertions(+), 1 deletion(-) diff --git a/api-report/purchases-js.api.json b/api-report/purchases-js.api.json index 2d089710c..ffc5190fe 100644 --- a/api-report/purchases-js.api.json +++ b/api-report/purchases-js.api.json @@ -4007,7 +4007,7 @@ { "kind": "PropertySignature", "canonicalReference": "@revenuecat/purchases-js!PresentPaywallParams#customVariables:member", - "docComment": "/**\n * Custom variables to pass to the paywall at runtime, overriding defaults set in the RevenueCat dashboard.\n *\n * Variables must be defined in the dashboard first. Reference them in paywall text using the `custom.` prefix (e.g. `{{ custom.player_name }}`).\n *\n * @example\n * ```ts\n * presentPaywall({\n * customVariables: {\n * player_name: CustomVariableValue.string('Ada'),\n * level: CustomVariableValue.string('42'),\n * },\n * });\n * ```\n *\n */\n", + "docComment": "/**\n * Custom variables to pass to the paywall at runtime, overriding defaults set in the RevenueCat dashboard.\n *\n * Variables must be defined in the dashboard first. Reference them in paywall text using the `custom.` prefix (e.g. `{{ custom.player_name }}`).\n *\n * @example\n * ```ts\n * presentPaywall({\n * customVariables: {\n * player_name: CustomVariableValue.string('Ada'),\n * level: CustomVariableValue.number(42),\n * is_premium: CustomVariableValue.boolean(true),\n * },\n * });\n * ```\n *\n */\n", "excerptTokens": [ { "kind": "Content", @@ -5604,6 +5604,38 @@ "name": "PurchaseResult", "preserveMemberOrder": false, "members": [ + { + "kind": "PropertySignature", + "canonicalReference": "@revenuecat/purchases-js!PurchaseResult#attributionMetadata:member", + "docComment": "/**\n * Opaque attribution metadata returned by the checkout status response.\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "readonly attributionMetadata?: " + }, + { + "kind": "Reference", + "text": "Record", + "canonicalReference": "!Record:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isReadonly": true, + "isOptional": true, + "releaseTag": "Public", + "name": "attributionMetadata", + "propertyTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + }, { "kind": "PropertySignature", "canonicalReference": "@revenuecat/purchases-js!PurchaseResult#customerInfo:member", diff --git a/api-report/purchases-js.api.md b/api-report/purchases-js.api.md index 6b21a99f7..ed80ed63e 100644 --- a/api-report/purchases-js.api.md +++ b/api-report/purchases-js.api.md @@ -393,6 +393,7 @@ export interface PurchaseParams { // @public export interface PurchaseResult { + readonly attributionMetadata?: Record; readonly customerInfo: CustomerInfo; readonly operationSessionId: string; readonly redemptionInfo: RedemptionInfo | null; diff --git a/src/entities/purchase-result.ts b/src/entities/purchase-result.ts index 945752e84..5aae108d3 100644 --- a/src/entities/purchase-result.ts +++ b/src/entities/purchase-result.ts @@ -26,6 +26,11 @@ export interface PurchaseResult { * The store transaction associated with the purchase. */ readonly storeTransaction: StoreTransaction; + + /** + * Opaque attribution metadata returned by the checkout status response. + */ + readonly attributionMetadata?: Record; } /** diff --git a/src/helpers/purchase-operation-helper.ts b/src/helpers/purchase-operation-helper.ts index b9852f0dd..4a5d30f4b 100644 --- a/src/helpers/purchase-operation-helper.ts +++ b/src/helpers/purchase-operation-helper.ts @@ -113,6 +113,7 @@ export interface OperationSessionSuccessfulResult { storeTransactionIdentifier: string; productIdentifier: string; purchaseDate: Date; + attributionMetadata?: Record; } export class PurchaseOperationHelper { @@ -333,6 +334,8 @@ export class PurchaseOperationHelper { storeTransactionIdentifier: storeTransactionIdentifier, productIdentifier: productIdentifier, purchaseDate: purchaseDate, + attributionMetadata: + operationResponse.attribution_metadata ?? undefined, }); return; case CheckoutSessionStatus.Failed: diff --git a/src/main.ts b/src/main.ts index 5ee0832f2..60c7b70c8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -950,6 +950,7 @@ export class Purchases { customerInfo: await this._getCustomerInfoForUserId(appUserId), redemptionInfo: operationResult.redemptionInfo, operationSessionId: operationResult.operationSessionId, + attributionMetadata: operationResult.attributionMetadata, storeTransaction: { storeTransactionId: operationResult.storeTransactionIdentifier, productIdentifier: rcPackage.webBillingProduct.identifier, @@ -1492,6 +1493,7 @@ export class Purchases { customerInfo: await this._getCustomerInfoForUserId(appUserId), redemptionInfo: operationResult.redemptionInfo, operationSessionId: operationResult.operationSessionId, + attributionMetadata: operationResult.attributionMetadata, storeTransaction: { storeTransactionId: operationResult.storeTransactionIdentifier, productIdentifier: rcPackage.webBillingProduct.identifier, diff --git a/src/networking/responses/checkout-status-response.ts b/src/networking/responses/checkout-status-response.ts index 8ac9c596d..88205d3be 100644 --- a/src/networking/responses/checkout-status-response.ts +++ b/src/networking/responses/checkout-status-response.ts @@ -34,4 +34,5 @@ export interface CheckoutStatusInnerResponse { export interface CheckoutStatusResponse { readonly operation: CheckoutStatusInnerResponse; + readonly attribution_metadata?: Record; } diff --git a/src/paddle/paddle-service.ts b/src/paddle/paddle-service.ts index 7805bf4a4..b9cdc05de 100644 --- a/src/paddle/paddle-service.ts +++ b/src/paddle/paddle-service.ts @@ -310,6 +310,8 @@ export class PaddleService { storeTransactionIdentifier: storeTransactionIdentifier ?? "", productIdentifier: productIdentifier, purchaseDate: purchaseDate ?? new Date(), + attributionMetadata: + operationResponse.attribution_metadata ?? undefined, }); return; case CheckoutSessionStatus.Failed: diff --git a/src/tests/helpers/purchase-operation-helper.test.ts b/src/tests/helpers/purchase-operation-helper.test.ts index a77107b08..7a2b13274 100644 --- a/src/tests/helpers/purchase-operation-helper.test.ts +++ b/src/tests/helpers/purchase-operation-helper.test.ts @@ -716,6 +716,14 @@ describe("PurchaseOperationHelper", () => { }), ); const getCheckoutStatusResponse: CheckoutStatusResponse = { + attribution_metadata: { + meta: { + canonical_event_id: "fb-order-id", + canonical_event_name: "Subscribe", + workflow_event_id: "workflow-event-id", + workflow_event_name: "workflows_purchase", + }, + }, operation: { status: CheckoutSessionStatus.Succeeded, is_expired: false, @@ -753,6 +761,14 @@ describe("PurchaseOperationHelper", () => { ); expect(pollResult.productIdentifier).toEqual("test-product_identifier"); expect(pollResult.purchaseDate).toEqual(new Date("2025-07-15T04:21:11Z")); + expect(pollResult.attributionMetadata).toEqual({ + meta: { + canonical_event_id: "fb-order-id", + canonical_event_name: "Subscribe", + workflow_event_id: "workflow-event-id", + workflow_event_name: "workflows_purchase", + }, + }); }); test("pollCurrentPurchaseForCompletion success with missing info in poll returns error", async () => { diff --git a/src/tests/main.test.ts b/src/tests/main.test.ts index c755d21f8..37a18c16f 100644 --- a/src/tests/main.test.ts +++ b/src/tests/main.test.ts @@ -835,6 +835,59 @@ describe("Purchases.purchase()", () => { expect(performStripePurchaseSpy).not.toHaveBeenCalled(); }); + test("passes attributionMetadata through the purchase result", async () => { + const purchases = configurePurchases(); + const customerInfo = { originalAppUserId: "test-user-id" } as CustomerInfo; + type PurchasesWithCustomerInfoGetter = Purchases & { + _getCustomerInfoForUserId: (appUserId: string) => Promise; + }; + const purchasesWithCustomerInfoGetter = + purchases as PurchasesWithCustomerInfoGetter; + vi.spyOn( + purchasesWithCustomerInfoGetter, + "_getCustomerInfoForUserId", + ).mockResolvedValue(customerInfo); + + const resolve = vi.fn(); + const onFinished = purchases["createCheckoutOnFinishedHandler"]( + resolve, + "test-app-user-id", + createMonthlyPackageMock(), + ); + + const attributionMetadata = { + meta: { + canonical_event_id: "fb-order-id", + canonical_event_name: "Subscribe", + workflow_event_id: "workflow-event-id", + workflow_event_name: "workflows_purchase", + }, + }; + + await onFinished({ + redemptionInfo: null, + operationSessionId: "test-operation-session-id", + storeTransactionIdentifier: "test-store-transaction-id", + productIdentifier: "test-product-id", + purchaseDate: new Date("2024-01-01T00:00:00.000Z"), + attributionMetadata, + }); + + expect(resolve).toHaveBeenCalledWith( + expect.objectContaining({ + customerInfo, + redemptionInfo: null, + operationSessionId: "test-operation-session-id", + attributionMetadata, + storeTransaction: { + storeTransactionId: "test-store-transaction-id", + productIdentifier: "monthly", + purchaseDate: new Date("2024-01-01T00:00:00.000Z"), + }, + }), + ); + }); + test("throws error if api key is not provided", () => { // @ts-expect-error - we want to test the error case expect(() => Purchases.configure()).toThrowError(PurchasesError); From e4ac5cae4e499a5cfbe0f69ff3129aba7142aa22 Mon Sep 17 00:00:00 2001 From: Jamie Holwill Date: Thu, 2 Apr 2026 16:56:14 +0100 Subject: [PATCH 2/3] Make new field internal only --- api-report/purchases-js.api.json | 32 -------------------------------- api-report/purchases-js.api.md | 2 +- src/entities/purchase-result.ts | 1 + 3 files changed, 2 insertions(+), 33 deletions(-) diff --git a/api-report/purchases-js.api.json b/api-report/purchases-js.api.json index ffc5190fe..af82c76f9 100644 --- a/api-report/purchases-js.api.json +++ b/api-report/purchases-js.api.json @@ -5604,38 +5604,6 @@ "name": "PurchaseResult", "preserveMemberOrder": false, "members": [ - { - "kind": "PropertySignature", - "canonicalReference": "@revenuecat/purchases-js!PurchaseResult#attributionMetadata:member", - "docComment": "/**\n * Opaque attribution metadata returned by the checkout status response.\n */\n", - "excerptTokens": [ - { - "kind": "Content", - "text": "readonly attributionMetadata?: " - }, - { - "kind": "Reference", - "text": "Record", - "canonicalReference": "!Record:type" - }, - { - "kind": "Content", - "text": "" - }, - { - "kind": "Content", - "text": ";" - } - ], - "isReadonly": true, - "isOptional": true, - "releaseTag": "Public", - "name": "attributionMetadata", - "propertyTypeTokenRange": { - "startIndex": 1, - "endIndex": 3 - } - }, { "kind": "PropertySignature", "canonicalReference": "@revenuecat/purchases-js!PurchaseResult#customerInfo:member", diff --git a/api-report/purchases-js.api.md b/api-report/purchases-js.api.md index ed80ed63e..f77c6cdec 100644 --- a/api-report/purchases-js.api.md +++ b/api-report/purchases-js.api.md @@ -393,11 +393,11 @@ export interface PurchaseParams { // @public export interface PurchaseResult { - readonly attributionMetadata?: Record; readonly customerInfo: CustomerInfo; readonly operationSessionId: string; readonly redemptionInfo: RedemptionInfo | null; readonly storeTransaction: StoreTransaction; + /* Excluded from this release type: attributionMetadata */ } // @public diff --git a/src/entities/purchase-result.ts b/src/entities/purchase-result.ts index 5aae108d3..4cb16a8b8 100644 --- a/src/entities/purchase-result.ts +++ b/src/entities/purchase-result.ts @@ -29,6 +29,7 @@ export interface PurchaseResult { /** * Opaque attribution metadata returned by the checkout status response. + * @internal */ readonly attributionMetadata?: Record; } From c53b1c6cf4fe720fe8d0deaf01cdab8b3538601c Mon Sep 17 00:00:00 2001 From: Jamie Holwill Date: Thu, 2 Apr 2026 21:29:09 +0100 Subject: [PATCH 3/3] Revert unrelated api-report doc comment change The customVariables example update was picked up during rebase but is unrelated to this PR's attribution metadata changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- api-report/purchases-js.api.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-report/purchases-js.api.json b/api-report/purchases-js.api.json index af82c76f9..2d089710c 100644 --- a/api-report/purchases-js.api.json +++ b/api-report/purchases-js.api.json @@ -4007,7 +4007,7 @@ { "kind": "PropertySignature", "canonicalReference": "@revenuecat/purchases-js!PresentPaywallParams#customVariables:member", - "docComment": "/**\n * Custom variables to pass to the paywall at runtime, overriding defaults set in the RevenueCat dashboard.\n *\n * Variables must be defined in the dashboard first. Reference them in paywall text using the `custom.` prefix (e.g. `{{ custom.player_name }}`).\n *\n * @example\n * ```ts\n * presentPaywall({\n * customVariables: {\n * player_name: CustomVariableValue.string('Ada'),\n * level: CustomVariableValue.number(42),\n * is_premium: CustomVariableValue.boolean(true),\n * },\n * });\n * ```\n *\n */\n", + "docComment": "/**\n * Custom variables to pass to the paywall at runtime, overriding defaults set in the RevenueCat dashboard.\n *\n * Variables must be defined in the dashboard first. Reference them in paywall text using the `custom.` prefix (e.g. `{{ custom.player_name }}`).\n *\n * @example\n * ```ts\n * presentPaywall({\n * customVariables: {\n * player_name: CustomVariableValue.string('Ada'),\n * level: CustomVariableValue.string('42'),\n * },\n * });\n * ```\n *\n */\n", "excerptTokens": [ { "kind": "Content",