From 7b77235f66824b6df93041343c34f3dd2ff3f8c1 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 3 Oct 2025 13:32:46 -0700 Subject: [PATCH 01/82] test: add unit tests for useDeviceOrientation --- src/core/hooks/useDeviceOrientation.test.tsx | 424 +++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 src/core/hooks/useDeviceOrientation.test.tsx diff --git a/src/core/hooks/useDeviceOrientation.test.tsx b/src/core/hooks/useDeviceOrientation.test.tsx new file mode 100644 index 000000000..68bbf7262 --- /dev/null +++ b/src/core/hooks/useDeviceOrientation.test.tsx @@ -0,0 +1,424 @@ +import { renderHook, act } from '@testing-library/react-native'; + +import { useDeviceOrientation, type IterableDeviceOrientation } from './useDeviceOrientation'; + +describe('useDeviceOrientation', () => { + let useWindowDimensionsSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + + // Spy on useWindowDimensions + useWindowDimensionsSpy = jest.spyOn(require('react-native'), 'useWindowDimensions'); + }); + + afterEach(() => { + useWindowDimensionsSpy.mockRestore(); + }); + + describe('initial state', () => { + it('should return portrait orientation for portrait screen dimensions', () => { + // GIVEN screen dimensions in portrait mode + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return portrait orientation + expect(result.current).toEqual({ + height: 800, + width: 400, + isPortrait: true, + }); + }); + + it('should return landscape orientation for landscape screen dimensions', () => { + // GIVEN screen dimensions in landscape mode + useWindowDimensionsSpy.mockReturnValue({ + height: 400, + width: 800, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return landscape orientation + expect(result.current).toEqual({ + height: 400, + width: 800, + isPortrait: false, + }); + }); + + it('should return portrait orientation for square screen dimensions', () => { + // GIVEN square screen dimensions (height >= width should be portrait) + useWindowDimensionsSpy.mockReturnValue({ + height: 500, + width: 500, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return portrait orientation + expect(result.current).toEqual({ + height: 500, + width: 500, + isPortrait: true, + }); + }); + + it('should handle edge case where height is slightly larger than width', () => { + // GIVEN screen dimensions where height is slightly larger + useWindowDimensionsSpy.mockReturnValue({ + height: 401, + width: 400, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return portrait orientation + expect(result.current).toEqual({ + height: 401, + width: 400, + isPortrait: true, + }); + }); + }); + + describe('orientation changes', () => { + it('should update orientation when screen rotates from portrait to landscape', () => { + // GIVEN initial portrait dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + const { result, rerender } = renderHook(() => useDeviceOrientation()); + + // THEN initial state should be portrait + expect(result.current.isPortrait).toBe(true); + + // WHEN screen rotates to landscape + useWindowDimensionsSpy.mockReturnValue({ + height: 400, + width: 800, + scale: 1, + fontScale: 1, + }); + + act(() => { + rerender(() => useDeviceOrientation()); + }); + + // THEN orientation should update to landscape + expect(result.current).toEqual({ + height: 400, + width: 800, + isPortrait: false, + }); + }); + + it('should update orientation when screen rotates from landscape to portrait', () => { + // GIVEN initial landscape dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 400, + width: 800, + scale: 1, + fontScale: 1, + }); + + const { result, rerender } = renderHook(() => useDeviceOrientation()); + + // THEN initial state should be landscape + expect(result.current.isPortrait).toBe(false); + + // WHEN screen rotates to portrait + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + act(() => { + rerender(() => useDeviceOrientation()); + }); + + // THEN orientation should update to portrait + expect(result.current).toEqual({ + height: 800, + width: 400, + isPortrait: true, + }); + }); + + it('should handle multiple orientation changes', () => { + // GIVEN initial portrait dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + const { result, rerender } = renderHook(() => useDeviceOrientation()); + + // THEN initial state should be portrait + expect(result.current.isPortrait).toBe(true); + + // WHEN rotating to landscape + useWindowDimensionsSpy.mockReturnValue({ + height: 400, + width: 800, + scale: 1, + fontScale: 1, + }); + + act(() => { + rerender(() => useDeviceOrientation()); + }); + + expect(result.current.isPortrait).toBe(false); + + // WHEN rotating back to portrait + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + act(() => { + rerender(() => useDeviceOrientation()); + }); + + expect(result.current.isPortrait).toBe(true); + + // WHEN rotating to landscape again + useWindowDimensionsSpy.mockReturnValue({ + height: 400, + width: 800, + scale: 1, + fontScale: 1, + }); + + act(() => { + rerender(() => useDeviceOrientation()); + }); + + expect(result.current.isPortrait).toBe(false); + }); + }); + + describe('edge cases', () => { + it('should handle zero dimensions', () => { + // GIVEN zero dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 0, + width: 0, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return portrait (height >= width) + expect(result.current).toEqual({ + height: 0, + width: 0, + isPortrait: true, + }); + }); + + it('should handle very large dimensions', () => { + // GIVEN very large dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 10000, + width: 5000, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return portrait + expect(result.current).toEqual({ + height: 10000, + width: 5000, + isPortrait: true, + }); + }); + + it('should handle negative dimensions', () => { + // GIVEN negative dimensions (edge case) + useWindowDimensionsSpy.mockReturnValue({ + height: -100, + width: -200, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return landscape (height >= width, -100 >= -200) + expect(result.current).toEqual({ + height: -100, + width: -200, + isPortrait: true, + }); + }); + + it('should handle decimal dimensions', () => { + // GIVEN decimal dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 800.5, + width: 400.3, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should return portrait + expect(result.current).toEqual({ + height: 800.5, + width: 400.3, + isPortrait: true, + }); + }); + }); + + describe('hook behavior', () => { + it('should maintain consistent state across re-renders with same dimensions', () => { + // GIVEN consistent dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + const { result, rerender } = renderHook(() => useDeviceOrientation()); + + const initialResult = result.current; + + // WHEN component re-renders with same dimensions + act(() => { + rerender(() => useDeviceOrientation()); + }); + + // THEN state should remain consistent + expect(result.current).toEqual(initialResult); + }); + + it('should return new object reference when dimensions change', () => { + // GIVEN initial dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + const { result, rerender } = renderHook(() => useDeviceOrientation()); + + const initialResult = result.current; + + // WHEN dimensions change + useWindowDimensionsSpy.mockReturnValue({ + height: 400, + width: 800, + scale: 1, + fontScale: 1, + }); + + act(() => { + rerender(() => useDeviceOrientation()); + }); + + // THEN new object reference should be returned + expect(result.current).not.toBe(initialResult); + expect(result.current).toEqual({ + height: 400, + width: 800, + isPortrait: false, + }); + }); + + it('should handle rapid dimension changes', () => { + // GIVEN initial dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + const { result, rerender } = renderHook(() => useDeviceOrientation()); + + // WHEN rapid dimension changes occur + const dimensions = [ + { height: 400, width: 800 }, // landscape + { height: 800, width: 400 }, // portrait + { height: 600, width: 600 }, // square + { height: 300, width: 900 }, // landscape + ]; + + dimensions.forEach((dim) => { + useWindowDimensionsSpy.mockReturnValue({ + ...dim, + scale: 1, + fontScale: 1, + }); + + act(() => { + rerender(() => useDeviceOrientation()); + }); + + expect(result.current.height).toBe(dim.height); + expect(result.current.width).toBe(dim.width); + expect(result.current.isPortrait).toBe(dim.height >= dim.width); + }); + }); + }); + + describe('type safety', () => { + it('should return correct IterableDeviceOrientation interface', () => { + // GIVEN screen dimensions + useWindowDimensionsSpy.mockReturnValue({ + height: 800, + width: 400, + scale: 1, + fontScale: 1, + }); + + // WHEN the hook is rendered + const { result } = renderHook(() => useDeviceOrientation()); + + // THEN it should match the interface + const orientation: IterableDeviceOrientation = result.current; + expect(typeof orientation.height).toBe('number'); + expect(typeof orientation.width).toBe('number'); + expect(typeof orientation.isPortrait).toBe('boolean'); + }); + }); +}); From 94665c54a5726bfeb329a9e06d5a3c19ce57e7d9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 3 Oct 2025 13:39:40 -0700 Subject: [PATCH 02/82] test: implement unit tests for useDeviceOrientation hook to ensure accurate orientation detection --- src/core/classes/IterableUtil.test.ts | 222 ++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 src/core/classes/IterableUtil.test.ts diff --git a/src/core/classes/IterableUtil.test.ts b/src/core/classes/IterableUtil.test.ts new file mode 100644 index 000000000..ad4a0f210 --- /dev/null +++ b/src/core/classes/IterableUtil.test.ts @@ -0,0 +1,222 @@ +import { IterableUtil } from './IterableUtil'; + +/** + * Tests for IterableUtil class. + * + * Note: The current implementation of readBoolean has a limitation - it doesn't actually + * validate that the value is a boolean. It returns any truthy value as-is, or false for falsy values. + * This behavior is tested below, but the implementation may need to be updated to properly + * validate boolean types as suggested in the TODO comment in the source code. + */ +describe('IterableUtil', () => { + describe('readBoolean', () => { + it('should return true when the key exists and value is true', () => { + // GIVEN a dictionary with a true boolean value + const dict = { testKey: true }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return true + expect(result).toBe(true); + }); + + it('should return false when the key exists and value is false', () => { + // GIVEN a dictionary with a false boolean value + const dict = { testKey: false }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should return false when the key does not exist', () => { + // GIVEN a dictionary without the key + const dict = { otherKey: true }; + + // WHEN reading a non-existent key + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should return false when the key exists but value is undefined', () => { + // GIVEN a dictionary with undefined value + const dict = { testKey: undefined }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should return false when the key exists but value is null', () => { + // GIVEN a dictionary with null value + const dict = { testKey: null }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should return false when the key exists but value is 0', () => { + // GIVEN a dictionary with 0 value + const dict = { testKey: 0 }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should return false when the key exists but value is empty string', () => { + // GIVEN a dictionary with empty string value + const dict = { testKey: '' }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should return false when the key exists but value is NaN', () => { + // GIVEN a dictionary with NaN value + const dict = { testKey: NaN }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should return truthy string as boolean when key exists', () => { + // GIVEN a dictionary with truthy string value + const dict = { testKey: 'true' }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return the string cast to boolean (truthy) + expect(result).toBe('true'); + }); + + it('should return truthy number as boolean when key exists', () => { + // GIVEN a dictionary with truthy number value + const dict = { testKey: 1 }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return the number cast to boolean (truthy) + expect(result).toBe(1); + }); + + it('should return truthy object as boolean when key exists', () => { + // GIVEN a dictionary with truthy object value + const dict = { testKey: {} }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return the object cast to boolean (truthy) + expect(result).toEqual({}); + }); + + it('should return truthy array as boolean when key exists', () => { + // GIVEN a dictionary with truthy array value + const dict = { testKey: [] }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return the array cast to boolean (truthy) + expect(result).toEqual([]); + }); + + it('should return truthy function as boolean when key exists', () => { + // GIVEN a dictionary with truthy function value + const dict = { testKey: () => {} }; + + // WHEN reading the boolean value + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return the function cast to boolean (truthy) + expect(result).toBeInstanceOf(Function); + }); + + it('should handle empty dictionary', () => { + // GIVEN an empty dictionary + const dict = {}; + + // WHEN reading a key from empty dictionary + const result = IterableUtil.readBoolean(dict, 'testKey'); + + // THEN it should return false + expect(result).toBe(false); + }); + + it('should handle dictionary with multiple keys', () => { + // GIVEN a dictionary with multiple keys + const dict = { + key1: true, + key2: false, + key3: 'string', + key4: 123 + }; + + // WHEN reading different keys + const result1 = IterableUtil.readBoolean(dict, 'key1'); + const result2 = IterableUtil.readBoolean(dict, 'key2'); + const result3 = IterableUtil.readBoolean(dict, 'key3'); + const result4 = IterableUtil.readBoolean(dict, 'key4'); + const result5 = IterableUtil.readBoolean(dict, 'nonExistentKey'); + + // THEN it should return correct values + expect(result1).toBe(true); + expect(result2).toBe(false); + expect(result3).toBe('string'); // truthy string is returned as-is + expect(result4).toBe(123); // truthy number is returned as-is + expect(result5).toBe(false); // key doesn't exist + }); + + it('should handle special boolean values', () => { + // GIVEN a dictionary with special boolean values + const dict = { + trueValue: true, + falseValue: false + }; + + // WHEN reading boolean values + const trueResult = IterableUtil.readBoolean(dict, 'trueValue'); + const falseResult = IterableUtil.readBoolean(dict, 'falseValue'); + + // THEN it should return the actual boolean values + expect(trueResult).toBe(true); + expect(falseResult).toBe(false); + }); + + it('should handle case sensitivity in keys', () => { + // GIVEN a dictionary with case-sensitive keys + const dict = { TestKey: true, testkey: false }; + + // WHEN reading with different case + const result1 = IterableUtil.readBoolean(dict, 'TestKey'); + const result2 = IterableUtil.readBoolean(dict, 'testkey'); + const result3 = IterableUtil.readBoolean(dict, 'TESTKEY'); + + // THEN it should be case sensitive + expect(result1).toBe(true); + expect(result2).toBe(false); + expect(result3).toBe(false); // key doesn't exist + }); + }); +}); From 523b638f868c135a1e12528390a1cc30adfd6e52 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 3 Oct 2025 13:43:25 -0700 Subject: [PATCH 03/82] test: add TODO comments to IterableUtil tests for type verification of return values --- src/core/classes/IterableUtil.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/classes/IterableUtil.test.ts b/src/core/classes/IterableUtil.test.ts index ad4a0f210..35217c619 100644 --- a/src/core/classes/IterableUtil.test.ts +++ b/src/core/classes/IterableUtil.test.ts @@ -98,6 +98,7 @@ describe('IterableUtil', () => { expect(result).toBe(false); }); + // TODO: Verify that we want this to return a string instead of a boolean it('should return truthy string as boolean when key exists', () => { // GIVEN a dictionary with truthy string value const dict = { testKey: 'true' }; @@ -109,6 +110,7 @@ describe('IterableUtil', () => { expect(result).toBe('true'); }); + // TODO: Verify that we want this to return a number instead of a boolean it('should return truthy number as boolean when key exists', () => { // GIVEN a dictionary with truthy number value const dict = { testKey: 1 }; @@ -120,6 +122,7 @@ describe('IterableUtil', () => { expect(result).toBe(1); }); + // TODO: Verify that we want this to return an object instead of a boolean it('should return truthy object as boolean when key exists', () => { // GIVEN a dictionary with truthy object value const dict = { testKey: {} }; @@ -131,6 +134,7 @@ describe('IterableUtil', () => { expect(result).toEqual({}); }); + // TODO: Verify that we want this to return an array instead of a boolean it('should return truthy array as boolean when key exists', () => { // GIVEN a dictionary with truthy array value const dict = { testKey: [] }; @@ -142,6 +146,7 @@ describe('IterableUtil', () => { expect(result).toEqual([]); }); + // TODO: Verify that we want this to return a function instead of a boolean it('should return truthy function as boolean when key exists', () => { // GIVEN a dictionary with truthy function value const dict = { testKey: () => {} }; From fd08ea5dddba4c79e2e812e29359ae222503ee6a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 3 Oct 2025 13:49:39 -0700 Subject: [PATCH 04/82] test: add unit tests for IterableEdgeInsets --- src/core/classes/IterableEdgeInsets.test.ts | 347 ++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 src/core/classes/IterableEdgeInsets.test.ts diff --git a/src/core/classes/IterableEdgeInsets.test.ts b/src/core/classes/IterableEdgeInsets.test.ts new file mode 100644 index 000000000..5be75f056 --- /dev/null +++ b/src/core/classes/IterableEdgeInsets.test.ts @@ -0,0 +1,347 @@ +import { IterableEdgeInsets } from './IterableEdgeInsets'; +import type { IterableEdgeInsetDetails } from '../types'; + +describe('IterableEdgeInsets', () => { + describe('constructor', () => { + it('should create instance with valid parameters', () => { + // GIVEN valid edge inset parameters + const top = 10; + const left = 20; + const bottom = 30; + const right = 40; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should have the correct properties + expect(edgeInsets.top).toBe(top); + expect(edgeInsets.left).toBe(left); + expect(edgeInsets.bottom).toBe(bottom); + expect(edgeInsets.right).toBe(right); + }); + + it('should create instance with zero values', () => { + // GIVEN zero edge inset parameters + const top = 0; + const left = 0; + const bottom = 0; + const right = 0; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should have zero values + expect(edgeInsets.top).toBe(0); + expect(edgeInsets.left).toBe(0); + expect(edgeInsets.bottom).toBe(0); + expect(edgeInsets.right).toBe(0); + }); + + it('should create instance with negative values', () => { + // GIVEN negative edge inset parameters + const top = -5; + const left = -10; + const bottom = -15; + const right = -20; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should have the negative values + expect(edgeInsets.top).toBe(-5); + expect(edgeInsets.left).toBe(-10); + expect(edgeInsets.bottom).toBe(-15); + expect(edgeInsets.right).toBe(-20); + }); + + it('should create instance with decimal values', () => { + // GIVEN decimal edge inset parameters + const top = 1.5; + const left = 2.7; + const bottom = 3.9; + const right = 4.1; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should have the decimal values + expect(edgeInsets.top).toBe(1.5); + expect(edgeInsets.left).toBe(2.7); + expect(edgeInsets.bottom).toBe(3.9); + expect(edgeInsets.right).toBe(4.1); + }); + + it('should create instance with large values', () => { + // GIVEN large edge inset parameters + const top = 1000; + const left = 2000; + const bottom = 3000; + const right = 4000; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should have the large values + expect(edgeInsets.top).toBe(1000); + expect(edgeInsets.left).toBe(2000); + expect(edgeInsets.bottom).toBe(3000); + expect(edgeInsets.right).toBe(4000); + }); + }); + + describe('fromDict', () => { + it('should create instance from valid dictionary', () => { + // GIVEN a valid dictionary with edge inset details + const dict: IterableEdgeInsetDetails = { + top: 10, + left: 20, + bottom: 30, + right: 40 + }; + + // WHEN creating from dictionary + const edgeInsets = IterableEdgeInsets.fromDict(dict); + + // THEN it should have the correct properties + expect(edgeInsets.top).toBe(10); + expect(edgeInsets.left).toBe(20); + expect(edgeInsets.bottom).toBe(30); + expect(edgeInsets.right).toBe(40); + }); + + it('should create instance from dictionary with zero values', () => { + // GIVEN a dictionary with zero values + const dict: IterableEdgeInsetDetails = { + top: 0, + left: 0, + bottom: 0, + right: 0 + }; + + // WHEN creating from dictionary + const edgeInsets = IterableEdgeInsets.fromDict(dict); + + // THEN it should have zero values + expect(edgeInsets.top).toBe(0); + expect(edgeInsets.left).toBe(0); + expect(edgeInsets.bottom).toBe(0); + expect(edgeInsets.right).toBe(0); + }); + + it('should create instance from dictionary with negative values', () => { + // GIVEN a dictionary with negative values + const dict: IterableEdgeInsetDetails = { + top: -5, + left: -10, + bottom: -15, + right: -20 + }; + + // WHEN creating from dictionary + const edgeInsets = IterableEdgeInsets.fromDict(dict); + + // THEN it should have the negative values + expect(edgeInsets.top).toBe(-5); + expect(edgeInsets.left).toBe(-10); + expect(edgeInsets.bottom).toBe(-15); + expect(edgeInsets.right).toBe(-20); + }); + + it('should create instance from dictionary with decimal values', () => { + // GIVEN a dictionary with decimal values + const dict: IterableEdgeInsetDetails = { + top: 1.5, + left: 2.7, + bottom: 3.9, + right: 4.1 + }; + + // WHEN creating from dictionary + const edgeInsets = IterableEdgeInsets.fromDict(dict); + + // THEN it should have the decimal values + expect(edgeInsets.top).toBe(1.5); + expect(edgeInsets.left).toBe(2.7); + expect(edgeInsets.bottom).toBe(3.9); + expect(edgeInsets.right).toBe(4.1); + }); + + it('should create instance from dictionary with mixed positive and negative values', () => { + // GIVEN a dictionary with mixed values + const dict: IterableEdgeInsetDetails = { + top: 10, + left: -5, + bottom: 0, + right: 15 + }; + + // WHEN creating from dictionary + const edgeInsets = IterableEdgeInsets.fromDict(dict); + + // THEN it should have the mixed values + expect(edgeInsets.top).toBe(10); + expect(edgeInsets.left).toBe(-5); + expect(edgeInsets.bottom).toBe(0); + expect(edgeInsets.right).toBe(15); + }); + }); + + describe('property access', () => { + it('should allow property modification', () => { + // GIVEN an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(10, 20, 30, 40); + + // WHEN modifying properties + edgeInsets.top = 100; + edgeInsets.left = 200; + edgeInsets.bottom = 300; + edgeInsets.right = 400; + + // THEN the properties should be updated + expect(edgeInsets.top).toBe(100); + expect(edgeInsets.left).toBe(200); + expect(edgeInsets.bottom).toBe(300); + expect(edgeInsets.right).toBe(400); + }); + + it('should maintain property independence', () => { + // GIVEN an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(10, 20, 30, 40); + + // WHEN modifying one property + edgeInsets.top = 999; + + // THEN other properties should remain unchanged + expect(edgeInsets.top).toBe(999); + expect(edgeInsets.left).toBe(20); + expect(edgeInsets.bottom).toBe(30); + expect(edgeInsets.right).toBe(40); + }); + }); + + describe('edge cases', () => { + it('should handle NaN values', () => { + // GIVEN NaN values + const top = NaN; + const left = NaN; + const bottom = NaN; + const right = NaN; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should store NaN values + expect(edgeInsets.top).toBeNaN(); + expect(edgeInsets.left).toBeNaN(); + expect(edgeInsets.bottom).toBeNaN(); + expect(edgeInsets.right).toBeNaN(); + }); + + it('should handle Infinity values', () => { + // GIVEN Infinity values + const top = Infinity; + const left = -Infinity; + const bottom = Infinity; + const right = -Infinity; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should store Infinity values + expect(edgeInsets.top).toBe(Infinity); + expect(edgeInsets.left).toBe(-Infinity); + expect(edgeInsets.bottom).toBe(Infinity); + expect(edgeInsets.right).toBe(-Infinity); + }); + + it('should handle very small decimal values', () => { + // GIVEN very small decimal values + const top = 0.0001; + const left = 0.0002; + const bottom = 0.0003; + const right = 0.0004; + + // WHEN creating an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(top, left, bottom, right); + + // THEN it should store the small decimal values + expect(edgeInsets.top).toBe(0.0001); + expect(edgeInsets.left).toBe(0.0002); + expect(edgeInsets.bottom).toBe(0.0003); + expect(edgeInsets.right).toBe(0.0004); + }); + }); + + describe('interface compliance', () => { + it('should implement IterableEdgeInsetDetails interface', () => { + // GIVEN an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(10, 20, 30, 40); + + // THEN it should have all required properties + expect(edgeInsets).toHaveProperty('top'); + expect(edgeInsets).toHaveProperty('left'); + expect(edgeInsets).toHaveProperty('bottom'); + expect(edgeInsets).toHaveProperty('right'); + + // AND all properties should be numbers + expect(typeof edgeInsets.top).toBe('number'); + expect(typeof edgeInsets.left).toBe('number'); + expect(typeof edgeInsets.bottom).toBe('number'); + expect(typeof edgeInsets.right).toBe('number'); + }); + + it('should be assignable to IterableEdgeInsetDetails', () => { + // GIVEN an IterableEdgeInsets instance + const edgeInsets = new IterableEdgeInsets(10, 20, 30, 40); + + // WHEN assigning to IterableEdgeInsetDetails + const details: IterableEdgeInsetDetails = edgeInsets; + + // THEN it should work without type errors + expect(details.top).toBe(10); + expect(details.left).toBe(20); + expect(details.bottom).toBe(30); + expect(details.right).toBe(40); + }); + }); + + describe('fromDict with edge cases', () => { + it('should handle dictionary with NaN values', () => { + // GIVEN a dictionary with NaN values + const dict: IterableEdgeInsetDetails = { + top: NaN, + left: NaN, + bottom: NaN, + right: NaN + }; + + // WHEN creating from dictionary + const edgeInsets = IterableEdgeInsets.fromDict(dict); + + // THEN it should have NaN values + expect(edgeInsets.top).toBeNaN(); + expect(edgeInsets.left).toBeNaN(); + expect(edgeInsets.bottom).toBeNaN(); + expect(edgeInsets.right).toBeNaN(); + }); + + it('should handle dictionary with Infinity values', () => { + // GIVEN a dictionary with Infinity values + const dict: IterableEdgeInsetDetails = { + top: Infinity, + left: -Infinity, + bottom: Infinity, + right: -Infinity + }; + + // WHEN creating from dictionary + const edgeInsets = IterableEdgeInsets.fromDict(dict); + + // THEN it should have Infinity values + expect(edgeInsets.top).toBe(Infinity); + expect(edgeInsets.left).toBe(-Infinity); + expect(edgeInsets.bottom).toBe(Infinity); + expect(edgeInsets.right).toBe(-Infinity); + }); + }); +}); From 51f7df890cd75abe9e12c9d75fd4e13d8b02bd18 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 3 Oct 2025 13:55:08 -0700 Subject: [PATCH 05/82] test: add unit tests for IterableHtmlInAppContent --- .../classes/IterableHtmlInAppContent.test.ts | 402 ++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 src/inApp/classes/IterableHtmlInAppContent.test.ts diff --git a/src/inApp/classes/IterableHtmlInAppContent.test.ts b/src/inApp/classes/IterableHtmlInAppContent.test.ts new file mode 100644 index 000000000..aed6af5d1 --- /dev/null +++ b/src/inApp/classes/IterableHtmlInAppContent.test.ts @@ -0,0 +1,402 @@ +import { IterableEdgeInsets } from '../../core'; + +import { IterableInAppContentType } from '../enums'; +import { IterableHtmlInAppContent } from './IterableHtmlInAppContent'; +import type { IterableHtmlInAppContentRaw } from '../types'; + +describe('IterableHtmlInAppContent', () => { + describe('constructor', () => { + it('should create instance with valid parameters', () => { + // GIVEN valid parameters + const edgeInsets = new IterableEdgeInsets(10, 20, 30, 40); + const html = '
Hello World
'; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should have the correct properties + expect(content.edgeInsets).toBe(edgeInsets); + expect(content.html).toBe(html); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance with empty HTML', () => { + // GIVEN empty HTML + const edgeInsets = new IterableEdgeInsets(0, 0, 0, 0); + const html = ''; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should have empty HTML + expect(content.html).toBe(''); + expect(content.edgeInsets).toBe(edgeInsets); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance with complex HTML', () => { + // GIVEN complex HTML content + const edgeInsets = new IterableEdgeInsets(5, 10, 15, 20); + const html = ` + + + Test + + + +
+

Welcome

+

This is a test message

+
+ + + `; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should have the complex HTML + expect(content.html).toBe(html); + expect(content.edgeInsets).toBe(edgeInsets); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance with HTML containing special characters', () => { + // GIVEN HTML with special characters + const edgeInsets = new IterableEdgeInsets(1, 2, 3, 4); + const html = '
Hello & Welcome! "Test" <tag>
'; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should preserve special characters + expect(content.html).toBe(html); + expect(content.edgeInsets).toBe(edgeInsets); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance with HTML containing JavaScript', () => { + // GIVEN HTML with JavaScript + const edgeInsets = new IterableEdgeInsets(10, 10, 10, 10); + const html = ` +
+ +

Content with script

+
+ `; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should preserve the JavaScript + expect(content.html).toBe(html); + expect(content.edgeInsets).toBe(edgeInsets); + expect(content.type).toBe(IterableInAppContentType.html); + }); + }); + + describe('fromDict', () => { + it('should create instance from valid dictionary', () => { + // GIVEN a valid dictionary + const dict: IterableHtmlInAppContentRaw = { + edgeInsets: { + top: 10, + left: 20, + bottom: 30, + right: 40 + }, + html: '
Hello World
' + }; + + // WHEN creating from dictionary + const content = IterableHtmlInAppContent.fromDict(dict); + + // THEN it should have the correct properties + expect(content.html).toBe('
Hello World
'); + expect(content.edgeInsets.top).toBe(10); + expect(content.edgeInsets.left).toBe(20); + expect(content.edgeInsets.bottom).toBe(30); + expect(content.edgeInsets.right).toBe(40); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance from dictionary with empty HTML', () => { + // GIVEN a dictionary with empty HTML + const dict: IterableHtmlInAppContentRaw = { + edgeInsets: { + top: 0, + left: 0, + bottom: 0, + right: 0 + }, + html: '' + }; + + // WHEN creating from dictionary + const content = IterableHtmlInAppContent.fromDict(dict); + + // THEN it should have empty HTML + expect(content.html).toBe(''); + expect(content.edgeInsets.top).toBe(0); + expect(content.edgeInsets.left).toBe(0); + expect(content.edgeInsets.bottom).toBe(0); + expect(content.edgeInsets.right).toBe(0); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance from dictionary with complex HTML', () => { + // GIVEN a dictionary with complex HTML + const dict: IterableHtmlInAppContentRaw = { + edgeInsets: { + top: 5, + left: 10, + bottom: 15, + right: 20 + }, + html: ` + + Test + +
+

Title

+

Paragraph with bold text

+
+ + + ` + }; + + // WHEN creating from dictionary + const content = IterableHtmlInAppContent.fromDict(dict); + + // THEN it should have the complex HTML + expect(content.html).toBe(dict.html); + expect(content.edgeInsets.top).toBe(5); + expect(content.edgeInsets.left).toBe(10); + expect(content.edgeInsets.bottom).toBe(15); + expect(content.edgeInsets.right).toBe(20); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance from dictionary with negative edge insets', () => { + // GIVEN a dictionary with negative edge insets + const dict: IterableHtmlInAppContentRaw = { + edgeInsets: { + top: -5, + left: -10, + bottom: -15, + right: -20 + }, + html: '
Negative insets
' + }; + + // WHEN creating from dictionary + const content = IterableHtmlInAppContent.fromDict(dict); + + // THEN it should have negative edge insets + expect(content.html).toBe('
Negative insets
'); + expect(content.edgeInsets.top).toBe(-5); + expect(content.edgeInsets.left).toBe(-10); + expect(content.edgeInsets.bottom).toBe(-15); + expect(content.edgeInsets.right).toBe(-20); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should create instance from dictionary with decimal edge insets', () => { + // GIVEN a dictionary with decimal edge insets + const dict: IterableHtmlInAppContentRaw = { + edgeInsets: { + top: 1.5, + left: 2.7, + bottom: 3.9, + right: 4.1 + }, + html: '
Decimal insets
' + }; + + // WHEN creating from dictionary + const content = IterableHtmlInAppContent.fromDict(dict); + + // THEN it should have decimal edge insets + expect(content.html).toBe('
Decimal insets
'); + expect(content.edgeInsets.top).toBe(1.5); + expect(content.edgeInsets.left).toBe(2.7); + expect(content.edgeInsets.bottom).toBe(3.9); + expect(content.edgeInsets.right).toBe(4.1); + expect(content.type).toBe(IterableInAppContentType.html); + }); + }); + + describe('property access', () => { + it('should allow property modification', () => { + // GIVEN an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent( + new IterableEdgeInsets(10, 20, 30, 40), + '
Original
' + ); + + // WHEN modifying properties + const newEdgeInsets = new IterableEdgeInsets(100, 200, 300, 400); + content.edgeInsets = newEdgeInsets; + content.html = '
Modified
'; + + // THEN the properties should be updated + expect(content.edgeInsets).toBe(newEdgeInsets); + expect(content.html).toBe('
Modified
'); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should maintain type property as html', () => { + // GIVEN an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent( + new IterableEdgeInsets(10, 20, 30, 40), + '
Test
' + ); + + // THEN the type should always be html + expect(content.type).toBe(IterableInAppContentType.html); + + // AND it should remain html even after property changes + content.html = '
Changed
'; + expect(content.type).toBe(IterableInAppContentType.html); + }); + }); + + describe('edge cases', () => { + it('should handle HTML with only whitespace', () => { + // GIVEN HTML with only whitespace + const edgeInsets = new IterableEdgeInsets(10, 20, 30, 40); + const html = ' \n\t '; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should preserve the whitespace + expect(content.html).toBe(' \n\t '); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should handle HTML with unicode characters', () => { + // GIVEN HTML with unicode characters + const edgeInsets = new IterableEdgeInsets(5, 5, 5, 5); + const html = '
Hello δΈ–η•Œ 🌍 πŸš€
'; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should preserve unicode characters + expect(content.html).toBe('
Hello δΈ–η•Œ 🌍 πŸš€
'); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should handle very long HTML content', () => { + // GIVEN very long HTML content + const edgeInsets = new IterableEdgeInsets(1, 1, 1, 1); + const longHtml = '
' + 'x'.repeat(10000) + '
'; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, longHtml); + + // THEN it should handle the long content + expect(content.html).toBe(longHtml); + expect(content.html.length).toBe(longHtml.length); // Should match the original length + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should handle HTML with malformed tags', () => { + // GIVEN HTML with malformed tags + const edgeInsets = new IterableEdgeInsets(10, 10, 10, 10); + const html = '
Unclosed tag

Another unclosed Nested

'; + + // WHEN creating an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent(edgeInsets, html); + + // THEN it should preserve the malformed HTML + expect(content.html).toBe(html); + expect(content.type).toBe(IterableInAppContentType.html); + }); + }); + + describe('interface compliance', () => { + it('should implement IterableInAppContent interface', () => { + // GIVEN an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent( + new IterableEdgeInsets(10, 20, 30, 40), + '
Test
' + ); + + // THEN it should have the required type property + expect(content).toHaveProperty('type'); + expect(typeof content.type).toBe('number'); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should be assignable to IterableInAppContent', () => { + // GIVEN an IterableHtmlInAppContent instance + const content = new IterableHtmlInAppContent( + new IterableEdgeInsets(10, 20, 30, 40), + '
Test
' + ); + + // WHEN assigning to IterableInAppContent + const inAppContent: { type: IterableInAppContentType } = content; + + // THEN it should work without type errors + expect(inAppContent.type).toBe(IterableInAppContentType.html); + }); + }); + + describe('fromDict with edge cases', () => { + it('should handle dictionary with NaN edge insets', () => { + // GIVEN a dictionary with NaN edge insets + const dict: IterableHtmlInAppContentRaw = { + edgeInsets: { + top: NaN, + left: NaN, + bottom: NaN, + right: NaN + }, + html: '
NaN insets
' + }; + + // WHEN creating from dictionary + const content = IterableHtmlInAppContent.fromDict(dict); + + // THEN it should handle NaN values + expect(content.html).toBe('
NaN insets
'); + expect(content.edgeInsets.top).toBeNaN(); + expect(content.edgeInsets.left).toBeNaN(); + expect(content.edgeInsets.bottom).toBeNaN(); + expect(content.edgeInsets.right).toBeNaN(); + expect(content.type).toBe(IterableInAppContentType.html); + }); + + it('should handle dictionary with Infinity edge insets', () => { + // GIVEN a dictionary with Infinity edge insets + const dict: IterableHtmlInAppContentRaw = { + edgeInsets: { + top: Infinity, + left: -Infinity, + bottom: Infinity, + right: -Infinity + }, + html: '
Infinity insets
' + }; + + // WHEN creating from dictionary + const content = IterableHtmlInAppContent.fromDict(dict); + + // THEN it should handle Infinity values + expect(content.html).toBe('
Infinity insets
'); + expect(content.edgeInsets.top).toBe(Infinity); + expect(content.edgeInsets.left).toBe(-Infinity); + expect(content.edgeInsets.bottom).toBe(Infinity); + expect(content.edgeInsets.right).toBe(-Infinity); + expect(content.type).toBe(IterableInAppContentType.html); + }); + }); +}); From 553ee16a67080c50a269eab0b1595e962011f8b7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 3 Oct 2025 14:15:55 -0700 Subject: [PATCH 06/82] test: add mock methods for inbox message retrieval and HTML content generation in MockRNIterableAPI --- src/__mocks__/MockRNIterableAPI.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index 390263153..45c259a55 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -82,6 +82,25 @@ export class MockRNIterableAPI { }); } + static async getInboxMessages(): Promise { + return await new Promise((resolve) => { + // Filter messages that are marked for inbox + const inboxMessages = MockRNIterableAPI.messages?.filter(msg => msg.saveToInbox) || []; + resolve(inboxMessages); + }); + } + + static async getHtmlInAppContentForMessage(messageId: string): Promise { + return await new Promise((resolve) => { + // Mock HTML content for testing + const mockHtmlContent = { + edgeInsets: { top: 10, left: 20, bottom: 30, right: 40 }, + html: `
Mock HTML content for message ${messageId}
`, + }; + resolve(mockHtmlContent); + }); + } + static setAutoDisplayPaused = jest.fn(); static async showMessage( From 605d28c59c10f1f4f22b903f494799192bacf39a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 3 Oct 2025 14:16:14 -0700 Subject: [PATCH 07/82] test: add unit tests for IterableInAppManager --- .../classes/IterableInAppManager.test.ts | 396 ++++++++++++++++++ 1 file changed, 396 insertions(+) create mode 100644 src/inApp/classes/IterableInAppManager.test.ts diff --git a/src/inApp/classes/IterableInAppManager.test.ts b/src/inApp/classes/IterableInAppManager.test.ts new file mode 100644 index 000000000..ce0fc6a48 --- /dev/null +++ b/src/inApp/classes/IterableInAppManager.test.ts @@ -0,0 +1,396 @@ +import { IterableInAppDeleteSource, IterableInAppLocation } from '../enums'; +import { IterableInAppManager } from './IterableInAppManager'; +import { IterableInAppMessage } from './IterableInAppMessage'; + +describe('IterableInAppManager', () => { + let manager: IterableInAppManager; + + beforeEach(() => { + manager = new IterableInAppManager(); + }); + + describe('constructor', () => { + it('should create an instance', () => { + // WHEN creating a new manager + const newManager = new IterableInAppManager(); + + // THEN it should be an instance of IterableInAppManager + expect(newManager).toBeInstanceOf(IterableInAppManager); + }); + }); + + describe('method signatures', () => { + it('should have getMessages method', () => { + // THEN the manager should have getMessages method + expect(typeof manager.getMessages).toBe('function'); + }); + + it('should have getInboxMessages method', () => { + // THEN the manager should have getInboxMessages method + expect(typeof manager.getInboxMessages).toBe('function'); + }); + + it('should have showMessage method', () => { + // THEN the manager should have showMessage method + expect(typeof manager.showMessage).toBe('function'); + }); + + it('should have removeMessage method', () => { + // THEN the manager should have removeMessage method + expect(typeof manager.removeMessage).toBe('function'); + }); + + it('should have setReadForMessage method', () => { + // THEN the manager should have setReadForMessage method + expect(typeof manager.setReadForMessage).toBe('function'); + }); + + it('should have getHtmlContentForMessage method', () => { + // THEN the manager should have getHtmlContentForMessage method + expect(typeof manager.getHtmlContentForMessage).toBe('function'); + }); + + it('should have setAutoDisplayPaused method', () => { + // THEN the manager should have setAutoDisplayPaused method + expect(typeof manager.setAutoDisplayPaused).toBe('function'); + }); + }); + + describe('getInboxMessages', () => { + it('should be a function', () => { + // THEN the method should be a function + expect(typeof manager.getInboxMessages).toBe('function'); + }); + + it('should have the correct method name', () => { + // THEN the method should be named getInboxMessages + expect(manager.getInboxMessages.name).toBe('getInboxMessages'); + }); + + it('should be a different method from getMessages', () => { + // THEN getInboxMessages should be different from getMessages + expect(manager.getInboxMessages).not.toBe(manager.getMessages); + expect(manager.getInboxMessages.name).not.toBe(manager.getMessages.name); + }); + + it('should return a Promise when called', async () => { + // WHEN calling getInboxMessages + const result = manager.getInboxMessages(); + + // THEN it should return a Promise + expect(result).toBeInstanceOf(Promise); + }); + + it('should return empty array when no inbox messages exist', async () => { + // GIVEN no messages are set in the mock + const { MockRNIterableAPI } = await import('../../__mocks__/MockRNIterableAPI'); + MockRNIterableAPI.setMessages([]); + + // WHEN calling getInboxMessages + const result = await manager.getInboxMessages(); + + // THEN it should return empty array + expect(result).toEqual([]); + }); + + it('should return only inbox messages when mixed messages exist', async () => { + // GIVEN mixed messages with some marked for inbox + const { MockRNIterableAPI } = await import('../../__mocks__/MockRNIterableAPI'); + const mockMessages = [ + { messageId: 'msg1', campaignId: 1, saveToInbox: true } as IterableInAppMessage, + { messageId: 'msg2', campaignId: 2, saveToInbox: false } as IterableInAppMessage, + { messageId: 'msg3', campaignId: 3, saveToInbox: true } as IterableInAppMessage, + ]; + MockRNIterableAPI.setMessages(mockMessages); + + // WHEN calling getInboxMessages + const result = await manager.getInboxMessages(); + + // THEN it should return only inbox messages + expect(result).toHaveLength(2); + expect(result?.[0]?.messageId).toBe('msg1'); + expect(result?.[1]?.messageId).toBe('msg3'); + }); + + it('should return all messages when all are marked for inbox', async () => { + // GIVEN all messages are marked for inbox + const { MockRNIterableAPI } = await import('../../__mocks__/MockRNIterableAPI'); + const mockMessages = [ + { messageId: 'msg1', campaignId: 1, saveToInbox: true } as IterableInAppMessage, + { messageId: 'msg2', campaignId: 2, saveToInbox: true } as IterableInAppMessage, + ]; + MockRNIterableAPI.setMessages(mockMessages); + + // WHEN calling getInboxMessages + const result = await manager.getInboxMessages(); + + // THEN it should return all messages + expect(result).toHaveLength(2); + expect(result).toEqual(mockMessages); + }); + + it('should handle undefined messages gracefully', async () => { + // GIVEN messages are undefined + const { MockRNIterableAPI } = await import('../../__mocks__/MockRNIterableAPI'); + MockRNIterableAPI.setMessages(undefined as unknown as IterableInAppMessage[]); + + // WHEN calling getInboxMessages + const result = await manager.getInboxMessages(); + + // THEN it should return empty array + expect(result).toEqual([]); + }); + }); + + describe('showMessage parameters', () => { + it('should accept IterableInAppMessage and boolean parameters', () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling showMessage with valid parameters + // THEN it should not throw an error + expect(() => { + manager.showMessage(mockMessage, true); + manager.showMessage(mockMessage, false); + }).not.toThrow(); + }); + }); + + describe('removeMessage parameters', () => { + it('should accept IterableInAppMessage, IterableInAppLocation, and IterableInAppDeleteSource parameters', () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling removeMessage with valid parameters + // THEN it should not throw an error + expect(() => { + manager.removeMessage( + mockMessage, + IterableInAppLocation.inApp, + IterableInAppDeleteSource.deleteButton + ); + manager.removeMessage( + mockMessage, + IterableInAppLocation.inbox, + IterableInAppDeleteSource.inboxSwipe + ); + manager.removeMessage( + mockMessage, + IterableInAppLocation.inApp, + IterableInAppDeleteSource.unknown + ); + }).not.toThrow(); + }); + }); + + describe('setReadForMessage parameters', () => { + it('should accept IterableInAppMessage and boolean parameters', () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling setReadForMessage with valid parameters + // THEN it should not throw an error + expect(() => { + manager.setReadForMessage(mockMessage, true); + manager.setReadForMessage(mockMessage, false); + }).not.toThrow(); + }); + }); + + describe('getHtmlContentForMessage', () => { + it('should be a function', () => { + // THEN the method should be a function + expect(typeof manager.getHtmlContentForMessage).toBe('function'); + }); + + it('should return a Promise when called', async () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling getHtmlContentForMessage + const result = manager.getHtmlContentForMessage(mockMessage); + + // THEN it should return a Promise + expect(result).toBeInstanceOf(Promise); + }); + + it('should return HTML content for a message', async () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling getHtmlContentForMessage + const result = await manager.getHtmlContentForMessage(mockMessage); + + // THEN it should return HTML content + expect(result).toEqual({ + edgeInsets: { top: 10, left: 20, bottom: 30, right: 40 }, + html: '
Mock HTML content for message test-message-id
', + }); + }); + + it('should handle different message IDs', async () => { + // GIVEN different mock messages + const message1 = { messageId: 'msg1', campaignId: 1 } as IterableInAppMessage; + const message2 = { messageId: 'msg2', campaignId: 2 } as IterableInAppMessage; + + // WHEN calling getHtmlContentForMessage with different messages + const result1 = await manager.getHtmlContentForMessage(message1); + const result2 = await manager.getHtmlContentForMessage(message2); + + // THEN it should return different HTML content for each message + expect(result1).toEqual({ + edgeInsets: { top: 10, left: 20, bottom: 30, right: 40 }, + html: '
Mock HTML content for message msg1
', + }); + expect(result2).toEqual({ + edgeInsets: { top: 10, left: 20, bottom: 30, right: 40 }, + html: '
Mock HTML content for message msg2
', + }); + }); + }); + + describe('setAutoDisplayPaused parameters', () => { + it('should accept boolean parameter', () => { + // WHEN calling setAutoDisplayPaused with valid parameters + // THEN it should not throw an error + expect(() => { + manager.setAutoDisplayPaused(true); + manager.setAutoDisplayPaused(false); + }).not.toThrow(); + }); + }); + + describe('enum values', () => { + it('should have correct IterableInAppLocation enum values', () => { + // THEN the enum values should be correct + expect(IterableInAppLocation.inApp).toBe(0); + expect(IterableInAppLocation.inbox).toBe(1); + }); + + it('should have correct IterableInAppDeleteSource enum values', () => { + // THEN the enum values should be correct + expect(IterableInAppDeleteSource.inboxSwipe).toBe(0); + expect(IterableInAppDeleteSource.deleteButton).toBe(1); + expect(IterableInAppDeleteSource.unknown).toBe(100); + }); + }); + + describe('method return types', () => { + it('should return Promise for async methods', () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling async methods that don't require native modules + const showMessagePromise = manager.showMessage(mockMessage, true); + + // THEN they should return Promises + expect(showMessagePromise).toBeInstanceOf(Promise); + }); + + it('should return void for sync methods', () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling sync methods + const removeMessageResult = manager.removeMessage( + mockMessage, + IterableInAppLocation.inApp, + IterableInAppDeleteSource.deleteButton + ); + const setReadResult = manager.setReadForMessage(mockMessage, true); + const setAutoDisplayResult = manager.setAutoDisplayPaused(true); + + // THEN they should return undefined (void) + expect(removeMessageResult).toBeUndefined(); + expect(setReadResult).toBeUndefined(); + expect(setAutoDisplayResult).toBeUndefined(); + }); + }); + + describe('error handling', () => { + it('should handle null message parameters', () => { + // WHEN calling methods with null message + // THEN it should throw appropriate errors + expect(() => { + manager.removeMessage(null as unknown as IterableInAppMessage, IterableInAppLocation.inApp, IterableInAppDeleteSource.unknown); + }).toThrow(); + + expect(() => { + manager.setReadForMessage(null as unknown as IterableInAppMessage, true); + }).toThrow(); + + expect(() => { + manager.getHtmlContentForMessage(null as unknown as IterableInAppMessage); + }).toThrow(); + }); + + it('should handle undefined message parameters', () => { + // WHEN calling methods with undefined message + // THEN it should throw appropriate errors + expect(() => { + manager.removeMessage(undefined as unknown as IterableInAppMessage, IterableInAppLocation.inApp, IterableInAppDeleteSource.unknown); + }).toThrow(); + + expect(() => { + manager.setReadForMessage(undefined as unknown as IterableInAppMessage, true); + }).toThrow(); + + expect(() => { + manager.getHtmlContentForMessage(undefined as unknown as IterableInAppMessage); + }).toThrow(); + }); + }); + + describe('parameter validation', () => { + it('should handle invalid enum values gracefully', () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling removeMessage with invalid enum values + // THEN it should not throw an error (values are passed through) + expect(() => { + manager.removeMessage(mockMessage, 999 as IterableInAppLocation, 888 as IterableInAppDeleteSource); + }).not.toThrow(); + }); + + it('should handle invalid boolean parameters', () => { + // GIVEN a mock message + const mockMessage = { + messageId: 'test-message-id', + campaignId: 123, + } as IterableInAppMessage; + + // WHEN calling methods with invalid boolean parameters + // THEN it should not throw an error (values are passed through) + expect(() => { + manager.showMessage(mockMessage, 'true' as unknown as boolean); + manager.setReadForMessage(mockMessage, 'false' as unknown as boolean); + manager.setAutoDisplayPaused('true' as unknown as boolean); + }).not.toThrow(); + }); + }); +}); From 67a806d5a05d46cc0933bdfbed34084450cc9eb9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 09:54:01 -0700 Subject: [PATCH 08/82] fix: removed comment description that no longer applies --- src/inApp/classes/IterableInAppMessage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/inApp/classes/IterableInAppMessage.ts b/src/inApp/classes/IterableInAppMessage.ts index f372043b6..c77b08e63 100644 --- a/src/inApp/classes/IterableInAppMessage.ts +++ b/src/inApp/classes/IterableInAppMessage.ts @@ -133,7 +133,6 @@ export class IterableInAppMessage { * * @param viewToken - The `ViewToken` containing the in-app message data. * @returns A new instance of `IterableInAppMessage` populated with data from the `viewToken`. - * @throws Error if the viewToken or its item or inAppMessage is null/undefined. */ static fromViewToken(viewToken: ViewToken) { const inAppMessage = viewToken?.item?.inAppMessage as IterableInAppMessage; From d9a7b82ecc5fbf5bfa1c50e6fba1f64df0fdc2d4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 10:15:45 -0700 Subject: [PATCH 09/82] test: prettify Iterable test file --- src/core/classes/Iterable.test.ts | 296 +++++++++++++++--------------- 1 file changed, 148 insertions(+), 148 deletions(-) diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index 4c044165e..d8540dc92 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -1,8 +1,8 @@ -import { NativeEventEmitter, Platform } from "react-native"; +import { NativeEventEmitter, Platform } from 'react-native'; -import { MockLinking } from "../../__mocks__/MockLinking"; -import { MockRNIterableAPI } from "../../__mocks__/MockRNIterableAPI"; -import { IterableLogger } from ".."; +import { MockLinking } from '../../__mocks__/MockLinking'; +import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; +import { IterableLogger } from '..'; // import from the same location that consumers import from import { Iterable, @@ -23,8 +23,8 @@ import { IterableInAppTriggerType, IterableAuthResponse, IterableInAppShowResponse, -} from "../.."; -import { TestHelper } from "../../__tests__/TestHelper"; +} from '../..'; +import { TestHelper } from '../../__tests__/TestHelper'; const getDefaultConfig = () => { const config = new IterableConfig(); @@ -32,7 +32,7 @@ const getDefaultConfig = () => { return config; }; -describe("Iterable", () => { +describe('Iterable', () => { beforeEach(() => { jest.clearAllMocks(); const config = getDefaultConfig(); @@ -55,11 +55,11 @@ describe("Iterable", () => { jest.clearAllTimers(); }); - describe("setEmail", () => { - it("should set the email", async () => { - const result = "user@example.com"; + describe('setEmail', () => { + it('should set the email', async () => { + const result = 'user@example.com'; // GIVEN an email - const email = "user@example.com"; + const email = 'user@example.com'; // WHEN Iterable.setEmail is called with the given email Iterable.setEmail(email); // THEN Iterable.getEmail returns the given email @@ -69,11 +69,11 @@ describe("Iterable", () => { }); }); - describe("setUserId", () => { - it("should set the userId", async () => { - const result = "user1"; + describe('setUserId', () => { + it('should set the userId', async () => { + const result = 'user1'; // GIVEN an userId - const userId = "user1"; + const userId = 'user1'; // WHEN Iterable.setUserId is called with the given userId Iterable.setUserId(userId); // THEN Iterable.getUserId returns the given userId @@ -83,8 +83,8 @@ describe("Iterable", () => { }); }); - describe("disableDeviceForCurrentUser", () => { - it("should disable the device for the current user", () => { + describe('disableDeviceForCurrentUser', () => { + it('should disable the device for the current user', () => { // GIVEN no parameters // WHEN Iterable.disableDeviceForCurrentUser is called Iterable.disableDeviceForCurrentUser(); @@ -93,12 +93,12 @@ describe("Iterable", () => { }); }); - describe("getLastPushPayload", () => { - it("should return the last push payload", async () => { - const result = { var1: "val1", var2: true }; + describe('getLastPushPayload', () => { + it('should return the last push payload', async () => { + const result = { var1: 'val1', var2: true }; // GIVEN no parameters // WHEN the lastPushPayload is set - MockRNIterableAPI.lastPushPayload = { var1: "val1", var2: true }; + MockRNIterableAPI.lastPushPayload = { var1: 'val1', var2: true }; // THEN the lastPushPayload is returned when getLastPushPayload is called return await Iterable.getLastPushPayload().then((payload) => { expect(payload).toEqual(result); @@ -106,14 +106,14 @@ describe("Iterable", () => { }); }); - describe("trackPushOpenWithCampaignId", () => { - it("should track the push open with the campaign id", () => { + describe('trackPushOpenWithCampaignId', () => { + it('should track the push open with the campaign id', () => { // GIVEN the following parameters const campaignId = 123; const templateId = 234; - const messageId = "someMessageId"; + const messageId = 'someMessageId'; const appAlreadyRunning = false; - const dataFields = { dataFieldKey: "dataFieldValue" }; + const dataFields = { dataFieldKey: 'dataFieldValue' }; // WHEN Iterable.trackPushOpenWithCampaignId is called Iterable.trackPushOpenWithCampaignId( campaignId, @@ -133,10 +133,10 @@ describe("Iterable", () => { }); }); - describe("updateCart", () => { - it("should call IterableAPI.updateCart with the correct items", () => { + describe('updateCart', () => { + it('should call IterableAPI.updateCart with the correct items', () => { // GIVEN list of items - const items = [new IterableCommerceItem("id1", "Boba Tea", 18, 26)]; + const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; // WHEN Iterable.updateCart is called Iterable.updateCart(items); // THEN corresponding function is called on RNIterableAPI @@ -144,12 +144,12 @@ describe("Iterable", () => { }); }); - describe("trackPurchase", () => { - it("should track the purchase", () => { + describe('trackPurchase', () => { + it('should track the purchase', () => { // GIVEN the following parameters const total = 10; - const items = [new IterableCommerceItem("id1", "Boba Tea", 18, 26)]; - const dataFields = { dataFieldKey: "dataFieldValue" }; + const items = [new IterableCommerceItem('id1', 'Boba Tea', 18, 26)]; + const dataFields = { dataFieldKey: 'dataFieldValue' }; // WHEN Iterable.trackPurchase is called Iterable.trackPurchase(total, items, dataFields); // THEN corresponding function is called on RNIterableAPI @@ -160,23 +160,23 @@ describe("Iterable", () => { ); }); - it("should track the purchase when called with optional fields", () => { + it('should track the purchase when called with optional fields', () => { // GIVEN the following parameters const total = 5; const items = [ new IterableCommerceItem( - "id", - "swordfish", + 'id', + 'swordfish', 64, 1, - "SKU", - "description", - "url", - "imageUrl", - ["sword", "shield"] + 'SKU', + 'description', + 'url', + 'imageUrl', + ['sword', 'shield'] ), ]; - const dataFields = { key: "value" }; + const dataFields = { key: 'value' }; // WHEN Iterable.trackPurchase is called Iterable.trackPurchase(total, items, dataFields); // THEN corresponding function is called on RNIterableAPI @@ -188,11 +188,11 @@ describe("Iterable", () => { }); }); - describe("trackEvent", () => { - it("should call IterableAPI.trackEvent with the correct name and dataFields", () => { + describe('trackEvent', () => { + it('should call IterableAPI.trackEvent with the correct name and dataFields', () => { // GIVEN the following parameters - const name = "EventName"; - const dataFields = { DatafieldKey: "DatafieldValue" }; + const name = 'EventName'; + const dataFields = { DatafieldKey: 'DatafieldValue' }; // WHEN Iterable.trackEvent is called Iterable.trackEvent(name, dataFields); // THEN corresponding function is called on RNIterableAPI @@ -200,12 +200,12 @@ describe("Iterable", () => { }); }); - describe("setAttributionInfo", () => { - it("should set the attribution info", async () => { + describe('setAttributionInfo', () => { + it('should set the attribution info', async () => { // GIVEN attribution info const campaignId = 1234; const templateId = 5678; - const messageId = "qwer"; + const messageId = 'qwer'; // WHEN Iterable.setAttributionInfo is called with the given attribution info Iterable.setAttributionInfo( new IterableAttributionInfo(campaignId, templateId, messageId) @@ -219,10 +219,10 @@ describe("Iterable", () => { }); }); - describe("updateUser", () => { - it("should update the user", () => { + describe('updateUser', () => { + it('should update the user', () => { // GIVEN the following parameters - const dataFields = { field: "value1" }; + const dataFields = { field: 'value1' }; // WHEN Iterable.updateUser is called Iterable.updateUser(dataFields, false); // THEN corresponding function is called on RNIterableAPI @@ -230,20 +230,20 @@ describe("Iterable", () => { }); }); - describe("updateEmail", () => { - it("should call IterableAPI.updateEmail with the correct email", () => { + describe('updateEmail', () => { + it('should call IterableAPI.updateEmail with the correct email', () => { // GIVEN the new email - const newEmail = "woo@newemail.com"; + const newEmail = 'woo@newemail.com'; // WHEN Iterable.updateEmail is called Iterable.updateEmail(newEmail); // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.updateEmail).toBeCalledWith(newEmail, undefined); }); - it("should call IterableAPI.updateEmail with the correct email and token", () => { + it('should call IterableAPI.updateEmail with the correct email and token', () => { // GIVEN the new email and a token - const newEmail = "woo@newemail.com"; - const newToken = "token2"; + const newEmail = 'woo@newemail.com'; + const newToken = 'token2'; // WHEN Iterable.updateEmail is called Iterable.updateEmail(newEmail, newToken); // THEN corresponding function is called on RNITerableAPI @@ -251,8 +251,8 @@ describe("Iterable", () => { }); }); - describe("iterableConfig", () => { - it("should have default values", () => { + describe('iterableConfig', () => { + it('should have default values', () => { // GIVEN no parameters // WHEN config is initialized const config = new IterableConfig(); @@ -291,8 +291,8 @@ describe("Iterable", () => { }); }); - describe("urlHandler", () => { - it("should open the url when canOpenURL returns true and urlHandler returns false", async () => { + describe('urlHandler', () => { + it('should open the url when canOpenURL returns true and urlHandler returns false', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled); @@ -304,7 +304,7 @@ describe("Iterable", () => { return false; }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN canOpenUrl set to return a promise that resolves to true MockLinking.canOpenURL = jest.fn(async () => { return await new Promise((resolve) => { @@ -312,11 +312,11 @@ describe("Iterable", () => { }); }); MockLinking.openURL.mockReset(); - const expectedUrl = "https://somewhere.com"; - const actionDict = { type: "openUrl" }; + const expectedUrl = 'https://somewhere.com'; + const actionDict = { type: 'openUrl' }; const dict = { url: expectedUrl, - context: { action: actionDict, source: "inApp" }, + context: { action: actionDict, source: 'inApp' }, }; // WHEN handleUrlCalled event is emitted nativeEmitter.emit(IterableEventName.handleUrlCalled, dict); @@ -327,7 +327,7 @@ describe("Iterable", () => { }); }); - it("should not open the url when canOpenURL returns false and urlHandler returns false", async () => { + it('should not open the url when canOpenURL returns false and urlHandler returns false', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled); @@ -339,7 +339,7 @@ describe("Iterable", () => { return false; }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN canOpenUrl set to return a promise that resolves to false MockLinking.canOpenURL = jest.fn(async () => { return await new Promise((resolve) => { @@ -347,11 +347,11 @@ describe("Iterable", () => { }); }); MockLinking.openURL.mockReset(); - const expectedUrl = "https://somewhere.com"; - const actionDict = { type: "openUrl" }; + const expectedUrl = 'https://somewhere.com'; + const actionDict = { type: 'openUrl' }; const dict = { url: expectedUrl, - context: { action: actionDict, source: "inApp" }, + context: { action: actionDict, source: 'inApp' }, }; // WHEN handleUrlCalled event is emitted nativeEmitter.emit(IterableEventName.handleUrlCalled, dict); @@ -362,7 +362,7 @@ describe("Iterable", () => { }); }); - it("should not open the url when canOpenURL returns true and urlHandler returns true", async () => { + it('should not open the url when canOpenURL returns true and urlHandler returns true', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleUrlCalled); @@ -374,7 +374,7 @@ describe("Iterable", () => { return true; }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN canOpenUrl set to return a promise that resolves to true MockLinking.canOpenURL = jest.fn(async () => { return await new Promise((resolve) => { @@ -382,11 +382,11 @@ describe("Iterable", () => { }); }); MockLinking.openURL.mockReset(); - const expectedUrl = "https://somewhere.com"; - const actionDict = { type: "openUrl" }; + const expectedUrl = 'https://somewhere.com'; + const actionDict = { type: 'openUrl' }; const dict = { url: expectedUrl, - context: { action: actionDict, source: "inApp" }, + context: { action: actionDict, source: 'inApp' }, }; // WHEN handleUrlCalled event is emitted nativeEmitter.emit(IterableEventName.handleUrlCalled, dict); @@ -398,8 +398,8 @@ describe("Iterable", () => { }); }); - describe("customActionHandler", () => { - it("should be called with the correct action and context", () => { + describe('customActionHandler', () => { + it('should be called with the correct action and context', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners( @@ -415,10 +415,10 @@ describe("Iterable", () => { } ); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN custom action name and custom action data - const actionName = "zeeActionName"; - const actionData = "zeeActionData"; + const actionName = 'zeeActionName'; + const actionData = 'zeeActionData'; const actionDict = { type: actionName, data: actionData }; const actionSource = IterableActionSource.inApp; const dict = { @@ -440,10 +440,10 @@ describe("Iterable", () => { }); }); - describe("handleAppLink", () => { - it("should call IterableAPI.handleAppLink", () => { + describe('handleAppLink', () => { + it('should call IterableAPI.handleAppLink', () => { // GIVEN a link - const link = "https://somewhere.com/link/something"; + const link = 'https://somewhere.com/link/something'; // WHEN Iterable.handleAppLink is called Iterable.handleAppLink(link); // THEN corresponding function is called on RNITerableAPI @@ -451,8 +451,8 @@ describe("Iterable", () => { }); }); - describe("updateSubscriptions", () => { - it("should call IterableAPI.updateSubscriptions with the correct parameters", () => { + describe('updateSubscriptions', () => { + it('should call IterableAPI.updateSubscriptions with the correct parameters', () => { // GIVEN the following parameters const emailListIds = [1, 2, 3]; const unsubscribedChannelIds = [4, 5, 6]; @@ -481,10 +481,10 @@ describe("Iterable", () => { }); }); - describe("initialize", () => { - it("should call IterableAPI.initializeWithApiKey and save the config", async () => { + describe('initialize', () => { + it('should call IterableAPI.initializeWithApiKey and save the config', async () => { // GIVEN an API key and config - const apiKey = "test-api-key"; + const apiKey = 'test-api-key'; const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.logLevel = IterableLogLevel.debug; @@ -500,9 +500,9 @@ describe("Iterable", () => { expect(result).toBe(true); }); - it("should give the default config if no config is provided", async () => { + it('should give the default config if no config is provided', async () => { // GIVEN an API key - const apiKey = "test-api-key"; + const apiKey = 'test-api-key'; // WHEN Iterable.initialize is called const result = await Iterable.initialize(apiKey); // THEN corresponding function is called on RNIterableAPI and config is saved @@ -511,13 +511,13 @@ describe("Iterable", () => { }); }); - describe("initialize2", () => { - it("should call IterableAPI.initialize2WithApiKey with an endpoint and save the config", async () => { + describe('initialize2', () => { + it('should call IterableAPI.initialize2WithApiKey with an endpoint and save the config', async () => { // GIVEN an API key, config, and endpoint - const apiKey = "test-api-key"; + const apiKey = 'test-api-key'; const config = new IterableConfig(); config.logReactNativeSdkCalls = false; - const apiEndPoint = "https://api.staging.iterable.com"; + const apiEndPoint = 'https://api.staging.iterable.com'; // WHEN Iterable.initialize2 is called const result = await Iterable.initialize2(apiKey, config, apiEndPoint); // THEN corresponding function is called on RNIterableAPI and config is saved @@ -531,10 +531,10 @@ describe("Iterable", () => { expect(result).toBe(true); }); - it("should give the default config if no config is provided", async () => { + it('should give the default config if no config is provided', async () => { // GIVEN an API key - const apiKey = "test-api-key"; - const apiEndPoint = "https://api.staging.iterable.com"; + const apiKey = 'test-api-key'; + const apiEndPoint = 'https://api.staging.iterable.com'; // WHEN Iterable.initialize is called const result = await Iterable.initialize2(apiKey, undefined, apiEndPoint); // THEN corresponding function is called on RNIterableAPI and config is saved @@ -543,12 +543,12 @@ describe("Iterable", () => { }); }); - describe("wakeApp", () => { - it("should call IterableAPI.wakeApp on Android", () => { + describe('wakeApp', () => { + it('should call IterableAPI.wakeApp on Android', () => { // GIVEN Android platform const originalPlatform = Platform.OS; - Object.defineProperty(Platform, "OS", { - value: "android", + Object.defineProperty(Platform, 'OS', { + value: 'android', writable: true, }); // WHEN Iterable.wakeApp is called @@ -556,17 +556,17 @@ describe("Iterable", () => { // THEN corresponding function is called on RNIterableAPI expect(MockRNIterableAPI.wakeApp).toBeCalled(); // Restore original platform - Object.defineProperty(Platform, "OS", { + Object.defineProperty(Platform, 'OS', { value: originalPlatform, writable: true, }); }); - it("should not call IterableAPI.wakeApp on iOS", () => { + it('should not call IterableAPI.wakeApp on iOS', () => { // GIVEN iOS platform const originalPlatform = Platform.OS; - Object.defineProperty(Platform, "OS", { - value: "ios", + Object.defineProperty(Platform, 'OS', { + value: 'ios', writable: true, }); // WHEN Iterable.wakeApp is called @@ -574,18 +574,18 @@ describe("Iterable", () => { // THEN corresponding function is not called on RNIterableAPI expect(MockRNIterableAPI.wakeApp).not.toBeCalled(); // Restore original platform - Object.defineProperty(Platform, "OS", { + Object.defineProperty(Platform, 'OS', { value: originalPlatform, writable: true, }); }); }); - describe("trackInAppOpen", () => { - it("should call IterableAPI.trackInAppOpen with the correct parameters", () => { + describe('trackInAppOpen', () => { + it('should call IterableAPI.trackInAppOpen with the correct parameters', () => { // GIVEN an in-app message and location const message = new IterableInAppMessage( - "1234", + '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), @@ -607,11 +607,11 @@ describe("Iterable", () => { }); }); - describe("trackInAppClick", () => { - it("should call IterableAPI.trackInAppClick with the correct parameters", () => { + describe('trackInAppClick', () => { + it('should call IterableAPI.trackInAppClick with the correct parameters', () => { // GIVEN an in-app message, location, and clicked URL const message = new IterableInAppMessage( - "1234", + '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), @@ -623,7 +623,7 @@ describe("Iterable", () => { 0 ); const location = IterableInAppLocation.inApp; - const clickedUrl = "https://www.example.com"; + const clickedUrl = 'https://www.example.com'; // WHEN Iterable.trackInAppClick is called Iterable.trackInAppClick(message, location, clickedUrl); // THEN corresponding function is called on RNIterableAPI @@ -635,11 +635,11 @@ describe("Iterable", () => { }); }); - describe("trackInAppClose", () => { - it("should call IterableAPI.trackInAppClose with the correct parameters", () => { + describe('trackInAppClose', () => { + it('should call IterableAPI.trackInAppClose with the correct parameters', () => { // GIVEN an in-app message, location, and source (no URL) const message = new IterableInAppMessage( - "1234", + '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), @@ -663,10 +663,10 @@ describe("Iterable", () => { ); }); - it("should call IterableAPI.trackInAppClose with a clicked URL when provided", () => { + it('should call IterableAPI.trackInAppClose with a clicked URL when provided', () => { // GIVEN an in-app message, location, source, and clicked URL const message = new IterableInAppMessage( - "1234", + '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), @@ -679,7 +679,7 @@ describe("Iterable", () => { ); const location = IterableInAppLocation.inApp; const source = IterableInAppCloseSource.back; - const clickedUrl = "https://www.example.com"; + const clickedUrl = 'https://www.example.com'; // WHEN Iterable.trackInAppClose is called Iterable.trackInAppClose(message, location, source, clickedUrl); // THEN corresponding function is called on RNIterableAPI @@ -692,11 +692,11 @@ describe("Iterable", () => { }); }); - describe("inAppConsume", () => { - it("should call IterableAPI.inAppConsume with the correct parameters", () => { + describe('inAppConsume', () => { + it('should call IterableAPI.inAppConsume with the correct parameters', () => { // GIVEN an in-app message, location, and delete source const message = new IterableInAppMessage( - "1234", + '1234', 4567, new IterableInAppTrigger(IterableInAppTriggerType.immediate), new Date(), @@ -720,19 +720,19 @@ describe("Iterable", () => { }); }); - describe("getVersionFromPackageJson", () => { - it("should return the version from the package.json file", () => { + describe('getVersionFromPackageJson', () => { + it('should return the version from the package.json file', () => { // GIVEN no parameters // WHEN Iterable.getVersionFromPackageJson is called const version = Iterable.getVersionFromPackageJson(); // THEN a version string is returned - expect(typeof version).toBe("string"); + expect(typeof version).toBe('string'); expect(version.length).toBeGreaterThan(0); }); }); - describe("setupEventHandlers", () => { - it("should call inAppHandler when handleInAppCalled event is emitted", () => { + describe('setupEventHandlers', () => { + it('should call inAppHandler when handleInAppCalled event is emitted', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleInAppCalled); @@ -743,10 +743,10 @@ describe("Iterable", () => { return IterableInAppShowResponse.show; }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN message dictionary const messageDict = { - messageId: "1234", + messageId: '1234', campaignId: 4567, trigger: { type: 0 }, createdAt: new Date().toISOString(), @@ -768,8 +768,8 @@ describe("Iterable", () => { ); }); - describe("authHandler", () => { - it("should call authHandler when handleAuthCalled event is emitted", async () => { + describe('authHandler', () => { + it('should call authHandler when handleAuthCalled event is emitted', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); @@ -785,14 +785,14 @@ describe("Iterable", () => { const successCallback = jest.fn(); const failureCallback = jest.fn(); const authResponse = new IterableAuthResponse(); - authResponse.authToken = "test-token"; + authResponse.authToken = 'test-token'; authResponse.successCallback = successCallback; authResponse.failureCallback = failureCallback; config.authHandler = jest.fn(() => { return Promise.resolve(authResponse); }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN auth handler returns AuthResponse // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); @@ -801,14 +801,14 @@ describe("Iterable", () => { // THEN passAlongAuthToken is called with the token and success callback is called after timeout return await TestHelper.delayed(1100, () => { expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith( - "test-token" + 'test-token' ); expect(successCallback).toBeCalled(); expect(failureCallback).not.toBeCalled(); }); }); - it("should call authHandler when handleAuthFailureCalled event is emitted", async () => { + it('should call authHandler when handleAuthFailureCalled event is emitted', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); @@ -824,7 +824,7 @@ describe("Iterable", () => { const successCallback = jest.fn(); const failureCallback = jest.fn(); const authResponse = new IterableAuthResponse(); - authResponse.authToken = "test-token"; + authResponse.authToken = 'test-token'; authResponse.successCallback = successCallback; authResponse.failureCallback = failureCallback; config.authHandler = jest.fn(() => { @@ -832,7 +832,7 @@ describe("Iterable", () => { return Promise.resolve(authResponse); }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN auth handler returns AuthResponse // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); @@ -841,14 +841,14 @@ describe("Iterable", () => { // THEN passAlongAuthToken is called with the token and failure callback is called after timeout return await TestHelper.delayed(1100, () => { expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith( - "test-token" + 'test-token' ); expect(failureCallback).toBeCalled(); expect(successCallback).not.toBeCalled(); }); }); - it("should call authHandler when handleAuthCalled event is emitted and returns a string token", async () => { + it('should call authHandler when handleAuthCalled event is emitted and returns a string token', async () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); @@ -856,22 +856,22 @@ describe("Iterable", () => { const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.authHandler = jest.fn(() => { - return Promise.resolve("string-token"); + return Promise.resolve('string-token'); }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN auth handler returns string token // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); // THEN passAlongAuthToken is called with the string token return await TestHelper.delayed(100, () => { expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith( - "string-token" + 'string-token' ); }); }); - it("should call authHandler when handleAuthCalled event is emitted and returns an unexpected response", () => { + it('should call authHandler when handleAuthCalled event is emitted and returns an unexpected response', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); @@ -879,12 +879,12 @@ describe("Iterable", () => { const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.authHandler = jest.fn(() => { - return Promise.resolve({ unexpected: "object" } as unknown as + return Promise.resolve({ unexpected: 'object' } as unknown as | string | IterableAuthResponse); }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN auth handler returns unexpected response // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); @@ -892,7 +892,7 @@ describe("Iterable", () => { expect(MockRNIterableAPI.passAlongAuthToken).not.toBeCalled(); }); - it("should call authHandler when handleAuthCalled event is emitted and rejects the promise", () => { + it('should call authHandler when handleAuthCalled event is emitted and rejects the promise', () => { // sets up event emitter const nativeEmitter = new NativeEventEmitter(); nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled); @@ -900,10 +900,10 @@ describe("Iterable", () => { const config = new IterableConfig(); config.logReactNativeSdkCalls = false; config.authHandler = jest.fn(() => { - return Promise.reject(new Error("Auth failed")); + return Promise.reject(new Error('Auth failed')); }); // initialize Iterable object - Iterable.initialize("apiKey", config); + Iterable.initialize('apiKey', config); // GIVEN auth handler rejects promise // WHEN handleAuthCalled event is emitted nativeEmitter.emit(IterableEventName.handleAuthCalled); From 71ac5c4d2826e28e0545bc599d3e6ab58a15d353 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 10:20:09 -0700 Subject: [PATCH 10/82] docs: add better comments to IterableAuthManager --- src/core/classes/IterableAuthManager.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/classes/IterableAuthManager.ts b/src/core/classes/IterableAuthManager.ts index 6ad93f689..9df15c03a 100644 --- a/src/core/classes/IterableAuthManager.ts +++ b/src/core/classes/IterableAuthManager.ts @@ -6,9 +6,7 @@ import { IterableAuthResponse } from './IterableAuthResponse'; * * @example * ```typescript - * const config = new IterableConfig(); - * const logger = new IterableLogger(config); - * const authManager = new IterableAuthManager(logger); + * const authManager = new IterableAuthManager(); * ``` */ export class IterableAuthManager { @@ -31,6 +29,12 @@ export class IterableAuthManager { * Pass along an auth token to the SDK. * * @param authToken - The auth token to pass along + * + * @example + * ```typescript + * const authManager = new IterableAuthManager(); + * authManager.passAlongAuthToken(MY_AUTH_TOKEN); + * ``` */ passAlongAuthToken( authToken: string | null | undefined From d95ae90d0170dcd5a950966c390492c2fc7c4ef8 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 10:24:19 -0700 Subject: [PATCH 11/82] fix: standardize authentication failure reason representation across platforms --- src/core/enums/IterableAuthFailureReason.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/enums/IterableAuthFailureReason.ts b/src/core/enums/IterableAuthFailureReason.ts index a61f7fa7e..51c610c4f 100644 --- a/src/core/enums/IterableAuthFailureReason.ts +++ b/src/core/enums/IterableAuthFailureReason.ts @@ -2,6 +2,10 @@ * The reason for the failure of an authentication attempt. * * This is generally related to JWT token validation. + * + * FIXME: Android returns the string (EG: `'AUTH_TOKEN_EXPIRATION_INVALID'`), + * but iOS returns the enum value (EG: `0`). These should be standardized so + * that they both return the same type on either platform. */ export enum IterableAuthFailureReason { /** From 5667794edce63d194123a5e1edc7c8e74ca3ec9a Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 14:44:55 -0700 Subject: [PATCH 12/82] fix: update project.pbxproj to correct file references and improve build configuration settings --- .../project.pbxproj | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 2bf23431b..f00c4a8a8 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -9,10 +9,10 @@ /* Begin PBXBuildFile section */ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 6F9115D0765337926C434A5A /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5A9EB6D281A44211C67E972 /* libPods-ReactNativeSdkExample.a */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; - CC7C0C660DB585466CC95446 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D7C71B2515F0E53180477AEC /* libPods-ReactNativeSdkExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -29,18 +29,18 @@ 00E356EE1AD99517003FC87E /* ReactNativeSdkExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactNativeSdkExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactNativeSdkExampleTests.m; sourceTree = ""; }; - 054F9627BFE1F378023F2570 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; + 022D97CC9F8308D0FB66F0F3 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* ReactNativeSdkExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactNativeSdkExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeSdkExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 2BE74655C68E80463F6CD81B /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; + 29B3DE16F9D8D26016B6194D /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; - D7C71B2515F0E53180477AEC /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + E5A9EB6D281A44211C67E972 /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -56,7 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CC7C0C660DB585466CC95446 /* libPods-ReactNativeSdkExample.a in Frameworks */, + 6F9115D0765337926C434A5A /* libPods-ReactNativeSdkExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,7 +99,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - D7C71B2515F0E53180477AEC /* libPods-ReactNativeSdkExample.a */, + E5A9EB6D281A44211C67E972 /* libPods-ReactNativeSdkExample.a */, ); name = Frameworks; sourceTree = ""; @@ -138,8 +138,8 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 054F9627BFE1F378023F2570 /* Pods-ReactNativeSdkExample.debug.xcconfig */, - 2BE74655C68E80463F6CD81B /* Pods-ReactNativeSdkExample.release.xcconfig */, + 29B3DE16F9D8D26016B6194D /* Pods-ReactNativeSdkExample.debug.xcconfig */, + 022D97CC9F8308D0FB66F0F3 /* Pods-ReactNativeSdkExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -169,13 +169,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - 787BEB56F90C9C0AEE4C88D5 /* [CP] Check Pods Manifest.lock */, + 9387C8F6AAA2060085FB74FB /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 152370F00B0C82FBF20ABDA2 /* [CP] Embed Pods Frameworks */, - 6099F4827CE15646F9A0205B /* [CP] Copy Pods Resources */, + E5BD57BE40CF3045494B4994 /* [CP] Embed Pods Frameworks */, + 0AE3C6D0BFADD5B0DDF5BC6C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -260,24 +260,7 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 152370F00B0C82FBF20ABDA2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 6099F4827CE15646F9A0205B /* [CP] Copy Pods Resources */ = { + 0AE3C6D0BFADD5B0DDF5BC6C /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -294,7 +277,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; - 787BEB56F90C9C0AEE4C88D5 /* [CP] Check Pods Manifest.lock */ = { + 9387C8F6AAA2060085FB74FB /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -316,6 +299,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + E5BD57BE40CF3045494B4994 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -406,7 +406,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 054F9627BFE1F378023F2570 /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = 29B3DE16F9D8D26016B6194D /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -436,7 +436,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2BE74655C68E80463F6CD81B /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = 022D97CC9F8308D0FB66F0F3 /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; From 35b74d4f2c87fddb3f00105a6582c4d7025231d4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 15:16:24 -0700 Subject: [PATCH 13/82] feat: add syncEmbeddedMessages method to ReactIterableAPI and expose it in RNIterableAPI.mm --- ios/RNIterableAPI/RNIterableAPI.mm | 8 ++++++++ ios/RNIterableAPI/ReactIterableAPI.swift | 7 +++++++ ios/RNIterableAPI/Serialization.swift | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index 91955f797..f65e2e60a 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -277,6 +277,10 @@ - (void)pauseAuthRetries:(BOOL)pauseRetry { [_swiftAPI pauseAuthRetries:pauseRetry]; } +- (void)syncEmbeddedMessages { + [_swiftAPI syncEmbeddedMessages]; +} + - (void)wakeApp { // Placeholder function -- this method is only used in Android } @@ -507,6 +511,10 @@ - (void)wakeApp { [_swiftAPI pauseAuthRetries:pauseRetry]; } +RCT_EXPORT_METHOD(syncEmbeddedMessages) { + [_swiftAPI syncEmbeddedMessages]; +} + RCT_EXPORT_METHOD(wakeApp) { // Placeholder function -- this method is only used in Android } diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index f04b08e42..b13e1cd6d 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -490,6 +490,12 @@ import React IterableAPI.pauseAuthRetries(pauseRetry) } + @objc(syncEmbeddedMessages) + public func syncEmbeddedMessages() { + ITBInfo() + IterableAPI.embeddedManager.syncMessages(completion: {}) + } + // MARK: Private private var shouldEmit = false private let _methodQueue = DispatchQueue(label: String(describing: ReactIterableAPI.self)) @@ -554,6 +560,7 @@ import React apiEndPointOverride: apiEndPointOverride ) { result in resolver(result) + IterableAPI.embeddedManager.syncMessages(completion: {}) } IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version) diff --git a/ios/RNIterableAPI/Serialization.swift b/ios/RNIterableAPI/Serialization.swift index 478262924..439a1151d 100644 --- a/ios/RNIterableAPI/Serialization.swift +++ b/ios/RNIterableAPI/Serialization.swift @@ -94,9 +94,15 @@ extension IterableConfig { } } + if let enableEmbeddedMessaging = dict["enableEmbeddedMesssaging"] as? Bool { + config.enableEmbeddedMessaging = enableEmbeddedMessaging + } + return config } + + private static func createLogDelegate(logLevelNumber: NSNumber) -> IterableLogDelegate { DefaultLogDelegate(minLogLevel: LogLevel.from(number: logLevelNumber)) } From 0e8b24c91213a0b1b209e25488487263330bd228 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 13 Oct 2025 16:07:55 -0700 Subject: [PATCH 14/82] feat: implement getEmbeddedMessages method. not working -- giving back empty array --- example/src/components/Embedded/Embedded.tsx | 24 +++-- ios/RNIterableAPI/RNIterableAPI.mm | 11 +++ ios/RNIterableAPI/ReactIterableAPI.swift | 31 +++++++ ios/RNIterableAPI/Serialization.swift | 93 ++++++++++++++++++++ 4 files changed, 150 insertions(+), 9 deletions(-) diff --git a/example/src/components/Embedded/Embedded.tsx b/example/src/components/Embedded/Embedded.tsx index 68b748048..64c31a65e 100644 --- a/example/src/components/Embedded/Embedded.tsx +++ b/example/src/components/Embedded/Embedded.tsx @@ -9,7 +9,7 @@ import { import styles from './Embedded.styles'; export const Embedded = () => { - const [placementIds, setPlacementIds] = useState([]); + const [placementIds] = useState([10, 2112]); const [embeddedMessages, setEmbeddedMessages] = useState< IterableEmbeddedMessage[] >([]); @@ -19,11 +19,11 @@ export const Embedded = () => { }, []); const getPlacementIds = useCallback(() => { - return Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { - console.log(ids); - setPlacementIds(ids as number[]); - return ids; - }); + // return Iterable.embeddedManager.getPlacementIds().then((ids: unknown) => { + // console.log(ids); + // setPlacementIds(ids as number[]); + // return ids; + // }); }, []); const startEmbeddedSession = useCallback(() => { @@ -41,13 +41,19 @@ export const Embedded = () => { }, []); const getEmbeddedMessages = useCallback(() => { - getPlacementIds() - .then((ids: number[]) => Iterable.embeddedManager.getMessages(ids)) + Iterable.embeddedManager + .getMessages(placementIds) .then((messages: IterableEmbeddedMessage[]) => { setEmbeddedMessages(messages); console.log(messages); }); - }, [getPlacementIds]); + // getPlacementIds() + // .then((ids: number[]) => Iterable.embeddedManager.getMessages(ids)) + // .then((messages: IterableEmbeddedMessage[]) => { + // setEmbeddedMessages(messages); + // console.log(messages); + // }); + }, [placementIds]); const startEmbeddedImpression = useCallback( (message: IterableEmbeddedMessage) => { diff --git a/ios/RNIterableAPI/RNIterableAPI.mm b/ios/RNIterableAPI/RNIterableAPI.mm index f65e2e60a..a61e1329f 100644 --- a/ios/RNIterableAPI/RNIterableAPI.mm +++ b/ios/RNIterableAPI/RNIterableAPI.mm @@ -281,6 +281,12 @@ - (void)syncEmbeddedMessages { [_swiftAPI syncEmbeddedMessages]; } +- (void)getEmbeddedMessages:(NSArray *_Nullable)placementIds + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + [_swiftAPI getEmbeddedMessages:placementIds resolver:resolve rejecter:reject]; +} + - (void)wakeApp { // Placeholder function -- this method is only used in Android } @@ -515,6 +521,11 @@ - (void)wakeApp { [_swiftAPI syncEmbeddedMessages]; } +RCT_EXPORT_METHOD(getEmbeddedMessages : (NSArray *_Nullable)placementIds resolve : (RCTPromiseResolveBlock) + resolve reject : (RCTPromiseRejectBlock)reject) { + [_swiftAPI getEmbeddedMessages:placementIds resolver:resolve rejecter:reject]; +} + RCT_EXPORT_METHOD(wakeApp) { // Placeholder function -- this method is only used in Android } diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index b13e1cd6d..b88710ebd 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -496,6 +496,37 @@ import React IterableAPI.embeddedManager.syncMessages(completion: {}) } + @objc(getEmbeddedMessages:resolver:rejecter:) + public func getEmbeddedMessages( + placementIds: [NSNumber]?, + resolver: RCTPromiseResolveBlock, + rejecter: RCTPromiseRejectBlock + ) { + ITBInfo() + ITBInfo("getEmbeddedMessages called with placementIds: \(String(describing: placementIds))") + var allMessages: [IterableEmbeddedMessage] = [] + + if let placementIds = placementIds, !placementIds.isEmpty { + // Get messages for each specified placement ID + ITBInfo("Getting messages for \(placementIds.count) placement IDs") + for placementId in placementIds { + ITBInfo("Getting messages for placement ID: \(placementId.intValue)") + let messages = IterableAPI.embeddedManager.getMessages(for: placementId.intValue) + ITBInfo("Found \(messages.count) messages for placement ID: \(placementId.intValue)") + allMessages.append(contentsOf: messages) + } + } else { + // Get messages for all placements by getting placement IDs first + ITBInfo("Getting all messages (no placement IDs specified)") + let messages = IterableAPI.embeddedManager.getMessages() + ITBInfo("Found \(messages.count) total messages") + allMessages.append(contentsOf: messages) + } + + ITBInfo("Returning \(allMessages.count) total embedded messages") + resolver(allMessages.map { $0.toDict() }) + } + // MARK: Private private var shouldEmit = false private let _methodQueue = DispatchQueue(label: String(describing: ReactIterableAPI.self)) diff --git a/ios/RNIterableAPI/Serialization.swift b/ios/RNIterableAPI/Serialization.swift index 439a1151d..c95f7b42e 100644 --- a/ios/RNIterableAPI/Serialization.swift +++ b/ios/RNIterableAPI/Serialization.swift @@ -273,3 +273,96 @@ extension InboxImpressionTracker.RowInfo { return rows.compactMap(InboxImpressionTracker.RowInfo.from(dict:)) } } + +extension IterableEmbeddedMessage { + func toDict() -> [AnyHashable: Any] { + var dict = [AnyHashable: Any]() + + // Metadata + var metadata = [AnyHashable: Any]() + metadata["messageId"] = self.metadata.messageId + metadata["placementId"] = self.metadata.placementId + if let campaignId = self.metadata.campaignId { + metadata["campaignId"] = campaignId + } + if let isProof = self.metadata.isProof { + metadata["isProof"] = isProof + } + dict["metadata"] = metadata + + // Elements + if let elements = self.elements { + var elementsDict = [AnyHashable: Any]() + + if let title = elements.title { + elementsDict["title"] = title + } + + if let body = elements.body { + elementsDict["body"] = body + } + + if let mediaUrl = elements.mediaUrl { + elementsDict["mediaUrl"] = mediaUrl + } + + if let mediaUrlCaption = elements.mediaUrlCaption { + elementsDict["mediaUrlCaption"] = mediaUrlCaption + } + + if let defaultAction = elements.defaultAction { + var actionDict = [AnyHashable: Any]() + actionDict["type"] = defaultAction.type + if let data = defaultAction.data { + actionDict["data"] = data + } + elementsDict["defaultAction"] = actionDict + } + + if let buttons = elements.buttons { + var buttonsArray = [[AnyHashable: Any]]() + for button in buttons { + var buttonDict = [AnyHashable: Any]() + buttonDict["id"] = button.id + if let title = button.title { + buttonDict["title"] = title + } + if let action = button.action { + var actionDict = [AnyHashable: Any]() + actionDict["type"] = action.type + if let data = action.data { + actionDict["data"] = data + } + buttonDict["action"] = actionDict + } else { + buttonDict["action"] = NSNull() + } + buttonsArray.append(buttonDict) + } + elementsDict["buttons"] = buttonsArray + } + + if let text = elements.text { + var textArray = [[AnyHashable: Any]]() + for textElement in text { + var textDict = [AnyHashable: Any]() + textDict["id"] = textElement.id + if let textValue = textElement.text { + textDict["text"] = textValue + } + textArray.append(textDict) + } + elementsDict["text"] = textArray + } + + dict["elements"] = elementsDict + } + + // Payload + if let payload = self.payload { + dict["payload"] = payload + } + + return dict + } +} From b7d638994ded4468e44fb5b0210f424243cf4158 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 12:44:52 -0700 Subject: [PATCH 15/82] fix: add missing commas in IterableHtmlInAppContent test cases for consistency --- .../classes/IterableHtmlInAppContent.test.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/inApp/classes/IterableHtmlInAppContent.test.ts b/src/inApp/classes/IterableHtmlInAppContent.test.ts index aed6af5d1..3a836472a 100644 --- a/src/inApp/classes/IterableHtmlInAppContent.test.ts +++ b/src/inApp/classes/IterableHtmlInAppContent.test.ts @@ -107,9 +107,9 @@ describe('IterableHtmlInAppContent', () => { top: 10, left: 20, bottom: 30, - right: 40 + right: 40, }, - html: '
Hello World
' + html: '
Hello World
', }; // WHEN creating from dictionary @@ -131,9 +131,9 @@ describe('IterableHtmlInAppContent', () => { top: 0, left: 0, bottom: 0, - right: 0 + right: 0, }, - html: '' + html: '', }; // WHEN creating from dictionary @@ -155,7 +155,7 @@ describe('IterableHtmlInAppContent', () => { top: 5, left: 10, bottom: 15, - right: 20 + right: 20, }, html: ` @@ -167,7 +167,7 @@ describe('IterableHtmlInAppContent', () => { - ` + `, }; // WHEN creating from dictionary @@ -189,9 +189,9 @@ describe('IterableHtmlInAppContent', () => { top: -5, left: -10, bottom: -15, - right: -20 + right: -20, }, - html: '
Negative insets
' + html: '
Negative insets
', }; // WHEN creating from dictionary @@ -213,9 +213,9 @@ describe('IterableHtmlInAppContent', () => { top: 1.5, left: 2.7, bottom: 3.9, - right: 4.1 + right: 4.1, }, - html: '
Decimal insets
' + html: '
Decimal insets
', }; // WHEN creating from dictionary @@ -358,9 +358,9 @@ describe('IterableHtmlInAppContent', () => { top: NaN, left: NaN, bottom: NaN, - right: NaN + right: NaN, }, - html: '
NaN insets
' + html: '
NaN insets
', }; // WHEN creating from dictionary @@ -382,9 +382,9 @@ describe('IterableHtmlInAppContent', () => { top: Infinity, left: -Infinity, bottom: Infinity, - right: -Infinity + right: -Infinity, }, - html: '
Infinity insets
' + html: '
Infinity insets
', }; // WHEN creating from dictionary From 0191b7bb6f9e68efa31a61036cf8af9b56f46386 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 13:59:01 -0700 Subject: [PATCH 16/82] chore: removed onTokenRegistrationFailed method as per PR comment --- .../com/iterable/reactnative/RNIterableAPIModuleImpl.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 3207bb5dc..57cf9a0b8 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -600,11 +600,6 @@ public void onTokenRegistrationSuccessful(String authToken) { sendEvent(EventName.handleAuthSuccessCalled.name(), null); } - public void onTokenRegistrationFailed(Throwable object) { - IterableLogger.v(TAG, "Failed to set authToken"); - sendEvent(EventName.handleAuthFailureCalled.name(), null); - } - public void addListener(String eventName) { // Keep: Required for RN built in Event Emitter Calls. } From 70fa36a2a8f065255e4bc4f90a9a6724d1fd0fae Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 15:15:06 -0700 Subject: [PATCH 17/82] feat: implement retry policy configuration in IterableConfig for iOS --- .../project.pbxproj | 58 +++++++++---------- ios/RNIterableAPI/Serialization.swift | 12 ++++ 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 2bf23431b..74e4dc4c9 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -11,8 +11,8 @@ 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; + 81F6A9EA0E1CCC1AD730C5D9 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56080B9DEED42A97AD1B3D5C /* libPods-ReactNativeSdkExample.a */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; - CC7C0C660DB585466CC95446 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D7C71B2515F0E53180477AEC /* libPods-ReactNativeSdkExample.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -29,18 +29,18 @@ 00E356EE1AD99517003FC87E /* ReactNativeSdkExampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReactNativeSdkExampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactNativeSdkExampleTests.m; sourceTree = ""; }; - 054F9627BFE1F378023F2570 /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* ReactNativeSdkExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ReactNativeSdkExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ReactNativeSdkExample/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ReactNativeSdkExample/Info.plist; sourceTree = ""; }; 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = ReactNativeSdkExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; - 2BE74655C68E80463F6CD81B /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; + 3A95ED4563D4389808EDEA8F /* Pods-ReactNativeSdkExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.debug.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.debug.xcconfig"; sourceTree = ""; }; + 56080B9DEED42A97AD1B3D5C /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; - D7C71B2515F0E53180477AEC /* libPods-ReactNativeSdkExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ReactNativeSdkExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + EA19B65827A1D757CC5AAC97 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; /* End PBXFileReference section */ @@ -56,7 +56,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - CC7C0C660DB585466CC95446 /* libPods-ReactNativeSdkExample.a in Frameworks */, + 81F6A9EA0E1CCC1AD730C5D9 /* libPods-ReactNativeSdkExample.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -99,7 +99,7 @@ isa = PBXGroup; children = ( ED297162215061F000B7C4FE /* JavaScriptCore.framework */, - D7C71B2515F0E53180477AEC /* libPods-ReactNativeSdkExample.a */, + 56080B9DEED42A97AD1B3D5C /* libPods-ReactNativeSdkExample.a */, ); name = Frameworks; sourceTree = ""; @@ -138,8 +138,8 @@ BBD78D7AC51CEA395F1C20DB /* Pods */ = { isa = PBXGroup; children = ( - 054F9627BFE1F378023F2570 /* Pods-ReactNativeSdkExample.debug.xcconfig */, - 2BE74655C68E80463F6CD81B /* Pods-ReactNativeSdkExample.release.xcconfig */, + 3A95ED4563D4389808EDEA8F /* Pods-ReactNativeSdkExample.debug.xcconfig */, + EA19B65827A1D757CC5AAC97 /* Pods-ReactNativeSdkExample.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -169,13 +169,13 @@ isa = PBXNativeTarget; buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ReactNativeSdkExample" */; buildPhases = ( - 787BEB56F90C9C0AEE4C88D5 /* [CP] Check Pods Manifest.lock */, + B07642200E1BCDE7A80934E9 /* [CP] Check Pods Manifest.lock */, 13B07F871A680F5B00A75B9A /* Sources */, 13B07F8C1A680F5B00A75B9A /* Frameworks */, 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, - 152370F00B0C82FBF20ABDA2 /* [CP] Embed Pods Frameworks */, - 6099F4827CE15646F9A0205B /* [CP] Copy Pods Resources */, + 756F1571292F7FB66FB0F625 /* [CP] Embed Pods Frameworks */, + C5D9D662E100C568A4F9922D /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -260,7 +260,7 @@ shellPath = /bin/sh; shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; }; - 152370F00B0C82FBF20ABDA2 /* [CP] Embed Pods Frameworks */ = { + 756F1571292F7FB66FB0F625 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -277,43 +277,43 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 6099F4827CE15646F9A0205B /* [CP] Copy Pods Resources */ = { + B07642200E1BCDE7A80934E9 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Copy Pods Resources"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 787BEB56F90C9C0AEE4C88D5 /* [CP] Check Pods Manifest.lock */ = { + C5D9D662E100C568A4F9922D /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ReactNativeSdkExample-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -406,7 +406,7 @@ }; 13B07F941A680F5B00A75B9A /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 054F9627BFE1F378023F2570 /* Pods-ReactNativeSdkExample.debug.xcconfig */; + baseConfigurationReference = 3A95ED4563D4389808EDEA8F /* Pods-ReactNativeSdkExample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; @@ -436,7 +436,7 @@ }; 13B07F951A680F5B00A75B9A /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 2BE74655C68E80463F6CD81B /* Pods-ReactNativeSdkExample.release.xcconfig */; + baseConfigurationReference = EA19B65827A1D757CC5AAC97 /* Pods-ReactNativeSdkExample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; diff --git a/ios/RNIterableAPI/Serialization.swift b/ios/RNIterableAPI/Serialization.swift index 478262924..3f837ab2c 100644 --- a/ios/RNIterableAPI/Serialization.swift +++ b/ios/RNIterableAPI/Serialization.swift @@ -94,6 +94,18 @@ extension IterableConfig { } } + if let retryPolicyDict = dict["retryPolicy"] as? [AnyHashable: Any] { + if let maxRetry = retryPolicyDict["maxRetry"] as? Int, + let retryInterval = retryPolicyDict["retryInterval"] as? TimeInterval, + let retryBackoffString = retryPolicyDict["retryBackoff"] as? String + { + let retryBackoffType: RetryPolicy.BackoffType = + retryBackoffString == "EXPONENTIAL" ? .exponential : .linear + config.retryPolicy = RetryPolicy( + maxRetry: maxRetry, retryInterval: retryInterval, retryBackoff: retryBackoffType) + } + } + return config } From 23d719a54f1e456db8c60060b08cbe6dc3ea23b7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 16:45:30 -0700 Subject: [PATCH 18/82] test: add tests to IterableApi --- src/__mocks__/MockRNIterableAPI.ts | 50 +- src/core/classes/IterableApi.test.ts | 1132 ++++++++++++++++++++++++++ 2 files changed, 1167 insertions(+), 15 deletions(-) create mode 100644 src/core/classes/IterableApi.test.ts diff --git a/src/__mocks__/MockRNIterableAPI.ts b/src/__mocks__/MockRNIterableAPI.ts index 1949c15bf..c7f325677 100644 --- a/src/__mocks__/MockRNIterableAPI.ts +++ b/src/__mocks__/MockRNIterableAPI.ts @@ -16,10 +16,10 @@ export class MockRNIterableAPI { }); } - static setEmail(email: string, authToken?: string): void { + static setEmail = jest.fn((email: string, authToken?: string): void => { MockRNIterableAPI.email = email; MockRNIterableAPI.token = authToken; - } + }); static async getUserId(): Promise { return await new Promise((resolve) => { @@ -27,10 +27,10 @@ export class MockRNIterableAPI { }); } - static setUserId(userId: string, authToken?: string): void { + static setUserId = jest.fn((userId: string, authToken?: string): void => { MockRNIterableAPI.userId = userId; MockRNIterableAPI.token = authToken; - } + }); static disableDeviceForCurrentUser = jest.fn(); @@ -62,9 +62,11 @@ export class MockRNIterableAPI { }); } - static setAttributionInfo(attributionInfo?: IterableAttributionInfo): void { - MockRNIterableAPI.attributionInfo = attributionInfo; - } + static setAttributionInfo = jest.fn( + (attributionInfo?: IterableAttributionInfo): void => { + MockRNIterableAPI.attributionInfo = attributionInfo; + } + ); static initializeWithApiKey = jest.fn().mockResolvedValue(true); @@ -86,14 +88,16 @@ export class MockRNIterableAPI { static setAutoDisplayPaused = jest.fn(); - static async showMessage( - _message: IterableInAppMessage, - _consume: boolean - ): Promise { - return await new Promise((resolve) => { - resolve(MockRNIterableAPI.clickedUrl); - }); - } + static showMessage = jest.fn( + async ( + _messageId: string, + _consume: boolean + ): Promise => { + return await new Promise((resolve) => { + resolve(MockRNIterableAPI.clickedUrl); + }); + } + ); static removeMessage = jest.fn(); @@ -109,6 +113,22 @@ export class MockRNIterableAPI { static updateSubscriptions = jest.fn(); + static getInboxMessages = jest.fn( + async (): Promise => { + return await new Promise((resolve) => { + resolve(MockRNIterableAPI.messages); + }); + } + ); + + static startSession = jest.fn(); + + static endSession = jest.fn(); + + static updateVisibleRows = jest.fn(); + + static getHtmlInAppContentForMessage = jest.fn(); + // set messages function is to set the messages static property // this is for testing purposes only static setMessages(messages: IterableInAppMessage[]): void { diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts new file mode 100644 index 000000000..ee41c3784 --- /dev/null +++ b/src/core/classes/IterableApi.test.ts @@ -0,0 +1,1132 @@ +import { Platform } from 'react-native'; + +import { MockRNIterableAPI } from '../../__mocks__/MockRNIterableAPI'; +import { IterableApi } from './IterableApi'; +import { IterableConfig } from './IterableConfig'; +import { IterableAttributionInfo } from './IterableAttributionInfo'; +import { IterableCommerceItem } from './IterableCommerceItem'; +import { IterableInAppMessage } from '../../inApp/classes/IterableInAppMessage'; +import { IterableInAppTrigger } from '../../inApp/classes/IterableInAppTrigger'; +import { IterableInAppTriggerType } from '../../inApp/enums/IterableInAppTriggerType'; +import { IterableInAppLocation } from '../../inApp/enums/IterableInAppLocation'; +import { IterableInAppCloseSource } from '../../inApp/enums/IterableInAppCloseSource'; +import { IterableInAppDeleteSource } from '../../inApp/enums/IterableInAppDeleteSource'; +import { IterableInAppShowResponse } from '../../inApp/enums/IterableInAppShowResponse'; +import { type IterableInboxImpressionRowInfo } from '../../inbox/types/IterableInboxImpressionRowInfo'; + +// Mock the RNIterableAPI module +jest.mock('../../api', () => ({ + __esModule: true, + default: MockRNIterableAPI, +})); + +describe('IterableApi', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // ====================================================== // + // ===================== INITIALIZE ===================== // + // ====================================================== // + + describe('initializeWithApiKey', () => { + it('should call RNIterableAPI.initializeWithApiKey with correct parameters', async () => { + // GIVEN an API key, config, and version + const apiKey = 'test-api-key'; + const config = new IterableConfig(); + const version = '1.0.0'; + + // WHEN initializeWithApiKey is called + const result = await IterableApi.initializeWithApiKey(apiKey, { + config, + version, + }); + + // THEN RNIterableAPI.initializeWithApiKey is called with correct parameters + expect(MockRNIterableAPI.initializeWithApiKey).toBeCalledWith( + apiKey, + config.toDict(), + version + ); + expect(result).toBe(true); + }); + + it('should use default config when not provided', async () => { + // GIVEN an API key and version + const apiKey = 'test-api-key'; + const version = '1.0.0'; + + // WHEN initializeWithApiKey is called without config + const result = await IterableApi.initializeWithApiKey(apiKey, { + config: new IterableConfig(), + version, + }); + + // THEN RNIterableAPI.initializeWithApiKey is called with default config + expect(MockRNIterableAPI.initializeWithApiKey).toBeCalledWith( + apiKey, + expect.any(Object), + version + ); + expect(result).toBe(true); + }); + }); + + describe('initialize2WithApiKey', () => { + it('should call RNIterableAPI.initialize2WithApiKey with correct parameters', async () => { + // GIVEN an API key, config, version, and endpoint + const apiKey = 'test-api-key'; + const config = new IterableConfig(); + const version = '1.0.0'; + const apiEndPoint = 'https://api.staging.iterable.com'; + + // WHEN initialize2WithApiKey is called + const result = await IterableApi.initialize2WithApiKey(apiKey, { + config, + version, + apiEndPoint, + }); + + // THEN RNIterableAPI.initialize2WithApiKey is called with correct parameters + expect(MockRNIterableAPI.initialize2WithApiKey).toBeCalledWith( + apiKey, + config.toDict(), + version, + apiEndPoint + ); + expect(result).toBe(true); + }); + + it('should use default config when not provided', async () => { + // GIVEN an API key, version, and endpoint + const apiKey = 'test-api-key'; + const version = '1.0.0'; + const apiEndPoint = 'https://api.staging.iterable.com'; + + // WHEN initialize2WithApiKey is called without config + const result = await IterableApi.initialize2WithApiKey(apiKey, { + version, + apiEndPoint, + config: new IterableConfig(), + }); + + // THEN RNIterableAPI.initialize2WithApiKey is called with default config + expect(MockRNIterableAPI.initialize2WithApiKey).toBeCalledWith( + apiKey, + expect.any(Object), + version, + apiEndPoint + ); + expect(result).toBe(true); + }); + }); + + // ====================================================== // + // ===================== USER MANAGEMENT ================ // + // ====================================================== // + + describe('setEmail', () => { + it('should call RNIterableAPI.setEmail with email only', () => { + // GIVEN an email + const email = 'user@example.com'; + + // WHEN setEmail is called + IterableApi.setEmail(email); + + // THEN RNIterableAPI.setEmail is called with email + expect(MockRNIterableAPI.setEmail).toBeCalledWith(email, undefined); + }); + + it('should call RNIterableAPI.setEmail with email and auth token', () => { + // GIVEN an email and auth token + const email = 'user@example.com'; + const authToken = 'jwt-token'; + + // WHEN setEmail is called + IterableApi.setEmail(email, authToken); + + // THEN RNIterableAPI.setEmail is called with email and auth token + expect(MockRNIterableAPI.setEmail).toBeCalledWith(email, authToken); + }); + + it('should call RNIterableAPI.setEmail with null email', () => { + // GIVEN null email + const email = null; + + // WHEN setEmail is called + IterableApi.setEmail(email); + + // THEN RNIterableAPI.setEmail is called with null email + expect(MockRNIterableAPI.setEmail).toBeCalledWith(null, undefined); + }); + }); + + describe('getEmail', () => { + it('should return the email from RNIterableAPI', async () => { + // GIVEN a mock email + const expectedEmail = 'user@example.com'; + MockRNIterableAPI.email = expectedEmail; + + // WHEN getEmail is called + const result = await IterableApi.getEmail(); + + // THEN the email is returned + expect(result).toBe(expectedEmail); + }); + }); + + describe('setUserId', () => { + it('should call RNIterableAPI.setUserId with userId only', () => { + // GIVEN a userId + const userId = 'user123'; + + // WHEN setUserId is called + IterableApi.setUserId(userId); + + // THEN RNIterableAPI.setUserId is called with userId + expect(MockRNIterableAPI.setUserId).toBeCalledWith(userId, undefined); + }); + + it('should call RNIterableAPI.setUserId with userId and auth token', () => { + // GIVEN a userId and auth token + const userId = 'user123'; + const authToken = 'jwt-token'; + + // WHEN setUserId is called + IterableApi.setUserId(userId, authToken); + + // THEN RNIterableAPI.setUserId is called with userId and auth token + expect(MockRNIterableAPI.setUserId).toBeCalledWith(userId, authToken); + }); + + it('should call RNIterableAPI.setUserId with null userId', () => { + // GIVEN null userId + const userId = null; + + // WHEN setUserId is called + IterableApi.setUserId(userId); + + // THEN RNIterableAPI.setUserId is called with null userId + expect(MockRNIterableAPI.setUserId).toBeCalledWith(null, undefined); + }); + + it('should call RNIterableAPI.setUserId with undefined userId', () => { + // GIVEN undefined userId + const userId = undefined; + + // WHEN setUserId is called + IterableApi.setUserId(userId); + + // THEN RNIterableAPI.setUserId is called with undefined userId + expect(MockRNIterableAPI.setUserId).toBeCalledWith(undefined, undefined); + }); + }); + + describe('getUserId', () => { + it('should return the userId from RNIterableAPI', async () => { + // GIVEN a mock userId + const expectedUserId = 'user123'; + MockRNIterableAPI.userId = expectedUserId; + + // WHEN getUserId is called + const result = await IterableApi.getUserId(); + + // THEN the userId is returned + expect(result).toBe(expectedUserId); + }); + }); + + describe('disableDeviceForCurrentUser', () => { + it('should call RNIterableAPI.disableDeviceForCurrentUser', () => { + // GIVEN no parameters + // WHEN disableDeviceForCurrentUser is called + IterableApi.disableDeviceForCurrentUser(); + + // THEN RNIterableAPI.disableDeviceForCurrentUser is called + expect(MockRNIterableAPI.disableDeviceForCurrentUser).toBeCalled(); + }); + }); + + describe('updateUser', () => { + it('should call RNIterableAPI.updateUser with data fields and merge flag', () => { + // GIVEN data fields and merge flag + const dataFields = { name: 'John', age: 30 }; + const mergeNestedObjects = true; + + // WHEN updateUser is called + IterableApi.updateUser(dataFields, mergeNestedObjects); + + // THEN RNIterableAPI.updateUser is called with correct parameters + expect(MockRNIterableAPI.updateUser).toBeCalledWith( + dataFields, + mergeNestedObjects + ); + }); + + it('should call RNIterableAPI.updateUser with mergeNestedObjects false', () => { + // GIVEN data fields and merge flag set to false + const dataFields = { name: 'Jane' }; + const mergeNestedObjects = false; + + // WHEN updateUser is called + IterableApi.updateUser(dataFields, mergeNestedObjects); + + // THEN RNIterableAPI.updateUser is called with correct parameters + expect(MockRNIterableAPI.updateUser).toBeCalledWith( + dataFields, + mergeNestedObjects + ); + }); + }); + + describe('updateEmail', () => { + it('should call RNIterableAPI.updateEmail with email only', () => { + // GIVEN a new email + const email = 'newuser@example.com'; + + // WHEN updateEmail is called + IterableApi.updateEmail(email); + + // THEN RNIterableAPI.updateEmail is called with email + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(email, undefined); + }); + + it('should call RNIterableAPI.updateEmail with email and auth token', () => { + // GIVEN a new email and auth token + const email = 'newuser@example.com'; + const authToken = 'new-jwt-token'; + + // WHEN updateEmail is called + IterableApi.updateEmail(email, authToken); + + // THEN RNIterableAPI.updateEmail is called with email and auth token + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(email, authToken); + }); + + it('should call RNIterableAPI.updateEmail with null auth token', () => { + // GIVEN a new email and null auth token + const email = 'newuser@example.com'; + const authToken = null; + + // WHEN updateEmail is called + IterableApi.updateEmail(email, authToken); + + // THEN RNIterableAPI.updateEmail is called with email and null auth token + expect(MockRNIterableAPI.updateEmail).toBeCalledWith(email, null); + }); + }); + + // ====================================================== // + // ===================== TRACKING ====================== // + // ====================================================== // + + describe('trackPushOpenWithCampaignId', () => { + it('should call RNIterableAPI.trackPushOpenWithCampaignId with all parameters', () => { + // GIVEN push open parameters + const params = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + appAlreadyRunning: false, + dataFields: { source: 'push' }, + }; + + // WHEN trackPushOpenWithCampaignId is called + IterableApi.trackPushOpenWithCampaignId(params); + + // THEN RNIterableAPI.trackPushOpenWithCampaignId is called with correct parameters + expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( + params.campaignId, + params.templateId, + params.messageId, + params.appAlreadyRunning, + params.dataFields + ); + }); + + it('should call RNIterableAPI.trackPushOpenWithCampaignId without dataFields', () => { + // GIVEN push open parameters without dataFields + const params = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + appAlreadyRunning: true, + }; + + // WHEN trackPushOpenWithCampaignId is called + IterableApi.trackPushOpenWithCampaignId(params); + + // THEN RNIterableAPI.trackPushOpenWithCampaignId is called with correct parameters + expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( + params.campaignId, + params.templateId, + params.messageId, + params.appAlreadyRunning, + undefined + ); + }); + + it('should call RNIterableAPI.trackPushOpenWithCampaignId with null messageId', () => { + // GIVEN push open parameters with null messageId + const params = { + campaignId: 123, + templateId: 456, + messageId: null, + appAlreadyRunning: false, + }; + + // WHEN trackPushOpenWithCampaignId is called + IterableApi.trackPushOpenWithCampaignId(params); + + // THEN RNIterableAPI.trackPushOpenWithCampaignId is called with correct parameters + expect(MockRNIterableAPI.trackPushOpenWithCampaignId).toBeCalledWith( + params.campaignId, + params.templateId, + null, + params.appAlreadyRunning, + undefined + ); + }); + }); + + describe('trackPurchase', () => { + it('should call RNIterableAPI.trackPurchase with all parameters', () => { + // GIVEN purchase parameters + const total = 99.99; + const items = [ + new IterableCommerceItem('item1', 'Product 1', 49.99, 1), + new IterableCommerceItem('item2', 'Product 2', 49.99, 1), + ]; + const dataFields = { currency: 'USD', discount: 10 }; + + // WHEN trackPurchase is called + IterableApi.trackPurchase({ total, items, dataFields }); + + // THEN RNIterableAPI.trackPurchase is called with correct parameters + expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( + total, + items, + dataFields + ); + }); + + it('should call RNIterableAPI.trackPurchase without dataFields', () => { + // GIVEN purchase parameters without dataFields + const total = 50.0; + const items = [new IterableCommerceItem('item1', 'Product 1', 50.0, 1)]; + + // WHEN trackPurchase is called + IterableApi.trackPurchase({ total, items }); + + // THEN RNIterableAPI.trackPurchase is called with correct parameters + expect(MockRNIterableAPI.trackPurchase).toBeCalledWith( + total, + items, + undefined + ); + }); + }); + + describe('trackInAppOpen', () => { + it('should call RNIterableAPI.trackInAppOpen with message and location', () => { + // GIVEN an in-app message and location + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + + // WHEN trackInAppOpen is called + IterableApi.trackInAppOpen({ message, location }); + + // THEN RNIterableAPI.trackInAppOpen is called with correct parameters + expect(MockRNIterableAPI.trackInAppOpen).toBeCalledWith( + message.messageId, + location + ); + }); + }); + + describe('trackInAppClick', () => { + it('should call RNIterableAPI.trackInAppClick with message, location, and clickedUrl', () => { + // GIVEN an in-app message, location, and clicked URL + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const clickedUrl = 'https://example.com'; + + // WHEN trackInAppClick is called + IterableApi.trackInAppClick({ message, location, clickedUrl }); + + // THEN RNIterableAPI.trackInAppClick is called with correct parameters + expect(MockRNIterableAPI.trackInAppClick).toBeCalledWith( + message.messageId, + location, + clickedUrl + ); + }); + }); + + describe('trackInAppClose', () => { + it('should call RNIterableAPI.trackInAppClose with message, location, and source', () => { + // GIVEN an in-app message, location, and source + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const source = IterableInAppCloseSource.back; + + // WHEN trackInAppClose is called + IterableApi.trackInAppClose({ message, location, source }); + + // THEN RNIterableAPI.trackInAppClose is called with correct parameters + expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( + message.messageId, + location, + source, + undefined + ); + }); + + it('should call RNIterableAPI.trackInAppClose with clickedUrl when provided', () => { + // GIVEN an in-app message, location, source, and clicked URL + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const source = IterableInAppCloseSource.link; + const clickedUrl = 'https://example.com'; + + // WHEN trackInAppClose is called + IterableApi.trackInAppClose({ message, location, source, clickedUrl }); + + // THEN RNIterableAPI.trackInAppClose is called with correct parameters + expect(MockRNIterableAPI.trackInAppClose).toBeCalledWith( + message.messageId, + location, + source, + clickedUrl + ); + }); + }); + + describe('trackEvent', () => { + it('should call RNIterableAPI.trackEvent with name and dataFields', () => { + // GIVEN event name and data fields + const name = 'customEvent'; + const dataFields = { category: 'user_action', value: 100 }; + + // WHEN trackEvent is called + IterableApi.trackEvent({ name, dataFields }); + + // THEN RNIterableAPI.trackEvent is called with correct parameters + expect(MockRNIterableAPI.trackEvent).toBeCalledWith(name, dataFields); + }); + + it('should call RNIterableAPI.trackEvent with name only', () => { + // GIVEN event name only + const name = 'simpleEvent'; + + // WHEN trackEvent is called + IterableApi.trackEvent({ name }); + + // THEN RNIterableAPI.trackEvent is called with correct parameters + expect(MockRNIterableAPI.trackEvent).toBeCalledWith(name, undefined); + }); + }); + + // ====================================================== // + // ======================= AUTH ======================= // + // ====================================================== // + + describe('pauseAuthRetries', () => { + it('should call RNIterableAPI.pauseAuthRetries with true', () => { + // GIVEN pauseRetry is true + const pauseRetry = true; + + // WHEN pauseAuthRetries is called + IterableApi.pauseAuthRetries(pauseRetry); + + // THEN RNIterableAPI.pauseAuthRetries is called with true + expect(MockRNIterableAPI.pauseAuthRetries).toBeCalledWith(true); + }); + + it('should call RNIterableAPI.pauseAuthRetries with false', () => { + // GIVEN pauseRetry is false + const pauseRetry = false; + + // WHEN pauseAuthRetries is called + IterableApi.pauseAuthRetries(pauseRetry); + + // THEN RNIterableAPI.pauseAuthRetries is called with false + expect(MockRNIterableAPI.pauseAuthRetries).toBeCalledWith(false); + }); + }); + + describe('passAlongAuthToken', () => { + it('should call RNIterableAPI.passAlongAuthToken with valid token', () => { + // GIVEN a valid auth token + const authToken = 'valid-jwt-token'; + + // WHEN passAlongAuthToken is called + IterableApi.passAlongAuthToken(authToken); + + // THEN RNIterableAPI.passAlongAuthToken is called with the token + expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(authToken); + }); + + it('should call RNIterableAPI.passAlongAuthToken with null token', () => { + // GIVEN a null auth token + const authToken = null; + + // WHEN passAlongAuthToken is called + IterableApi.passAlongAuthToken(authToken); + + // THEN RNIterableAPI.passAlongAuthToken is called with null + expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(null); + }); + + it('should call RNIterableAPI.passAlongAuthToken with undefined token', () => { + // GIVEN an undefined auth token + const authToken = undefined; + + // WHEN passAlongAuthToken is called + IterableApi.passAlongAuthToken(authToken); + + // THEN RNIterableAPI.passAlongAuthToken is called with undefined + expect(MockRNIterableAPI.passAlongAuthToken).toBeCalledWith(undefined); + }); + }); + + // ====================================================== // + // ======================= IN-APP ======================= // + // ====================================================== // + + describe('inAppConsume', () => { + it('should call RNIterableAPI.inAppConsume with message, location, and source', () => { + // GIVEN an in-app message, location, and delete source + const message = new IterableInAppMessage( + 'msg123', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ); + const location = IterableInAppLocation.inApp; + const source = IterableInAppDeleteSource.deleteButton; + + // WHEN inAppConsume is called + IterableApi.inAppConsume(message, location, source); + + // THEN RNIterableAPI.inAppConsume is called with correct parameters + expect(MockRNIterableAPI.inAppConsume).toBeCalledWith( + message.messageId, + location, + source + ); + }); + }); + + describe('getInAppMessages', () => { + it('should return in-app messages from RNIterableAPI', async () => { + // GIVEN mock in-app messages + const mockMessages = [ + new IterableInAppMessage( + 'msg1', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + false, + undefined, + undefined, + false, + 0 + ), + new IterableInAppMessage( + 'msg2', + 456, + new IterableInAppTrigger(IterableInAppTriggerType.event), + new Date(), + new Date(), + true, + undefined, + undefined, + false, + 0 + ), + ]; + MockRNIterableAPI.messages = mockMessages; + + // WHEN getInAppMessages is called + const result = await IterableApi.getInAppMessages(); + + // THEN the messages are returned + expect(result).toBe(mockMessages); + }); + }); + + describe('getInboxMessages', () => { + it('should return inbox messages from RNIterableAPI', async () => { + // GIVEN mock inbox messages + const mockMessages = [ + new IterableInAppMessage( + 'msg1', + 123, + new IterableInAppTrigger(IterableInAppTriggerType.immediate), + new Date(), + new Date(), + true, // saveToInbox + undefined, + undefined, + false, + 0 + ), + ]; + MockRNIterableAPI.messages = mockMessages; + + // WHEN getInboxMessages is called + const result = await IterableApi.getInboxMessages(); + + // THEN the messages are returned + expect(result).toBe(mockMessages); + }); + }); + + describe('showMessage', () => { + it('should call RNIterableAPI.showMessage with messageId and consume flag', async () => { + // GIVEN a message ID and consume flag + const messageId = 'msg123'; + const consume = true; + const expectedUrl = 'https://example.com'; + MockRNIterableAPI.clickedUrl = expectedUrl; + + // WHEN showMessage is called + const result = await IterableApi.showMessage(messageId, consume); + + // THEN RNIterableAPI.showMessage is called with correct parameters + expect(MockRNIterableAPI.showMessage).toBeCalledWith(messageId, consume); + expect(result).toBe(expectedUrl); + }); + + it('should call RNIterableAPI.showMessage with consume set to false', async () => { + // GIVEN a message ID and consume flag set to false + const messageId = 'msg123'; + const consume = false; + + // WHEN showMessage is called + await IterableApi.showMessage(messageId, consume); + + // THEN RNIterableAPI.showMessage is called with consume set to false + expect(MockRNIterableAPI.showMessage).toBeCalledWith(messageId, false); + }); + }); + + describe('removeMessage', () => { + it('should call RNIterableAPI.removeMessage with messageId, location, and source', () => { + // GIVEN a message ID, location, and source + const messageId = 'msg123'; + const location = 1; // IterableInAppLocation.inApp + const source = 2; // IterableInAppDeleteSource.deleteButton + + // WHEN removeMessage is called + IterableApi.removeMessage(messageId, location, source); + + // THEN RNIterableAPI.removeMessage is called with correct parameters + expect(MockRNIterableAPI.removeMessage).toBeCalledWith( + messageId, + location, + source + ); + }); + }); + + describe('setReadForMessage', () => { + it('should call RNIterableAPI.setReadForMessage with messageId and read status', () => { + // GIVEN a message ID and read status + const messageId = 'msg123'; + const read = true; + + // WHEN setReadForMessage is called + IterableApi.setReadForMessage(messageId, read); + + // THEN RNIterableAPI.setReadForMessage is called with correct parameters + expect(MockRNIterableAPI.setReadForMessage).toBeCalledWith( + messageId, + read + ); + }); + + it('should call RNIterableAPI.setReadForMessage with read set to false', () => { + // GIVEN a message ID and read status set to false + const messageId = 'msg123'; + const read = false; + + // WHEN setReadForMessage is called + IterableApi.setReadForMessage(messageId, read); + + // THEN RNIterableAPI.setReadForMessage is called with read set to false + expect(MockRNIterableAPI.setReadForMessage).toBeCalledWith( + messageId, + false + ); + }); + }); + + describe('setAutoDisplayPaused', () => { + it('should call RNIterableAPI.setAutoDisplayPaused with true', () => { + // GIVEN autoDisplayPaused is true + const autoDisplayPaused = true; + + // WHEN setAutoDisplayPaused is called + IterableApi.setAutoDisplayPaused(autoDisplayPaused); + + // THEN RNIterableAPI.setAutoDisplayPaused is called with true + expect(MockRNIterableAPI.setAutoDisplayPaused).toBeCalledWith(true); + }); + + it('should call RNIterableAPI.setAutoDisplayPaused with false', () => { + // GIVEN autoDisplayPaused is false + const autoDisplayPaused = false; + + // WHEN setAutoDisplayPaused is called + IterableApi.setAutoDisplayPaused(autoDisplayPaused); + + // THEN RNIterableAPI.setAutoDisplayPaused is called with false + expect(MockRNIterableAPI.setAutoDisplayPaused).toBeCalledWith(false); + }); + }); + + describe('getHtmlInAppContentForMessage', () => { + it('should call RNIterableAPI.getHtmlInAppContentForMessage with messageId', async () => { + // GIVEN a message ID + const messageId = 'msg123'; + const mockContent = { html: '
Test content
' }; + MockRNIterableAPI.getHtmlInAppContentForMessage = jest + .fn() + .mockResolvedValue(mockContent); + + // WHEN getHtmlInAppContentForMessage is called + const result = await IterableApi.getHtmlInAppContentForMessage(messageId); + + // THEN RNIterableAPI.getHtmlInAppContentForMessage is called with messageId + expect(MockRNIterableAPI.getHtmlInAppContentForMessage).toBeCalledWith( + messageId + ); + expect(result).toBe(mockContent); + }); + }); + + describe('setInAppShowResponse', () => { + it('should call RNIterableAPI.setInAppShowResponse with response', () => { + // GIVEN an in-app show response + const response = IterableInAppShowResponse.show; + + // WHEN setInAppShowResponse is called + IterableApi.setInAppShowResponse(response); + + // THEN RNIterableAPI.setInAppShowResponse is called with response + expect(MockRNIterableAPI.setInAppShowResponse).toBeCalledWith(response); + }); + }); + + describe('startSession', () => { + it('should call RNIterableAPI.startSession with visible rows', () => { + // GIVEN visible rows + const visibleRows: IterableInboxImpressionRowInfo[] = [ + { messageId: 'msg1', silentInbox: true }, + { messageId: 'msg2', silentInbox: false }, + ]; + + // WHEN startSession is called + IterableApi.startSession(visibleRows); + + // THEN RNIterableAPI.startSession is called with visible rows + expect(MockRNIterableAPI.startSession).toBeCalledWith(visibleRows); + }); + }); + + describe('endSession', () => { + it('should call RNIterableAPI.endSession', () => { + // GIVEN no parameters + // WHEN endSession is called + IterableApi.endSession(); + + // THEN RNIterableAPI.endSession is called + expect(MockRNIterableAPI.endSession).toBeCalled(); + }); + }); + + describe('updateVisibleRows', () => { + it('should call RNIterableAPI.updateVisibleRows with visible rows', () => { + // GIVEN visible rows + const visibleRows: IterableInboxImpressionRowInfo[] = [ + { messageId: 'msg1', silentInbox: true }, + ]; + + // WHEN updateVisibleRows is called + IterableApi.updateVisibleRows(visibleRows); + + // THEN RNIterableAPI.updateVisibleRows is called with visible rows + expect(MockRNIterableAPI.updateVisibleRows).toBeCalledWith(visibleRows); + }); + + it('should call RNIterableAPI.updateVisibleRows with empty array when no rows provided', () => { + // GIVEN no visible rows + // WHEN updateVisibleRows is called without parameters + IterableApi.updateVisibleRows(); + + // THEN RNIterableAPI.updateVisibleRows is called with empty array + expect(MockRNIterableAPI.updateVisibleRows).toBeCalledWith([]); + }); + }); + + // ====================================================== // + // ======================= MOSC ======================= // + // ====================================================== // + + describe('updateCart', () => { + it('should call RNIterableAPI.updateCart with items', () => { + // GIVEN cart items + const items = [ + new IterableCommerceItem('item1', 'Product 1', 25.99, 2), + new IterableCommerceItem('item2', 'Product 2', 15.99, 1), + ]; + + // WHEN updateCart is called + IterableApi.updateCart(items); + + // THEN RNIterableAPI.updateCart is called with items + expect(MockRNIterableAPI.updateCart).toBeCalledWith(items); + }); + }); + + describe('wakeApp', () => { + it('should call RNIterableAPI.wakeApp on Android', () => { + // GIVEN Android platform + const originalPlatform = Platform.OS; + Object.defineProperty(Platform, 'OS', { + value: 'android', + writable: true, + }); + + // WHEN wakeApp is called + IterableApi.wakeApp(); + + // THEN RNIterableAPI.wakeApp is called + expect(MockRNIterableAPI.wakeApp).toBeCalled(); + + // Restore original platform + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); + + it('should not call RNIterableAPI.wakeApp on iOS', () => { + // GIVEN iOS platform + const originalPlatform = Platform.OS; + Object.defineProperty(Platform, 'OS', { + value: 'ios', + writable: true, + }); + + // WHEN wakeApp is called + IterableApi.wakeApp(); + + // THEN RNIterableAPI.wakeApp is not called + expect(MockRNIterableAPI.wakeApp).not.toBeCalled(); + + // Restore original platform + Object.defineProperty(Platform, 'OS', { + value: originalPlatform, + writable: true, + }); + }); + }); + + describe('handleAppLink', () => { + it('should call RNIterableAPI.handleAppLink with link', () => { + // GIVEN a link + const link = 'https://example.com/deep-link'; + + // WHEN handleAppLink is called + IterableApi.handleAppLink(link); + + // THEN RNIterableAPI.handleAppLink is called with link + expect(MockRNIterableAPI.handleAppLink).toBeCalledWith(link); + }); + }); + + describe('updateSubscriptions', () => { + it('should call RNIterableAPI.updateSubscriptions with all parameters', () => { + // GIVEN subscription parameters + const params = { + emailListIds: [1, 2, 3], + unsubscribedChannelIds: [4, 5], + unsubscribedMessageTypeIds: [6, 7, 8], + subscribedMessageTypeIds: [9, 10], + campaignId: 11, + templateId: 12, + }; + + // WHEN updateSubscriptions is called + IterableApi.updateSubscriptions(params); + + // THEN RNIterableAPI.updateSubscriptions is called with correct parameters + expect(MockRNIterableAPI.updateSubscriptions).toBeCalledWith( + params.emailListIds, + params.unsubscribedChannelIds, + params.unsubscribedMessageTypeIds, + params.subscribedMessageTypeIds, + params.campaignId, + params.templateId + ); + }); + + it('should call RNIterableAPI.updateSubscriptions with null arrays', () => { + // GIVEN subscription parameters with null arrays + const params = { + emailListIds: null, + unsubscribedChannelIds: null, + unsubscribedMessageTypeIds: null, + subscribedMessageTypeIds: null, + campaignId: 11, + templateId: 12, + }; + + // WHEN updateSubscriptions is called + IterableApi.updateSubscriptions(params); + + // THEN RNIterableAPI.updateSubscriptions is called with null arrays + expect(MockRNIterableAPI.updateSubscriptions).toBeCalledWith( + null, + null, + null, + null, + params.campaignId, + params.templateId + ); + }); + }); + + describe('getLastPushPayload', () => { + it('should return the last push payload from RNIterableAPI', async () => { + // GIVEN a mock push payload + const mockPayload = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + customData: { key: 'value' }, + }; + MockRNIterableAPI.lastPushPayload = mockPayload; + + // WHEN getLastPushPayload is called + const result = await IterableApi.getLastPushPayload(); + + // THEN the push payload is returned + expect(result).toBe(mockPayload); + }); + }); + + describe('getAttributionInfo', () => { + it('should return IterableAttributionInfo when attribution info exists', async () => { + // GIVEN mock attribution info + const mockAttributionDict = { + campaignId: 123, + templateId: 456, + messageId: 'msg123', + }; + MockRNIterableAPI.getAttributionInfo = jest + .fn() + .mockResolvedValue(mockAttributionDict); + + // WHEN getAttributionInfo is called + const result = await IterableApi.getAttributionInfo(); + + // THEN IterableAttributionInfo is returned + expect(result).toBeInstanceOf(IterableAttributionInfo); + expect(result?.campaignId).toBe(123); + expect(result?.templateId).toBe(456); + expect(result?.messageId).toBe('msg123'); + }); + + it('should return undefined when attribution info is null', async () => { + // GIVEN null attribution info + MockRNIterableAPI.getAttributionInfo = jest.fn().mockResolvedValue(null); + + // WHEN getAttributionInfo is called + const result = await IterableApi.getAttributionInfo(); + + // THEN undefined is returned + expect(result).toBeUndefined(); + }); + }); + + describe('setAttributionInfo', () => { + it('should call RNIterableAPI.setAttributionInfo with attribution info', () => { + // GIVEN attribution info + const attributionInfo = new IterableAttributionInfo(123, 456, 'msg123'); + + // WHEN setAttributionInfo is called + IterableApi.setAttributionInfo(attributionInfo); + + // THEN RNIterableAPI.setAttributionInfo is called with attribution info + expect(MockRNIterableAPI.setAttributionInfo).toBeCalledWith( + attributionInfo + ); + }); + + it('should call RNIterableAPI.setAttributionInfo with undefined', () => { + // GIVEN undefined attribution info + const attributionInfo = undefined; + + // WHEN setAttributionInfo is called + IterableApi.setAttributionInfo(attributionInfo); + + // THEN RNIterableAPI.setAttributionInfo is called with undefined + expect(MockRNIterableAPI.setAttributionInfo).toBeCalledWith(undefined); + }); + }); +}); From a693494dba423b4c9ce6cc3aa2444252f4897aad Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 14 Oct 2025 16:58:01 -0700 Subject: [PATCH 19/82] docs: enhance IterableLogger documentation with descriptions and examples --- src/core/classes/IterableLogger.ts | 33 ++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/core/classes/IterableLogger.ts b/src/core/classes/IterableLogger.ts index 21e947df7..50c39ea78 100644 --- a/src/core/classes/IterableLogger.ts +++ b/src/core/classes/IterableLogger.ts @@ -37,7 +37,9 @@ export class IterableLogger { static loggingEnabled = DEFAULT_LOGGING_ENABLED; /** - * The level of logging to show in the developer console. + * The level of logging. + * + * This controls which logs will show when using the {@link IterableLogger.error}, {@link IterableLogger.debug}, and {@link IterableLogger.info} methods. */ static logLevel = DEFAULT_LOG_LEVEL; @@ -67,6 +69,11 @@ export class IterableLogger { * Logs a message to the console if logging is enabled. * * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.log('I will show if logging is enabled'); + * ``` */ static log(message?: unknown, ...optionalParams: unknown[]) { if (!IterableLogger.loggingEnabled) return; @@ -75,9 +82,14 @@ export class IterableLogger { } /** - * Logs a message to the console if the log level is error. + * Logs a message to the console if the log level is {@link IterableLogLevel.error}. * * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.error('I will only show if the log level is error and logging is enabled'); + * ``` */ static error(message?: unknown, ...optionalParams: unknown[]) { if (!IterableLogger.loggingEnabled) return; @@ -87,9 +99,15 @@ export class IterableLogger { } /** - * Logs a message to the console if the log level is debug or lower. + * Logs a message to the console if the log level is {@link IterableLogLevel.debug} or lower. * * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.debug('I will show if the log level is debug and logging is enabled'); + * IterableLogger.debug('I will also show if the log level is error and logging is enabled'); + * ``` */ static debug(message?: unknown, ...optionalParams: unknown[]) { if (!IterableLogger.loggingEnabled) return; @@ -104,9 +122,16 @@ export class IterableLogger { } /** - * Logs a message to the console if the log level is info or lower. + * Logs a message to the console if the log level is {@link IterableLogLevel.info} or lower. * * @param message - The message to be logged. + * + * @example + * ```typescript + * IterableLogger.info('I will show if the log level is info and logging is enabled'); + * IterableLogger.info('I will also show if the log level is debug and logging is enabled'); + * IterableLogger.info('I will also show if the log level is error and logging is enabled'); + * ``` */ static info(message?: unknown, ...optionalParams: unknown[]) { if (!IterableLogger.loggingEnabled) return; From 74a6409b76c33125f706a412c2fc3b4a1b3104d4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 10:55:53 -0700 Subject: [PATCH 20/82] feat: add back icon assets for multiple resolutions and platforms --- src/inbox/assets/back-icon.png | Bin 0 -> 207 bytes src/inbox/assets/back-icon@1x.android.png | Bin 0 -> 100 bytes src/inbox/assets/back-icon@1x.ios.png | Bin 0 -> 290 bytes src/inbox/assets/back-icon@2x.android.png | Bin 0 -> 134 bytes src/inbox/assets/back-icon@2x.ios.png | Bin 0 -> 405 bytes src/inbox/assets/back-icon@3x.android.png | Bin 0 -> 167 bytes src/inbox/assets/back-icon@3x.ios.png | Bin 0 -> 761 bytes src/inbox/assets/back-icon@4x.android.png | Bin 0 -> 207 bytes src/inbox/assets/back-icon@4x.ios.png | Bin 0 -> 809 bytes 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/inbox/assets/back-icon.png create mode 100644 src/inbox/assets/back-icon@1x.android.png create mode 100644 src/inbox/assets/back-icon@1x.ios.png create mode 100644 src/inbox/assets/back-icon@2x.android.png create mode 100644 src/inbox/assets/back-icon@2x.ios.png create mode 100644 src/inbox/assets/back-icon@3x.android.png create mode 100644 src/inbox/assets/back-icon@3x.ios.png create mode 100644 src/inbox/assets/back-icon@4x.android.png create mode 100644 src/inbox/assets/back-icon@4x.ios.png diff --git a/src/inbox/assets/back-icon.png b/src/inbox/assets/back-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..17e52e8550e5668f7117bcb755beb70c3a21c9e9 GIT binary patch literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgrg^$JhEy=Vy>(TP*-+rv1Kz7M z%I*XNJ!(VLzdH4Ge@o8ai4)e^3vK%P+bZq#{MQci z?P`C0d3kxcKa^1~`zt>B;jiLd|K_aKfAEz>edFmDy)D6SUw`+o@R*?B*u=sqq~bB* zgRIJrKN^lr3+tD}bFTI}~XjubmVU5CWQ(LZ;AgeuH{an^LB{Ts5E|^v% literal 0 HcmV?d00001 diff --git a/src/inbox/assets/back-icon@1x.android.png b/src/inbox/assets/back-icon@1x.android.png new file mode 100644 index 0000000000000000000000000000000000000000..083db295f474b9903408258c71818c2c49151d35 GIT binary patch literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1ZBG}+kP60RiDL^GIXr{@&3`D~ x%$9kI*}bEvW6`UVO)xQz+sFfYmle_k4J{c^Y-u@of;y~NbrRr@}xDqOoe#7(!ujI zW)IKPtpOg9x^ZGiV^G!WUnY384Jwg^1?qzoV?Vqi%~F-r131d0cdTWi1E3pVgr)a^ zbA?Lk#0WF#3+Yvg4uEz<4l(HmchxGX6;goJ^ovoWl9~bgne?yCQ=RAlXaeYC>D}3~ oOlpASO$!ea%F`=`*pIb34xO&@x>?fx0{{R307*qoM6N<$f~pOBr2qf` literal 0 HcmV?d00001 diff --git a/src/inbox/assets/back-icon@2x.android.png b/src/inbox/assets/back-icon@2x.android.png new file mode 100644 index 0000000000000000000000000000000000000000..6de0a1cbb365dfd5d9274890d243ca2321f32235 GIT binary patch literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tUr!gukP61P(+)Bo5D;KluXOkB z|6f;`Uoa>nrTyP5;4+Jm;dJDxe#>oP@r$y*#>rHDd13s5eN)8##R;WH*ZD4M;?i1i i;i&z7`QNqd4J-@3aX4v+{p181%;4$j=d#Wzp$P!F!7nZV literal 0 HcmV?d00001 diff --git a/src/inbox/assets/back-icon@2x.ios.png b/src/inbox/assets/back-icon@2x.ios.png new file mode 100644 index 0000000000000000000000000000000000000000..63de0e303aae4f3f09bbf5834290131501232f64 GIT binary patch literal 405 zcmV;G0c!q$3ng)KwR%cav2NeVj#yi|twB8nJ$ zhJ9XSgC81to-du2Xkr{|@HcXhBf~opLkgz`JWJYh7b%=;afNgn{8BUDujC*_hPNS> z7&ls+kwoI8u%elk7pg@(StZ8P4^;l-*^fZJ1`5~OHBd;D;qqM+{Bs~b$&5^qEx(K8 zx9Nf9ZEAjO1n+^rUo`oHziD{zE6kAXuYyr+hHU>u3T%dKe|C&IvG2#fso;0*pv4!B zOc_R!8Jq}55TlLyCq{c8swLA2SGn}>CsPgp4HtE6hRq5700000NkvXXu0mjfokp-j literal 0 HcmV?d00001 diff --git a/src/inbox/assets/back-icon@3x.android.png b/src/inbox/assets/back-icon@3x.android.png new file mode 100644 index 0000000000000000000000000000000000000000..15a983a67d97c9a6c39d91550a528c37c53a9e3e GIT binary patch literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw@;qG}Ln;{G-aN{9*nr0&aBl6p z15q2fw>DO<$f$_t-m`#N(R0!@qfL6}is!BA-aP5mqcux*S)AYDzOuXc@WF!zU-mWw z!RNIrdq3B7GJgpV3USMNx+#Bt3lxOZSlm}!I$z&?|8f=2ODy@*gQAaCs?P@6%;4$j K=d#Wzp$P!X&_+4{ literal 0 HcmV?d00001 diff --git a/src/inbox/assets/back-icon@3x.ios.png b/src/inbox/assets/back-icon@3x.ios.png new file mode 100644 index 0000000000000000000000000000000000000000..2320fd74d1781dc8a74cc131d8c3bb17fc52bae4 GIT binary patch literal 761 zcmVAUlM5g1Q_DYVitMm-$7Tx z*ro&sTZ*z6>QNY&<*{MBLwj|YS(XqXD^V764Y9I1Okis$Okg8b+mr|wUq@l0HcEq; zVTpm+Kw?IyI+FyL&Ez%>QDU|-7*wFaOtGZEY$rQ2K-HNzTzn^q>8HU=v}CyWZt?*4 zR&^#h0``%e>7l`lw-g9CKz639sxv7OaEQQsKxaB>Fyky00*;WF4yw+iM!+!&6ScM) z%veiJ{LO>gk?HFakuuxmtTUZbi;r*1=ySHXQyoauVV}3fsCFdksL$Qv?Q2bBWbLdnCsmVuHOp65Sf{(`X7f(3UB_ENM%STv@ zvIOdikIG_Hb<}I(pkKw$XH%o9lBnz0zU=BVRS7|_2-Hp7L=H8o0zzIAXX+Lzml{=$ zMBTymK^Jb-%uZCPG}qfJds0JQ2XB$6bg6-+U-Ef=(TP*-+rv1Kz7M z%I*XNJ!(VLzdH4Ge@o8ai4)e^3vK%P+bZq#{MQci z?P`C0d3kxcKa^1~`zt>B;jiLd|K_aKfAEz>edFmDy)D6SUw`+o@R*?B*u=sqq~bB* zgRIJrKN^lr3+tD}bFTI}~XjubmVU5CWQ(LZ;AgeuH{an^LB{Ts5E|^v% literal 0 HcmV?d00001 diff --git a/src/inbox/assets/back-icon@4x.ios.png b/src/inbox/assets/back-icon@4x.ios.png new file mode 100644 index 0000000000000000000000000000000000000000..b26634e594135d181375c3316b2ce059a31c6665 GIT binary patch literal 809 zcmV+^1J?YBP)YD1miR7mE{DV$Qg{||{bo~dA%(lFKsoPR23s4@v0mrvvjQGomaY7ADLDuqH1 zvGK4`4{>#jmgPlOeM+>Mf_`%D1%nu+I@RZN_76>V5WO1tOIiKYhsr9>jYgGKs=`*N zv}jyyRB5p)?goPB+{p5Jb-|H(#biuZo$8V=bj@m<&Aw*Mzvy;mD@cB;LBZVYApQ(D zdpPq|r)sw;OT$xM%b9U3xD%GW6CGq63GPHqLE>$f#-ZTp)T|)+jjzs91<7wcE?A); z`OV+XYNgUb`*8ccu~0$s2Ow%Rm0<*BXJiDW8UkwI7}mD<7*>7|TUr^wfilifZVG;4 n@$E^mUzjZzWv9I6RnQJ~H00000NkvXXu0mjf?i-Uu literal 0 HcmV?d00001 From 4308017aeb447334ccaac64e356b64b7a09cf4d9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 10:56:22 -0700 Subject: [PATCH 21/82] refactor: add HeaderBackButton component and integrate it into IterableInboxMessageDisplay --- src/inbox/components/HeaderBackButton.tsx | 39 +++++++++++++++++++ .../IterableInboxMessageDisplay.tsx | 12 +++++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 src/inbox/components/HeaderBackButton.tsx diff --git a/src/inbox/components/HeaderBackButton.tsx b/src/inbox/components/HeaderBackButton.tsx new file mode 100644 index 000000000..e46948050 --- /dev/null +++ b/src/inbox/components/HeaderBackButton.tsx @@ -0,0 +1,39 @@ +import { + StyleSheet, + Text, + TouchableWithoutFeedback, + View, + type TouchableWithoutFeedbackProps, +} from 'react-native'; +import { Icon } from 'react-native-vector-icons/Icon'; +import { ITERABLE_INBOX_COLORS } from '../constants/colors'; + +const styles = StyleSheet.create({ + + returnButton: { + alignItems: 'center', + flexDirection: 'row', + }, + + returnButtonIcon: { + color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, + fontSize: 40, + paddingLeft: 0, + }, + + returnButtonText: { + color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, + fontSize: 20, + }, +}); + +export const HeaderBackButton = (props: TouchableWithoutFeedbackProps) => { + return ( + + + + Inbox + + + ); +}; diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index 7e6798c73..6799dafea 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -23,9 +23,9 @@ import { IterableInAppCloseSource, IterableInAppLocation, } from '../../inApp'; - import { ITERABLE_INBOX_COLORS } from '../constants'; import { type IterableInboxRowViewModel } from '../types'; +import { HeaderBackButton } from './HeaderBackButton'; /** * Props for the IterableInboxMessageDisplay component. @@ -222,6 +222,16 @@ export const IterableInboxMessageDisplay = ({ + { + returnToInbox(); + Iterable.trackInAppClose( + rowViewModel.inAppMessage, + IterableInAppLocation.inbox, + IterableInAppCloseSource.back + ); + }} + /> { returnToInbox(); From 2c9a954bf9f0656540f5f3bb3b2268cc5dcb46b3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 13:24:19 -0700 Subject: [PATCH 22/82] refactor: create HeaderBackButton and replace back-arrow --- src/inbox/components/HeaderBackButton.tsx | 75 ++++++++++++++++--- .../IterableInboxMessageDisplay.tsx | 39 +--------- 2 files changed, 68 insertions(+), 46 deletions(-) diff --git a/src/inbox/components/HeaderBackButton.tsx b/src/inbox/components/HeaderBackButton.tsx index e46948050..15687ac4e 100644 --- a/src/inbox/components/HeaderBackButton.tsx +++ b/src/inbox/components/HeaderBackButton.tsx @@ -1,38 +1,93 @@ import { + Image, + PixelRatio, + Platform, StyleSheet, Text, TouchableWithoutFeedback, View, + type ImageSourcePropType, type TouchableWithoutFeedbackProps, } from 'react-native'; -import { Icon } from 'react-native-vector-icons/Icon'; import { ITERABLE_INBOX_COLORS } from '../constants/colors'; +// Base64 encoded back arrow icons +// [Original image](https://github.com/react-navigation/react-navigation/blob/main/packages/elements/src/assets/back-icon%404x.ios.png) +const backArrowLarge = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAABUCAYAAADEZ7TLAAAC8ElEQVR42u2bWchOaxTH/xfnHMcZnHkwRobwJfdIkkSSkpKSEikREcla+81X5tR34c61O8kQIknmKUnGyCzzjczzz817Kfm+9dbasn/13P/X/1n72XuvZy2VGmeQnA1yrsh4LGe/DNda2qnUNPOdjBY572XwiXVeNZpUStbxvZyNMvjsci7Ud6Jk4o3NMvjCVStT2vwgY2srxCPnYHnEO9tl0Mr1WOmspZ2cHTJow7qY7fyPcnbJoI1rvdJoob2M3QHxr9RM/yznf5KxJyAeOQsyxe8NiS9YpRTW8LOMfUHnl2c5/4uMA0HnlymF1fwq53BIvLEky/kOco4G06ZQCqv4Tc7xoPOLs5z/XcaJoPhFSmElf8g5+XWe8yv4S86p4GkzLytt/pZzOuj8nCzn/5FzJuj8rCzn/5VzLiD+gwpmKoXl/CfjfEi8M0MpGB1lXAw6Py0rbTrJuBR4WN+rYGqW+C5yLofEG1OUgtNVzpVA2rxTjclZzneTcy0k3pmUJb67jOuBtHkrZ6JSKOgh42ZIfMGELPE95dwKpM0bFYzPEt9bxu2QeGdcVs73kXEnkDav5YzNEt9Xzt1Q0ckYkyW+v4z7QfGjlUKNJjkPAmnzUgUjs8QPkPEw4PwLFYzIEj9QzqOA889VMDwr5/8PiTeeqWCY0jDWB5x/KmeoMgkcl0/kDFGA/ABqDFadKoVCD7HxsFEPcXWMluVFVn1KtJmCfnLuBYMYpTrV53T2D012EL0a+UtZ/dSHyirOjUaVVarCVqi0aFxtVGmxKu4GgujcyPJ6dcGResVkTFed6pIvdM1qnM2+Zs2/6HZmK0h+q0HB3Owg/gw3ezjzVadqtylLw1Ney5lxLLgTlr0THWQcCe5ETUHibZfGoeDp1Kwg+Y2vzlIFyW89LlihIPnN385qpdJC+3D7vbFQQfIHIGo0lWEEZWdkBKUaAmrgGNa2+BhWfhBbWhnAgTLOUW4KfmLkT7K2Zhi0GsetBqK/dT4Csa/f2ZlrPiMAAAAASUVORK5CYII='; + +export const ICON_SIZE = Platform.OS === 'ios' ? 21 : 24; +export const ICON_MARGIN = Platform.OS === 'ios' ? 8 : 3; +const ICON_COLOR = ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT; + const styles = StyleSheet.create({ + icon: { + height: ICON_SIZE, + margin: ICON_MARGIN, + width: ICON_SIZE, + }, returnButton: { alignItems: 'center', + display: 'flex', flexDirection: 'row', }, - returnButtonIcon: { - color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, - fontSize: 40, - paddingLeft: 0, - }, - returnButtonText: { color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, fontSize: 20, }, }); -export const HeaderBackButton = (props: TouchableWithoutFeedbackProps) => { +/** + * Props for the HeaderBackButton component. + */ +export interface HeaderBackButtonProps extends TouchableWithoutFeedbackProps { + /** + * The text to display next to the back arrow. + */ + label?: string; + /** + * The URI of the image to display. + * + * This defaults to a base64 encoded version of [the back arrow used in react-navigation/elements](https://github.com/react-navigation/react-navigation/blob/main/packages/elements/src/assets/back-icon%404x.ios.png) + */ + imageUri?: string; + /** + * The source of the image to display. + */ + imageSource?: ImageSourcePropType; +} + +/** + * A back arrow button used in a header + * + * @returns A button with a back arrow + */ +export const HeaderBackButton = ({ + label, + imageUri = backArrowLarge, + imageSource = { + uri: imageUri, + width: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE), + height: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE), + }, + ...props +}: HeaderBackButtonProps) => { return ( - - Inbox + + {label && {label}} ); diff --git a/src/inbox/components/IterableInboxMessageDisplay.tsx b/src/inbox/components/IterableInboxMessageDisplay.tsx index 6799dafea..d42306a04 100644 --- a/src/inbox/components/IterableInboxMessageDisplay.tsx +++ b/src/inbox/components/IterableInboxMessageDisplay.tsx @@ -1,13 +1,12 @@ import { useEffect, useState } from 'react'; import { Linking, + Platform, ScrollView, StyleSheet, Text, - TouchableWithoutFeedback, View, } from 'react-native'; -import Icon from 'react-native-vector-icons/Ionicons'; import { WebView, type WebViewMessageEvent } from 'react-native-webview'; import { @@ -86,6 +85,7 @@ export const IterableInboxMessageDisplay = ({ header: { flexDirection: 'row', + height: Platform.OS === 'ios' ? 44 : 56, justifyContent: 'center', width: '100%', }, @@ -119,11 +119,6 @@ export const IterableInboxMessageDisplay = ({ fontWeight: 'bold', }, - returnButton: { - alignItems: 'center', - flexDirection: 'row', - }, - returnButtonContainer: { alignItems: 'center', flexDirection: 'row', @@ -133,17 +128,6 @@ export const IterableInboxMessageDisplay = ({ width: '25%', ...(isPortrait ? {} : { marginLeft: 80 }), }, - - returnButtonIcon: { - color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, - fontSize: 40, - paddingLeft: 0, - }, - - returnButtonText: { - color: ITERABLE_INBOX_COLORS.BUTTON_PRIMARY_TEXT, - fontSize: 20, - }, }); const JS = ` @@ -223,6 +207,7 @@ export const IterableInboxMessageDisplay = ({ { returnToInbox(); Iterable.trackInAppClose( @@ -232,24 +217,6 @@ export const IterableInboxMessageDisplay = ({ ); }} /> - { - returnToInbox(); - Iterable.trackInAppClose( - rowViewModel.inAppMessage, - IterableInAppLocation.inbox, - IterableInAppCloseSource.back - ); - }} - > - - - Inbox - - From 9bf11789bc9c7fc0eeaa8750964a9dba4bea2a87 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 13:24:39 -0700 Subject: [PATCH 23/82] chore: add prettier-eslint and update dependencies in package.json and yarn.lock --- package.json | 1 + yarn.lock | 253 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 249 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index ef832a4ad..c3e1dfc52 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "eslint-plugin-tsdoc": "^0.3.0", "jest": "^29.7.0", "prettier": "^3.0.3", + "prettier-eslint": "^16.4.2", "react": "19.0.0", "react-native": "0.79.3", "react-native-builder-bob": "^0.40.4", diff --git a/yarn.lock b/yarn.lock index d9961ac53..ea98be24c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3142,6 +3142,7 @@ __metadata: eslint-plugin-tsdoc: ^0.3.0 jest: ^29.7.0 prettier: ^3.0.3 + prettier-eslint: ^16.4.2 react: 19.0.0 react-native: 0.79.3 react-native-builder-bob: ^0.40.4 @@ -4750,6 +4751,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/parser@npm:6.21.0" + dependencies: + "@typescript-eslint/scope-manager": 6.21.0 + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/typescript-estree": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + debug: ^4.3.4 + peerDependencies: + eslint: ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68 + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:^7.1.1": version: 7.18.0 resolution: "@typescript-eslint/parser@npm:7.18.0" @@ -4796,6 +4815,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/scope-manager@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + checksum: 71028b757da9694528c4c3294a96cc80bc7d396e383a405eab3bc224cda7341b88e0fc292120b35d3f31f47beac69f7083196c70616434072fbcd3d3e62d3376 + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/scope-manager@npm:7.18.0" @@ -4855,6 +4884,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/types@npm:6.21.0" + checksum: 9501b47d7403417af95fc1fb72b2038c5ac46feac0e1598a46bcb43e56a606c387e9dcd8a2a0abe174c91b509f2d2a8078b093786219eb9a01ab2fbf9ee7b684 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/types@npm:7.18.0" @@ -4887,6 +4923,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/typescript-estree@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + "@typescript-eslint/visitor-keys": 6.21.0 + debug: ^4.3.4 + globby: ^11.1.0 + is-glob: ^4.0.3 + minimatch: 9.0.3 + semver: ^7.5.4 + ts-api-utils: ^1.0.1 + peerDependenciesMeta: + typescript: + optional: true + checksum: dec02dc107c4a541e14fb0c96148f3764b92117c3b635db3a577b5a56fc48df7a556fa853fb82b07c0663b4bf2c484c9f245c28ba3e17e5cb0918ea4cab2ea21 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/typescript-estree@npm:7.18.0" @@ -4981,6 +5036,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:6.21.0": + version: 6.21.0 + resolution: "@typescript-eslint/visitor-keys@npm:6.21.0" + dependencies: + "@typescript-eslint/types": 6.21.0 + eslint-visitor-keys: ^3.4.1 + checksum: 67c7e6003d5af042d8703d11538fca9d76899f0119130b373402819ae43f0bc90d18656aa7add25a24427ccf1a0efd0804157ba83b0d4e145f06107d7d1b7433 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:7.18.0": version: 7.18.0 resolution: "@typescript-eslint/visitor-keys@npm:7.18.0" @@ -5186,6 +5251,13 @@ __metadata: languageName: node linkType: hard +"ansi-regex@npm:^2.0.0": + version: 2.1.1 + resolution: "ansi-regex@npm:2.1.1" + checksum: 190abd03e4ff86794f338a31795d262c1dfe8c91f7e01d04f13f646f1dcb16c5800818f886047876f1272f065570ab86b24b99089f8b68a0e11ff19aed4ca8f1 + languageName: node + linkType: hard + "ansi-regex@npm:^4.1.0": version: 4.1.1 resolution: "ansi-regex@npm:4.1.1" @@ -5207,6 +5279,13 @@ __metadata: languageName: node linkType: hard +"ansi-styles@npm:^2.2.1": + version: 2.2.1 + resolution: "ansi-styles@npm:2.2.1" + checksum: ebc0e00381f2a29000d1dac8466a640ce11943cef3bda3cd0020dc042e31e1058ab59bf6169cd794a54c3a7338a61ebc404b7c91e004092dd20e028c432c9c2c + languageName: node + linkType: hard + "ansi-styles@npm:^3.2.0, ansi-styles@npm:^3.2.1": version: 3.2.1 resolution: "ansi-styles@npm:3.2.1" @@ -5916,6 +5995,19 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^1.1.3": + version: 1.1.3 + resolution: "chalk@npm:1.1.3" + dependencies: + ansi-styles: ^2.2.1 + escape-string-regexp: ^1.0.2 + has-ansi: ^2.0.0 + strip-ansi: ^3.0.0 + supports-color: ^2.0.0 + checksum: 9d2ea6b98fc2b7878829eec223abcf404622db6c48396a9b9257f6d0ead2acf18231ae368d6a664a83f272b0679158da12e97b5229f794939e555cc574478acd + languageName: node + linkType: hard + "chalk@npm:^2.4.2": version: 2.4.2 resolution: "chalk@npm:2.4.2" @@ -6222,6 +6314,13 @@ __metadata: languageName: node linkType: hard +"common-tags@npm:^1.8.2": + version: 1.8.2 + resolution: "common-tags@npm:1.8.2" + checksum: 767a6255a84bbc47df49a60ab583053bb29a7d9687066a18500a516188a062c4e4cd52de341f22de0b07062e699b1b8fe3cfa1cb55b241cb9301aeb4f45b4dff + languageName: node + linkType: hard + "compare-func@npm:^2.0.0": version: 2.0.0 resolution: "compare-func@npm:2.0.0" @@ -6937,6 +7036,13 @@ __metadata: languageName: node linkType: hard +"dlv@npm:^1.1.3": + version: 1.1.3 + resolution: "dlv@npm:1.1.3" + checksum: d7381bca22ed11933a1ccf376db7a94bee2c57aa61e490f680124fa2d1cd27e94eba641d9f45be57caab4f9a6579de0983466f620a2cd6230d7ec93312105ae7 + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -7293,7 +7399,7 @@ __metadata: languageName: node linkType: hard -"escape-string-regexp@npm:^1.0.5": +"escape-string-regexp@npm:^1.0.2, escape-string-regexp@npm:^1.0.5": version: 1.0.5 resolution: "escape-string-regexp@npm:1.0.5" checksum: 6092fda75c63b110c706b6a9bfde8a612ad595b628f0bd2147eea1d3406723020810e591effc7db1da91d80a71a737a313567c5abb3813e8d9c71f4aa595b410 @@ -7510,7 +7616,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.2.2": +"eslint-scope@npm:^7.1.1, eslint-scope@npm:^7.2.2": version: 7.2.2 resolution: "eslint-scope@npm:7.2.2" dependencies: @@ -7534,7 +7640,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.51.0": +"eslint@npm:^8.51.0, eslint@npm:^8.57.1": version: 8.57.1 resolution: "eslint@npm:8.57.1" dependencies: @@ -7582,7 +7688,7 @@ __metadata: languageName: node linkType: hard -"espree@npm:^9.6.0, espree@npm:^9.6.1": +"espree@npm:^9.3.1, espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" dependencies: @@ -7603,7 +7709,7 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": +"esquery@npm:^1.4.0, esquery@npm:^1.4.2": version: 1.6.0 resolution: "esquery@npm:1.6.0" dependencies: @@ -8419,6 +8525,15 @@ __metadata: languageName: node linkType: hard +"has-ansi@npm:^2.0.0": + version: 2.0.0 + resolution: "has-ansi@npm:2.0.0" + dependencies: + ansi-regex: ^2.0.0 + checksum: 1b51daa0214440db171ff359d0a2d17bc20061164c57e76234f614c91dbd2a79ddd68dfc8ee73629366f7be45a6df5f2ea9de83f52e1ca24433f2cc78c35d8ec + languageName: node + linkType: hard + "has-bigints@npm:^1.0.1, has-bigints@npm:^1.0.2": version: 1.0.2 resolution: "has-bigints@npm:1.0.2" @@ -10427,6 +10542,23 @@ __metadata: languageName: node linkType: hard +"loglevel-colored-level-prefix@npm:^1.0.0": + version: 1.0.0 + resolution: "loglevel-colored-level-prefix@npm:1.0.0" + dependencies: + chalk: ^1.1.3 + loglevel: ^1.4.1 + checksum: 146aa7d0ea900d6d8523e945b2265be240e4c7c4752dae678983764dd756c44194684af1ee8ea721feff4c4f8c5771544a02a6cd8b269a663cffe9b4fcf955f1 + languageName: node + linkType: hard + +"loglevel@npm:^1.4.1": + version: 1.9.2 + resolution: "loglevel@npm:1.9.2" + checksum: 896c67b90a507bfcfc1e9a4daa7bf789a441dd70d95cd13b998d6dd46233a3bfadfb8fadb07250432bbfb53bf61e95f2520f9b11f9d3175cc460e5c251eca0af + languageName: node + linkType: hard + "loose-envify@npm:^1.0.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" @@ -11164,6 +11296,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.3": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -12097,6 +12238,34 @@ __metadata: languageName: node linkType: hard +"prettier-eslint@npm:^16.4.2": + version: 16.4.2 + resolution: "prettier-eslint@npm:16.4.2" + dependencies: + "@typescript-eslint/parser": ^6.21.0 + common-tags: ^1.8.2 + dlv: ^1.1.3 + eslint: ^8.57.1 + indent-string: ^4.0.0 + lodash.merge: ^4.6.2 + loglevel-colored-level-prefix: ^1.0.0 + prettier: ^3.5.3 + pretty-format: ^29.7.0 + require-relative: ^0.8.7 + tslib: ^2.8.1 + vue-eslint-parser: ^9.4.3 + peerDependencies: + prettier-plugin-svelte: ^3.0.0 + svelte-eslint-parser: "*" + peerDependenciesMeta: + prettier-plugin-svelte: + optional: true + svelte-eslint-parser: + optional: true + checksum: ad420f2d3b6f0c055e0eefed2f32876e4ac29d5c0202778ae531438224c7d07b67dcfb64054bc61a0cc88f231988198f229395361a9b2112ad048d08b6d5bc80 + languageName: node + linkType: hard + "prettier-linter-helpers@npm:^1.0.0": version: 1.0.0 resolution: "prettier-linter-helpers@npm:1.0.0" @@ -12115,6 +12284,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.5.3": + version: 3.6.2 + resolution: "prettier@npm:3.6.2" + bin: + prettier: bin/prettier.cjs + checksum: 0206f5f437892e8858f298af8850bf9d0ef1c22e21107a213ba56bfb9c2387a2020bfda244a20161d8e3dad40c6b04101609a55d370dece53d0a31893b64f861 + languageName: node + linkType: hard + "pretty-format@npm:30.2.0, pretty-format@npm:^30.0.5": version: 30.2.0 resolution: "pretty-format@npm:30.2.0" @@ -12999,6 +13177,13 @@ __metadata: languageName: node linkType: hard +"require-relative@npm:^0.8.7": + version: 0.8.7 + resolution: "require-relative@npm:0.8.7" + checksum: f1c3be06977823bba43600344d9ea6fbf8a55bdb81ec76533126849ab4024e6c31c6666f37fa4b5cfeda9c41dee89b8e19597cac02bdefaab42255c6708661ab + languageName: node + linkType: hard + "reselect@npm:^4.1.7": version: 4.1.8 resolution: "reselect@npm:4.1.8" @@ -13274,6 +13459,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.6": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: f013a3ee4607857bcd3503b6ac1d80165f7f8ea94f5d55e2d3e33df82fce487aa3313b987abf9b39e0793c83c9fc67b76c36c067625141a9f6f704ae0ea18db2 + languageName: node + linkType: hard + "semver@npm:^7.6.3": version: 7.7.2 resolution: "semver@npm:7.7.2" @@ -13818,6 +14012,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^3.0.0": + version: 3.0.1 + resolution: "strip-ansi@npm:3.0.1" + dependencies: + ansi-regex: ^2.0.0 + checksum: 9b974de611ce5075c70629c00fa98c46144043db92ae17748fb780f706f7a789e9989fd10597b7c2053ae8d1513fd707816a91f1879b2f71e6ac0b6a863db465 + languageName: node + linkType: hard + "strip-ansi@npm:^5.0.0": version: 5.2.0 resolution: "strip-ansi@npm:5.2.0" @@ -13903,6 +14106,13 @@ __metadata: languageName: node linkType: hard +"supports-color@npm:^2.0.0": + version: 2.0.0 + resolution: "supports-color@npm:2.0.0" + checksum: 602538c5812b9006404370b5a4b885d3e2a1f6567d314f8b4a41974ffe7d08e525bf92ae0f9c7030e3b4c78e4e34ace55d6a67a74f1571bc205959f5972f88f0 + languageName: node + linkType: hard + "supports-color@npm:^5.3.0": version: 5.5.0 resolution: "supports-color@npm:5.5.0" @@ -14077,6 +14287,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^1.0.1": + version: 1.4.3 + resolution: "ts-api-utils@npm:1.4.3" + peerDependencies: + typescript: ">=4.2.0" + checksum: ea00dee382d19066b2a3d8929f1089888b05fec797e32e7a7004938eda1dccf2e77274ee2afcd4166f53fab9b8d7ee90ebb225a3183f9ba8817d636f688a148d + languageName: node + linkType: hard + "ts-api-utils@npm:^1.3.0": version: 1.4.0 resolution: "ts-api-utils@npm:1.4.0" @@ -14100,6 +14319,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.8.1": + version: 2.8.1 + resolution: "tslib@npm:2.8.1" + checksum: e4aba30e632b8c8902b47587fd13345e2827fa639e7c3121074d5ee0880723282411a8838f830b55100cbe4517672f84a2472667d355b81e8af165a55dc6203a + languageName: node + linkType: hard + "tsutils@npm:^3.21.0": version: 3.21.0 resolution: "tsutils@npm:3.21.0" @@ -14630,6 +14856,23 @@ __metadata: languageName: node linkType: hard +"vue-eslint-parser@npm:^9.4.3": + version: 9.4.3 + resolution: "vue-eslint-parser@npm:9.4.3" + dependencies: + debug: ^4.3.4 + eslint-scope: ^7.1.1 + eslint-visitor-keys: ^3.3.0 + espree: ^9.3.1 + esquery: ^1.4.0 + lodash: ^4.17.21 + semver: ^7.3.6 + peerDependencies: + eslint: ">=6.0.0" + checksum: 8d5b7ef7c5ee264ca2ba78da4b95ac7a66175a458d153a35e92cd7c55b794db0f2c31a8fdd40021bab4496f2f64ab80d7dbb6dccff4103beb4564c439a88fa42 + languageName: node + linkType: hard + "walker@npm:^1.0.7, walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" From 0e1bb0411c0a97ba335b80ad0699645ecbebfbb6 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 13:25:17 -0700 Subject: [PATCH 24/82] chore: remove unused back icon assets for various resolutions and platforms --- src/inbox/assets/back-icon.png | Bin 207 -> 0 bytes src/inbox/assets/back-icon@1x.android.png | Bin 100 -> 0 bytes src/inbox/assets/back-icon@1x.ios.png | Bin 290 -> 0 bytes src/inbox/assets/back-icon@2x.android.png | Bin 134 -> 0 bytes src/inbox/assets/back-icon@2x.ios.png | Bin 405 -> 0 bytes src/inbox/assets/back-icon@3x.android.png | Bin 167 -> 0 bytes src/inbox/assets/back-icon@3x.ios.png | Bin 761 -> 0 bytes src/inbox/assets/back-icon@4x.android.png | Bin 207 -> 0 bytes src/inbox/assets/back-icon@4x.ios.png | Bin 809 -> 0 bytes 9 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/inbox/assets/back-icon.png delete mode 100644 src/inbox/assets/back-icon@1x.android.png delete mode 100644 src/inbox/assets/back-icon@1x.ios.png delete mode 100644 src/inbox/assets/back-icon@2x.android.png delete mode 100644 src/inbox/assets/back-icon@2x.ios.png delete mode 100644 src/inbox/assets/back-icon@3x.android.png delete mode 100644 src/inbox/assets/back-icon@3x.ios.png delete mode 100644 src/inbox/assets/back-icon@4x.android.png delete mode 100644 src/inbox/assets/back-icon@4x.ios.png diff --git a/src/inbox/assets/back-icon.png b/src/inbox/assets/back-icon.png deleted file mode 100644 index 17e52e8550e5668f7117bcb755beb70c3a21c9e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207 zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD0wg^q?%xcgrg^$JhEy=Vy>(TP*-+rv1Kz7M z%I*XNJ!(VLzdH4Ge@o8ai4)e^3vK%P+bZq#{MQci z?P`C0d3kxcKa^1~`zt>B;jiLd|K_aKfAEz>edFmDy)D6SUw`+o@R*?B*u=sqq~bB* zgRIJrKN^lr3+tD}bFTI}~XjubmVU5CWQ(LZ;AgeuH{an^LB{Ts5E|^v% diff --git a/src/inbox/assets/back-icon@1x.android.png b/src/inbox/assets/back-icon@1x.android.png deleted file mode 100644 index 083db295f474b9903408258c71818c2c49151d35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 100 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1ZBG}+kP60RiDL^GIXr{@&3`D~ x%$9kI*}bEvW6`UVO)xQz+sFfYmle_k4J{c^Y-u@of;y~NbrRr@}xDqOoe#7(!ujI zW)IKPtpOg9x^ZGiV^G!WUnY384Jwg^1?qzoV?Vqi%~F-r131d0cdTWi1E3pVgr)a^ zbA?Lk#0WF#3+Yvg4uEz<4l(HmchxGX6;goJ^ovoWl9~bgne?yCQ=RAlXaeYC>D}3~ oOlpASO$!ea%F`=`*pIb34xO&@x>?fx0{{R307*qoM6N<$f~pOBr2qf` diff --git a/src/inbox/assets/back-icon@2x.android.png b/src/inbox/assets/back-icon@2x.android.png deleted file mode 100644 index 6de0a1cbb365dfd5d9274890d243ca2321f32235..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 134 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tUr!gukP61P(+)Bo5D;KluXOkB z|6f;`Uoa>nrTyP5;4+Jm;dJDxe#>oP@r$y*#>rHDd13s5eN)8##R;WH*ZD4M;?i1i i;i&z7`QNqd4J-@3aX4v+{p181%;4$j=d#Wzp$P!F!7nZV diff --git a/src/inbox/assets/back-icon@2x.ios.png b/src/inbox/assets/back-icon@2x.ios.png deleted file mode 100644 index 63de0e303aae4f3f09bbf5834290131501232f64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 405 zcmV;G0c!q$3ng)KwR%cav2NeVj#yi|twB8nJ$ zhJ9XSgC81to-du2Xkr{|@HcXhBf~opLkgz`JWJYh7b%=;afNgn{8BUDujC*_hPNS> z7&ls+kwoI8u%elk7pg@(StZ8P4^;l-*^fZJ1`5~OHBd;D;qqM+{Bs~b$&5^qEx(K8 zx9Nf9ZEAjO1n+^rUo`oHziD{zE6kAXuYyr+hHU>u3T%dKe|C&IvG2#fso;0*pv4!B zOc_R!8Jq}55TlLyCq{c8swLA2SGn}>CsPgp4HtE6hRq5700000NkvXXu0mjfokp-j diff --git a/src/inbox/assets/back-icon@3x.android.png b/src/inbox/assets/back-icon@3x.android.png deleted file mode 100644 index 15a983a67d97c9a6c39d91550a528c37c53a9e3e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhaw@;qG}Ln;{G-aN{9*nr0&aBl6p z15q2fw>DO<$f$_t-m`#N(R0!@qfL6}is!BA-aP5mqcux*S)AYDzOuXc@WF!zU-mWw z!RNIrdq3B7GJgpV3USMNx+#Bt3lxOZSlm}!I$z&?|8f=2ODy@*gQAaCs?P@6%;4$j K=d#Wzp$P!X&_+4{ diff --git a/src/inbox/assets/back-icon@3x.ios.png b/src/inbox/assets/back-icon@3x.ios.png deleted file mode 100644 index 2320fd74d1781dc8a74cc131d8c3bb17fc52bae4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 761 zcmVAUlM5g1Q_DYVitMm-$7Tx z*ro&sTZ*z6>QNY&<*{MBLwj|YS(XqXD^V764Y9I1Okis$Okg8b+mr|wUq@l0HcEq; zVTpm+Kw?IyI+FyL&Ez%>QDU|-7*wFaOtGZEY$rQ2K-HNzTzn^q>8HU=v}CyWZt?*4 zR&^#h0``%e>7l`lw-g9CKz639sxv7OaEQQsKxaB>Fyky00*;WF4yw+iM!+!&6ScM) z%veiJ{LO>gk?HFakuuxmtTUZbi;r*1=ySHXQyoauVV}3fsCFdksL$Qv?Q2bBWbLdnCsmVuHOp65Sf{(`X7f(3UB_ENM%STv@ zvIOdikIG_Hb<}I(pkKw$XH%o9lBnz0zU=BVRS7|_2-Hp7L=H8o0zzIAXX+Lzml{=$ zMBTymK^Jb-%uZCPG}qfJds0JQ2XB$6bg6-+U-Ef=(TP*-+rv1Kz7M z%I*XNJ!(VLzdH4Ge@o8ai4)e^3vK%P+bZq#{MQci z?P`C0d3kxcKa^1~`zt>B;jiLd|K_aKfAEz>edFmDy)D6SUw`+o@R*?B*u=sqq~bB* zgRIJrKN^lr3+tD}bFTI}~XjubmVU5CWQ(LZ;AgeuH{an^LB{Ts5E|^v% diff --git a/src/inbox/assets/back-icon@4x.ios.png b/src/inbox/assets/back-icon@4x.ios.png deleted file mode 100644 index b26634e594135d181375c3316b2ce059a31c6665..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 809 zcmV+^1J?YBP)YD1miR7mE{DV$Qg{||{bo~dA%(lFKsoPR23s4@v0mrvvjQGomaY7ADLDuqH1 zvGK4`4{>#jmgPlOeM+>Mf_`%D1%nu+I@RZN_76>V5WO1tOIiKYhsr9>jYgGKs=`*N zv}jyyRB5p)?goPB+{p5Jb-|H(#biuZo$8V=bj@m<&Aw*Mzvy;mD@cB;LBZVYApQ(D zdpPq|r)sw;OT$xM%b9U3xD%GW6CGq63GPHqLE>$f#-ZTp)T|)+jjzs91<7wcE?A); z`OV+XYNgUb`*8ccu~0$s2Ow%Rm0<*BXJiDW8UkwI7}mD<7*>7|TUr^wfilifZVG;4 n@$E^mUzjZzWv9I6RnQJ~H00000NkvXXu0mjf?i-Uu From 4fe7c80dc9ee1a3dc43f1f89bcfbc3b77b0620cb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 14:14:35 -0700 Subject: [PATCH 25/82] feat: replace icon references with base64 encoded images --- example/src/components/App/App.constants.ts | 18 +++++++++++--- example/src/components/App/App.utils.tsx | 27 ++++++++++++++++++--- example/src/components/App/Main.tsx | 2 +- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/example/src/components/App/App.constants.ts b/example/src/components/App/App.constants.ts index f84c390cb..65bf314e6 100644 --- a/example/src/components/App/App.constants.ts +++ b/example/src/components/App/App.constants.ts @@ -1,7 +1,19 @@ import { Route } from '../../constants'; +// Taken from https://github.com/ionic-team/ionicons/blob/main/src/svg/cash-outline.svg +export const cashIcon = + 'data:image/png;base64,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'; + +// Taken from https://github.com/ionic-team/ionicons/blob/main/src/svg/person-outline.svg +export const personIcon = + 'data:image/png;base64,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'; + +// Taken from https://github.com/ionic-team/ionicons/blob/main/src/svg/mail-outline.svg +export const mailIcon = + 'data:image/png;base64,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'; + export const routeIcon = { - [Route.Commerce]: 'cash-outline', - [Route.Inbox]: 'mail-outline', - [Route.User]: 'person-outline', + [Route.Commerce]: cashIcon, + [Route.Inbox]: mailIcon, + [Route.User]: personIcon, }; diff --git a/example/src/components/App/App.utils.tsx b/example/src/components/App/App.utils.tsx index 2dcde5374..6a3990472 100644 --- a/example/src/components/App/App.utils.tsx +++ b/example/src/components/App/App.utils.tsx @@ -1,5 +1,24 @@ -import Icon from 'react-native-vector-icons/Ionicons'; +import { Image, View } from 'react-native'; +import type { Route } from '../../constants/routes'; -export const getIcon = (name: string, props: Record) => ( - -); +export const getIcon = (name: Route, props: Record) => { + const { color, size = 25 } = props; + + return ( + + + + ); +}; diff --git a/example/src/components/App/Main.tsx b/example/src/components/App/Main.tsx index 55b0d74e2..b944801fe 100644 --- a/example/src/components/App/Main.tsx +++ b/example/src/components/App/Main.tsx @@ -25,7 +25,7 @@ export const Main = () => { screenOptions={({ route }) => { const iconName = routeIcon[route.name]; return { - tabBarIcon: (props) => getIcon(iconName, props), + tabBarIcon: (props) => getIcon(iconName as Route, props), tabBarActiveTintColor: colors.brandPurple, tabBarInactiveTintColor: colors.textSecondary, headerShown: false, From 6ece6e031147935ecd50f46e65be519d306f00ab Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 14:15:19 -0700 Subject: [PATCH 26/82] chore: remove react-native-vector-icons dependency and related configurations --- README.md | 1 - example/android/app/build.gradle | 2 - example/package.json | 1 - jest.config.js | 2 +- package.json | 3 -- yarn.lock | 68 +------------------------------- 6 files changed, 3 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index fb5a88a63..9f8ead8e9 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,6 @@ Iterable's React Native SDK relies on: _UI Components require additional peer dependencies_ - [React Navigation 6+](https://github.com/react-navigation/react-navigation) - [React Native Safe Area Context 4+](https://github.com/th3rdwave/react-native-safe-area-context) - - [React Native Vector Icons 10+](https://github.com/oblador/react-native-vector-icons) - [React Native WebView 13+](https://github.com/react-native-webview/react-native-webview) - **iOS** diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 060f74075..a060fdcce 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -117,5 +117,3 @@ dependencies { implementation jscFlavor } } - -apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle") diff --git a/example/package.json b/example/package.json index 40ca703a4..20dddd07b 100644 --- a/example/package.json +++ b/example/package.json @@ -19,7 +19,6 @@ "react-native-gesture-handler": "^2.24.0", "react-native-safe-area-context": "^5.1.0", "react-native-screens": "^4.9.1", - "react-native-vector-icons": "^10.2.0", "react-native-webview": "^13.13.1" }, "devDependencies": { diff --git a/jest.config.js b/jest.config.js index 482f53a77..c5b75e86c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { ], testMatch: ['/src/**/*.(test|spec).[jt]s?(x)'], transformIgnorePatterns: [ - 'node_modules/(?!(react-native|@react-native|@react-navigation|react-native-screens|react-native-safe-area-context|react-native-gesture-handler|react-native-webview|react-native-vector-icons)/)', + 'node_modules/(?!(react-native|@react-native|@react-navigation|react-native-screens|react-native-safe-area-context|react-native-gesture-handler|react-native-webview)/)', ], collectCoverageFrom: [ 'src/**/*.{cjs,js,jsx,mjs,ts,tsx}', diff --git a/package.json b/package.json index c3e1dfc52..bb356a6f5 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "@testing-library/react-native": "^13.3.3", "@types/jest": "^29.5.5", "@types/react": "^19.0.0", - "@types/react-native-vector-icons": "^6.4.18", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", "commitlint": "^19.6.1", @@ -96,7 +95,6 @@ "react-native-gesture-handler": "^2.26.0", "react-native-safe-area-context": "^5.4.0", "react-native-screens": "^4.10.0", - "react-native-vector-icons": "^10.2.0", "react-native-webview": "^13.14.1", "react-test-renderer": "19.0.0", "release-it": "^17.10.0", @@ -114,7 +112,6 @@ "react": "*", "react-native": "*", "react-native-safe-area-context": "*", - "react-native-vector-icons": "*", "react-native-webview": "*" }, "peerDependenciesMeta": { diff --git a/yarn.lock b/yarn.lock index ea98be24c..ec4f21c4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3108,7 +3108,6 @@ __metadata: react-native-gesture-handler: ^2.24.0 react-native-safe-area-context: ^5.1.0 react-native-screens: ^4.9.1 - react-native-vector-icons: ^10.2.0 react-native-webview: ^13.13.1 react-test-renderer: 19.0.0 languageName: unknown @@ -3130,7 +3129,6 @@ __metadata: "@testing-library/react-native": ^13.3.3 "@types/jest": ^29.5.5 "@types/react": ^19.0.0 - "@types/react-native-vector-icons": ^6.4.18 "@typescript-eslint/eslint-plugin": ^8.13.0 "@typescript-eslint/parser": ^8.13.0 commitlint: ^19.6.1 @@ -3149,7 +3147,6 @@ __metadata: react-native-gesture-handler: ^2.26.0 react-native-safe-area-context: ^5.4.0 react-native-screens: ^4.10.0 - react-native-vector-icons: ^10.2.0 react-native-webview: ^13.14.1 react-test-renderer: 19.0.0 release-it: ^17.10.0 @@ -3163,7 +3160,6 @@ __metadata: react: "*" react-native: "*" react-native-safe-area-context: "*" - react-native-vector-icons: "*" react-native-webview: "*" peerDependenciesMeta: expo: @@ -4615,25 +4611,6 @@ __metadata: languageName: node linkType: hard -"@types/react-native-vector-icons@npm:^6.4.18": - version: 6.4.18 - resolution: "@types/react-native-vector-icons@npm:6.4.18" - dependencies: - "@types/react": "*" - "@types/react-native": ^0.70 - checksum: 1ef458cb5e7a37f41eb400e3153940b1b152e4df76a7c06c7a47c712dbfe46e14b9999f04dde1bd074f338f850e161c6c925174ddea33386b74f8112c940065b - languageName: node - linkType: hard - -"@types/react-native@npm:^0.70": - version: 0.70.19 - resolution: "@types/react-native@npm:0.70.19" - dependencies: - "@types/react": "*" - checksum: 79b504fa56340631079e7c20ea0d9412ec14147b76d0ce189f4403936f529ef1e6fd031383afab117846c5ae039123bcf3afc948bae4432269c6780282726f71 - languageName: node - linkType: hard - "@types/react-test-renderer@npm:^19.0.0": version: 19.1.0 resolution: "@types/react-test-renderer@npm:19.1.0" @@ -6172,17 +6149,6 @@ __metadata: languageName: node linkType: hard -"cliui@npm:^7.0.2": - version: 7.0.4 - resolution: "cliui@npm:7.0.4" - dependencies: - string-width: ^4.2.0 - strip-ansi: ^6.0.0 - wrap-ansi: ^7.0.0 - checksum: ce2e8f578a4813806788ac399b9e866297740eecd4ad1823c27fd344d78b22c5f8597d548adbcc46f0573e43e21e751f39446c5a5e804a12aace402b7a315d7f - languageName: node - linkType: hard - "cliui@npm:^8.0.1": version: 8.0.1 resolution: "cliui@npm:8.0.1" @@ -12370,7 +12336,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -12760,21 +12726,6 @@ __metadata: languageName: node linkType: hard -"react-native-vector-icons@npm:^10.2.0": - version: 10.2.0 - resolution: "react-native-vector-icons@npm:10.2.0" - dependencies: - prop-types: ^15.7.2 - yargs: ^16.1.1 - bin: - fa-upgrade.sh: bin/fa-upgrade.sh - fa5-upgrade: bin/fa5-upgrade.sh - fa6-upgrade: bin/fa6-upgrade.sh - generate-icon: bin/generate-icon.js - checksum: fda930df4e63f12533268f5b339ebe4c77c691eae43503328466b3087ed868a06a4593fd246e75ac6b5ec955543eec35608c7922191bdcc3b3a94ed7f3575ef0 - languageName: node - linkType: hard - "react-native-webview@npm:^13.13.1": version: 13.14.1 resolution: "react-native-webview@npm:13.14.1" @@ -15200,7 +15151,7 @@ __metadata: languageName: node linkType: hard -"yargs-parser@npm:^20.2.2, yargs-parser@npm:^20.2.9": +"yargs-parser@npm:^20.2.9": version: 20.2.9 resolution: "yargs-parser@npm:20.2.9" checksum: 8bb69015f2b0ff9e17b2c8e6bfe224ab463dd00ca211eece72a4cd8a906224d2703fb8a326d36fdd0e68701e201b2a60ed7cf81ce0fd9b3799f9fe7745977ae3 @@ -15226,21 +15177,6 @@ __metadata: languageName: node linkType: hard -"yargs@npm:^16.1.1": - version: 16.2.0 - resolution: "yargs@npm:16.2.0" - dependencies: - cliui: ^7.0.2 - escalade: ^3.1.1 - get-caller-file: ^2.0.5 - require-directory: ^2.1.1 - string-width: ^4.2.0 - y18n: ^5.0.5 - yargs-parser: ^20.2.2 - checksum: b14afbb51e3251a204d81937c86a7e9d4bdbf9a2bcee38226c900d00f522969ab675703bee2a6f99f8e20103f608382936034e64d921b74df82b63c07c5e8f59 - languageName: node - linkType: hard - "yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" From c1f3dec93b6ececc78e2d566b8d7e7d44131ad7d Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 15 Oct 2025 14:29:20 -0700 Subject: [PATCH 27/82] test: add comprehensive tests for HeaderBackButton component --- .eslintrc.js | 1 + .../components/HeaderBackButton.test.tsx | 243 ++++++++++++++++++ 2 files changed, 244 insertions(+) create mode 100644 src/inbox/components/HeaderBackButton.test.tsx diff --git a/.eslintrc.js b/.eslintrc.js index e0f808c23..bad66d857 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -48,4 +48,5 @@ module.exports = { }, }, ], + ignorePatterns: ['coverage/**/*', 'lib/**/*', 'docs/**/*'], }; diff --git a/src/inbox/components/HeaderBackButton.test.tsx b/src/inbox/components/HeaderBackButton.test.tsx new file mode 100644 index 000000000..76917e0c0 --- /dev/null +++ b/src/inbox/components/HeaderBackButton.test.tsx @@ -0,0 +1,243 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { fireEvent, render } from '@testing-library/react-native'; +import { PixelRatio } from 'react-native'; +import { HeaderBackButton, ICON_MARGIN, ICON_SIZE } from './HeaderBackButton'; + +describe('HeaderBackButton', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering', () => { + it('should render without crashing', () => { + const { getByTestId } = render(); + expect(getByTestId('back-button')).toBeTruthy(); + }); + + it('should render with default back arrow image', () => { + const { UNSAFE_getByType } = render(); + const image = UNSAFE_getByType('Image' as any); + expect(image).toBeTruthy(); + expect(image.props.source).toMatchObject({ + uri: expect.stringContaining('data:image/png;base64'), + width: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE), + height: PixelRatio.getPixelSizeForLayoutSize(ICON_SIZE), + }); + }); + + it('should render without label by default', () => { + const { queryByText } = render(); + expect(queryByText(/./)).toBeNull(); + }); + + it('should render with label when provided', () => { + const label = 'Back'; + const { getByText } = render(); + expect(getByText(label)).toBeTruthy(); + }); + + it('should render with custom label text', () => { + const customLabel = 'Go Back to Home'; + const { getByText } = render(); + expect(getByText(customLabel)).toBeTruthy(); + }); + }); + + describe('Custom Image Props', () => { + it('should render with custom imageUri', () => { + const customUri = 'https://example.com/custom-back-icon.png'; + const { UNSAFE_getByType } = render( + + ); + const image = UNSAFE_getByType('Image' as any); + expect(image.props.source).toMatchObject({ + uri: customUri, + }); + }); + + it('should render with custom imageSource', () => { + const customSource = { uri: 'https://example.com/icon.png' }; + const { UNSAFE_getByType } = render( + + ); + const image = UNSAFE_getByType('Image' as any); + expect(image.props.source).toEqual(customSource); + }); + + it('should prioritize imageSource over imageUri when both are provided', () => { + const customUri = 'https://example.com/custom-back-icon.png'; + const customSource = { uri: 'https://example.com/icon.png' }; + const { UNSAFE_getByType } = render( + + ); + const image = UNSAFE_getByType('Image' as any); + expect(image.props.source).toEqual(customSource); + }); + }); + + describe('Image Properties', () => { + it('should render image with correct properties', () => { + const { UNSAFE_getByType } = render(); + const image = UNSAFE_getByType('Image' as any); + + expect(image.props.resizeMode).toBe('contain'); + expect(image.props.fadeDuration).toBe(0); + expect(image.props.height).toBe(ICON_SIZE); + expect(image.props.width).toBe(ICON_SIZE); + expect(image.props.resizeMethod).toBe('scale'); + expect(image.props.tintColor).toBeTruthy(); + }); + + it('should apply correct style to image', () => { + const { UNSAFE_getByType } = render(); + const image = UNSAFE_getByType('Image' as any); + + expect(image.props.style).toMatchObject({ + height: ICON_SIZE, + margin: ICON_MARGIN, + width: ICON_SIZE, + }); + }); + }); + + describe('Touch Interaction', () => { + it('should call onPress when button is pressed', () => { + const onPressMock = jest.fn(); + const { getByTestId } = render( + + ); + + fireEvent.press(getByTestId('back-button')); + expect(onPressMock).toHaveBeenCalledTimes(1); + }); + + it('should call onPressIn when touch starts', () => { + const onPressInMock = jest.fn(); + const { getByTestId } = render( + + ); + + fireEvent(getByTestId('back-button'), 'pressIn'); + expect(onPressInMock).toHaveBeenCalledTimes(1); + }); + + it('should call onPressOut when touch ends', () => { + const onPressOutMock = jest.fn(); + const { getByTestId } = render( + + ); + + fireEvent(getByTestId('back-button'), 'pressOut'); + expect(onPressOutMock).toHaveBeenCalledTimes(1); + }); + + it('should not trigger onPress when disabled', () => { + const onPressMock = jest.fn(); + const { getByTestId } = render( + + ); + + fireEvent.press(getByTestId('back-button')); + expect(onPressMock).not.toHaveBeenCalled(); + }); + }); + + describe('Platform-specific behavior', () => { + it('should export correct icon size constant', () => { + // ICON_SIZE is evaluated at module load time based on Platform.OS + expect(ICON_SIZE).toBeDefined(); + expect([21, 24]).toContain(ICON_SIZE); + }); + + it('should export correct icon margin constant', () => { + // ICON_MARGIN is evaluated at module load time based on Platform.OS + expect(ICON_MARGIN).toBeDefined(); + expect([3, 8]).toContain(ICON_MARGIN); + }); + + it('should use consistent icon size in image props', () => { + const { UNSAFE_getByType } = render(); + const image = UNSAFE_getByType('Image' as any); + + expect(image.props.height).toBe(ICON_SIZE); + expect(image.props.width).toBe(ICON_SIZE); + }); + }); + + describe('Accessibility', () => { + it('should accept accessibility props', () => { + const { getByTestId } = render( + + ); + + const button = getByTestId('back-button'); + expect(button.props.accessible).toBe(true); + expect(button.props.accessibilityLabel).toBe('Navigate back'); + expect(button.props.accessibilityHint).toBe('Returns to previous screen'); + }); + }); + + describe('Component Structure', () => { + it('should render View with correct flex direction', () => { + const { UNSAFE_getAllByType } = render(); + const views = UNSAFE_getAllByType('View' as any); + + // Find the view with returnButton style + const returnButtonView = views.find( + (view) => + view.props.style?.flexDirection === 'row' && + view.props.style?.alignItems === 'center' + ); + expect(returnButtonView).toBeTruthy(); + }); + + it('should render label text with correct style when provided', () => { + const { getByText } = render(); + const labelElement = getByText('Back'); + + expect(labelElement.props.style).toMatchObject({ + fontSize: 20, + }); + }); + }); + + describe('Edge Cases', () => { + it('should handle empty string label', () => { + const { queryByText } = render(); + // Empty string should not render text + expect(queryByText('')).toBeNull(); + }); + + it('should handle multiple props correctly', () => { + const onPressMock = jest.fn(); + const label = 'Custom Back'; + const customUri = 'https://example.com/icon.png'; + + const { getByText, getByTestId } = render( + + ); + + expect(getByText(label)).toBeTruthy(); + expect(getByTestId('back-button').props.accessible).toBe(true); + + fireEvent.press(getByTestId('back-button')); + expect(onPressMock).toHaveBeenCalledTimes(1); + }); + }); +}); From 5517bf7a11f404a3613c1c5569f919cc1d87dcf2 Mon Sep 17 00:00:00 2001 From: Loren <3190869+lposen@users.noreply.github.com> Date: Wed, 15 Oct 2025 17:20:34 -0700 Subject: [PATCH 28/82] Update src/inApp/classes/IterableInAppManager.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/inApp/classes/IterableInAppManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/inApp/classes/IterableInAppManager.ts b/src/inApp/classes/IterableInAppManager.ts index 3d6a3cbf8..ae34f80d1 100644 --- a/src/inApp/classes/IterableInAppManager.ts +++ b/src/inApp/classes/IterableInAppManager.ts @@ -22,7 +22,7 @@ import { IterableInAppMessage } from './IterableInAppMessage'; * console.log('Messages:', messages); * }); * - * // You can also access an instance on `Iterable.inAppManager.inAppManager` + * // You can also access an instance on `Iterable.inAppManager` * Iterable.inAppManager.getMessages().then(messages => { * console.log('Messages:', messages); * }); From 7144ab1be9edd2f0a06ed8bfef960935ba1a4c38 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 16 Oct 2025 12:47:36 -0700 Subject: [PATCH 29/82] chore: update version to 2.0.4 in package.json and itblBuildInfo.ts --- package.json | 2 +- src/itblBuildInfo.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bb356a6f5..7a49b12ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iterable/react-native-sdk", - "version": "2.0.3", + "version": "2.0.4", "description": "Iterable SDK for React Native.", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/src/itblBuildInfo.ts b/src/itblBuildInfo.ts index 0cdaa2c54..5af8adaf2 100644 --- a/src/itblBuildInfo.ts +++ b/src/itblBuildInfo.ts @@ -3,5 +3,5 @@ * It contains the version of the package */ export const buildInfo = { - version: '2.0.3', + version: '2.0.4', }; From d32bdd2119c86e7cdfae04c93a447579abf003a8 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 16 Oct 2025 13:00:40 -0700 Subject: [PATCH 30/82] chore: update CHANGELOG for version 2.0.4 --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c08e0ed2b..9c24bb007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## 2.0.4 + +## Updates +- Added API documentation via Netlify([1087275](https://github.com/Iterable/react-native-sdk/commit/1087275)) +- Removed dependency on `react-native-vector-icons`, per issues + [#513](https://github.com/Iterable/react-native-sdk/issues/513), + [#683](https://github.com/Iterable/react-native-sdk/issues/683) and + [#675](https://github.com/Iterable/react-native-sdk/issues/675) + ([6ece6e0](https://github.com/Iterable/react-native-sdk/commit/6ece6e0)) +- Updated dependencies +- Added unit tests + ## 2.0.3 ### Updates From d274c0f884b0a0114fa04b42a090efc5e6a8f23d Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 16 Oct 2025 13:30:46 -0700 Subject: [PATCH 31/82] chore: update CHANGELOG to remove unit tests entry for version 2.0.4 --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c24bb007..a89e153e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ [#675](https://github.com/Iterable/react-native-sdk/issues/675) ([6ece6e0](https://github.com/Iterable/react-native-sdk/commit/6ece6e0)) - Updated dependencies -- Added unit tests ## 2.0.3 From c0a2c20f10d0ff889ec058553cd4cf342cf5946e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 17 Oct 2025 11:39:53 -0700 Subject: [PATCH 32/82] docs: removed a TODO and enhanced documentation for IterableLogger --- src/core/classes/IterableLogger.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/core/classes/IterableLogger.ts b/src/core/classes/IterableLogger.ts index 50c39ea78..be9c6ae67 100644 --- a/src/core/classes/IterableLogger.ts +++ b/src/core/classes/IterableLogger.ts @@ -6,14 +6,15 @@ const DEFAULT_LOGGING_ENABLED = true; /** * A logger class for the Iterable SDK. * - * This class is responsible for logging messages based on the configuration provided. - * - * TODO: add a logLevel property to the IterableLogger class to control the level of logging. + * This class is responsible for logging messages based on the configuration + * provided, is useful in unit testing or debug environments. * * @remarks * The logging behavior is controlled by the `logReactNativeSdkCalls` property * in {@link IterableConfig}. - * If this property is not set, logging defaults to `true`, which is useful in unit testing or debug environments. + * + * If this property is not set, logging defaults to `true`, which is useful in + * unit testing or debug environments. * * @example * ```typescript @@ -99,7 +100,8 @@ export class IterableLogger { } /** - * Logs a message to the console if the log level is {@link IterableLogLevel.debug} or lower. + * Logs a message to the console if the log level is + * {@link IterableLogLevel.debug} or {@link IterableLogLevel.error}. * * @param message - The message to be logged. * @@ -122,7 +124,9 @@ export class IterableLogger { } /** - * Logs a message to the console if the log level is {@link IterableLogLevel.info} or lower. + * Logs a message to the console if the log level is + * {@link IterableLogLevel.info}, {@link IterableLogLevel.debug} or + * {@link IterableLogLevel.error}. * * @param message - The message to be logged. * From 6828a7a753c718192e9e3a58f94f90e5e88127b7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 17 Oct 2025 11:55:09 -0700 Subject: [PATCH 33/82] fix: change default log level from info to debug in IterableConfig --- src/core/classes/IterableConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index c8ee67400..173b57ab3 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -230,7 +230,7 @@ export class IterableConfig { * * By default, you will be able to see info level logs printed in IDE when running the app. */ - logLevel: IterableLogLevel = IterableLogLevel.info; + logLevel: IterableLogLevel = IterableLogLevel.debug; /** * Configuration for JWT refresh retry behavior. From e2c6148fc8dae242329aa8a6ee166029634f93c9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Fri, 17 Oct 2025 12:05:43 -0700 Subject: [PATCH 34/82] refactor: update default log level to debug in IterableLogger and related tests --- src/core/classes/Iterable.test.ts | 4 ++-- src/core/classes/IterableLogger.test.ts | 14 +++++++------- src/core/classes/IterableLogger.ts | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index afc5100dd..7774b5bba 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -256,7 +256,7 @@ describe('Iterable', () => { expect(config.customActionHandler).toBe(undefined); expect(config.inAppHandler).toBe(undefined); expect(config.authHandler).toBe(undefined); - expect(config.logLevel).toBe(IterableLogLevel.info); + expect(config.logLevel).toBe(IterableLogLevel.debug); expect(config.logReactNativeSdkCalls).toBe(true); expect(config.expiringAuthTokenRefreshPeriod).toBe(60.0); expect(config.allowedProtocols).toEqual([]); @@ -272,7 +272,7 @@ describe('Iterable', () => { expect(configDict.customActionHandlerPresent).toBe(false); expect(configDict.inAppHandlerPresent).toBe(false); expect(configDict.authHandlerPresent).toBe(false); - expect(configDict.logLevel).toBe(IterableLogLevel.info); + expect(configDict.logLevel).toBe(IterableLogLevel.debug); expect(configDict.expiringAuthTokenRefreshPeriod).toBe(60.0); expect(configDict.allowedProtocols).toEqual([]); expect(configDict.androidSdkUseInMemoryStorageForInApps).toBe(false); diff --git a/src/core/classes/IterableLogger.test.ts b/src/core/classes/IterableLogger.test.ts index 9d35b4552..8caacdf86 100644 --- a/src/core/classes/IterableLogger.test.ts +++ b/src/core/classes/IterableLogger.test.ts @@ -1,5 +1,5 @@ import { IterableLogLevel } from '../enums/IterableLogLevel'; -import { IterableLogger } from './IterableLogger'; +import { IterableLogger, DEFAULT_LOG_LEVEL, DEFAULT_LOGGING_ENABLED } from './IterableLogger'; // Mock console.log to capture log output const mockConsoleLog = jest.fn(); @@ -8,8 +8,8 @@ const originalConsoleLog = console.log; describe('IterableLogger', () => { beforeEach(() => { // Reset to default values before each test - IterableLogger.loggingEnabled = true; - IterableLogger.logLevel = IterableLogLevel.info; + IterableLogger.loggingEnabled = DEFAULT_LOGGING_ENABLED; + IterableLogger.logLevel = DEFAULT_LOG_LEVEL; // Mock console.log console.log = mockConsoleLog; @@ -26,8 +26,8 @@ describe('IterableLogger', () => { expect(IterableLogger.loggingEnabled).toBe(true); }); - test('should have default log level as info', () => { - expect(IterableLogger.logLevel).toBe(IterableLogLevel.info); + test('should have default log level as debug', () => { + expect(IterableLogger.logLevel).toBe(IterableLogLevel.debug); }); test('should allow setting loggingEnabled directly', () => { @@ -86,9 +86,9 @@ describe('IterableLogger', () => { expect(IterableLogger.logLevel).toBe(IterableLogLevel.info); }); - test('should default to info when passed undefined', () => { + test('should default to debug when passed undefined', () => { IterableLogger.setLogLevel(undefined); - expect(IterableLogger.logLevel).toBe(IterableLogLevel.info); + expect(IterableLogger.logLevel).toBe(IterableLogLevel.debug); }); }); diff --git a/src/core/classes/IterableLogger.ts b/src/core/classes/IterableLogger.ts index be9c6ae67..6ce8d0d7c 100644 --- a/src/core/classes/IterableLogger.ts +++ b/src/core/classes/IterableLogger.ts @@ -1,7 +1,7 @@ import { IterableLogLevel } from '../enums/IterableLogLevel'; -const DEFAULT_LOG_LEVEL = IterableLogLevel.info; -const DEFAULT_LOGGING_ENABLED = true; +export const DEFAULT_LOG_LEVEL = IterableLogLevel.debug; +export const DEFAULT_LOGGING_ENABLED = true; /** * A logger class for the Iterable SDK. From 2dcb96984580bdabb0d2230922f7f4aefc045f08 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 17:01:43 -0700 Subject: [PATCH 35/82] chore: update yarn.lock and enhance example configuration documentation --- example/.env.example | 15 +++++++++++---- example/README.md | 21 ++++++++++++++------- yarn.lock | 18 ------------------ 3 files changed, 25 insertions(+), 29 deletions(-) diff --git a/example/.env.example b/example/.env.example index e1efc4169..ada1a4c4f 100644 --- a/example/.env.example +++ b/example/.env.example @@ -9,11 +9,18 @@ # 4. Fill in the following fields: # - Name: A descriptive name for the API key # - Type: Mobile -# - JWT authentication: Leave **unchecked** (IMPORTANT) +# - JWT authentication: Whether or not you want to use JWT # 5. Click "Create API Key" -# 6. Copy the generated API key -# 7. Replace the placeholder text next to `ITBL_API_KEY=` with the copied API key +# 6. Copy the generated API key and replace the placeholder text next to +# `ITBL_API_KEY=` with the copied API key +# 7. If you chose to enable JWT authentication, copy the JWT secret and and +# replace the placeholder text next to `ITBL_JWT_SECRET=` with the copied +# JWT secret ITBL_API_KEY=replace_this_with_your_iterable_api_key +# Your JWT Secret, created when making your API key (see above) +ITBL_JWT_SECRET=replace_this_with_your_jwt_secret +# Is your api token JWT Enabled +ITBL_IS_JWT_ENABLED=true # Your Iterable user ID or email address -ITBL_ID=replace_this_with_your_user_id_or_email \ No newline at end of file +ITBL_ID=replace_this_with_your_user_id_or_email diff --git a/example/README.md b/example/README.md index 4ba5d0e6d..819bc6826 100644 --- a/example/README.md +++ b/example/README.md @@ -23,7 +23,8 @@ _example app directory_. To do so, run the following: ```bash cd ios -pod install +bundle install +bundle exec pod install ``` Once this is done, `cd` back into the _example app directory_: @@ -40,12 +41,18 @@ In it, you will find: ```shell ITBL_API_KEY=replace_this_with_your_iterable_api_key +ITBL_JWT_SECRET=replace_this_with_your_jwt_secret +ITBL_IS_JWT_ENABLED=true ITBL_ID=replace_this_with_your_user_id_or_email ``` -Replace `replace_this_with_your_iterable_api_key` with your _mobile_ Iterable API key, -and replace `replace_this_with_your_user_id_or_email` with the email or user id -that you use to log into Iterable. +- Replace `replace_this_with_your_iterable_api_key` with your **_mobile_ +Iterable API key** +- Replace `replace_this_with_your_jwt_secret` with your **JWT Secret** (if you +have a JWT-enabled API key) +- Set `ITBL_IS_JWT_ENABLED` to true if you have a JWT-enabled key, and false if you do not. +- Replace `replace_this_with_your_user_id_or_email` with the **email or user +id** that you use to log into Iterable. Follow the steps below if you do not have a mobile Iterable API key. @@ -54,12 +61,12 @@ To add an API key, do the following: 1. Sign into your Iterable account 2. Go to [Integrations > API Keys](https://app.iterable.com/settings/apiKeys) 3. Click "New API Key" in the top right corner - 4. Fill in the followsing fields: + 4. Fill in the following fields: - Name: A descriptive name for the API key - Type: Mobile - - JWT authentication: Leave **unchecked** (IMPORTANT) + - JWT authentication: Check to enable JWT authentication. If enabled, will need to create a [JWT generator](https://support.iterable.com/hc/en-us/articles/360050801231-JWT-Enabled-API-Keys#sample-python-code-for-jwt-generation) to generate the JWT token. 5. Click "Create API Key" - 6. Copy the generated API key + 6. Copy the generated API key and JWT secret into your _.env_ file ## Step 3: Start the Metro Server diff --git a/yarn.lock b/yarn.lock index eb5b0a861..348741043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3555,24 +3555,6 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/parser@npm:^6.21.0": - version: 6.21.0 - resolution: "@typescript-eslint/parser@npm:6.21.0" - dependencies: - "@typescript-eslint/scope-manager": 6.21.0 - "@typescript-eslint/types": 6.21.0 - "@typescript-eslint/typescript-estree": 6.21.0 - "@typescript-eslint/visitor-keys": 6.21.0 - debug: ^4.3.4 - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 162fe3a867eeeffda7328bce32dae45b52283c68c8cb23258fb9f44971f761991af61f71b8c9fe1aa389e93dfe6386f8509c1273d870736c507d76dd40647b68 - languageName: node - linkType: hard - "@typescript-eslint/parser@npm:^7.1.1": version: 7.18.0 resolution: "@typescript-eslint/parser@npm:7.18.0" From 1bfa9d55bd69a08be5d13a09ead6b49c3cb824a6 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 17:04:24 -0700 Subject: [PATCH 36/82] chore: add .env.local to .gitignore and update example configuration comments --- .gitignore | 1 + example/.env.example | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 356417c19..f042551bd 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,7 @@ ios/generated android/generated # Iterable +.env.local .env .xcode.env.local coverage/ diff --git a/example/.env.example b/example/.env.example index ada1a4c4f..9d1ff99bc 100644 --- a/example/.env.example +++ b/example/.env.example @@ -19,7 +19,8 @@ ITBL_API_KEY=replace_this_with_your_iterable_api_key # Your JWT Secret, created when making your API key (see above) ITBL_JWT_SECRET=replace_this_with_your_jwt_secret -# Is your api token JWT Enabled +# Is your api token JWT Enabled? +# Defaults to true ITBL_IS_JWT_ENABLED=true # Your Iterable user ID or email address From d471800c0d829e928334f19244f1c74b754ae840 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 17:15:31 -0700 Subject: [PATCH 37/82] refactor: small updates to ReactIterable.swift --- ios/RNIterableAPI/ReactIterableAPI.swift | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ios/RNIterableAPI/ReactIterableAPI.swift b/ios/RNIterableAPI/ReactIterableAPI.swift index f04b08e42..80c0a9cab 100644 --- a/ios/RNIterableAPI/ReactIterableAPI.swift +++ b/ios/RNIterableAPI/ReactIterableAPI.swift @@ -215,7 +215,8 @@ import React ITBError("Could not find message with id: \(messageId)") return } - IterableAPI.track(inAppOpen: message, location: InAppLocation.from(number: locationNumber as NSNumber)) + IterableAPI.track( + inAppOpen: message, location: InAppLocation.from(number: locationNumber as NSNumber)) } @objc(trackInAppClick:location:clickedUrl:) @@ -414,8 +415,10 @@ import React templateId: Double ) { ITBInfo() - let finalCampaignId: NSNumber? = (campaignId as NSNumber).intValue <= 0 ? nil : campaignId as NSNumber - let finalTemplateId: NSNumber? = (templateId as NSNumber).intValue <= 0 ? nil : templateId as NSNumber + let finalCampaignId: NSNumber? = + (campaignId as NSNumber).intValue <= 0 ? nil : campaignId as NSNumber + let finalTemplateId: NSNumber? = + (templateId as NSNumber).intValue <= 0 ? nil : templateId as NSNumber IterableAPI.updateSubscriptions( emailListIds, unsubscribedChannelIds: unsubscribedChannelIds, @@ -480,7 +483,7 @@ import React @objc(passAlongAuthToken:) public func passAlongAuthToken(authToken: String?) { ITBInfo() - passedAuthToken = authToken + self.passedAuthToken = authToken authHandlerSemaphore.signal() } @@ -537,7 +540,9 @@ import React iterableConfig.inAppDelegate = self } - if let authHandlerPresent = configDict["authHandlerPresent"] as? Bool, authHandlerPresent { + if let authHandlerPresent = configDict["authHandlerPresent"] as? Bool, + authHandlerPresent == true + { iterableConfig.authDelegate = self } From 6a9972af66641ae024672baeac0a98d3f998d4ef Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 18:24:47 -0700 Subject: [PATCH 38/82] feat: implement JWT generation module for React Native in example --- .../example/IterableJwtGenerator.java | 106 ++++++++++++++++ .../reactnativesdk/example/JwtTokenModule.kt | 37 ++++++ .../reactnativesdk/example/JwtTokenPackage.kt | 34 +++++ .../reactnativesdk/example/MainApplication.kt | 1 + .../ReactNativeSdkExample-Bridging-Header.h | 3 + .../IterableJwtGenerator.swift | 118 ++++++++++++++++++ .../ReactNativeSdkExample/JwtTokenModule.m | 25 ++++ .../JwtTokenModule.swift | 40 ++++++ example/src/hooks/useIterableApp.tsx | 75 ++++++++--- example/src/utility/NativeJwtTokenModule.ts | 45 +++++++ example/src/utility/index.ts | 2 + src/core/classes/Iterable.ts | 4 +- yarn.lock | 16 +++ 13 files changed, 484 insertions(+), 22 deletions(-) create mode 100644 example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java create mode 100644 example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt create mode 100644 example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt create mode 100644 example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift create mode 100644 example/ios/ReactNativeSdkExample/JwtTokenModule.m create mode 100644 example/ios/ReactNativeSdkExample/JwtTokenModule.swift create mode 100644 example/src/utility/NativeJwtTokenModule.ts create mode 100644 example/src/utility/index.ts diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java b/example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java new file mode 100644 index 000000000..4fa760e33 --- /dev/null +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/IterableJwtGenerator.java @@ -0,0 +1,106 @@ +package com.iterable; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Base64; +import java.util.Base64.Encoder; + +/** +* Utility class to generate JWTs for use with the Iterable API +* +* @author engineering@iterable.com +*/ +public class IterableJwtGenerator { + static Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + + private static final String algorithm = "HmacSHA256"; + + // Iterable enforces a 1-year maximum token lifetime + private static final Duration maxTokenLifetime = Duration.ofDays(365); + + private static long millisToSeconds(long millis) { + return millis / 1000; + } + + private static final String encodedHeader = encoder.encodeToString( + "{\"alg\":\"HS256\",\"typ\":\"JWT\"}".getBytes(StandardCharsets.UTF_8) + ); + + /** + * Generates a JWT from the provided secret, header, and payload. Does not + * validate the header or payload. + * + * @param secret Your organization's shared secret with Iterable + * @param payload The JSON payload + * + * @return a signed JWT + */ + public static String generateToken(String secret, String payload) { + try { + String encodedPayload = encoder.encodeToString( + payload.getBytes(StandardCharsets.UTF_8) + ); + String encodedHeaderAndPayload = encodedHeader + "." + encodedPayload; + + // HMAC setup + Mac hmac = Mac.getInstance(algorithm); + SecretKeySpec keySpec = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), algorithm + ); + hmac.init(keySpec); + + String signature = encoder.encodeToString( + hmac.doFinal( + encodedHeaderAndPayload.getBytes(StandardCharsets.UTF_8) + ) + ); + + return encodedHeaderAndPayload + "." + signature; + + } catch (Exception e) { + throw new RuntimeException(e.getMessage()); + } + } + + /** + * Generates a JWT (issued now, expires after the provided duration). + * + * @param secret Your organization's shared secret with Iterable. + * @param duration The token's expiration time. Up to one year. + * @param email The email to included in the token, or null. + * @param userId The userId to include in the token, or null. + * + * @return A JWT string + */ + public static String generateToken( + String secret, Duration duration, String email, String userId) { + + if (duration.compareTo(maxTokenLifetime) > 0) + throw new IllegalArgumentException( + "Duration must be one year or less." + ); + + if ((userId != null && email != null) || (userId == null && email == null)) + throw new IllegalArgumentException( + "The token must include a userId or email, but not both." + ); + + long now = millisToSeconds(System.currentTimeMillis()); + + String payload; + if (userId != null) + payload = String.format( + "{ \"userId\": \"%s\", \"iat\": %d, \"exp\": %d }", + userId, now, now + millisToSeconds(duration.toMillis()) + ); + else + payload = String.format( + "{ \"email\": \"%s\", \"iat\": %d, \"exp\": %d }", + email, now, now + millisToSeconds(duration.toMillis()) + ); + + return generateToken(secret, payload); + } +} diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt new file mode 100644 index 000000000..8a6f7f018 --- /dev/null +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenModule.kt @@ -0,0 +1,37 @@ +package iterable.reactnativesdk.example + +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.Promise +import com.iterable.IterableJwtGenerator +import java.time.Duration + +class JwtTokenModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + override fun getName(): String { + return NAME + } + + @ReactMethod + fun generateJwtToken( + secret: String, + durationMs: Double, + email: String?, + userId: String?, + promise: Promise + ) { + try { + val duration = Duration.ofMillis(durationMs.toLong()) + val token = IterableJwtGenerator.generateToken(secret, duration, email, userId) + promise.resolve(token) + } catch (e: Exception) { + promise.reject("JWT_GENERATION_ERROR", e.message, e) + } + } + + companion object { + const val NAME = "JwtTokenModule" + } +} + diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt new file mode 100644 index 000000000..e05384909 --- /dev/null +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/JwtTokenPackage.kt @@ -0,0 +1,34 @@ +package iterable.reactnativesdk.example + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + +class JwtTokenPackage : BaseReactPackage() { + + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + return if (name == JwtTokenModule.NAME) { + JwtTokenModule(reactContext) + } else { + null + } + } + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { + return ReactModuleInfoProvider { + val moduleInfos: MutableMap = HashMap() + moduleInfos[JwtTokenModule.NAME] = ReactModuleInfo( + JwtTokenModule.NAME, + JwtTokenModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false // isCxxModule + ) + moduleInfos + } + } +} + diff --git a/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt b/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt index d0fba2035..9c004c88b 100644 --- a/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt +++ b/example/android/app/src/main/java/iterable/reactnativesdk/example/MainApplication.kt @@ -20,6 +20,7 @@ class MainApplication : Application(), ReactApplication { PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: // add(MyReactNativePackage()) + add(JwtTokenPackage()) } override fun getJSMainModuleName(): String = "index" diff --git a/example/ios/ReactNativeSdkExample-Bridging-Header.h b/example/ios/ReactNativeSdkExample-Bridging-Header.h index 339994e93..856694030 100644 --- a/example/ios/ReactNativeSdkExample-Bridging-Header.h +++ b/example/ios/ReactNativeSdkExample-Bridging-Header.h @@ -2,3 +2,6 @@ // Use this file to import your target's public headers that you would like to // expose to Swift. // + +#import +#import diff --git a/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift b/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift new file mode 100644 index 000000000..c0f0344e0 --- /dev/null +++ b/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift @@ -0,0 +1,118 @@ +// +// IterableJwtGenerator.swift +// ReactNativeSdkExample +// +// Utility class to generate JWTs for use with the Iterable API +// + +import CryptoKit +import Foundation + +class IterableJwtGenerator { + + private static let algorithm = "HS256" + private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds + + private static func millisToSeconds(_ millis: Int64) -> Int64 { + return millis / 1000 + } + + /// Base64 URL encode without padding + private static func base64UrlEncode(_ data: Data) -> String { + let base64 = data.base64EncodedString() + return + base64 + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + private static let encodedHeader: String = { + let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" + let headerData = header.data(using: .utf8)! + return base64UrlEncode(headerData) + }() + + /// Generates a JWT from the provided secret and payload + /// - Parameters: + /// - secret: Your organization's shared secret with Iterable + /// - payload: The JSON payload + /// - Returns: A signed JWT + static func generateToken(secret: String, payload: String) throws -> String { + guard let payloadData = payload.data(using: .utf8) else { + throw NSError( + domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) + } + + let encodedPayload = base64UrlEncode(payloadData) + let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" + + guard let secretData = secret.data(using: .utf8), + let messageData = encodedHeaderAndPayload.data(using: .utf8) + else { + throw NSError( + domain: "JWTGenerator", code: 2, + userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) + } + + // HMAC-SHA256 signature + let key = SymmetricKey(data: secretData) + let signature = HMAC.authenticationCode(for: messageData, using: key) + let signatureData = Data(signature) + let encodedSignature = base64UrlEncode(signatureData) + + return "\(encodedHeaderAndPayload).\(encodedSignature)" + } + + /// Generates a JWT (issued now, expires after the provided duration) + /// - Parameters: + /// - secret: Your organization's shared secret with Iterable + /// - durationMs: The token's expiration time in milliseconds. Up to one year. + /// - email: The email to include in the token, or nil + /// - userId: The userId to include in the token, or nil + /// - Returns: A JWT string + static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) + throws -> String + { + guard durationMs <= maxTokenLifetimeMs else { + throw NSError( + domain: "JWTGenerator", code: 3, + userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) + } + + let hasEmail = email != nil && !email!.isEmpty + let hasUserId = userId != nil && !userId!.isEmpty + + guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { + throw NSError( + domain: "JWTGenerator", code: 4, + userInfo: [ + NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." + ]) + } + + let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) + let exp = now + millisToSeconds(durationMs) + + var payloadDict: [String: Any] = [ + "iat": now, + "exp": exp, + ] + + if let userId = userId { + payloadDict["userId"] = userId + } else if let email = email { + payloadDict["email"] = email + } + + guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), + let payload = String(data: payloadData, encoding: .utf8) + else { + throw NSError( + domain: "JWTGenerator", code: 5, + userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) + } + + return try generateToken(secret: secret, payload: payload) + } +} diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.m b/example/ios/ReactNativeSdkExample/JwtTokenModule.m new file mode 100644 index 000000000..10390bc43 --- /dev/null +++ b/example/ios/ReactNativeSdkExample/JwtTokenModule.m @@ -0,0 +1,25 @@ +// +// JwtTokenModule.m +// ReactNativeSdkExample +// +// React Native module bridge for JWT token generation +// + +#import + +@interface RCT_EXTERN_MODULE(JwtTokenModule, NSObject) + +RCT_EXTERN_METHOD(generateJwtToken:(NSString *)secret + durationMs:(double)durationMs + email:(NSString *)email + userId:(NSString *)userId + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) + ++ (BOOL)requiresMainQueueSetup +{ + return NO; +} + +@end + diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.swift b/example/ios/ReactNativeSdkExample/JwtTokenModule.swift new file mode 100644 index 000000000..5c121143b --- /dev/null +++ b/example/ios/ReactNativeSdkExample/JwtTokenModule.swift @@ -0,0 +1,40 @@ +// +// JwtTokenModule.swift +// ReactNativeSdkExample +// +// React Native module to generate JWT tokens +// + +import Foundation +import React + +@objc(JwtTokenModule) +class JwtTokenModule: NSObject { + + @objc + static func requiresMainQueueSetup() -> Bool { + return false + } + + @objc + func generateJwtToken( + _ secret: String, + durationMs: Double, + email: String?, + userId: String?, + resolver resolve: @escaping RCTPromiseResolveBlock, + rejecter reject: @escaping RCTPromiseRejectBlock + ) { + do { + let token = try IterableJwtGenerator.generateToken( + secret: secret, + durationMs: Int64(durationMs), + email: email, + userId: userId + ) + resolve(token) + } catch { + reject("JWT_GENERATION_ERROR", error.localizedDescription, error) + } + } +} diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index d648dd25c..08516aa48 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -20,6 +20,7 @@ import { import { Route } from '../constants/routes'; import type { RootStackParamList } from '../types/navigation'; +import NativeJwtTokenModule from '../utility/NativeJwtTokenModule'; type Navigation = StackNavigationProp; @@ -86,9 +87,18 @@ const IterableAppContext = createContext({ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const getIsEmail = (id: string) => EMAIL_REGEX.test(id); + export const IterableAppProvider: FunctionComponent< React.PropsWithChildren > = ({ children }) => { + console.log('process.env.ITBL_JWT_SECRET', process.env.ITBL_JWT_SECRET); + console.log('process.env.ITBL_ID', process.env.ITBL_ID); + console.log( + 'process.env.ITBL_IS_JWT_ENABLED', + process.env.ITBL_IS_JWT_ENABLED + ); + console.log('process.env.ITBL_API_KEY', process.env.ITBL_API_KEY); const [returnToInboxTrigger, setReturnToInboxTrigger] = useState(false); const [isInboxTab, setIsInboxTab] = useState(false); @@ -105,6 +115,21 @@ export const IterableAppProvider: FunctionComponent< const getUserId = useCallback(() => userId ?? process.env.ITBL_ID, [userId]); + const getJwtToken = useCallback(async () => { + const id = userId ?? process.env.ITBL_ID; + const idType = getIsEmail(id as string) ? 'email' : 'userId'; + const secret = process.env.ITBL_JWT_SECRET ?? ''; + const duration = 1000 * 60 * 60 * 24; // 1 day in milliseconds + const jwtToken = await NativeJwtTokenModule.generateJwtToken( + secret, + duration, + idType === 'email' ? (id as string) : null, // Email (can be null if userId is provided) + idType === 'userId' ? (id as string) : null // UserId (can be null if email is provided) + ); + + return jwtToken; + }, [userId]); + const login = useCallback(() => { const id = userId ?? process.env.ITBL_ID; @@ -112,10 +137,9 @@ export const IterableAppProvider: FunctionComponent< setLoginInProgress(true); - const isEmail = EMAIL_REGEX.test(id); - const fn = isEmail ? Iterable.setEmail : Iterable.setUserId; + const fn = getIsEmail(id) ? Iterable.setEmail : Iterable.setUserId; - fn(id); + fn(id, process.env.ITBL_JWT_SECRET); setIsLoggedIn(true); setLoginInProgress(false); @@ -173,23 +197,36 @@ export const IterableAppProvider: FunctionComponent< config.inAppHandler = () => IterableInAppShowResponse.show; - // NOTE: Uncomment to test authHandler failure - // config.authHandler = () => { - // console.log(`authHandler`); - - // return Promise.resolve({ - // authToken: 'SomethingNotValid', - // successCallback: () => { - // console.log(`authHandler > success`); - // }, - // // This is not firing - // failureCallback: () => { - // console.log(`authHandler > failure`); - // }, - // }); - // }; + console.log('getJwtToken', getJwtToken()); + + if ( + process.env.ITBL_IS_JWT_ENABLED === 'true' && + process.env.ITBL_JWT_SECRET + ) { + console.log('CONFIGURED AUTH HANDLER'); + config.authHandler = async () => { + console.log(`authHandler`); + + const token = await getJwtToken(); + + console.log(`πŸš€ > IterableAppProvider > token:`, token); + + return Promise.resolve({ + // authToken: 'SomethingNotValid', + authToken: token, + successCallback: () => { + console.log(`authHandler > success`); + }, + // This is not firing + failureCallback: () => { + console.log(`authHandler > failure`); + }, + }); + }; + } setItblConfig(config); + console.log(`πŸš€ > IterableAppProvider > config:`, config); const key = apiKey ?? process.env.ITBL_API_KEY; @@ -232,7 +269,7 @@ export const IterableAppProvider: FunctionComponent< return Promise.resolve(true); }); }, - [apiKey, getUserId, login] + [apiKey, getUserId, login, getJwtToken] ); const logout = useCallback(() => { diff --git a/example/src/utility/NativeJwtTokenModule.ts b/example/src/utility/NativeJwtTokenModule.ts new file mode 100644 index 000000000..464cdc37b --- /dev/null +++ b/example/src/utility/NativeJwtTokenModule.ts @@ -0,0 +1,45 @@ +import { NativeModules, TurboModuleRegistry } from 'react-native'; +import type { TurboModule } from 'react-native'; + +export interface Spec extends TurboModule { + generateJwtToken( + secret: string, + durationMs: number, + email: string | null, + userId: string | null + ): Promise; +} + +// Try to use TurboModule if available (New Architecture) +// Fall back to NativeModules (Old Architecture) +const isTurboModuleEnabled = + '__turboModuleProxy' in global && + (global as Record).__turboModuleProxy != null; + +let JwtTokenModule: Spec | null = null; + +try { + JwtTokenModule = isTurboModuleEnabled + ? TurboModuleRegistry.getEnforcing('JwtTokenModule') + : NativeModules.JwtTokenModule; +} catch { + // Module not available - will throw error when used + console.warn('JwtTokenModule native module is not available yet'); +} + +// Create a proxy that throws a helpful error when methods are called +const createModuleProxy = (): Spec => { + const handler: ProxyHandler = { + get(_target, prop) { + if (!JwtTokenModule) { + throw new Error( + `JwtTokenModule native module is not available. Make sure the native module is properly linked and the app has been rebuilt.\n\nFor iOS: Add Swift files to Xcode project (see SETUP_GUIDE.md)\nFor Android: Ensure JwtTokenPackage is registered in MainApplication.kt` + ); + } + return JwtTokenModule[prop as keyof Spec]; + }, + }; + return new Proxy({} as Spec, handler); +}; + +export default createModuleProxy(); diff --git a/example/src/utility/index.ts b/example/src/utility/index.ts new file mode 100644 index 000000000..fe6e37ed9 --- /dev/null +++ b/example/src/utility/index.ts @@ -0,0 +1,2 @@ +export { default as NativeJwtTokenModule } from './NativeJwtTokenModule'; +export { JwtTokenExample } from './JwtTokenExample'; diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 9ef784679..75321d4bc 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -957,7 +957,7 @@ export class Iterable { (promiseResult as IterableAuthResponse).authToken ); - const timeoutId = setTimeout(() => { + setTimeout(() => { if ( authResponseCallback === IterableAuthResponseResult.SUCCESS ) { @@ -976,8 +976,6 @@ export class Iterable { IterableLogger?.log('No callback received from native layer'); } }, 1000); - // Use unref() to prevent the timeout from keeping the process alive - timeoutId.unref(); } else if (typeof promiseResult === 'string') { //If promise only returns string Iterable.authManager.passAlongAuthToken(promiseResult as string); diff --git a/yarn.lock b/yarn.lock index 348741043..8a1310796 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1958,9 +1958,11 @@ __metadata: "@react-navigation/native": ^7.1.14 "@react-navigation/native-stack": ^7.0.0 "@react-navigation/stack": ^7.4.2 + "@types/crypto-js": ^4.2.2 "@types/jest": ^29.5.13 "@types/react": ^19.0.0 "@types/react-test-renderer": ^19.0.0 + crypto-js: ^4.2.0 react: 19.0.0 react-native: 0.79.3 react-native-builder-bob: ^0.30.2 @@ -3339,6 +3341,13 @@ __metadata: languageName: node linkType: hard +"@types/crypto-js@npm:^4.2.2": + version: 4.2.2 + resolution: "@types/crypto-js@npm:4.2.2" + checksum: 727daa0d2db35f0abefbab865c23213b6ee6a270e27e177939bbe4b70d1e84c2202d9fac4ea84859c4b4d49a4ee50f948f601327a39b69ec013288018ba07ca5 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -5492,6 +5501,13 @@ __metadata: languageName: node linkType: hard +"crypto-js@npm:^4.2.0": + version: 4.2.0 + resolution: "crypto-js@npm:4.2.0" + checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 + languageName: node + linkType: hard + "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" From ad4878247fa0b57a31ce30cd21b8211e5bf837b7 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 18:26:11 -0700 Subject: [PATCH 39/82] chore: remove unused crypto-js and @types/crypto-js dependencies from yarn.lock --- yarn.lock | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8a1310796..348741043 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1958,11 +1958,9 @@ __metadata: "@react-navigation/native": ^7.1.14 "@react-navigation/native-stack": ^7.0.0 "@react-navigation/stack": ^7.4.2 - "@types/crypto-js": ^4.2.2 "@types/jest": ^29.5.13 "@types/react": ^19.0.0 "@types/react-test-renderer": ^19.0.0 - crypto-js: ^4.2.0 react: 19.0.0 react-native: 0.79.3 react-native-builder-bob: ^0.30.2 @@ -3341,13 +3339,6 @@ __metadata: languageName: node linkType: hard -"@types/crypto-js@npm:^4.2.2": - version: 4.2.2 - resolution: "@types/crypto-js@npm:4.2.2" - checksum: 727daa0d2db35f0abefbab865c23213b6ee6a270e27e177939bbe4b70d1e84c2202d9fac4ea84859c4b4d49a4ee50f948f601327a39b69ec013288018ba07ca5 - languageName: node - linkType: hard - "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -5501,13 +5492,6 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.2.0": - version: 4.2.0 - resolution: "crypto-js@npm:4.2.0" - checksum: f051666dbc077c8324777f44fbd3aaea2986f198fe85092535130d17026c7c2ccf2d23ee5b29b36f7a4a07312db2fae23c9094b644cc35f7858b1b4fcaf27774 - languageName: node - linkType: hard - "csstype@npm:^3.0.2": version: 3.1.3 resolution: "csstype@npm:3.1.3" From 5df923fb87a9d241014d8ce0ab69d6fff7fa1fda Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 19:35:28 -0700 Subject: [PATCH 40/82] feat: add IterableJwtGenerator and JwtTokenModule for JWT token generation in React Native --- example/ios/IterableJwtGenerator.swift | 229 ++++++++++++++++++ .../JwtTokenModule.m => JwtTokenModule.mm} | 0 .../JwtTokenModule.swift | 0 .../project.pbxproj | 30 ++- .../IterableJwtGenerator.swift | 118 --------- example/src/hooks/useIterableApp.tsx | 63 ++--- 6 files changed, 272 insertions(+), 168 deletions(-) create mode 100644 example/ios/IterableJwtGenerator.swift rename example/ios/{ReactNativeSdkExample/JwtTokenModule.m => JwtTokenModule.mm} (100%) rename example/ios/{ReactNativeSdkExample => }/JwtTokenModule.swift (100%) delete mode 100644 example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift diff --git a/example/ios/IterableJwtGenerator.swift b/example/ios/IterableJwtGenerator.swift new file mode 100644 index 000000000..a2854014f --- /dev/null +++ b/example/ios/IterableJwtGenerator.swift @@ -0,0 +1,229 @@ +// +// IterableJwtGenerator.swift +// ReactNativeSdkExample +// +// Utility class to generate JWTs for use with the Iterable API +// + +import CryptoKit +import Foundation + +// class IterableJwtGenerator { + +// private static let algorithm = "HS256" +// private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds + +// private static func millisToSeconds(_ millis: Int64) -> Int64 { +// return millis / 1000 +// } + +// /// Base64 URL encode without padding +// private static func base64UrlEncode(_ data: Data) -> String { +// let base64 = data.base64EncodedString() +// return +// base64 +// .replacingOccurrences(of: "+", with: "-") +// .replacingOccurrences(of: "/", with: "_") +// .replacingOccurrences(of: "=", with: "") +// } + +// private static let encodedHeader: String = { +// let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" +// let headerData = header.data(using: .utf8)! +// return base64UrlEncode(headerData) +// }() + +// /// Generates a JWT from the provided secret and payload +// /// - Parameters: +// /// - secret: Your organization's shared secret with Iterable +// /// - payload: The JSON payload +// /// - Returns: A signed JWT +// static func generateToken(secret: String, payload: String) throws -> String { +// guard let payloadData = payload.data(using: .utf8) else { +// throw NSError( +// domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) +// } + +// let encodedPayload = base64UrlEncode(payloadData) +// let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" + +// guard let secretData = secret.data(using: .utf8), +// let messageData = encodedHeaderAndPayload.data(using: .utf8) +// else { +// throw NSError( +// domain: "JWTGenerator", code: 2, +// userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) +// } + +// // HMAC-SHA256 signature +// let key = SymmetricKey(data: secretData) +// let signature = HMAC.authenticationCode(for: messageData, using: key) +// let signatureData = Data(signature) +// let encodedSignature = base64UrlEncode(signatureData) + +// return "\(encodedHeaderAndPayload).\(encodedSignature)" +// } + +// /// Generates a JWT (issued now, expires after the provided duration) +// /// - Parameters: +// /// - secret: Your organization's shared secret with Iterable +// /// - durationMs: The token's expiration time in milliseconds. Up to one year. +// /// - email: The email to include in the token, or nil +// /// - userId: The userId to include in the token, or nil +// /// - Returns: A JWT string +// static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) +// throws -> String +// { +// guard durationMs <= maxTokenLifetimeMs else { +// throw NSError( +// domain: "JWTGenerator", code: 3, +// userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) +// } + +// let hasEmail = email != nil && !email!.isEmpty +// let hasUserId = userId != nil && !userId!.isEmpty + +// guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { +// throw NSError( +// domain: "JWTGenerator", code: 4, +// userInfo: [ +// NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." +// ]) +// } + +// let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) +// let exp = now + millisToSeconds(durationMs) + +// var payloadDict: [String: Any] = [ +// "iat": now, +// "exp": exp, +// ] + +// if let userId = userId { +// payloadDict["userId"] = userId +// } else if let email = email { +// payloadDict["email"] = email +// } + +// guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), +// let payload = String(data: payloadData, encoding: .utf8) +// else { +// throw NSError( +// domain: "JWTGenerator", code: 5, +// userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) +// } + +// return try generateToken(secret: secret, payload: payload) +// } +// } + +// +// IterableTokenGenerator.swift +// swift-sdk +// +// Created by Apple on 22/10/24. +// Copyright Β© 2024 Iterable. All rights reserved. +// + +@objcMembers public final class IterableJwtGenerator: NSObject { + + /// Base64 URL encode without padding (URL-safe base64 encoding for JWT) + private static func urlEncodedBase64(_ data: Data) -> String { + let base64 = data.base64EncodedString() + return + base64 + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + .replacingOccurrences(of: "=", with: "") + } + + public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String) + -> String + { + struct Header: Encodable { + let alg = "HS256" + let typ = "JWT" + } + + struct Payload: Encodable { + var email = "" + var iat = Int(Date().timeIntervalSince1970) + var exp = Int(Date().timeIntervalSince1970) + 60 + + } + let headerJsonData = try! JSONEncoder().encode(Header()) + let headerBase64 = urlEncodedBase64(headerJsonData) + + let payloadJsonData = try! JSONEncoder().encode(Payload(email: email, iat: iat, exp: exp)) + let payloadBase64 = urlEncodedBase64(payloadJsonData) + + let toSign = Data((headerBase64 + "." + payloadBase64).utf8) + + if #available(iOS 13.0, *) { + let privateKey = SymmetricKey(data: Data(secret.utf8)) + let signature = HMAC.authenticationCode(for: toSign, using: privateKey) + let signatureBase64 = urlEncodedBase64(Data(signature)) + + let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".") + + return token + } + return "" + } + + public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String) + -> String + { + struct Header: Encodable { + let alg = "HS256" + let typ = "JWT" + } + + struct Payload: Encodable { + var userId = "" + var iat = Int(Date().timeIntervalSince1970) + var exp = Int(Date().timeIntervalSince1970) + 60 + + } + let headerJsonData = try! JSONEncoder().encode(Header()) + let headerBase64 = urlEncodedBase64(headerJsonData) + + let payloadJsonData = try! JSONEncoder().encode(Payload(userId: userId, iat: iat, exp: exp)) + let payloadBase64 = urlEncodedBase64(payloadJsonData) + + let toSign = Data((headerBase64 + "." + payloadBase64).utf8) + + if #available(iOS 13.0, *) { + let privateKey = SymmetricKey(data: Data(secret.utf8)) + let signature = HMAC.authenticationCode(for: toSign, using: privateKey) + let signatureBase64 = urlEncodedBase64(Data(signature)) + + let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".") + + return token + } + return "" + } + + public static func generateToken( + secret: String, durationMs: Int64, email: String?, userId: String? + ) throws -> String { + // Convert durationMs from milliseconds to seconds + let durationSeconds = Double(durationMs) / 1000.0 + let currentTime = Date().timeIntervalSince1970 + + if userId != nil { + return generateJwtForUserId( + secret: secret, iat: Int(currentTime), + exp: Int(currentTime + durationSeconds), userId: userId!) + } else if email != nil { + return generateJwtForEmail( + secret: secret, iat: Int(currentTime), + exp: Int(currentTime + durationSeconds), email: email!) + } else { + throw NSError( + domain: "JWTGenerator", code: 6, + userInfo: [NSLocalizedDescriptionKey: "No email or userId provided"]) + } + } +} diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.m b/example/ios/JwtTokenModule.mm similarity index 100% rename from example/ios/ReactNativeSdkExample/JwtTokenModule.m rename to example/ios/JwtTokenModule.mm diff --git a/example/ios/ReactNativeSdkExample/JwtTokenModule.swift b/example/ios/JwtTokenModule.swift similarity index 100% rename from example/ios/ReactNativeSdkExample/JwtTokenModule.swift rename to example/ios/JwtTokenModule.swift diff --git a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj index 74e4dc4c9..766c9b681 100644 --- a/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj +++ b/example/ios/ReactNativeSdkExample.xcodeproj/project.pbxproj @@ -10,6 +10,9 @@ 00E356F31AD99517003FC87E /* ReactNativeSdkExampleTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ReactNativeSdkExampleTests.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 779227332DFA3FB500D69EC0 /* AppDelegate.swift */; }; + 77E3B5772EA71A4B001449CE /* IterableJwtGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */; }; + 77E3B5782EA71A4B001449CE /* JwtTokenModule.mm in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */; }; + 77E3B5792EA71A4B001449CE /* JwtTokenModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 81F6A9EA0E1CCC1AD730C5D9 /* libPods-ReactNativeSdkExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 56080B9DEED42A97AD1B3D5C /* libPods-ReactNativeSdkExample.a */; }; A3A40C20801B8F02005FA4C0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1FC6B09E65A7BD9F6864C5D8 /* PrivacyInfo.xcprivacy */; }; @@ -39,6 +42,9 @@ 779227312DFA3FB500D69EC0 /* ReactNativeSdkExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExample-Bridging-Header.h"; sourceTree = ""; }; 779227322DFA3FB500D69EC0 /* ReactNativeSdkExampleTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ReactNativeSdkExampleTests-Bridging-Header.h"; sourceTree = ""; }; 779227332DFA3FB500D69EC0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = ReactNativeSdkExample/AppDelegate.swift; sourceTree = ""; }; + 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IterableJwtGenerator.swift; sourceTree = ""; }; + 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = JwtTokenModule.mm; sourceTree = ""; }; + 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JwtTokenModule.swift; sourceTree = ""; }; 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = ReactNativeSdkExample/LaunchScreen.storyboard; sourceTree = ""; }; EA19B65827A1D757CC5AAC97 /* Pods-ReactNativeSdkExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ReactNativeSdkExample.release.xcconfig"; path = "Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample.release.xcconfig"; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; @@ -114,6 +120,9 @@ 83CBB9F61A601CBA00E9B192 = { isa = PBXGroup; children = ( + 77E3B5742EA71A4B001449CE /* IterableJwtGenerator.swift */, + 77E3B5752EA71A4B001449CE /* JwtTokenModule.mm */, + 77E3B5762EA71A4B001449CE /* JwtTokenModule.swift */, 13B07FAE1A68108700A75B9A /* ReactNativeSdkExample */, 832341AE1AAA6A7D00B99B32 /* Libraries */, 00E356EF1AD99517003FC87E /* ReactNativeSdkExampleTests */, @@ -268,10 +277,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-frameworks.sh\"\n"; @@ -307,10 +320,14 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-input-files.xcfilelist", ); + inputPaths = ( + ); name = "[CP] Copy Pods Resources"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources-${CONFIGURATION}-output-files.xcfilelist", ); + outputPaths = ( + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReactNativeSdkExample/Pods-ReactNativeSdkExample-resources.sh\"\n"; @@ -332,6 +349,9 @@ buildActionMask = 2147483647; files = ( 779227342DFA3FB500D69EC0 /* AppDelegate.swift in Sources */, + 77E3B5772EA71A4B001449CE /* IterableJwtGenerator.swift in Sources */, + 77E3B5782EA71A4B001449CE /* JwtTokenModule.mm in Sources */, + 77E3B5792EA71A4B001449CE /* JwtTokenModule.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -535,10 +555,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; @@ -611,10 +628,7 @@ "-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift b/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift deleted file mode 100644 index c0f0344e0..000000000 --- a/example/ios/ReactNativeSdkExample/IterableJwtGenerator.swift +++ /dev/null @@ -1,118 +0,0 @@ -// -// IterableJwtGenerator.swift -// ReactNativeSdkExample -// -// Utility class to generate JWTs for use with the Iterable API -// - -import CryptoKit -import Foundation - -class IterableJwtGenerator { - - private static let algorithm = "HS256" - private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds - - private static func millisToSeconds(_ millis: Int64) -> Int64 { - return millis / 1000 - } - - /// Base64 URL encode without padding - private static func base64UrlEncode(_ data: Data) -> String { - let base64 = data.base64EncodedString() - return - base64 - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - .replacingOccurrences(of: "=", with: "") - } - - private static let encodedHeader: String = { - let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" - let headerData = header.data(using: .utf8)! - return base64UrlEncode(headerData) - }() - - /// Generates a JWT from the provided secret and payload - /// - Parameters: - /// - secret: Your organization's shared secret with Iterable - /// - payload: The JSON payload - /// - Returns: A signed JWT - static func generateToken(secret: String, payload: String) throws -> String { - guard let payloadData = payload.data(using: .utf8) else { - throw NSError( - domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) - } - - let encodedPayload = base64UrlEncode(payloadData) - let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" - - guard let secretData = secret.data(using: .utf8), - let messageData = encodedHeaderAndPayload.data(using: .utf8) - else { - throw NSError( - domain: "JWTGenerator", code: 2, - userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) - } - - // HMAC-SHA256 signature - let key = SymmetricKey(data: secretData) - let signature = HMAC.authenticationCode(for: messageData, using: key) - let signatureData = Data(signature) - let encodedSignature = base64UrlEncode(signatureData) - - return "\(encodedHeaderAndPayload).\(encodedSignature)" - } - - /// Generates a JWT (issued now, expires after the provided duration) - /// - Parameters: - /// - secret: Your organization's shared secret with Iterable - /// - durationMs: The token's expiration time in milliseconds. Up to one year. - /// - email: The email to include in the token, or nil - /// - userId: The userId to include in the token, or nil - /// - Returns: A JWT string - static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) - throws -> String - { - guard durationMs <= maxTokenLifetimeMs else { - throw NSError( - domain: "JWTGenerator", code: 3, - userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) - } - - let hasEmail = email != nil && !email!.isEmpty - let hasUserId = userId != nil && !userId!.isEmpty - - guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { - throw NSError( - domain: "JWTGenerator", code: 4, - userInfo: [ - NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." - ]) - } - - let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) - let exp = now + millisToSeconds(durationMs) - - var payloadDict: [String: Any] = [ - "iat": now, - "exp": exp, - ] - - if let userId = userId { - payloadDict["userId"] = userId - } else if let email = email { - payloadDict["email"] = email - } - - guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), - let payload = String(data: payloadData, encoding: .utf8) - else { - throw NSError( - domain: "JWTGenerator", code: 5, - userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) - } - - return try generateToken(secret: secret, payload: payload) - } -} diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 08516aa48..caa6392f7 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -92,13 +92,6 @@ const getIsEmail = (id: string) => EMAIL_REGEX.test(id); export const IterableAppProvider: FunctionComponent< React.PropsWithChildren > = ({ children }) => { - console.log('process.env.ITBL_JWT_SECRET', process.env.ITBL_JWT_SECRET); - console.log('process.env.ITBL_ID', process.env.ITBL_ID); - console.log( - 'process.env.ITBL_IS_JWT_ENABLED', - process.env.ITBL_IS_JWT_ENABLED - ); - console.log('process.env.ITBL_API_KEY', process.env.ITBL_API_KEY); const [returnToInboxTrigger, setReturnToInboxTrigger] = useState(false); const [isInboxTab, setIsInboxTab] = useState(false); @@ -139,7 +132,7 @@ export const IterableAppProvider: FunctionComponent< const fn = getIsEmail(id) ? Iterable.setEmail : Iterable.setUserId; - fn(id, process.env.ITBL_JWT_SECRET); + fn(id); setIsLoggedIn(true); setLoginInProgress(false); @@ -197,36 +190,19 @@ export const IterableAppProvider: FunctionComponent< config.inAppHandler = () => IterableInAppShowResponse.show; - console.log('getJwtToken', getJwtToken()); - if ( process.env.ITBL_IS_JWT_ENABLED === 'true' && process.env.ITBL_JWT_SECRET ) { console.log('CONFIGURED AUTH HANDLER'); config.authHandler = async () => { - console.log(`authHandler`); - const token = await getJwtToken(); - - console.log(`πŸš€ > IterableAppProvider > token:`, token); - - return Promise.resolve({ - // authToken: 'SomethingNotValid', - authToken: token, - successCallback: () => { - console.log(`authHandler > success`); - }, - // This is not firing - failureCallback: () => { - console.log(`authHandler > failure`); - }, - }); + // return 'SomethingNotValid'; // Uncomment this to test the failure callback + return token; }; } setItblConfig(config); - console.log(`πŸš€ > IterableAppProvider > config:`, config); const key = apiKey ?? process.env.ITBL_API_KEY; @@ -240,33 +216,36 @@ export const IterableAppProvider: FunctionComponent< .then((isSuccessful) => { setIsInitialized(isSuccessful); - if (!isSuccessful) - return Promise.reject('`Iterable.initialize` failed'); + console.log('πŸš€ > IterableAppProvider > isSuccessful:', isSuccessful); - if (getUserId()) { + if (!isSuccessful) { + // return Promise.reject('`Iterable.initialize` failed'); + throw new Error('`Iterable.initialize` failed'); + } else if (getUserId()) { login(); } return isSuccessful; }) .catch((err) => { - console.error( - '`Iterable.initialize` failed with the following error', - err - ); - setIsInitialized(false); - setLoginInProgress(false); - return Promise.reject(err); + console.log(`πŸš€ > IterableAppProvider > err:`, err); + // console.error( + // '`Iterable.initialize` failed with the following error', + // err + // ); + // setIsInitialized(false); + // setLoginInProgress(false); + // return Promise.reject(err); }) .finally(() => { // For some reason, ios is throwing an error on initialize. // To temporarily fix this, we're using the finally block to login. // MOB-10419: Find out why initialize is throwing an error on ios - setIsInitialized(true); - if (getUserId()) { - login(); - } - return Promise.resolve(true); + // setIsInitialized(true); + // if (getUserId()) { + // login(); + // } + // return Promise.resolve(true); }); }, [apiKey, getUserId, login, getJwtToken] From 40c4bad1c298024084ff4302f787d9d76a04aa7c Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:20:35 -0700 Subject: [PATCH 41/82] fix: ensure login is called during initialization and improve error handling in IterableAppProvider --- example/src/hooks/useIterableApp.tsx | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index caa6392f7..a8baac7ab 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -141,6 +141,8 @@ export const IterableAppProvider: FunctionComponent< const initialize = useCallback( (navigation: Navigation) => { + login(); + const config = new IterableConfig(); config.inAppDisplayInterval = 1.0; // Min gap between in-apps. No need to set this in production. @@ -219,8 +221,7 @@ export const IterableAppProvider: FunctionComponent< console.log('πŸš€ > IterableAppProvider > isSuccessful:', isSuccessful); if (!isSuccessful) { - // return Promise.reject('`Iterable.initialize` failed'); - throw new Error('`Iterable.initialize` failed'); + return Promise.reject('`Iterable.initialize` failed'); } else if (getUserId()) { login(); } @@ -228,24 +229,23 @@ export const IterableAppProvider: FunctionComponent< return isSuccessful; }) .catch((err) => { - console.log(`πŸš€ > IterableAppProvider > err:`, err); - // console.error( - // '`Iterable.initialize` failed with the following error', - // err - // ); - // setIsInitialized(false); - // setLoginInProgress(false); - // return Promise.reject(err); + console.error( + '`Iterable.initialize` failed with the following error', + err + ); + setIsInitialized(false); + setLoginInProgress(false); + return Promise.reject(err); }) .finally(() => { // For some reason, ios is throwing an error on initialize. // To temporarily fix this, we're using the finally block to login. // MOB-10419: Find out why initialize is throwing an error on ios - // setIsInitialized(true); - // if (getUserId()) { - // login(); - // } - // return Promise.resolve(true); + setIsInitialized(true); + if (getUserId()) { + login(); + } + return Promise.resolve(true); }); }, [apiKey, getUserId, login, getJwtToken] From fea6232036965163a50540790cc0c1812bbb52ff Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:23:03 -0700 Subject: [PATCH 42/82] refactor: remove commented-out code from IterableJwtGenerator.swift to clean up the implementation --- example/ios/IterableJwtGenerator.swift | 117 ------------------------- 1 file changed, 117 deletions(-) diff --git a/example/ios/IterableJwtGenerator.swift b/example/ios/IterableJwtGenerator.swift index a2854014f..e9f56d7ba 100644 --- a/example/ios/IterableJwtGenerator.swift +++ b/example/ios/IterableJwtGenerator.swift @@ -8,123 +8,6 @@ import CryptoKit import Foundation -// class IterableJwtGenerator { - -// private static let algorithm = "HS256" -// private static let maxTokenLifetimeMs: Int64 = 365 * 24 * 60 * 60 * 1000 // 1 year in milliseconds - -// private static func millisToSeconds(_ millis: Int64) -> Int64 { -// return millis / 1000 -// } - -// /// Base64 URL encode without padding -// private static func base64UrlEncode(_ data: Data) -> String { -// let base64 = data.base64EncodedString() -// return -// base64 -// .replacingOccurrences(of: "+", with: "-") -// .replacingOccurrences(of: "/", with: "_") -// .replacingOccurrences(of: "=", with: "") -// } - -// private static let encodedHeader: String = { -// let header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}" -// let headerData = header.data(using: .utf8)! -// return base64UrlEncode(headerData) -// }() - -// /// Generates a JWT from the provided secret and payload -// /// - Parameters: -// /// - secret: Your organization's shared secret with Iterable -// /// - payload: The JSON payload -// /// - Returns: A signed JWT -// static func generateToken(secret: String, payload: String) throws -> String { -// guard let payloadData = payload.data(using: .utf8) else { -// throw NSError( -// domain: "JWTGenerator", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid payload"]) -// } - -// let encodedPayload = base64UrlEncode(payloadData) -// let encodedHeaderAndPayload = "\(encodedHeader).\(encodedPayload)" - -// guard let secretData = secret.data(using: .utf8), -// let messageData = encodedHeaderAndPayload.data(using: .utf8) -// else { -// throw NSError( -// domain: "JWTGenerator", code: 2, -// userInfo: [NSLocalizedDescriptionKey: "Invalid secret or message"]) -// } - -// // HMAC-SHA256 signature -// let key = SymmetricKey(data: secretData) -// let signature = HMAC.authenticationCode(for: messageData, using: key) -// let signatureData = Data(signature) -// let encodedSignature = base64UrlEncode(signatureData) - -// return "\(encodedHeaderAndPayload).\(encodedSignature)" -// } - -// /// Generates a JWT (issued now, expires after the provided duration) -// /// - Parameters: -// /// - secret: Your organization's shared secret with Iterable -// /// - durationMs: The token's expiration time in milliseconds. Up to one year. -// /// - email: The email to include in the token, or nil -// /// - userId: The userId to include in the token, or nil -// /// - Returns: A JWT string -// static func generateToken(secret: String, durationMs: Int64, email: String?, userId: String?) -// throws -> String -// { -// guard durationMs <= maxTokenLifetimeMs else { -// throw NSError( -// domain: "JWTGenerator", code: 3, -// userInfo: [NSLocalizedDescriptionKey: "Duration must be one year or less."]) -// } - -// let hasEmail = email != nil && !email!.isEmpty -// let hasUserId = userId != nil && !userId!.isEmpty - -// guard (hasEmail && !hasUserId) || (!hasEmail && hasUserId) else { -// throw NSError( -// domain: "JWTGenerator", code: 4, -// userInfo: [ -// NSLocalizedDescriptionKey: "The token must include a userId or email, but not both." -// ]) -// } - -// let now = millisToSeconds(Int64(Date().timeIntervalSince1970 * 1000)) -// let exp = now + millisToSeconds(durationMs) - -// var payloadDict: [String: Any] = [ -// "iat": now, -// "exp": exp, -// ] - -// if let userId = userId { -// payloadDict["userId"] = userId -// } else if let email = email { -// payloadDict["email"] = email -// } - -// guard let payloadData = try? JSONSerialization.data(withJSONObject: payloadDict, options: []), -// let payload = String(data: payloadData, encoding: .utf8) -// else { -// throw NSError( -// domain: "JWTGenerator", code: 5, -// userInfo: [NSLocalizedDescriptionKey: "Failed to serialize payload"]) -// } - -// return try generateToken(secret: secret, payload: payload) -// } -// } - -// -// IterableTokenGenerator.swift -// swift-sdk -// -// Created by Apple on 22/10/24. -// Copyright Β© 2024 Iterable. All rights reserved. -// - @objcMembers public final class IterableJwtGenerator: NSObject { /// Base64 URL encode without padding (URL-safe base64 encoding for JWT) From fd76f089078d3a63e5e7c8797154e452bc3ddbfc Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:29:00 -0700 Subject: [PATCH 43/82] refactor: remove unnecessary login calls and clean up error handling in IterableAppProvider --- example/src/hooks/useIterableApp.tsx | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index a8baac7ab..daa56244d 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -20,7 +20,7 @@ import { import { Route } from '../constants/routes'; import type { RootStackParamList } from '../types/navigation'; -import NativeJwtTokenModule from '../utility/NativeJwtTokenModule'; +import NativeJwtTokenModule from '../NativeJwtTokenModule'; type Navigation = StackNavigationProp; @@ -141,7 +141,9 @@ export const IterableAppProvider: FunctionComponent< const initialize = useCallback( (navigation: Navigation) => { - login(); + if (getUserId()) { + login(); + } const config = new IterableConfig(); @@ -222,8 +224,6 @@ export const IterableAppProvider: FunctionComponent< if (!isSuccessful) { return Promise.reject('`Iterable.initialize` failed'); - } else if (getUserId()) { - login(); } return isSuccessful; @@ -236,19 +236,9 @@ export const IterableAppProvider: FunctionComponent< setIsInitialized(false); setLoginInProgress(false); return Promise.reject(err); - }) - .finally(() => { - // For some reason, ios is throwing an error on initialize. - // To temporarily fix this, we're using the finally block to login. - // MOB-10419: Find out why initialize is throwing an error on ios - setIsInitialized(true); - if (getUserId()) { - login(); - } - return Promise.resolve(true); }); }, - [apiKey, getUserId, login, getJwtToken] + [getUserId, apiKey, login, getJwtToken] ); const logout = useCallback(() => { From b663dceacad8e730b82782ac6b6ad304fe91d573 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:31:48 -0700 Subject: [PATCH 44/82] refactor: remove console log statements to streamline IterableAppProvider --- example/src/hooks/useIterableApp.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index daa56244d..a4e1062b1 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -198,7 +198,6 @@ export const IterableAppProvider: FunctionComponent< process.env.ITBL_IS_JWT_ENABLED === 'true' && process.env.ITBL_JWT_SECRET ) { - console.log('CONFIGURED AUTH HANDLER'); config.authHandler = async () => { const token = await getJwtToken(); // return 'SomethingNotValid'; // Uncomment this to test the failure callback @@ -220,8 +219,6 @@ export const IterableAppProvider: FunctionComponent< .then((isSuccessful) => { setIsInitialized(isSuccessful); - console.log('πŸš€ > IterableAppProvider > isSuccessful:', isSuccessful); - if (!isSuccessful) { return Promise.reject('`Iterable.initialize` failed'); } From a58be2aad7e2eae11ab4ba5635c4080ab4e0db1b Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 20 Oct 2025 20:34:17 -0700 Subject: [PATCH 45/82] feat: introduce NativeJwtTokenModule for JWT token generation and remove old utility index --- example/src/{utility => }/NativeJwtTokenModule.ts | 0 example/src/utility/index.ts | 2 -- 2 files changed, 2 deletions(-) rename example/src/{utility => }/NativeJwtTokenModule.ts (100%) delete mode 100644 example/src/utility/index.ts diff --git a/example/src/utility/NativeJwtTokenModule.ts b/example/src/NativeJwtTokenModule.ts similarity index 100% rename from example/src/utility/NativeJwtTokenModule.ts rename to example/src/NativeJwtTokenModule.ts diff --git a/example/src/utility/index.ts b/example/src/utility/index.ts deleted file mode 100644 index fe6e37ed9..000000000 --- a/example/src/utility/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as NativeJwtTokenModule } from './NativeJwtTokenModule'; -export { JwtTokenExample } from './JwtTokenExample'; From 13012d971a202b7bd395ccf06448f0c5eef271db Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 21 Oct 2025 09:02:21 -0700 Subject: [PATCH 46/82] refactor: enhance JWT generation methods and update .env.example comments for clarity --- example/.env.example | 2 +- example/ios/IterableJwtGenerator.swift | 63 ++++++++++---------------- example/src/hooks/useIterableApp.tsx | 3 +- 3 files changed, 26 insertions(+), 42 deletions(-) diff --git a/example/.env.example b/example/.env.example index 9d1ff99bc..5b5663df6 100644 --- a/example/.env.example +++ b/example/.env.example @@ -20,7 +20,7 @@ ITBL_API_KEY=replace_this_with_your_iterable_api_key # Your JWT Secret, created when making your API key (see above) ITBL_JWT_SECRET=replace_this_with_your_jwt_secret # Is your api token JWT Enabled? -# Defaults to true +# Must be set to 'true' to enable JWT authentication ITBL_IS_JWT_ENABLED=true # Your Iterable user ID or email address diff --git a/example/ios/IterableJwtGenerator.swift b/example/ios/IterableJwtGenerator.swift index e9f56d7ba..8e4a9cbe7 100644 --- a/example/ios/IterableJwtGenerator.swift +++ b/example/ios/IterableJwtGenerator.swift @@ -10,6 +10,11 @@ import Foundation @objcMembers public final class IterableJwtGenerator: NSObject { + private struct Header: Encodable { + let alg = "HS256" + let typ = "JWT" + } + /// Base64 URL encode without padding (URL-safe base64 encoding for JWT) private static func urlEncodedBase64(_ data: Data) -> String { let base64 = data.base64EncodedString() @@ -20,24 +25,12 @@ import Foundation .replacingOccurrences(of: "=", with: "") } - public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String) - -> String - { - struct Header: Encodable { - let alg = "HS256" - let typ = "JWT" - } - - struct Payload: Encodable { - var email = "" - var iat = Int(Date().timeIntervalSince1970) - var exp = Int(Date().timeIntervalSince1970) + 60 - - } + /// Generic JWT generation helper that works with any Encodable payload + private static func generateJwt(secret: String, payload: T) -> String { let headerJsonData = try! JSONEncoder().encode(Header()) let headerBase64 = urlEncodedBase64(headerJsonData) - let payloadJsonData = try! JSONEncoder().encode(Payload(email: email, iat: iat, exp: exp)) + let payloadJsonData = try! JSONEncoder().encode(payload) let payloadBase64 = urlEncodedBase64(payloadJsonData) let toSign = Data((headerBase64 + "." + payloadBase64).utf8) @@ -54,38 +47,28 @@ import Foundation return "" } - public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String) + public static func generateJwtForEmail(secret: String, iat: Int, exp: Int, email: String) -> String { - struct Header: Encodable { - let alg = "HS256" - let typ = "JWT" - } - struct Payload: Encodable { - var userId = "" - var iat = Int(Date().timeIntervalSince1970) - var exp = Int(Date().timeIntervalSince1970) + 60 - + var email: String + var iat: Int + var exp: Int } - let headerJsonData = try! JSONEncoder().encode(Header()) - let headerBase64 = urlEncodedBase64(headerJsonData) - let payloadJsonData = try! JSONEncoder().encode(Payload(userId: userId, iat: iat, exp: exp)) - let payloadBase64 = urlEncodedBase64(payloadJsonData) - - let toSign = Data((headerBase64 + "." + payloadBase64).utf8) - - if #available(iOS 13.0, *) { - let privateKey = SymmetricKey(data: Data(secret.utf8)) - let signature = HMAC.authenticationCode(for: toSign, using: privateKey) - let signatureBase64 = urlEncodedBase64(Data(signature)) - - let token = [headerBase64, payloadBase64, signatureBase64].joined(separator: ".") + return generateJwt(secret: secret, payload: Payload(email: email, iat: iat, exp: exp)) + } - return token + public static func generateJwtForUserId(secret: String, iat: Int, exp: Int, userId: String) + -> String + { + struct Payload: Encodable { + var userId: String + var iat: Int + var exp: Int } - return "" + + return generateJwt(secret: secret, payload: Payload(userId: userId, iat: iat, exp: exp)) } public static func generateToken( diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index a4e1062b1..c215ff22f 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -235,7 +235,8 @@ export const IterableAppProvider: FunctionComponent< return Promise.reject(err); }); }, - [getUserId, apiKey, login, getJwtToken] + // eslint-disable-next-line react-hooks/exhaustive-deps + [getUserId, apiKey, login, getJwtToken, userId] ); const logout = useCallback(() => { From 84f7fff1be8618f6869ccd5463dd688e2918f986 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 21 Oct 2025 09:15:56 -0700 Subject: [PATCH 47/82] chore: remove font references from Info.plist to streamline configuration --- example/ios/ReactNativeSdkExample/Info.plist | 22 -------------------- 1 file changed, 22 deletions(-) diff --git a/example/ios/ReactNativeSdkExample/Info.plist b/example/ios/ReactNativeSdkExample/Info.plist index 4e0430cdd..19ccb2e8f 100644 --- a/example/ios/ReactNativeSdkExample/Info.plist +++ b/example/ios/ReactNativeSdkExample/Info.plist @@ -52,27 +52,5 @@ UIViewControllerBasedStatusBarAppearance - UIAppFonts - - AntDesign.ttf - Entypo.ttf - EvilIcons.ttf - Feather.ttf - FontAwesome.ttf - FontAwesome5_Brands.ttf - FontAwesome5_Regular.ttf - FontAwesome5_Solid.ttf - FontAwesome6_Brands.ttf - FontAwesome6_Regular.ttf - FontAwesome6_Solid.ttf - Foundation.ttf - Ionicons.ttf - MaterialIcons.ttf - MaterialCommunityIcons.ttf - SimpleLineIcons.ttf - Octicons.ttf - Zocial.ttf - Fontisto.ttf - From d7f81c0190d4caf1937da1e30bf78b753cb131a6 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 21 Oct 2025 09:37:59 -0700 Subject: [PATCH 48/82] chore: update package version to 2.2.0-alpha.0 in package.json and itblBuildInfo.ts --- package.json | 2 +- src/itblBuildInfo.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5222bf8cf..668caa89a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iterable/react-native-sdk", - "version": "2.1.0-beta.1", + "version": "2.2.0-alpha.0", "description": "Iterable SDK for React Native.", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/src/itblBuildInfo.ts b/src/itblBuildInfo.ts index 097bd43d2..9ab008e03 100644 --- a/src/itblBuildInfo.ts +++ b/src/itblBuildInfo.ts @@ -3,5 +3,5 @@ * It contains the version of the package */ export const buildInfo = { - version: '2.1.0-beta.1', + version: '2.2.0-alpha.0', }; From e7deb3d20395ef7a328557201d5c966a92f47135 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 21 Oct 2025 09:59:58 -0700 Subject: [PATCH 49/82] chore: update CHANGELOG.md for 2.2.0-alpha.0 release --- CHANGELOG.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e4a71be..6c9fc8682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,21 @@ +## 2.2.0-alpha.0 (2025-10-21) + +### Updates +- Updated Android SDK version to [3.6.1](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.1) +- Updated iOS SDK version to [6.6.1](https://github.com/Iterable/swift-sdk/releases/tag/6.6.1) +- Added JWT Capabilities: + - Added `Iterable.authhManager`, which manages the authentication flow + - Added `IterableRetryBackoff` and `IterableAuthFailureReason` enums + - Added `onJWTError` and `retryPolicy` for control over JWT flow +- Moved all native calls to `IterableApi.ts` +- Added JWT example to our example app + +### Fixes +- Created a standalone `IterableLogger` to avoid circular dependencies + ## 2.1.0-beta.1 -## Fixes +### Fixes - Add Temporary fix for circular paths, which break expo ([9c09743](https://github.com/Iterable/react-native-sdk/commit/9c09743)) ## 2.1.0-beta.0 From e494b191a9e1e24b85470a7a900fe102739387e2 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 22 Oct 2025 13:11:25 -0700 Subject: [PATCH 50/82] refactor: add type guard for IterableAuthResponse and simplify promise result handling --- src/core/classes/Iterable.ts | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 75321d4bc..4c64cb7da 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -23,6 +23,23 @@ import { IterableLogger } from './IterableLogger'; const RNEventEmitter = new NativeEventEmitter(RNIterableAPI); +/** + * Checks if the response is an IterableAuthResponse + */ +const isIterableAuthResponse = ( + response: IterableAuthResponse | string | undefined | null +): response is IterableAuthResponse => { + if (typeof response === 'string') return false; + if ( + response?.authToken || + response?.successCallback || + response?.failureCallback + ) { + return true; + } + return false; +}; + /* eslint-disable tsdoc/syntax */ /** * The main class for the Iterable React Native SDK. @@ -952,25 +969,23 @@ export class Iterable { // Promise result can be either just String OR of type AuthResponse. // If type AuthReponse, authToken will be parsed looking for `authToken` within promised object. Two additional listeners will be registered for success and failure callbacks sent by native bridge layer. // Else it will be looked for as a String. - if (typeof promiseResult === typeof new IterableAuthResponse()) { - Iterable.authManager.passAlongAuthToken( - (promiseResult as IterableAuthResponse).authToken - ); + if (isIterableAuthResponse(promiseResult)) { + Iterable.authManager.passAlongAuthToken(promiseResult.authToken); setTimeout(() => { if ( authResponseCallback === IterableAuthResponseResult.SUCCESS ) { - if ((promiseResult as IterableAuthResponse).successCallback) { - (promiseResult as IterableAuthResponse).successCallback?.(); + if (promiseResult.successCallback) { + promiseResult.successCallback?.(); } } else if ( authResponseCallback === IterableAuthResponseResult.FAILURE ) { // We are currently only reporting JWT related errors. In // the future, we should handle other types of errors as well. - if ((promiseResult as IterableAuthResponse).failureCallback) { - (promiseResult as IterableAuthResponse).failureCallback?.(); + if (promiseResult.failureCallback) { + promiseResult.failureCallback?.(); } } else { IterableLogger?.log('No callback received from native layer'); @@ -978,7 +993,7 @@ export class Iterable { }, 1000); } else if (typeof promiseResult === 'string') { //If promise only returns string - Iterable.authManager.passAlongAuthToken(promiseResult as string); + Iterable.authManager.passAlongAuthToken(promiseResult); } else { IterableLogger?.log( 'Unexpected promise returned. Auth token expects promise of String or AuthResponse type.' From ca8868328587bf2a9a3e1801e0f76c50591cc2a9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 3 Nov 2025 12:01:21 -0800 Subject: [PATCH 51/82] fix: remove node only call --- src/core/classes/Iterable.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index d49780c4c..8b16ae52e 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -998,7 +998,7 @@ export class Iterable { (promiseResult as IterableAuthResponse).authToken ); - const timeoutId = setTimeout(() => { + setTimeout(() => { if ( authResponseCallback === IterableAuthResponseResult.SUCCESS ) { @@ -1017,8 +1017,6 @@ export class Iterable { ); } }, 1000); - // Use unref() to prevent the timeout from keeping the process alive - timeoutId.unref(); } else if (typeof promiseResult === typeof '') { //If promise only returns string RNIterableAPI.passAlongAuthToken(promiseResult as string); From 2adf8d8e8adc0ddc593277139253b57a919524fd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:14:37 +0000 Subject: [PATCH 52/82] chore(deps): bump tar from 7.5.1 to 7.5.2 Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.1 to 7.5.2. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.2) --- updated-dependencies: - dependency-name: tar dependency-version: 7.5.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 418409009..013c5dc63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12822,15 +12822,15 @@ __metadata: linkType: hard "tar@npm:^7.4.3": - version: 7.5.1 - resolution: "tar@npm:7.5.1" + version: 7.5.2 + resolution: "tar@npm:7.5.2" dependencies: "@isaacs/fs-minipass": ^4.0.0 chownr: ^3.0.0 minipass: ^7.1.2 minizlib: ^3.1.0 yallist: ^5.0.0 - checksum: dbd55d4c3bd9e3c69aed137d9dc9fcb8f86afd103c28d97d52728ca80708f4c84b07e0a01d0bf1c8e820be84d37632325debf19f672a06e0c605c57a03636fd0 + checksum: 192559b0e7af17d57c7747592ef22c14d5eba2d9c35996320ccd20c3e2038160fe8d928fc5c08b2aa1b170c4d0a18c119441e81eae8f227ca2028d5bcaa6bf23 languageName: node linkType: hard From 2d4e38076d26312d9e93faa1ec82f029b5d9b550 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 3 Nov 2025 12:57:18 -0800 Subject: [PATCH 53/82] chore: update version to 2.1.0 in package.json and itblBuildInfo.ts --- package.json | 2 +- src/itblBuildInfo.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6336697df..ad25f554e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iterable/react-native-sdk", - "version": "2.1.0-beta.1", + "version": "2.1.0", "description": "Iterable SDK for React Native.", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/src/itblBuildInfo.ts b/src/itblBuildInfo.ts index 097bd43d2..20694481e 100644 --- a/src/itblBuildInfo.ts +++ b/src/itblBuildInfo.ts @@ -3,5 +3,5 @@ * It contains the version of the package */ export const buildInfo = { - version: '2.1.0-beta.1', + version: '2.1.0', }; From ebf660c0056189af8a814e84826326898e379304 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 3 Nov 2025 13:03:24 -0800 Subject: [PATCH 54/82] chore: update CHANGELOG for version 2.1.0 with new features and dependency updates --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e4a71be..9f12145b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +##2.1.0 +### Features +* SDK is now compatible with both New Architecture and Legacy Architecture. Fix + for #691, #602, #563. + +### Fixes +* Dependencies update + ## 2.1.0-beta.1 ## Fixes From c7a0ac81ba9397835260f77928edc1592a22a4e9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 3 Nov 2025 13:10:33 -0800 Subject: [PATCH 55/82] chore: update CHANGELOG for version 2.1.0 with updates and fixes --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f12145b2..260f562fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ ##2.1.0 -### Features +### Updates * SDK is now compatible with both New Architecture and Legacy Architecture. Fix for #691, #602, #563. @@ -8,7 +8,7 @@ ## 2.1.0-beta.1 -## Fixes +### Fixes - Add Temporary fix for circular paths, which break expo ([9c09743](https://github.com/Iterable/react-native-sdk/commit/9c09743)) ## 2.1.0-beta.0 @@ -22,7 +22,7 @@ ## 2.0.4 -## Updates +### Updates - Added API documentation via Netlify([1087275](https://github.com/Iterable/react-native-sdk/commit/1087275)) - Removed dependency on `react-native-vector-icons`, per issues [#513](https://github.com/Iterable/react-native-sdk/issues/513), From 41b0a52350ee51044de1fd1a96f6e747c0ee983f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 6 Nov 2025 22:39:22 +0000 Subject: [PATCH 56/82] chore(deps-dev): bump @react-native-community/cli from 18.0.0 to 18.0.1 Bumps [@react-native-community/cli](https://github.com/react-native-community/cli/tree/HEAD/packages/cli) from 18.0.0 to 18.0.1. - [Release notes](https://github.com/react-native-community/cli/releases) - [Changelog](https://github.com/react-native-community/cli/blob/main/packages/cli/CHANGELOG.md) - [Commits](https://github.com/react-native-community/cli/commits/v18.0.1/packages/cli) --- updated-dependencies: - dependency-name: "@react-native-community/cli" dependency-version: 18.0.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- example/package.json | 2 +- yarn.lock | 155 ++++++++++++++++++++++++++++++++----------- 2 files changed, 117 insertions(+), 40 deletions(-) diff --git a/example/package.json b/example/package.json index 7a59e93de..5e101d691 100644 --- a/example/package.json +++ b/example/package.json @@ -25,7 +25,7 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/runtime": "^7.25.0", - "@react-native-community/cli": "18.0.0", + "@react-native-community/cli": "18.0.1", "@react-native-community/cli-platform-android": "18.0.0", "@react-native-community/cli-platform-ios": "18.0.0", "@react-native/babel-preset": "0.79.3", diff --git a/yarn.lock b/yarn.lock index 418409009..97fd027c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1947,7 +1947,7 @@ __metadata: "@babel/core": ^7.25.2 "@babel/preset-env": ^7.25.3 "@babel/runtime": ^7.25.0 - "@react-native-community/cli": 18.0.0 + "@react-native-community/cli": 18.0.1 "@react-native-community/cli-platform-android": 18.0.0 "@react-native-community/cli-platform-ios": 18.0.0 "@react-native/babel-preset": 0.79.3 @@ -2599,15 +2599,15 @@ __metadata: languageName: node linkType: hard -"@react-native-community/cli-clean@npm:18.0.0": - version: 18.0.0 - resolution: "@react-native-community/cli-clean@npm:18.0.0" +"@react-native-community/cli-clean@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-clean@npm:18.0.1" dependencies: - "@react-native-community/cli-tools": 18.0.0 + "@react-native-community/cli-tools": 18.0.1 chalk: ^4.1.2 execa: ^5.0.0 fast-glob: ^3.3.2 - checksum: 901f9ba9c124447de7da76b4e4a52dd6c374ffd117def571368e23393e2a4591e907076d937f8a6a6a81d97a24fcc6f73b7d026d327d9319bf3c4e83f84a79c5 + checksum: f2bd017b172e1ea23f91c717eefad145deb175c501b1b041bf91efffdfebfeedef7f33ac1cd5ab98dde8d4ccde520b3060422840cd6e6e24efb70b1b0aa72a9e languageName: node linkType: hard @@ -2623,6 +2623,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config-android@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-config-android@npm:18.0.1" + dependencies: + "@react-native-community/cli-tools": 18.0.1 + chalk: ^4.1.2 + fast-glob: ^3.3.2 + fast-xml-parser: ^4.4.1 + checksum: 5343fef8b5feb32e8104a416048e7675dcf5a83de3af2ed0f00dcb5bbb3360dca665d93a973a7379de2f6ff8e0bc6608f763cc272784b6dc1dace6b97b947af2 + languageName: node + linkType: hard + "@react-native-community/cli-config-apple@npm:18.0.0": version: 18.0.0 resolution: "@react-native-community/cli-config-apple@npm:18.0.0" @@ -2635,29 +2647,41 @@ __metadata: languageName: node linkType: hard -"@react-native-community/cli-config@npm:18.0.0": - version: 18.0.0 - resolution: "@react-native-community/cli-config@npm:18.0.0" +"@react-native-community/cli-config-apple@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-config-apple@npm:18.0.1" dependencies: - "@react-native-community/cli-tools": 18.0.0 + "@react-native-community/cli-tools": 18.0.1 + chalk: ^4.1.2 + execa: ^5.0.0 + fast-glob: ^3.3.2 + checksum: 4c8716a0941af2c5f9910df71245df1f4cbce37cdbca55baa5b6aaff55f0b5fee5f24488146df0d225c157b0d339f76df94ddcf0f19e4374c67f72383ebd0fd7 + languageName: node + linkType: hard + +"@react-native-community/cli-config@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-config@npm:18.0.1" + dependencies: + "@react-native-community/cli-tools": 18.0.1 chalk: ^4.1.2 cosmiconfig: ^9.0.0 deepmerge: ^4.3.0 fast-glob: ^3.3.2 joi: ^17.2.1 - checksum: d4df3fdce60753667f654da6029577d7cfecaaf7eb193ee6ff437a90fa594cbbf0afe3894c938eb120b47f2b97a6e57729c1ffc46daff8f504bf7022da4068b4 + checksum: b67d691e8ef47307a9079d42243e6126f780a16730ffedd3fca000cfb5719966f6d409b284012bd8b424df9af12d3f188fe57e64c6880c9e61ba51192ff78742 languageName: node linkType: hard -"@react-native-community/cli-doctor@npm:18.0.0": - version: 18.0.0 - resolution: "@react-native-community/cli-doctor@npm:18.0.0" +"@react-native-community/cli-doctor@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-doctor@npm:18.0.1" dependencies: - "@react-native-community/cli-config": 18.0.0 - "@react-native-community/cli-platform-android": 18.0.0 - "@react-native-community/cli-platform-apple": 18.0.0 - "@react-native-community/cli-platform-ios": 18.0.0 - "@react-native-community/cli-tools": 18.0.0 + "@react-native-community/cli-config": 18.0.1 + "@react-native-community/cli-platform-android": 18.0.1 + "@react-native-community/cli-platform-apple": 18.0.1 + "@react-native-community/cli-platform-ios": 18.0.1 + "@react-native-community/cli-tools": 18.0.1 chalk: ^4.1.2 command-exists: ^1.2.8 deepmerge: ^4.3.0 @@ -2668,7 +2692,7 @@ __metadata: semver: ^7.5.2 wcwidth: ^1.0.1 yaml: ^2.2.1 - checksum: bcf703aabf63cf9f06b2fa1b6a1f7b1bbfd50f2d0486621a718718ccd8a1ad5ebd47335e9d8b9809d354684d8836c495606b77f49552698970ef5dd9dedcd8b5 + checksum: 605b08c443456a65a44540aad224b282206f872fef4b43e0027a162eef5f2dddc028d20268241c862618175b27c5718ffbd22b0d3d73aee0b252589cc145b6eb languageName: node linkType: hard @@ -2685,6 +2709,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-android@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-platform-android@npm:18.0.1" + dependencies: + "@react-native-community/cli-config-android": 18.0.1 + "@react-native-community/cli-tools": 18.0.1 + chalk: ^4.1.2 + execa: ^5.0.0 + logkitty: ^0.7.1 + checksum: 25a413e68cc2d41367a0445861fca37142ffd5c475a7983b4423e1d12d0014389ba632035bcd92ef5cd99df1087ce3554c275422fcb1b2197eb29b747e2aa978 + languageName: node + linkType: hard + "@react-native-community/cli-platform-apple@npm:18.0.0": version: 18.0.0 resolution: "@react-native-community/cli-platform-apple@npm:18.0.0" @@ -2698,6 +2735,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-apple@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-platform-apple@npm:18.0.1" + dependencies: + "@react-native-community/cli-config-apple": 18.0.1 + "@react-native-community/cli-tools": 18.0.1 + chalk: ^4.1.2 + execa: ^5.0.0 + fast-xml-parser: ^4.4.1 + checksum: 8efaa76b43521afca9bc6eb423b758839e38cee7b4cf3927bc0b6b3d348ad9c98bc8f33366f780f59c8604d02e487de2f4554814ca354700cff01e09430ba365 + languageName: node + linkType: hard + "@react-native-community/cli-platform-ios@npm:18.0.0": version: 18.0.0 resolution: "@react-native-community/cli-platform-ios@npm:18.0.0" @@ -2707,11 +2757,20 @@ __metadata: languageName: node linkType: hard -"@react-native-community/cli-server-api@npm:18.0.0": - version: 18.0.0 - resolution: "@react-native-community/cli-server-api@npm:18.0.0" +"@react-native-community/cli-platform-ios@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-platform-ios@npm:18.0.1" dependencies: - "@react-native-community/cli-tools": 18.0.0 + "@react-native-community/cli-platform-apple": 18.0.1 + checksum: 2eb0b662e9371721f524f242cfa04bccc62785d841ab110a3eef162a632216f7a5546d59afa0647bc4c3f7e0de305c030f96fd07119509df3cdef35e5f01f997 + languageName: node + linkType: hard + +"@react-native-community/cli-server-api@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-server-api@npm:18.0.1" + dependencies: + "@react-native-community/cli-tools": 18.0.1 body-parser: ^1.20.3 compression: ^1.7.1 connect: ^3.6.5 @@ -2721,7 +2780,7 @@ __metadata: pretty-format: ^26.6.2 serve-static: ^1.13.1 ws: ^6.2.3 - checksum: 839e9a97b8cb8b875d00ca8a3743ad125beb7a85b74ee07adc9b712896b78d9ed5a35b46c2b7ea5dbfc312a797f9ee96af1bf3462d315252f10375aa22315fe8 + checksum: ba0543bd6b7debdd2ca6e04075959ca1b04a9f4b5d883638112d0dbab2ee6b6f187880a44fb171ab3d59281dbd951914ada765811e089365f76abbcc8485c22c languageName: node linkType: hard @@ -2743,25 +2802,43 @@ __metadata: languageName: node linkType: hard -"@react-native-community/cli-types@npm:18.0.0": - version: 18.0.0 - resolution: "@react-native-community/cli-types@npm:18.0.0" +"@react-native-community/cli-tools@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-tools@npm:18.0.1" + dependencies: + "@vscode/sudo-prompt": ^9.0.0 + appdirsjs: ^1.2.4 + chalk: ^4.1.2 + execa: ^5.0.0 + find-up: ^5.0.0 + launch-editor: ^2.9.1 + mime: ^2.4.1 + ora: ^5.4.1 + prompts: ^2.4.2 + semver: ^7.5.2 + checksum: b2f40e9d8e442aacb5914ebb1ca00a729878184b2da96a3fb21c51d0050fb5b1f97789e6d6dfd39af269e840b74027de5716cab17b5ef983aa6a778e03e77f2c + languageName: node + linkType: hard + +"@react-native-community/cli-types@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli-types@npm:18.0.1" dependencies: joi: ^17.2.1 - checksum: 92768eb2dd74549069230b6b594b3ae4cdeae03f938504a642fcaed564c22b2b2bb516c4b6cd880a5b419f408206404d88034795e369f8bb8765bdb1f38ed07d + checksum: 26c5a92d31021fb54ec4ea700736105e24b48db8369ef5c75de9490faeaef96fa9f6a39fa298466854f63d71941c85404c2713ed1c4323c8b04cd519de511699 languageName: node linkType: hard -"@react-native-community/cli@npm:18.0.0": - version: 18.0.0 - resolution: "@react-native-community/cli@npm:18.0.0" +"@react-native-community/cli@npm:18.0.1": + version: 18.0.1 + resolution: "@react-native-community/cli@npm:18.0.1" dependencies: - "@react-native-community/cli-clean": 18.0.0 - "@react-native-community/cli-config": 18.0.0 - "@react-native-community/cli-doctor": 18.0.0 - "@react-native-community/cli-server-api": 18.0.0 - "@react-native-community/cli-tools": 18.0.0 - "@react-native-community/cli-types": 18.0.0 + "@react-native-community/cli-clean": 18.0.1 + "@react-native-community/cli-config": 18.0.1 + "@react-native-community/cli-doctor": 18.0.1 + "@react-native-community/cli-server-api": 18.0.1 + "@react-native-community/cli-tools": 18.0.1 + "@react-native-community/cli-types": 18.0.1 chalk: ^4.1.2 commander: ^9.4.1 deepmerge: ^4.3.0 @@ -2773,7 +2850,7 @@ __metadata: semver: ^7.5.2 bin: rnc-cli: build/bin.js - checksum: bd4d4142c95339393f35509038042fa854b4dd2d7dd458fcb2226d2e63d947cff561f20ce47253249bde310db35c071f836195377761dd7a8e64cb1ce1e35217 + checksum: 86b3154ce5fb27b654888e55529dab21ca0625b9c47143071d09bd3ee7741f63e8524b07c6c901734d7c9e33790990f1d63da541adf60f1279631cc33e9b25c2 languageName: node linkType: hard From cacb0ea520b9cd20c3898abee0fe7e1fa63fff36 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Mon, 10 Nov 2025 15:08:44 -0800 Subject: [PATCH 57/82] chore: update Iterable-iOS-SDK and Iterable API dependencies to version 6.6.2 --- Iterable-React-Native-SDK.podspec | 2 +- android/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Iterable-React-Native-SDK.podspec b/Iterable-React-Native-SDK.podspec index 0d023409f..47dd34cae 100644 --- a/Iterable-React-Native-SDK.podspec +++ b/Iterable-React-Native-SDK.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.private_header_files = "ios/**/*.h" # Load Iterables iOS SDK as a dependency - s.dependency "Iterable-iOS-SDK", "6.6.1" + s.dependency "Iterable-iOS-SDK", "6.6.2" # Basic Swift support s.pod_target_xcconfig = { diff --git a/android/build.gradle b/android/build.gradle index d546cce98..6a3eb970b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -105,7 +105,7 @@ def kotlin_version = getExtOrDefault("kotlinVersion") dependencies { implementation "com.facebook.react:react-android" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - api "com.iterable:iterableapi:3.6.1" + api "com.iterable:iterableapi:3.6.2" // api project(":iterableapi") // links to local android SDK repo rather than by release } From b397602d2c1445ac8b71c45aae388baaa7f638ab Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 11 Nov 2025 13:49:09 -0800 Subject: [PATCH 58/82] chore: update Iterable-iOS-SDK to 6.6.3 and add @react-native-community/cli as a dependency --- Iterable-React-Native-SDK.podspec | 2 +- package.json | 1 + yarn.lock | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Iterable-React-Native-SDK.podspec b/Iterable-React-Native-SDK.podspec index 47dd34cae..e85f0bf44 100644 --- a/Iterable-React-Native-SDK.podspec +++ b/Iterable-React-Native-SDK.podspec @@ -17,7 +17,7 @@ Pod::Spec.new do |s| s.private_header_files = "ios/**/*.h" # Load Iterables iOS SDK as a dependency - s.dependency "Iterable-iOS-SDK", "6.6.2" + s.dependency "Iterable-iOS-SDK", "6.6.3" # Basic Swift support s.pod_target_xcconfig = { diff --git a/package.json b/package.json index 668caa89a..75cb561fb 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "devDependencies": { "@commitlint/config-conventional": "^19.6.0", "@evilmartians/lefthook": "^1.5.0", + "@react-native-community/cli": "18.0.0", "@react-native/babel-preset": "0.79.3", "@react-native/eslint-config": "0.79.3", "@react-native/metro-config": "0.79.3", diff --git a/yarn.lock b/yarn.lock index 348741043..c72b5f113 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1979,6 +1979,7 @@ __metadata: dependencies: "@commitlint/config-conventional": ^19.6.0 "@evilmartians/lefthook": ^1.5.0 + "@react-native-community/cli": 18.0.0 "@react-native/babel-preset": 0.79.3 "@react-native/eslint-config": 0.79.3 "@react-native/metro-config": 0.79.3 From 03d7967a78c1de3755a7775ba6e5a551c27c36f9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 11 Nov 2025 18:41:00 -0800 Subject: [PATCH 59/82] fix: remove additional event listeners for auth success and failure in Iterable class --- src/core/classes/Iterable.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 4c64cb7da..2199fb7be 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -918,6 +918,8 @@ export class Iterable { IterableEventName.handleCustomActionCalled ); RNEventEmitter.removeAllListeners(IterableEventName.handleAuthCalled); + RNEventEmitter.removeAllListeners(IterableEventName.handleAuthSuccessCalled); + RNEventEmitter.removeAllListeners(IterableEventName.handleAuthFailureCalled); if (Iterable.savedConfig.urlHandler) { RNEventEmitter.addListener(IterableEventName.handleUrlCalled, (dict) => { @@ -966,7 +968,7 @@ export class Iterable { // Asks frontend of the client/app to pass authToken Iterable.savedConfig.authHandler!() .then((promiseResult) => { - // Promise result can be either just String OR of type AuthResponse. + // Promise result can be either just String OR of type AuthRespronse. // If type AuthReponse, authToken will be parsed looking for `authToken` within promised object. Two additional listeners will be registered for success and failure callbacks sent by native bridge layer. // Else it will be looked for as a String. if (isIterableAuthResponse(promiseResult)) { From 954df6561bfe4143aac905664cd6e08b9ab81fae Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 13 Nov 2025 13:32:39 -0800 Subject: [PATCH 60/82] feat: add logout method to Iterable class and refactor event listener management --- src/core/classes/Iterable.ts | 37 ++++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 2199fb7be..32d7754c0 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -890,6 +890,34 @@ export class Iterable { }); } + /** + * Logs out the current user from the Iterable SDK. + * + * This method will remove all event listeners for the Iterable SDK and set the email and user ID to null. + * + * @example + * ```typescript + * Iterable.logout(); + * ``` + */ + static logout() { + Iterable.removeAllEventListeners(); + Iterable.setEmail(null); + Iterable.setUserId(null); + } + + /** + * Removes all event listeners for the Iterable SDK. + */ + private static removeAllEventListeners() { + RNEventEmitter.removeAllListeners(IterableEventName.handleUrlCalled); + RNEventEmitter.removeAllListeners(IterableEventName.handleInAppCalled); + RNEventEmitter.removeAllListeners(IterableEventName.handleCustomActionCalled); + RNEventEmitter.removeAllListeners(IterableEventName.handleAuthCalled); + RNEventEmitter.removeAllListeners(IterableEventName.handleAuthSuccessCalled); + RNEventEmitter.removeAllListeners(IterableEventName.handleAuthFailureCalled); + } + /** * Sets up event handlers for various Iterable events. * @@ -912,14 +940,7 @@ export class Iterable { */ private static setupEventHandlers() { // Remove all listeners to avoid duplicate listeners - RNEventEmitter.removeAllListeners(IterableEventName.handleUrlCalled); - RNEventEmitter.removeAllListeners(IterableEventName.handleInAppCalled); - RNEventEmitter.removeAllListeners( - IterableEventName.handleCustomActionCalled - ); - RNEventEmitter.removeAllListeners(IterableEventName.handleAuthCalled); - RNEventEmitter.removeAllListeners(IterableEventName.handleAuthSuccessCalled); - RNEventEmitter.removeAllListeners(IterableEventName.handleAuthFailureCalled); + Iterable.removeAllEventListeners(); if (Iterable.savedConfig.urlHandler) { RNEventEmitter.addListener(IterableEventName.handleUrlCalled, (dict) => { From 900ee9cd58301cddf17c3698ac0b2a890a399cb0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 13 Nov 2025 13:38:10 -0800 Subject: [PATCH 61/82] test: add unit tests for Iterable.logout method to verify functionality --- src/core/classes/Iterable.test.ts | 49 +++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index 7774b5bba..6ae528c11 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -74,6 +74,55 @@ describe('Iterable', () => { }); }); + describe('logout', () => { + it('should call setEmail with null', () => { + // GIVEN no parameters + // WHEN Iterable.logout is called + const setEmailSpy = jest.spyOn(Iterable, 'setEmail'); + Iterable.logout(); + // THEN Iterable.setEmail is called with null + expect(setEmailSpy).toBeCalledWith(null); + setEmailSpy.mockRestore(); + }); + + it('should call setUserId with null', () => { + // GIVEN no parameters + // WHEN Iterable.logout is called + const setUserIdSpy = jest.spyOn(Iterable, 'setUserId'); + Iterable.logout(); + // THEN Iterable.setUserId is called with null + expect(setUserIdSpy).toBeCalledWith(null); + setUserIdSpy.mockRestore(); + }); + + it('should clear email and userId', async () => { + // GIVEN a user is logged in + Iterable.setEmail('user@example.com'); + Iterable.setUserId('user123'); + // WHEN Iterable.logout is called + Iterable.logout(); + // THEN email and userId are set to null + const email = await Iterable.getEmail(); + const userId = await Iterable.getUserId(); + expect(email).toBeNull(); + expect(userId).toBeNull(); + }); + + it('should call setEmail and setUserId with null', () => { + // GIVEN no parameters + const setEmailSpy = jest.spyOn(Iterable, 'setEmail'); + const setUserIdSpy = jest.spyOn(Iterable, 'setUserId'); + // WHEN Iterable.logout is called + Iterable.logout(); + // THEN both methods are called with null + expect(setEmailSpy).toBeCalledWith(null); + expect(setUserIdSpy).toBeCalledWith(null); + // Clean up + setEmailSpy.mockRestore(); + setUserIdSpy.mockRestore(); + }); + }); + describe('disableDeviceForCurrentUser', () => { it('should disable the device for the current user', () => { // GIVEN no parameters From f72f57810251ebee16773fd26d97da48a1196737 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 13 Nov 2025 21:25:47 -0800 Subject: [PATCH 62/82] fix: correct typo in comment regarding AuthResponse in Iterable class --- src/core/classes/Iterable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 32d7754c0..2a23693e2 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -989,7 +989,7 @@ export class Iterable { // Asks frontend of the client/app to pass authToken Iterable.savedConfig.authHandler!() .then((promiseResult) => { - // Promise result can be either just String OR of type AuthRespronse. + // Promise result can be either just String OR of type AuthResponse. // If type AuthReponse, authToken will be parsed looking for `authToken` within promised object. Two additional listeners will be registered for success and failure callbacks sent by native bridge layer. // Else it will be looked for as a String. if (isIterableAuthResponse(promiseResult)) { From b9401dfd8071a723fbbe50892458d3a85777b436 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 13 Nov 2025 21:30:08 -0800 Subject: [PATCH 63/82] test: add clarification comment in Iterable.logout test for better understanding --- src/core/classes/Iterable.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/classes/Iterable.test.ts b/src/core/classes/Iterable.test.ts index 6ae528c11..bfe4c26f6 100644 --- a/src/core/classes/Iterable.test.ts +++ b/src/core/classes/Iterable.test.ts @@ -97,6 +97,9 @@ describe('Iterable', () => { it('should clear email and userId', async () => { // GIVEN a user is logged in + + // This is just for testing purposed. + // Usually you'd either call `setEmail` or `setUserId`, but not both. Iterable.setEmail('user@example.com'); Iterable.setUserId('user123'); // WHEN Iterable.logout is called From fb680ae96a407018a11bebc82c83a20cdf8603e9 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 13 Nov 2025 21:34:16 -0800 Subject: [PATCH 64/82] chore: bump version to 2.2.0-alpha.1 in package.json and itblBuildInfo.ts --- package.json | 2 +- src/itblBuildInfo.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 75cb561fb..b59a90df9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iterable/react-native-sdk", - "version": "2.2.0-alpha.0", + "version": "2.2.0-alpha.1", "description": "Iterable SDK for React Native.", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/src/itblBuildInfo.ts b/src/itblBuildInfo.ts index 9ab008e03..d9b0c0ab1 100644 --- a/src/itblBuildInfo.ts +++ b/src/itblBuildInfo.ts @@ -3,5 +3,5 @@ * It contains the version of the package */ export const buildInfo = { - version: '2.2.0-alpha.0', + version: '2.2.0-alpha.1', }; From 95103af1ce72f52c75e9d443e62a6dd3cf407228 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 13 Nov 2025 21:39:26 -0800 Subject: [PATCH 65/82] chore: update CHANGELOG.md for version 2.2.0-alpha.1 with recent fixes and updates --- CHANGELOG.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6fd956c6..7b50f68db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ -## 2.2.0-alpha.0 (2025-10-21) +## 2.2.0-alpha.1 + +### Fixes +* [SDK-151] cannot-read-property-authtoken-of-undefined by @lposen in https://github.com/Iterable/react-native-sdk/pull/779 +* [SDK-181] update ios/android sdk versions by @lposen in https://github.com/Iterable/react-native-sdk/pull/780 +* [SDK-149] add-logout-functionality by @lposen in https://github.com/Iterable/react-native-sdk/pull/781 + +## 2.2.0-alpha.0 ### Updates - Updated Android SDK version to [3.6.1](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.1) From c6dc3701a544d972b1b3d706660fa0e321881dd6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:54:02 +0000 Subject: [PATCH 66/82] chore(deps): bump js-yaml from 3.14.1 to 3.14.2 Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.14.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 418409009..2bd65c02d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8904,14 +8904,14 @@ __metadata: linkType: hard "js-yaml@npm:^3.13.1": - version: 3.14.1 - resolution: "js-yaml@npm:3.14.1" + version: 3.14.2 + resolution: "js-yaml@npm:3.14.2" dependencies: argparse: ^1.0.7 esprima: ^4.0.0 bin: js-yaml: bin/js-yaml.js - checksum: bef146085f472d44dee30ec34e5cf36bf89164f5d585435a3d3da89e52622dff0b188a580e4ad091c3341889e14cb88cac6e4deb16dc5b1e9623bb0601fc255c + checksum: 626fc207734a3452d6ba84e1c8c226240e6d431426ed94d0ab043c50926d97c509629c08b1d636f5d27815833b7cfd225865631da9fb33cb957374490bf3e90b languageName: node linkType: hard From c4bd0093c1e4723376ea2c0544a7635e54996e10 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 14:12:13 -0800 Subject: [PATCH 67/82] fix: standardize casing of onJwtError in configuration and implementation --- CHANGELOG.md | 2 +- example/src/hooks/useIterableApp.tsx | 4 ++-- src/core/classes/Iterable.ts | 2 +- src/core/classes/IterableConfig.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b50f68db..7e154959d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Added JWT Capabilities: - Added `Iterable.authhManager`, which manages the authentication flow - Added `IterableRetryBackoff` and `IterableAuthFailureReason` enums - - Added `onJWTError` and `retryPolicy` for control over JWT flow + - Added `onJwtError` and `retryPolicy` for control over JWT flow - Moved all native calls to `IterableApi.ts` - Added JWT example to our example app diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index c215ff22f..4dac29f28 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -155,8 +155,8 @@ export const IterableAppProvider: FunctionComponent< retryBackoff: IterableRetryBackoff.LINEAR, }; - config.onJWTError = (authFailure) => { - console.log('onJWTError', authFailure); + config.onJwtError = (authFailure) => { + console.log('onJwtError', authFailure); const failureReason = typeof authFailure.failureReason === 'string' diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 2a23693e2..365aee94f 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -1041,7 +1041,7 @@ export class Iterable { authResponseCallback = IterableAuthResponseResult.FAILURE; // Call the actual JWT error with `authFailure` object. - Iterable.savedConfig?.onJWTError?.(authFailureResponse); + Iterable.savedConfig?.onJwtError?.(authFailureResponse); } ); } diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index 173b57ab3..cb9cfa24c 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -218,12 +218,12 @@ export class IterableConfig { * @example * ```typescript * const config = new IterableConfig(); - * config.onJWTError = (authFailure) => { + * config.onJwtError = (authFailure) => { * console.error('Error fetching JWT:', authFailure); * }; * ``` */ - onJWTError?: (authFailure: IterableAuthFailure) => void; + onJwtError?: (authFailure: IterableAuthFailure) => void; /** * Set the verbosity of Android and iOS project's log system. From d304442f759cce2955548a13740e720db88010f4 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 19:25:20 -0800 Subject: [PATCH 68/82] fix: standardize enum casing for IterableRetryBackoff and update documentation examples --- example/src/hooks/useIterableApp.tsx | 2 +- src/core/classes/IterableConfig.ts | 10 ++++++++++ src/core/enums/IterableRetryBackoff.ts | 12 ++++++------ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 4dac29f28..d35808d49 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -152,7 +152,7 @@ export const IterableAppProvider: FunctionComponent< config.retryPolicy = { maxRetry: 5, retryInterval: 10, - retryBackoff: IterableRetryBackoff.LINEAR, + retryBackoff: IterableRetryBackoff.Linear, }; config.onJwtError = (authFailure) => { diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index cb9cfa24c..b357f37bb 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -235,6 +235,16 @@ export class IterableConfig { /** * Configuration for JWT refresh retry behavior. * If not specified, the SDK will use default retry behavior. + * + * @example + * ```typescript + * const config = new IterableConfig(); + * config.retryPolicy = new IterableRetryPolicy({ + * maxRetries: 3, + * initialDelay: 1000, + * maxDelay: 10000, + * }); + * ``` */ retryPolicy?: IterableRetryPolicy; diff --git a/src/core/enums/IterableRetryBackoff.ts b/src/core/enums/IterableRetryBackoff.ts index 526b58eaf..576da1d62 100644 --- a/src/core/enums/IterableRetryBackoff.ts +++ b/src/core/enums/IterableRetryBackoff.ts @@ -1,17 +1,17 @@ -/* eslint-disable tsdoc/syntax */ - /** * The type of backoff to use when retrying a request. */ export enum IterableRetryBackoff { /** * Linear backoff (each retry will wait for a fixed interval) - * TODO: check with @Ayyanchira if this is correct + * + * EG: 2 seconds, 4 seconds, 6 seconds, 8 seconds, etc. */ - LINEAR = 'LINEAR', + Linear = 'LINEAR', /** * Exponential backoff (each retry will wait for an interval that increases exponentially) - * TODO: check with @Ayyanchira if this is correct + * + * EG: 2 seconds, 4 seconds, 8 seconds, 16 seconds, etc. */ - EXPONENTIAL = 'EXPONENTIAL', + Exponential = 'EXPONENTIAL', } From e21df58294b73830e8d6f33790151a850cfa17ca Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 19:29:32 -0800 Subject: [PATCH 69/82] chore: update CHANGELOG.md for version 2.1.0 and adjust README.md for SDK version references --- CHANGELOG.md | 9 ++------- README.md | 6 +++--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e154959d..1d36e35e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,20 +28,15 @@ ### Fixes * Dependencies update -## 2.1.0-beta.1 +## 2.1.0 ### Fixes -- Add Temporary fix for circular paths, which break expo ([9c09743](https://github.com/Iterable/react-native-sdk/commit/9c09743)) - -## 2.1.0-beta.0 - -### Updates - Update SDK so that it has full support for [React Native New Architecture](https://reactnative.dev/architecture/landing-page) +- Add Temporary fix for circular paths, which break expo ([9c09743](https://github.com/Iterable/react-native-sdk/commit/9c09743)) ### Chores - Update dependencies for React Navigation and related packages ([95053bb](https://github.com/Iterable/react-native-sdk/commit/95053bb)) - ## 2.0.4 ### Updates diff --git a/README.md b/README.md index 5bcf4cd66..16266121f 100644 --- a/README.md +++ b/README.md @@ -119,13 +119,13 @@ For quick reference, the following table lists the versions of the [Android SDK] | RN SDK Version | Android SDK Version | iOS SDK Version | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------- | -| [2.1.0-beta.0](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.1.0-beta.0) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) +| [2.2.0](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.2.0) | [3.6.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.2) | [6.6.3](https://github.com/Iterable/swift-sdk/releases/tag/6.6.3) +| [2.1.0](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.1.0) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) +| [2.0.4](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.0.4) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) | [2.0.3](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.0.3) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) | [2.0.2](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.0.2) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) | [2.0.1](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.0.1) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) | [2.0.0](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.0.0) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) -| [2.0.0-beta.1](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.0.0-beta.1) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) -| [2.0.0-beta](https://www.npmjs.com/package/@iterable/react-native-sdk/v/2.0.0-beta) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) | [1.3.21](https://www.npmjs.com/package/@iterable/react-native-sdk/v/1.3.20) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) | [1.3.20](https://www.npmjs.com/package/@iterable/react-native-sdk/v/1.3.20) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.4](https://github.com/Iterable/swift-sdk/releases/tag/6.5.4) | [1.3.19](https://www.npmjs.com/package/@iterable/react-native-sdk/v/1.3.19) | [3.5.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.5.2) | [6.5.3](https://github.com/Iterable/swift-sdk/releases/tag/6.5.3) From 6df9e798348197b835ae15a1c2c857189ecf27ac Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 19:34:36 -0800 Subject: [PATCH 70/82] fix: correct enum casing for IterableRetryBackoff and update usage in IterableAppProvider --- example/src/hooks/useIterableApp.tsx | 2 +- src/core/enums/IterableRetryBackoff.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index d35808d49..9f72f0cf8 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -152,7 +152,7 @@ export const IterableAppProvider: FunctionComponent< config.retryPolicy = { maxRetry: 5, retryInterval: 10, - retryBackoff: IterableRetryBackoff.Linear, + retryBackoff: IterableRetryBackoff.linear, }; config.onJwtError = (authFailure) => { diff --git a/src/core/enums/IterableRetryBackoff.ts b/src/core/enums/IterableRetryBackoff.ts index 576da1d62..2d0147a3b 100644 --- a/src/core/enums/IterableRetryBackoff.ts +++ b/src/core/enums/IterableRetryBackoff.ts @@ -7,11 +7,11 @@ export enum IterableRetryBackoff { * * EG: 2 seconds, 4 seconds, 6 seconds, 8 seconds, etc. */ - Linear = 'LINEAR', + linear = 'LINEAR', /** * Exponential backoff (each retry will wait for an interval that increases exponentially) * * EG: 2 seconds, 4 seconds, 8 seconds, 16 seconds, etc. */ - Exponential = 'EXPONENTIAL', + exponential = 'EXPONENTIAL', } From 8ae3889d634619a62ced2eafcf9f9d1357276603 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Tue, 18 Nov 2025 19:39:59 -0800 Subject: [PATCH 71/82] chore: update CHANGELOG.md --- CHANGELOG.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d36e35e9..0336d6cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,17 @@ ## 2.2.0-alpha.1 +### Updates +* [SDK-149] Added logout functionality + ### Fixes -* [SDK-151] cannot-read-property-authtoken-of-undefined by @lposen in https://github.com/Iterable/react-native-sdk/pull/779 -* [SDK-181] update ios/android sdk versions by @lposen in https://github.com/Iterable/react-native-sdk/pull/780 -* [SDK-149] add-logout-functionality by @lposen in https://github.com/Iterable/react-native-sdk/pull/781 +* [SDK-151] Fixed "cannot read property authtoken of undefined" error + ## 2.2.0-alpha.0 ### Updates -- Updated Android SDK version to [3.6.1](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.1) -- Updated iOS SDK version to [6.6.1](https://github.com/Iterable/swift-sdk/releases/tag/6.6.1) +- Updated Android SDK version to [3.6.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.2) +- Updated iOS SDK version to [6.6.3](https://github.com/Iterable/swift-sdk/releases/tag/6.6.3) - Added JWT Capabilities: - Added `Iterable.authhManager`, which manages the authentication flow - Added `IterableRetryBackoff` and `IterableAuthFailureReason` enums @@ -20,17 +22,13 @@ ### Fixes - Created a standalone `IterableLogger` to avoid circular dependencies -##2.1.0 +## 2.1.0 ### Updates * SDK is now compatible with both New Architecture and Legacy Architecture. Fix for #691, #602, #563. ### Fixes -* Dependencies update - -## 2.1.0 - -### Fixes +- Dependencies update - Update SDK so that it has full support for [React Native New Architecture](https://reactnative.dev/architecture/landing-page) - Add Temporary fix for circular paths, which break expo ([9c09743](https://github.com/Iterable/react-native-sdk/commit/9c09743)) From de5ed49cf2f4ff56586284c74f047f596334a1b2 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 19 Nov 2025 12:26:11 -0800 Subject: [PATCH 72/82] fix: convert retry interval from seconds to milliseconds in RetryPolicy configuration --- CHANGELOG.md | 11 ++++++++++- .../java/com/iterable/reactnative/Serialization.java | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0336d6cf8..e9ab073de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 2.2.0-alpha.2 + +### Updates +* Changed `onJWTError` to `onJwtError` +* Changed `IterableRetryBackoff` enum keys to be lowercase for consistency + across application + +### Fixes +* Fixed Android `retryInterval` not being respected - Android native SDK expects milliseconds while iOS expects seconds, added conversion in Android bridge layer + ## 2.2.0-alpha.1 ### Updates @@ -6,7 +16,6 @@ ### Fixes * [SDK-151] Fixed "cannot read property authtoken of undefined" error - ## 2.2.0-alpha.0 ### Updates diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 92c549554..883d96c7e 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -222,12 +222,17 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte JSONObject retryPolicyJson = iterableContextJSON.getJSONObject("retryPolicy"); int maxRetry = retryPolicyJson.getInt("maxRetry"); long retryInterval = retryPolicyJson.getLong("retryInterval"); + + // TODO [SDK-197]: Create consistency between Android and iOS + // instead of converting here + // Convert from seconds to milliseconds for Android native SDK + long retryIntervalMs = retryInterval * 1000; String retryBackoff = retryPolicyJson.getString("retryBackoff"); RetryPolicy.Type retryPolicyType = RetryPolicy.Type.LINEAR; if (retryBackoff.equals("EXPONENTIAL")) { retryPolicyType = RetryPolicy.Type.EXPONENTIAL; } - configBuilder.setAuthRetryPolicy(new RetryPolicy(maxRetry, retryInterval, retryPolicyType)); + configBuilder.setAuthRetryPolicy(new RetryPolicy(maxRetry, retryIntervalMs, retryPolicyType)); } return configBuilder; From e9cb76137b7d363f1389a0719bf6b9a43cf3e5cb Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 17:11:19 -0800 Subject: [PATCH 73/82] fix: update retry policy handling to respect re-initialization --- .../reactnative/RNIterableAPIModuleImpl.java | 59 ++++++++++++++++++- .../iterable/reactnative/Serialization.java | 3 +- example/src/hooks/useIterableApp.tsx | 24 ++++++-- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index 57cf9a0b8..e0ef85d43 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -24,6 +24,7 @@ import com.iterable.iterableapi.IterableActionContext; import com.iterable.iterableapi.IterableApi; import com.iterable.iterableapi.IterableAuthHandler; +import com.iterable.iterableapi.IterableAuthManager; import com.iterable.iterableapi.IterableConfig; import com.iterable.iterableapi.IterableCustomActionHandler; import com.iterable.iterableapi.IterableAttributionInfo; @@ -88,7 +89,34 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S configBuilder.setAuthHandler(this); } - IterableApi.initialize(reactContext, apiKey, configBuilder.build()); + IterableConfig config = configBuilder.build(); + IterableApi.initialize(reactContext, apiKey, config); + + // Update retry policy on existing authManager if it was already created + // This fixes the issue where retryInterval is not respected after re-initialization + try { + // Use reflection to access package-private fields and methods + java.lang.reflect.Field configRetryPolicyField = config.getClass().getDeclaredField("retryPolicy"); + configRetryPolicyField.setAccessible(true); + Object retryPolicy = configRetryPolicyField.get(config); + + if (retryPolicy != null) { + java.lang.reflect.Method getAuthManagerMethod = IterableApi.getInstance().getClass().getDeclaredMethod("getAuthManager"); + getAuthManagerMethod.setAccessible(true); + IterableAuthManager authManager = (IterableAuthManager) getAuthManagerMethod.invoke(IterableApi.getInstance()); + + if (authManager != null) { + // Update the retry policy field on the authManager + java.lang.reflect.Field authRetryPolicyField = authManager.getClass().getDeclaredField("authRetryPolicy"); + authRetryPolicyField.setAccessible(true); + authRetryPolicyField.set(authManager, retryPolicy); + IterableLogger.d(TAG, "Updated retry policy on existing authManager"); + } + } + } catch (Exception e) { + IterableLogger.e(TAG, "Failed to update retry policy: " + e.getMessage()); + } + IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); IterableApi.getInstance().getInAppManager().addListener(this); @@ -122,7 +150,34 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap, // override in the Android SDK. Check with @Ayyanchira and @evantk91 to // see what the best approach is. - IterableApi.initialize(reactContext, apiKey, configBuilder.build()); + IterableConfig config = configBuilder.build(); + IterableApi.initialize(reactContext, apiKey, config); + + // Update retry policy on existing authManager if it was already created + // This fixes the issue where retryInterval is not respected after re-initialization + try { + // Use reflection to access package-private fields and methods + java.lang.reflect.Field configRetryPolicyField = config.getClass().getDeclaredField("retryPolicy"); + configRetryPolicyField.setAccessible(true); + Object retryPolicy = configRetryPolicyField.get(config); + + if (retryPolicy != null) { + java.lang.reflect.Method getAuthManagerMethod = IterableApi.getInstance().getClass().getDeclaredMethod("getAuthManager"); + getAuthManagerMethod.setAccessible(true); + IterableAuthManager authManager = (IterableAuthManager) getAuthManagerMethod.invoke(IterableApi.getInstance()); + + if (authManager != null) { + // Update the retry policy field on the authManager + java.lang.reflect.Field authRetryPolicyField = authManager.getClass().getDeclaredField("authRetryPolicy"); + authRetryPolicyField.setAccessible(true); + authRetryPolicyField.set(authManager, retryPolicy); + IterableLogger.d(TAG, "Updated retry policy on existing authManager"); + } + } + } catch (Exception e) { + IterableLogger.e(TAG, "Failed to update retry policy: " + e.getMessage()); + } + IterableApi.getInstance().setDeviceAttribute("reactNativeSDKVersion", version); IterableApi.getInstance().getInAppManager().addListener(this); diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 883d96c7e..a4dae7c00 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -226,13 +226,12 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte // TODO [SDK-197]: Create consistency between Android and iOS // instead of converting here // Convert from seconds to milliseconds for Android native SDK - long retryIntervalMs = retryInterval * 1000; String retryBackoff = retryPolicyJson.getString("retryBackoff"); RetryPolicy.Type retryPolicyType = RetryPolicy.Type.LINEAR; if (retryBackoff.equals("EXPONENTIAL")) { retryPolicyType = RetryPolicy.Type.EXPONENTIAL; } - configBuilder.setAuthRetryPolicy(new RetryPolicy(maxRetry, retryIntervalMs, retryPolicyType)); + configBuilder.setAuthRetryPolicy(new RetryPolicy(maxRetry, retryInterval, retryPolicyType)); } return configBuilder; diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 9f72f0cf8..aa05146f4 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -89,6 +89,8 @@ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const getIsEmail = (id: string) => EMAIL_REGEX.test(id); +let lastTimeStamp = 0; + export const IterableAppProvider: FunctionComponent< React.PropsWithChildren > = ({ children }) => { @@ -141,9 +143,7 @@ export const IterableAppProvider: FunctionComponent< const initialize = useCallback( (navigation: Navigation) => { - if (getUserId()) { - login(); - } + logout(); const config = new IterableConfig(); @@ -151,7 +151,7 @@ export const IterableAppProvider: FunctionComponent< config.retryPolicy = { maxRetry: 5, - retryInterval: 10, + retryInterval:2, retryBackoff: IterableRetryBackoff.linear, }; @@ -199,8 +199,16 @@ export const IterableAppProvider: FunctionComponent< process.env.ITBL_JWT_SECRET ) { config.authHandler = async () => { + console.group('authHandler'); + const now = Date.now(); + if (lastTimeStamp !== 0) { + console.log('Time since last call:', now - lastTimeStamp); + } + lastTimeStamp = now; + console.groupEnd(); + + // return 'InvalidToken'; // Uncomment this to test the failure callback const token = await getJwtToken(); - // return 'SomethingNotValid'; // Uncomment this to test the failure callback return token; }; } @@ -219,6 +227,10 @@ export const IterableAppProvider: FunctionComponent< .then((isSuccessful) => { setIsInitialized(isSuccessful); + if (isSuccessful && getUserId()) { + return login(); + } + if (!isSuccessful) { return Promise.reject('`Iterable.initialize` failed'); } @@ -242,6 +254,8 @@ export const IterableAppProvider: FunctionComponent< const logout = useCallback(() => { Iterable.setEmail(null); Iterable.setUserId(null); + Iterable.logout(); + lastTimeStamp = 0; setIsLoggedIn(false); }, []); From c48da84f10172b185dac85cefd595bc53ca25735 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 17:11:37 -0800 Subject: [PATCH 74/82] fix: increase retry interval in RetryPolicy configuration from 2 to 5 --- example/src/hooks/useIterableApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index aa05146f4..1dc981580 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -151,7 +151,7 @@ export const IterableAppProvider: FunctionComponent< config.retryPolicy = { maxRetry: 5, - retryInterval:2, + retryInterval:5, retryBackoff: IterableRetryBackoff.linear, }; From ada0a72e0468c628325cd07d2c952124a9a70251 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 17:24:45 -0800 Subject: [PATCH 75/82] fix: update Android `retryInterval` handling to ensure it is correctly updated on re-initialization --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9ab073de..476661592 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ across application ### Fixes -* Fixed Android `retryInterval` not being respected - Android native SDK expects milliseconds while iOS expects seconds, added conversion in Android bridge layer +* Fixed Android `retryInterval` not being updated on re-initialization. ## 2.2.0-alpha.1 From 177308b275c216c82008fc51d8922710eb39a234 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 17:26:41 -0800 Subject: [PATCH 76/82] fix: improve handling of retryInterval on re-initialization and add TODO for root cause resolution --- .../com/iterable/reactnative/RNIterableAPIModuleImpl.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java index e0ef85d43..98340a1f7 100644 --- a/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java +++ b/android/src/main/java/com/iterable/reactnative/RNIterableAPIModuleImpl.java @@ -93,7 +93,9 @@ public void initializeWithApiKey(String apiKey, ReadableMap configReadableMap, S IterableApi.initialize(reactContext, apiKey, config); // Update retry policy on existing authManager if it was already created - // This fixes the issue where retryInterval is not respected after re-initialization + // This fixes the issue where retryInterval is not respected after + // re-initialization + // TODO [SDK-197]: Fix the root cause of this issue, instead of this hack try { // Use reflection to access package-private fields and methods java.lang.reflect.Field configRetryPolicyField = config.getClass().getDeclaredField("retryPolicy"); @@ -154,7 +156,9 @@ public void initialize2WithApiKey(String apiKey, ReadableMap configReadableMap, IterableApi.initialize(reactContext, apiKey, config); // Update retry policy on existing authManager if it was already created - // This fixes the issue where retryInterval is not respected after re-initialization + // This fixes the issue where retryInterval is not respected after + // re-initialization + // TODO [SDK-197]: Fix the root cause of this issue, instead of this hack try { // Use reflection to access package-private fields and methods java.lang.reflect.Field configRetryPolicyField = config.getClass().getDeclaredField("retryPolicy"); From 45be7edde2f83c2a61b3eeb1a2753dab17836465 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 17:27:51 -0800 Subject: [PATCH 77/82] refactor: remove outdated TODO comments --- .../src/main/java/com/iterable/reactnative/Serialization.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index a4dae7c00..9a1d8b3cd 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -223,9 +223,6 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte int maxRetry = retryPolicyJson.getInt("maxRetry"); long retryInterval = retryPolicyJson.getLong("retryInterval"); - // TODO [SDK-197]: Create consistency between Android and iOS - // instead of converting here - // Convert from seconds to milliseconds for Android native SDK String retryBackoff = retryPolicyJson.getString("retryBackoff"); RetryPolicy.Type retryPolicyType = RetryPolicy.Type.LINEAR; if (retryBackoff.equals("EXPONENTIAL")) { From 0fca9f219e82575dffbaf948022e8f0ff6fd8d3e Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 17:28:17 -0800 Subject: [PATCH 78/82] fix: remove unnecessary blank line in Serialization class --- .../src/main/java/com/iterable/reactnative/Serialization.java | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/java/com/iterable/reactnative/Serialization.java b/android/src/main/java/com/iterable/reactnative/Serialization.java index 9a1d8b3cd..92c549554 100644 --- a/android/src/main/java/com/iterable/reactnative/Serialization.java +++ b/android/src/main/java/com/iterable/reactnative/Serialization.java @@ -222,7 +222,6 @@ static IterableConfig.Builder getConfigFromReadableMap(ReadableMap iterableConte JSONObject retryPolicyJson = iterableContextJSON.getJSONObject("retryPolicy"); int maxRetry = retryPolicyJson.getInt("maxRetry"); long retryInterval = retryPolicyJson.getLong("retryInterval"); - String retryBackoff = retryPolicyJson.getString("retryBackoff"); RetryPolicy.Type retryPolicyType = RetryPolicy.Type.LINEAR; if (retryBackoff.equals("EXPONENTIAL")) { From 1a6d487e9b950449422cf11feb72e3fa24b62320 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 17:50:46 -0800 Subject: [PATCH 79/82] fix: ensure consistent formatting of retryInterval in RetryPolicy configuration --- example/src/hooks/useIterableApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/hooks/useIterableApp.tsx b/example/src/hooks/useIterableApp.tsx index 1dc981580..0022fdb4c 100644 --- a/example/src/hooks/useIterableApp.tsx +++ b/example/src/hooks/useIterableApp.tsx @@ -151,7 +151,7 @@ export const IterableAppProvider: FunctionComponent< config.retryPolicy = { maxRetry: 5, - retryInterval:5, + retryInterval: 5, retryBackoff: IterableRetryBackoff.linear, }; From e75b77592e03e0d9f3fe5a78c046ff16f8fcb8c3 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Thu, 20 Nov 2025 18:15:22 -0800 Subject: [PATCH 80/82] fix: update Iterable class and config to handle null values in auth token promises --- src/core/classes/Iterable.ts | 20 +++++++++++++++----- src/core/classes/IterableConfig.ts | 4 ++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/core/classes/Iterable.ts b/src/core/classes/Iterable.ts index 365aee94f..6b9431fc6 100644 --- a/src/core/classes/Iterable.ts +++ b/src/core/classes/Iterable.ts @@ -912,10 +912,16 @@ export class Iterable { private static removeAllEventListeners() { RNEventEmitter.removeAllListeners(IterableEventName.handleUrlCalled); RNEventEmitter.removeAllListeners(IterableEventName.handleInAppCalled); - RNEventEmitter.removeAllListeners(IterableEventName.handleCustomActionCalled); + RNEventEmitter.removeAllListeners( + IterableEventName.handleCustomActionCalled + ); RNEventEmitter.removeAllListeners(IterableEventName.handleAuthCalled); - RNEventEmitter.removeAllListeners(IterableEventName.handleAuthSuccessCalled); - RNEventEmitter.removeAllListeners(IterableEventName.handleAuthFailureCalled); + RNEventEmitter.removeAllListeners( + IterableEventName.handleAuthSuccessCalled + ); + RNEventEmitter.removeAllListeners( + IterableEventName.handleAuthFailureCalled + ); } /** @@ -1015,11 +1021,15 @@ export class Iterable { } }, 1000); } else if (typeof promiseResult === 'string') { - //If promise only returns string + // If promise only returns string + Iterable.authManager.passAlongAuthToken(promiseResult); + } else if (promiseResult === null || promiseResult === undefined) { + // Even though this will cause authentication to fail, we want to + // allow for this for JWT handling. Iterable.authManager.passAlongAuthToken(promiseResult); } else { IterableLogger?.log( - 'Unexpected promise returned. Auth token expects promise of String or AuthResponse type.' + 'Unexpected promise returned. Auth token expects promise of String, null, undefined, or AuthResponse type.' ); } }) diff --git a/src/core/classes/IterableConfig.ts b/src/core/classes/IterableConfig.ts index b357f37bb..664e08f1f 100644 --- a/src/core/classes/IterableConfig.ts +++ b/src/core/classes/IterableConfig.ts @@ -202,9 +202,9 @@ export class IterableConfig { * ``` * * @returns A promise that resolves to an `IterableAuthResponse`, a `string`, - * or `undefined`. + * `null`, or `undefined`. */ - authHandler?: () => Promise; + authHandler?: () => Promise; /** * A callback function that is called when the SDK encounters an error while From a2245a2ec6e93df689945b5b7987feefd68fa3d0 Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 10 Dec 2025 20:17:48 -0800 Subject: [PATCH 81/82] fix: update expectation in IterableApi test to use strict equality --- src/core/classes/IterableApi.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/classes/IterableApi.test.ts b/src/core/classes/IterableApi.test.ts index ee41c3784..77a229c58 100644 --- a/src/core/classes/IterableApi.test.ts +++ b/src/core/classes/IterableApi.test.ts @@ -730,7 +730,7 @@ describe('IterableApi', () => { const result = await IterableApi.getInboxMessages(); // THEN the messages are returned - expect(result).toBe(mockMessages); + expect(result).toStrictEqual(mockMessages); }); }); From 2e975e29ba8b763c12d5119af269cfe636f7d7be Mon Sep 17 00:00:00 2001 From: Loren Posen Date: Wed, 10 Dec 2025 20:34:21 -0800 Subject: [PATCH 82/82] chore: release version 2.2.0 with JWT flow --- CHANGELOG.md | 28 ++++++++-------------------- package.json | 2 +- src/itblBuildInfo.ts | 2 +- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476661592..74bcf9f30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,4 @@ -## 2.2.0-alpha.2 - -### Updates -* Changed `onJWTError` to `onJwtError` -* Changed `IterableRetryBackoff` enum keys to be lowercase for consistency - across application - -### Fixes -* Fixed Android `retryInterval` not being updated on re-initialization. - -## 2.2.0-alpha.1 - -### Updates -* [SDK-149] Added logout functionality - -### Fixes -* [SDK-151] Fixed "cannot read property authtoken of undefined" error - -## 2.2.0-alpha.0 +## 2.2.0 ### Updates - Updated Android SDK version to [3.6.2](https://github.com/Iterable/iterable-android-sdk/releases/tag/3.6.2) @@ -27,13 +9,19 @@ - Added `onJwtError` and `retryPolicy` for control over JWT flow - Moved all native calls to `IterableApi.ts` - Added JWT example to our example app +- Changed `onJWTError` to `onJwtError` +- Changed `IterableRetryBackoff` enum keys to be lowercase for consistency + across application +- [SDK-149] Added logout functionality ### Fixes - Created a standalone `IterableLogger` to avoid circular dependencies +- [SDK-151] Fixed "cannot read property authtoken of undefined" error +- Fixed Android `retryInterval` not being updated on re-initialization. ## 2.1.0 ### Updates -* SDK is now compatible with both New Architecture and Legacy Architecture. Fix +- SDK is now compatible with both New Architecture and Legacy Architecture. Fix for #691, #602, #563. ### Fixes diff --git a/package.json b/package.json index b59a90df9..f6ca88c1f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@iterable/react-native-sdk", - "version": "2.2.0-alpha.1", + "version": "2.2.0", "description": "Iterable SDK for React Native.", "source": "./src/index.tsx", "main": "./lib/module/index.js", diff --git a/src/itblBuildInfo.ts b/src/itblBuildInfo.ts index d9b0c0ab1..fd6c8af1b 100644 --- a/src/itblBuildInfo.ts +++ b/src/itblBuildInfo.ts @@ -3,5 +3,5 @@ * It contains the version of the package */ export const buildInfo = { - version: '2.2.0-alpha.1', + version: '2.2.0', };