Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api-report/purchases-js.api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what updated this?

"excerptTokens": [
{
"kind": "Content",
Expand Down
1 change: 1 addition & 0 deletions api-report/purchases-js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ export interface PurchaseResult {
readonly operationSessionId: string;
readonly redemptionInfo: RedemptionInfo | null;
readonly storeTransaction: StoreTransaction;
/* Excluded from this release type: attributionMetadata */
}

// @public
Expand Down
6 changes: 6 additions & 0 deletions src/entities/purchase-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ export interface PurchaseResult {
* The store transaction associated with the purchase.
*/
readonly storeTransaction: StoreTransaction;

/**
* Opaque attribution metadata returned by the checkout status response.
* @internal
*/
readonly attributionMetadata?: Record<string, unknown>;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/purchase-operation-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface OperationSessionSuccessfulResult {
storeTransactionIdentifier: string;
productIdentifier: string;
purchaseDate: Date;
attributionMetadata?: Record<string, unknown>;
}

export class PurchaseOperationHelper {
Expand Down Expand Up @@ -333,6 +334,8 @@ export class PurchaseOperationHelper {
storeTransactionIdentifier: storeTransactionIdentifier,
productIdentifier: productIdentifier,
purchaseDate: purchaseDate,
attributionMetadata:
operationResponse.attribution_metadata ?? undefined,
});
return;
case CheckoutSessionStatus.Failed:
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/networking/responses/checkout-status-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export interface CheckoutStatusInnerResponse {

export interface CheckoutStatusResponse {
readonly operation: CheckoutStatusInnerResponse;
readonly attribution_metadata?: Record<string, unknown>;
}
2 changes: 2 additions & 0 deletions src/paddle/paddle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export class PaddleService {
storeTransactionIdentifier: storeTransactionIdentifier ?? "",
productIdentifier: productIdentifier,
purchaseDate: purchaseDate ?? new Date(),
attributionMetadata:
operationResponse.attribution_metadata ?? undefined,
});
return;
case CheckoutSessionStatus.Failed:
Expand Down
16 changes: 16 additions & 0 deletions src/tests/helpers/purchase-operation-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand Down
53 changes: 53 additions & 0 deletions src/tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CustomerInfo>;
};
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);
Expand Down