Skip to content
Closed
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
81 changes: 81 additions & 0 deletions src/__tests__/inbox-message.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,85 @@ describe('InboxMessage', () => {
expect(Modules.Shared.clickMessage).toHaveBeenCalledWith('m-5');
});
});

/**
* trackingIds is published at the root of the message by both the GraphQL read and
* the iwpv=v2 socket, and reaches JS through the native toJson(). These pin the
* parse path and the clickTrackingId accessor, which exists for parity with the
* ios / android / flutter SDKs.
*/
describe('trackingIds', () => {
it('parses trackingIds from the native payload', () => {
const msg = InboxMessage.fromJson(
JSON.stringify({
messageId: 'm-6',
title: 'Welcome',
trackingIds: {
archiveTrackingId: 'archive-1',
channelTrackingId: 'channel-1',
clickTrackingId: 'click-1',
deliverTrackingId: 'deliver-1',
openTrackingId: 'open-1',
readTrackingId: 'read-1',
unreadTrackingId: 'unread-1',
},
})
);

expect(msg.trackingIds?.archiveTrackingId).toBe('archive-1');
expect(msg.trackingIds?.channelTrackingId).toBe('channel-1');
expect(msg.trackingIds?.clickTrackingId).toBe('click-1');
expect(msg.trackingIds?.deliverTrackingId).toBe('deliver-1');
expect(msg.trackingIds?.openTrackingId).toBe('open-1');
expect(msg.trackingIds?.readTrackingId).toBe('read-1');
expect(msg.trackingIds?.unreadTrackingId).toBe('unread-1');
});

it('exposes clickTrackingId, the id client.inbox.click needs', () => {
const msg = InboxMessage.fromJson(
JSON.stringify({
messageId: 'm-7',
trackingIds: { clickTrackingId: 'click-1' },
})
);

expect(msg.clickTrackingId).toBe('click-1');
});

it('clickTrackingId is null when the message carries no tracking ids', () => {
const msg = InboxMessage.fromJson(JSON.stringify({ messageId: 'm-8' }));

// fromJson passes undefined into a parameter defaulting to null, so the field
// lands as null rather than staying absent.
expect(msg.trackingIds).toBeNull();
expect(msg.clickTrackingId).toBeNull();
});

// Which ids get minted depends on the send, so a partial object is normal.
it('clickTrackingId is null when present but without a click id', () => {
const msg = InboxMessage.fromJson(
JSON.stringify({
messageId: 'm-9',
trackingIds: { readTrackingId: 'read-1' },
})
);

expect(msg.clickTrackingId).toBeNull();
expect(msg.trackingIds?.readTrackingId).toBe('read-1');
});

// v2 publishes trackingIds at the root only; a nested copy is the v1/legacy shape.
it('does not read a nested data.trackingIds copy', () => {
const msg = InboxMessage.fromJson(
JSON.stringify({
messageId: 'm-10',
data: {
trackingIds: { clickTrackingId: 'nested-should-be-ignored' },
},
})
);

expect(msg.clickTrackingId).toBeNull();
});
});
});
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export { CourierInfoViewStyle } from './models/CourierInfoViewStyle';
export { iOS_CourierCell } from './models/iOS_CourierCell';
export { iOS_CourierSheet } from './models/iOS_CourierSheet';
export { InboxMessage } from './models/InboxMessage';
export type { InboxMessageTrackingIds } from './models/InboxMessage';
export { InboxMessageFeed } from './models/InboxMessageFeed';
export { InboxMessageEvent } from './models/InboxMessageEvent';
export {
Expand Down
35 changes: 33 additions & 2 deletions src/models/InboxMessage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
import Courier from '..';
import { InboxAction } from './InboxAction';

/**
* Tracking ids for an inbox message, as published at the root of the message by both
* the GraphQL read and the `iwpv=v2` socket.
*
* These were previously reachable only as an untyped map, so a caller using the
* lower-level `client.inbox.click({ messageId, trackingId })` had to guess key names.
* The high-level `Courier.shared.clickMessage({ messageId })` resolves the id
* natively and is unaffected.
*/
export interface InboxMessageTrackingIds {
archiveTrackingId?: string | null;
channelTrackingId?: string | null;
clickTrackingId?: string | null;
deliverTrackingId?: string | null;
openTrackingId?: string | null;
readTrackingId?: string | null;
unreadTrackingId?: string | null;
}

export class InboxMessage {
readonly messageId: string;
readonly title?: string | null;
Expand All @@ -14,7 +33,7 @@ export class InboxMessage {
readonly archived?: boolean | null;
readonly subtitle?: string | null;
readonly time?: string;
readonly trackingIds?: { [key: string]: any } | null;
readonly trackingIds?: InboxMessageTrackingIds | null;

constructor(
messageId: string,
Expand All @@ -29,7 +48,7 @@ export class InboxMessage {
archived: boolean | null = null,
subtitle: string | null = null,
time: string = '',
trackingIds: { [key: string]: any } | null = null
trackingIds: InboxMessageTrackingIds | null = null
) {
this.messageId = messageId;
this.title = title;
Expand Down Expand Up @@ -58,6 +77,18 @@ export class InboxMessage {
return this.archived !== null;
}

/**
* Tracking id for a click on this message, for parity with the other SDKs
* (`InboxMessage.clickTrackingId` exists on ios, android and flutter).
*
* Only needed when calling the lower-level `client.inbox.click({ messageId,
* trackingId })`. The high-level {@link markAsClicked} resolves the id natively and
* does not need it.
*/
get clickTrackingId(): string | null {
return this.trackingIds?.clickTrackingId ?? null;
}

static fromJson(jsonString: string): InboxMessage {
try {
const parsed = JSON.parse(jsonString);
Expand Down
Loading