Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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