From 85c1e0a7bf3ec28508f110a589c4fe022d0fe35c Mon Sep 17 00:00:00 2001 From: Sumeru Chatterjee Date: Tue, 7 Apr 2026 16:16:39 +0100 Subject: [PATCH] fix(#581): convert createdAt and expiresAt to Date objects in fromDict The native bridge sends epoch milliseconds (number) for createdAt and expiresAt, but the previous code used setUTCMilliseconds which returns a number instead of a Date object. This caused runtime crashes when consumers tried to use createdAt as a Date. Replace with new Date() constructor to properly convert timestamps. Also removes the @ts-ignore workaround and provides a default for priorityLevel. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/__tests__/IterableInApp.test.ts | 40 +++++++++++++++++++++++ src/inApp/classes/IterableInAppMessage.ts | 22 ++++--------- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/__tests__/IterableInApp.test.ts b/src/__tests__/IterableInApp.test.ts index bddb3f3f9..c1ceb1034 100644 --- a/src/__tests__/IterableInApp.test.ts +++ b/src/__tests__/IterableInApp.test.ts @@ -178,6 +178,46 @@ describe('Iterable In App', () => { ); }); + test('fromDict_withCreatedAtAndExpiresAt_returnsDateObjects', () => { + // GIVEN a message dict with numeric timestamps from the native bridge + const createdAtMs = 1609459200000; // 2021-01-01T00:00:00.000Z + const expiresAtMs = 1609545600000; // 2021-01-02T00:00:00.000Z + const messageDict = { + messageId: 'message1', + campaignId: 1234, + trigger: { type: IterableInAppTriggerType.immediate }, + createdAt: createdAtMs, + expiresAt: expiresAtMs, + priorityLevel: 300.5, + }; + + // WHEN fromDict is called + const message = IterableInAppMessage.fromDict(messageDict); + + // THEN createdAt and expiresAt are Date objects with the correct values + expect(message.createdAt).toBeInstanceOf(Date); + expect(message.expiresAt).toBeInstanceOf(Date); + expect(message.createdAt?.getTime()).toBe(createdAtMs); + expect(message.expiresAt?.getTime()).toBe(expiresAtMs); + }); + + test('fromDict_withoutCreatedAtAndExpiresAt_returnsUndefined', () => { + // GIVEN a message dict without timestamps + const messageDict = { + messageId: 'message1', + campaignId: 1234, + trigger: { type: IterableInAppTriggerType.immediate }, + priorityLevel: 300.5, + }; + + // WHEN fromDict is called + const message = IterableInAppMessage.fromDict(messageDict); + + // THEN createdAt and expiresAt are undefined + expect(message.createdAt).toBeUndefined(); + expect(message.expiresAt).toBeUndefined(); + }); + test('getMessages_noParams_returnsMessages', async () => { // GIVEN a list of in-app messages representing the local queue const messageDicts = [ diff --git a/src/inApp/classes/IterableInAppMessage.ts b/src/inApp/classes/IterableInAppMessage.ts index c77b08e63..d22865d6d 100644 --- a/src/inApp/classes/IterableInAppMessage.ts +++ b/src/inApp/classes/IterableInAppMessage.ts @@ -175,16 +175,12 @@ export class IterableInAppMessage { const campaignId = dict.campaignId; const trigger = IterableInAppTrigger.fromDict(dict.trigger); - let createdAt = dict.createdAt; - if (createdAt) { - const dateObject = new Date(0); - createdAt = dateObject.setUTCMilliseconds(createdAt); - } - let expiresAt = dict.expiresAt; - if (expiresAt) { - const dateObject = new Date(0); - expiresAt = dateObject.setUTCMilliseconds(expiresAt); - } + const createdAt = dict.createdAt + ? new Date(dict.createdAt) + : undefined; + const expiresAt = dict.expiresAt + ? new Date(dict.expiresAt) + : undefined; const saveToInbox = IterableUtil.readBoolean(dict, 'saveToInbox'); const inboxMetadataDict = dict.inboxMetadata; let inboxMetadata: IterableInboxMetadata | undefined; @@ -196,16 +192,12 @@ export class IterableInAppMessage { const customPayload = dict.customPayload; const read = IterableUtil.readBoolean(dict, 'read'); - const priorityLevel = dict.priorityLevel; + const priorityLevel = dict.priorityLevel ?? 0; return new IterableInAppMessage( messageId, campaignId, trigger, - // MOB-10426: Speak to the team about `IterableInAppMessage` requiring a date - // object, but being passed a number in this case - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore createdAt, expiresAt, saveToInbox,