diff --git a/src/main.ts b/src/main.ts index 7f3f16cb..7dd22b75 100644 --- a/src/main.ts +++ b/src/main.ts @@ -408,7 +408,7 @@ export class Purchases { rcSource: this._flags.rcSource ?? null, workflowContext: this._context?.workflowContext, }); - this.backend = new Backend(this._API_KEY, httpConfig); + this.backend = new Backend(this._API_KEY, httpConfig, this._context); this.inMemoryCache = new InMemoryCache(); this.purchaseOperationHelper = new PurchaseOperationHelper( this.backend, diff --git a/src/networking/backend.ts b/src/networking/backend.ts index a536a94e..ab43e4c7 100644 --- a/src/networking/backend.ts +++ b/src/networking/backend.ts @@ -25,6 +25,7 @@ import type { PurchaseMetadata, PurchaseOption, } from "../entities/offerings"; +import type { PurchasesContext } from "../entities/purchases-config"; import type { CheckoutCompleteResponse } from "./responses/checkout-complete-response"; import type { CheckoutCalculateTaxResponse } from "./responses/checkout-calculate-tax-response"; import { SetAttributesEndpoint } from "./endpoints"; @@ -35,11 +36,17 @@ export class Backend { private readonly API_KEY: string; private readonly httpConfig: HttpConfig; private readonly isSandbox: boolean; + private readonly purchasesContext?: PurchasesContext; - constructor(API_KEY: string, httpConfig: HttpConfig = defaultHttpConfig) { + constructor( + API_KEY: string, + httpConfig: HttpConfig = defaultHttpConfig, + purchasesContext?: PurchasesContext, + ) { this.API_KEY = API_KEY; this.httpConfig = httpConfig; this.isSandbox = isWebBillingSandboxApiKey(API_KEY); + this.purchasesContext = purchasesContext; } getIsSandbox(): boolean { @@ -129,6 +136,7 @@ export class Backend { presented_offering_identifier: string; price_id: string; presented_placement_identifier?: string; + presented_workflow_id?: string; offer_id?: string; applied_targeting_rule?: { rule_id: string; @@ -169,6 +177,11 @@ export class Backend { presentedOfferingContext.placementIdentifier; } + if (this.purchasesContext?.workflowContext?.workflowIdentifier) { + requestBody.presented_workflow_id = + this.purchasesContext.workflowContext.workflowIdentifier; + } + return await performRequest< CheckoutStartRequestBody, CheckoutStartResponse @@ -299,6 +312,7 @@ export class Backend { app_user_id: string; presented_offering_identifier: string; presented_placement_identifier: string | null; + presented_workflow_id?: string | null; applied_targeting_rule?: PostReceiptTargetingRule | null; initiation_source: string; }; @@ -320,6 +334,8 @@ export class Backend { presentedOfferingContext.offeringIdentifier, presented_placement_identifier: presentedOfferingContext.placementIdentifier, + presented_workflow_id: + this.purchasesContext?.workflowContext?.workflowIdentifier, applied_targeting_rule: targetingInfo, initiation_source: initiationSource, }; diff --git a/src/tests/networking/backend.test.ts b/src/tests/networking/backend.test.ts index 1eb45c18..c99e8b72 100644 --- a/src/tests/networking/backend.test.ts +++ b/src/tests/networking/backend.test.ts @@ -578,6 +578,56 @@ describe("postCheckoutStart request", () => { expect(result).toEqual(checkoutStartResponse); }); + test("handles workflow identifier correctly", async () => { + const backendWithContext = new Backend("test_api_key", undefined, { + workflowContext: { workflowIdentifier: "workflow_456" }, + }); + + setCheckoutStartResponse( + HttpResponse.json(checkoutStartResponse, { status: 200 }), + ); + + await backendWithContext.postCheckoutStart( + "someAppUserId", + "monthly", + { + offeringIdentifier: "offering_1", + targetingContext: null, + placementIdentifier: null, + }, + { id: "base_option", priceId: "test_price_id" }, + "test-trace-id", + ); + + expect(purchaseMethodAPIMock).toHaveBeenCalledTimes(1); + const request = purchaseMethodAPIMock.mock.calls[0][0].request; + const requestBody = await request.json(); + expect(requestBody.presented_workflow_id).toBe("workflow_456"); + }); + + test("omits workflow identifier from request when not present", async () => { + setCheckoutStartResponse( + HttpResponse.json(checkoutStartResponse, { status: 200 }), + ); + + await backend.postCheckoutStart( + "someAppUserId", + "monthly", + { + offeringIdentifier: "offering_1", + targetingContext: null, + placementIdentifier: null, + }, + { id: "base_option", priceId: "test_price_id" }, + "test-trace-id", + ); + + expect(purchaseMethodAPIMock).toHaveBeenCalledTimes(1); + const request = purchaseMethodAPIMock.mock.calls[0][0].request; + const requestBody = await request.json(); + expect(requestBody.presented_workflow_id).toBeUndefined(); + }); + test("throws an error if the backend returns a server error", async () => { setCheckoutStartResponse( HttpResponse.json(null, { status: StatusCodes.INTERNAL_SERVER_ERROR }), @@ -1067,6 +1117,56 @@ describe("postReceipt request", () => { expect(requestBody.presented_placement_identifier).toBe("home_screen"); }); + test("handles workflow identifier correctly", async () => { + const backendWithContext = new Backend("test_api_key", undefined, { + workflowContext: { workflowIdentifier: "workflow_123" }, + }); + + setPostReceiptResponse( + HttpResponse.json(customerInfoResponse, { status: 200 }), + ); + + await backendWithContext.postReceipt( + "someAppUserId", + "monthly", + "EUR", + "test_fetch_token", + { + offeringIdentifier: "offering_1", + targetingContext: null, + placementIdentifier: null, + }, + "purchase", + ); + + const request = postReceiptAPIMock.mock.calls[0][0].request; + const requestBody = await request.json(); + expect(requestBody.presented_workflow_id).toBe("workflow_123"); + }); + + test("omits workflow identifier from postReceipt when not present", async () => { + setPostReceiptResponse( + HttpResponse.json(customerInfoResponse, { status: 200 }), + ); + + await backend.postReceipt( + "someAppUserId", + "monthly", + "EUR", + "test_fetch_token", + { + offeringIdentifier: "offering_1", + targetingContext: null, + placementIdentifier: null, + }, + "purchase", + ); + + const request = postReceiptAPIMock.mock.calls[0][0].request; + const requestBody = await request.json(); + expect(requestBody.presented_workflow_id).toBeUndefined(); + }); + test("throws an error if the backend returns a server error", async () => { setPostReceiptResponse( HttpResponse.json(null, { status: StatusCodes.INTERNAL_SERVER_ERROR }),