Skip to content
Draft
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
36 changes: 36 additions & 0 deletions src/behavioural-events/paywall-event.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { PresentedOfferingContext } from "../entities/offerings";
import { generateUUID } from "../helpers/uuid-helper";

export type PaywallEventType =
Expand All @@ -12,11 +13,18 @@ export interface PaywallEventData {
offeringId: string;
paywallRevision: number;
paywallRcPublicId: string | null;
presentedOfferingContext?: PresentedOfferingContext;
displayMode?: string;
darkMode?: boolean;
locale?: string;
}

type PresentedOfferingContextPayload = {
placement_identifier?: string;
targeting_revision?: number;
targeting_rule_id?: string;
};

type PaywallEventPayload = {
type: string;
version: number;
Expand All @@ -27,11 +35,32 @@ type PaywallEventPayload = {
paywall_revision: number;
timestamp: number;
paywall_rc_public_id: string | null;
presented_offering_context?: PresentedOfferingContextPayload;
display_mode?: string;
dark_mode?: boolean;
locale?: string;
};

function toPresentedOfferingContextPayload(
context: PresentedOfferingContext | undefined,
): PresentedOfferingContextPayload | undefined {
if (!context) return undefined;
if (!context.placementIdentifier && !context.targetingContext) {
return undefined;
}
return {
...(context.placementIdentifier
? { placement_identifier: context.placementIdentifier }
: {}),
...(context.targetingContext
? {
targeting_revision: context.targetingContext.revision,
targeting_rule_id: context.targetingContext.ruleId,
}
: {}),
};
}

export class PaywallEvent {
public readonly id: string;
public readonly timestamp: number;
Expand All @@ -56,6 +85,13 @@ export class PaywallEvent {
paywall_rc_public_id: this.data.paywallRcPublicId,
};

const contextPayload = toPresentedOfferingContextPayload(
this.data.presentedOfferingContext,
);
if (contextPayload) {
payload.presented_offering_context = contextPayload;
}

if (this.data.displayMode !== undefined) {
payload.display_mode = this.data.displayMode;
payload.dark_mode = this.data.darkMode ?? false;
Expand Down
5 changes: 5 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,10 @@ export class Purchases {

const paywallSessionId = generateUUID();

const presentedOfferingContext =
offering.availablePackages[0]?.webBillingProduct
?.presentedOfferingContext;

const trackPaywallEvent = (type: PaywallEventType) => {
this.eventsTracker.trackPaywallEvent({
type,
Expand All @@ -593,6 +597,7 @@ export class Purchases {
offeringId: offering.identifier,
paywallRevision: 0,
paywallRcPublicId: offering.paywallComponents?.id ?? null,
presentedOfferingContext,
...(type === "paywall_impression"
? {
displayMode: "full_screen",
Expand Down
148 changes: 147 additions & 1 deletion src/tests/behavioral-events/paywall-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const baseData: PaywallEventData = {
};

describe("PaywallEvent", () => {
it("serializes impression event with visual fields", () => {
it("serializes impression event with visual fields and no context", () => {
const event = new PaywallEvent({
...baseData,
displayMode: "full_screen",
Expand All @@ -42,6 +42,43 @@ describe("PaywallEvent", () => {
dark_mode: true,
locale: "es_ES",
});
expect(json.presented_offering_context).toBeUndefined();
});

it("serializes full impression event with context and visual fields", () => {
const event = new PaywallEvent({
...baseData,
displayMode: "full_screen",
darkMode: true,
locale: "es_ES",
presentedOfferingContext: {
offeringIdentifier: "offering-789",
placementIdentifier: "home_banner",
targetingContext: { revision: 3, ruleId: "rule_abc123" },
},
});

const json = event.toJSON();

expect(json).toEqual({
type: "paywall_impression",
version: 1,
id: "test-uuid-1234",
app_user_id: "user-123",
session_id: "session-456",
offering_id: "offering-789",
paywall_revision: 0,
timestamp: expect.any(Number),
paywall_rc_public_id: "pw-public-id",
presented_offering_context: {
placement_identifier: "home_banner",
targeting_revision: 3,
targeting_rule_id: "rule_abc123",
},
display_mode: "full_screen",
dark_mode: true,
locale: "es_ES",
});
});

it("serializes close event without visual fields", () => {
Expand Down Expand Up @@ -91,4 +128,113 @@ describe("PaywallEvent", () => {

expect(event.toJSON().paywall_rc_public_id).toBeNull();
});

it("includes presented_offering_context with placement and targeting", () => {
const event = new PaywallEvent({
...baseData,
presentedOfferingContext: {
offeringIdentifier: "offering-789",
placementIdentifier: "home_banner",
targetingContext: { revision: 3, ruleId: "rule_abc123" },
},
});

expect(event.toJSON().presented_offering_context).toEqual({
placement_identifier: "home_banner",
targeting_revision: 3,
targeting_rule_id: "rule_abc123",
});
});

it("includes presented_offering_context with placement only", () => {
const event = new PaywallEvent({
...baseData,
presentedOfferingContext: {
offeringIdentifier: "offering-789",
placementIdentifier: "home_banner",
targetingContext: null,
},
});

expect(event.toJSON().presented_offering_context).toEqual({
placement_identifier: "home_banner",
});
});

it("includes presented_offering_context with targeting only", () => {
const event = new PaywallEvent({
...baseData,
presentedOfferingContext: {
offeringIdentifier: "offering-789",
placementIdentifier: null,
targetingContext: { revision: 7, ruleId: "rule_xyz" },
},
});

expect(event.toJSON().presented_offering_context).toEqual({
targeting_revision: 7,
targeting_rule_id: "rule_xyz",
});
});

it("omits presented_offering_context when no placement or targeting", () => {
const event = new PaywallEvent({
...baseData,
presentedOfferingContext: {
offeringIdentifier: "offering-789",
placementIdentifier: null,
targetingContext: null,
},
});

expect(event.toJSON().presented_offering_context).toBeUndefined();
});

it("omits presented_offering_context when placementIdentifier is empty string", () => {
const event = new PaywallEvent({
...baseData,
presentedOfferingContext: {
offeringIdentifier: "offering-789",
placementIdentifier: "",
targetingContext: null,
},
});

expect(event.toJSON().presented_offering_context).toBeUndefined();
});

it("omits presented_offering_context when not provided", () => {
const event = new PaywallEvent(baseData);

expect(event.toJSON().presented_offering_context).toBeUndefined();
});

it("JSON.stringify round-trip preserves presented_offering_context", () => {
const event = new PaywallEvent({
...baseData,
presentedOfferingContext: {
offeringIdentifier: "offering-789",
placementIdentifier: "home_banner",
targetingContext: { revision: 3, ruleId: "rule_abc123" },
},
});

const serialized = JSON.stringify(event.toJSON());
const parsed = JSON.parse(serialized);

expect(parsed.presented_offering_context).toEqual({
placement_identifier: "home_banner",
targeting_revision: 3,
targeting_rule_id: "rule_abc123",
});
});

it("JSON.stringify omits presented_offering_context when absent", () => {
const event = new PaywallEvent(baseData);

const serialized = JSON.stringify(event.toJSON());
const parsed = JSON.parse(serialized);

expect(parsed.presented_offering_context).toBeUndefined();
});
});
23 changes: 23 additions & 0 deletions src/tests/present-paywall.events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,29 @@ describe("Purchases.presentPaywall() paywall events", () => {
).toEqual(["paywall_impression"]);
});

test("passes presentedOfferingContext to paywall events", async () => {
const purchases = configurePurchases();
const offering = createOfferingWithPaywall();
const trackPaywallEventSpy = vi.spyOn(
purchases["eventsTracker"],
"trackPaywallEvent",
);

const paywallPromise = purchases.presentPaywall({ offering });
void paywallPromise.catch(() => undefined);

expect(trackPaywallEventSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: "paywall_impression",
presentedOfferingContext: {
offeringIdentifier: "offering_1",
targetingContext: { ruleId: "test_rule_id", revision: 123 },
placementIdentifier: null,
},
}),
);
});

test("fires paywall_close when external code clears the paywall container", async () => {
const purchases = configurePurchases();
const offering = createOfferingWithPaywall();
Expand Down