Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ios/RNIterableAPI/RNIterableAPI.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ @protocol IterableInAppDelegate;
@protocol IterableCustomActionDelegate;
@protocol IterableAuthDelegate;
@protocol IterableURLDelegate;
@protocol IterableEmbeddedUpdateDelegate;
typedef NS_ENUM(NSInteger, InAppShowResponse) {
show = 0,
skip = 1,
Expand Down
32 changes: 32 additions & 0 deletions ios/RNIterableAPI/ReactIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import React
case receivedIterableInboxChanged
case handleAuthSuccessCalled
case handleAuthFailureCalled
case handleEmbeddedMessageUpdateCalled
case handleEmbeddedMessagingDisabledCalled
}

@objc public static var supportedEvents: [String] {
Expand Down Expand Up @@ -650,6 +652,14 @@ import React
}

IterableAPI.setDeviceAttribute(name: "reactNativeSDKVersion", value: version)

// Add embedded update listener if any callback is present
let onEmbeddedMessageUpdatePresent = configDict["onEmbeddedMessageUpdatePresent"] as? Bool ?? false
let onEmbeddedMessagingDisabledPresent = configDict["onEmbeddedMessagingDisabledPresent"] as? Bool ?? false

if onEmbeddedMessageUpdatePresent || onEmbeddedMessagingDisabledPresent {
IterableAPI.embeddedManager.addUpdateListener(self)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function with high complexity (count = 6): initialize [qlty:function-complexity]

}

Expand Down Expand Up @@ -807,3 +817,25 @@ extension ReactIterableAPI: IterableAuthDelegate {
public func onTokenRegistrationFailed(_ reason: String?) {
}
}

extension ReactIterableAPI: IterableEmbeddedUpdateDelegate {
public func onMessagesUpdated() {
ITBInfo()
guard shouldEmit else {
return
}
delegate?.sendEvent(
withName: EventName.handleEmbeddedMessageUpdateCalled.rawValue,
body: nil as Any?)
}

public func onEmbeddedMessagingDisabled() {
ITBInfo()
guard shouldEmit else {
return
}
delegate?.sendEvent(
withName: EventName.handleEmbeddedMessagingDisabledCalled.rawValue,
body: nil as Any?)
}
}
226 changes: 226 additions & 0 deletions src/core/classes/Iterable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ describe('Iterable', () => {
nativeEmitter.removeAllListeners(IterableEventName.handleAuthCalled);
nativeEmitter.removeAllListeners(IterableEventName.handleAuthSuccessCalled);
nativeEmitter.removeAllListeners(IterableEventName.handleAuthFailureCalled);
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);

// Clear any pending timers
jest.clearAllTimers();
Expand Down Expand Up @@ -1229,4 +1235,224 @@ describe('Iterable', () => {
expect(Iterable.embeddedManager.isEnabled).toBe(true);
});
});

describe('embedded messaging callbacks', () => {
describe('onEmbeddedMessageUpdate', () => {
it('should call onEmbeddedMessageUpdate when handleEmbeddedMessageUpdateCalled event is emitted', () => {
// sets up event emitter
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
// sets up config file and onEmbeddedMessageUpdate callback
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
config.onEmbeddedMessageUpdate = jest.fn();
// initialize Iterable object
Iterable.initialize('apiKey', config);
// WHEN handleEmbeddedMessageUpdateCalled event is emitted
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
// THEN onEmbeddedMessageUpdate callback is called
expect(config.onEmbeddedMessageUpdate).toHaveBeenCalled();
expect(config.onEmbeddedMessageUpdate).toHaveBeenCalledTimes(1);
});

it('should not set up listener if onEmbeddedMessageUpdate is not provided', () => {
// sets up event emitter
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
// sets up config without onEmbeddedMessageUpdate callback
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
// initialize Iterable object
Iterable.initialize('apiKey', config);
// WHEN handleEmbeddedMessageUpdateCalled event is emitted
// THEN no error should occur (no listener was set up)
expect(() => {
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
}).not.toThrow();
});

it('should call onEmbeddedMessageUpdate multiple times when event is emitted multiple times', () => {
// sets up event emitter
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
// sets up config with callback
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
config.onEmbeddedMessageUpdate = jest.fn();
// initialize Iterable object
Iterable.initialize('apiKey', config);
// WHEN handleEmbeddedMessageUpdateCalled event is emitted multiple times
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
// THEN onEmbeddedMessageUpdate callback is called three times
expect(config.onEmbeddedMessageUpdate).toHaveBeenCalledTimes(3);
});

it('should include onEmbeddedMessageUpdatePresent flag in config dict when callback is provided', () => {
// GIVEN a config with onEmbeddedMessageUpdate callback
const config = new IterableConfig();
config.onEmbeddedMessageUpdate = jest.fn();
// WHEN toDict is called
const configDict = config.toDict();
// THEN onEmbeddedMessageUpdatePresent is true
expect(configDict.onEmbeddedMessageUpdatePresent).toBe(true);
});

it('should set onEmbeddedMessageUpdatePresent flag to false when callback is not provided', () => {
// GIVEN a config without onEmbeddedMessageUpdate callback
const config = new IterableConfig();
// WHEN toDict is called
const configDict = config.toDict();
// THEN onEmbeddedMessageUpdatePresent is false
expect(configDict.onEmbeddedMessageUpdatePresent).toBe(false);
});
});

describe('onEmbeddedMessagingDisabled', () => {
it('should call onEmbeddedMessagingDisabled when handleEmbeddedMessagingDisabledCalled event is emitted', () => {
// sets up event emitter
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
// sets up config file and onEmbeddedMessagingDisabled callback
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
config.onEmbeddedMessagingDisabled = jest.fn();
// initialize Iterable object
Iterable.initialize('apiKey', config);
// WHEN handleEmbeddedMessagingDisabledCalled event is emitted
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
// THEN onEmbeddedMessagingDisabled callback is called
expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalled();
expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalledTimes(1);
});

it('should not set up listener if onEmbeddedMessagingDisabled is not provided', () => {
// sets up event emitter
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
// sets up config without onEmbeddedMessagingDisabled callback
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
// initialize Iterable object
Iterable.initialize('apiKey', config);
// WHEN handleEmbeddedMessagingDisabledCalled event is emitted
// THEN no error should occur (no listener was set up)
expect(() => {
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
}).not.toThrow();
});

it('should call onEmbeddedMessagingDisabled when embedded messaging becomes unavailable', () => {
// sets up event emitter
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
// sets up config with callback
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
config.onEmbeddedMessagingDisabled = jest.fn();
// initialize Iterable object
Iterable.initialize('apiKey', config);
// WHEN handleEmbeddedMessagingDisabledCalled event is emitted
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
// THEN onEmbeddedMessagingDisabled callback is called
expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalled();
});

it('should include onEmbeddedMessagingDisabledPresent flag in config dict when callback is provided', () => {
// GIVEN a config with onEmbeddedMessagingDisabled callback
const config = new IterableConfig();
config.onEmbeddedMessagingDisabled = jest.fn();
// WHEN toDict is called
const configDict = config.toDict();
// THEN onEmbeddedMessagingDisabledPresent is true
expect(configDict.onEmbeddedMessagingDisabledPresent).toBe(true);
});

it('should set onEmbeddedMessagingDisabledPresent flag to false when callback is not provided', () => {
// GIVEN a config without onEmbeddedMessagingDisabled callback
const config = new IterableConfig();
// WHEN toDict is called
const configDict = config.toDict();
// THEN onEmbeddedMessagingDisabledPresent is false
expect(configDict.onEmbeddedMessagingDisabledPresent).toBe(false);
});
});

describe('both embedded callbacks', () => {
it('should call both callbacks independently when both are provided', () => {
// sets up event emitter
const nativeEmitter = new NativeEventEmitter();
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
nativeEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
// sets up config with both callbacks
const config = new IterableConfig();
config.logReactNativeSdkCalls = false;
config.onEmbeddedMessageUpdate = jest.fn();
config.onEmbeddedMessagingDisabled = jest.fn();
// initialize Iterable object
Iterable.initialize('apiKey', config);
// WHEN handleEmbeddedMessageUpdateCalled event is emitted
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
// THEN only onEmbeddedMessageUpdate is called
expect(config.onEmbeddedMessageUpdate).toHaveBeenCalled();
expect(config.onEmbeddedMessagingDisabled).not.toHaveBeenCalled();
// Reset mocks
jest.clearAllMocks();
// WHEN handleEmbeddedMessagingDisabledCalled event is emitted
nativeEmitter.emit(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
// THEN only onEmbeddedMessagingDisabled is called
expect(config.onEmbeddedMessagingDisabled).toHaveBeenCalled();
expect(config.onEmbeddedMessageUpdate).not.toHaveBeenCalled();
});

it('should set both presence flags in config dict when both callbacks are provided', () => {
// GIVEN a config with both callbacks
const config = new IterableConfig();
config.onEmbeddedMessageUpdate = jest.fn();
config.onEmbeddedMessagingDisabled = jest.fn();
// WHEN toDict is called
const configDict = config.toDict();
// THEN both presence flags are true
expect(configDict.onEmbeddedMessageUpdatePresent).toBe(true);
expect(configDict.onEmbeddedMessagingDisabledPresent).toBe(true);
});
});
});
});
24 changes: 24 additions & 0 deletions src/core/classes/Iterable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -950,6 +950,12 @@ export class Iterable {
RNEventEmitter.removeAllListeners(
IterableEventName.handleAuthFailureCalled
);
RNEventEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessageUpdateCalled
);
RNEventEmitter.removeAllListeners(
IterableEventName.handleEmbeddedMessagingDisabledCalled
);
}

/**
Expand Down Expand Up @@ -1083,6 +1089,24 @@ export class Iterable {
}
);
}

if (Iterable.savedConfig.onEmbeddedMessageUpdate) {
RNEventEmitter.addListener(
IterableEventName.handleEmbeddedMessageUpdateCalled,
() => {
Iterable.savedConfig.onEmbeddedMessageUpdate?.();
}
);
}

if (Iterable.savedConfig.onEmbeddedMessagingDisabled) {
RNEventEmitter.addListener(
IterableEventName.handleEmbeddedMessagingDisabledCalled,
() => {
Iterable.savedConfig.onEmbeddedMessagingDisabled?.();
}
);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if Iterable.savedConfig.embeddedMessagingEnabled ?

Then add both listners.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

}

/**
Expand Down
51 changes: 51 additions & 0 deletions src/core/classes/IterableConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,42 @@ export class IterableConfig {
*/
enableEmbeddedMessaging = false;

/**
* A callback function that is called when embedded messages are updated.
*
* This callback is triggered when the local cache of embedded messages changes,
* such as when new messages arrive or existing messages are removed.
*
* @example
* ```typescript
* const config = new IterableConfig();
* config.onEmbeddedMessageUpdate = () => {
* console.log('Embedded messages updated!');
* // Refresh your UI to display the latest messages
* };
* Iterable.initialize('<YOUR_API_KEY>', config);
* ```
*/
onEmbeddedMessageUpdate?: () => void;

/**
* A callback function that is called when embedded messaging is disabled.
*
* This callback is triggered when embedded messaging becomes unavailable,
* which can happen due to configuration issues or API errors.
*
* @example
* ```typescript
* const config = new IterableConfig();
* config.onEmbeddedMessagingDisabled = () => {
* console.warn('Embedded messaging has been disabled');
* // Hide embedded message UI or show error state
* };
* Iterable.initialize('<YOUR_API_KEY>', config);
* ```
*/
onEmbeddedMessagingDisabled?: () => void;

/**
* Converts the IterableConfig instance to a dictionary object.
*
Expand Down Expand Up @@ -377,6 +413,21 @@ export class IterableConfig {
*/
// eslint-disable-next-line eqeqeq
authHandlerPresent: this.authHandler != undefined,
/**
* A boolean indicating if an embedded message update callback is present.
*
* TODO: Figure out if this is purposeful
*/
// eslint-disable-next-line eqeqeq
onEmbeddedMessageUpdatePresent: this.onEmbeddedMessageUpdate != undefined,
/**
* A boolean indicating if an embedded messaging disabled callback is present.
*
* TODO: Figure out if this is purposeful
*/
// eslint-disable-next-line eqeqeq

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'eqeqeq' rule is disabled but never reported. [eslint:eslint-comments/no-unused-disable]

Suggested change
// eslint-disable-next-line eqeqeq

onEmbeddedMessagingDisabledPresent:
this.onEmbeddedMessagingDisabled != undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expected '!==' and instead saw '!='. [eslint:eqeqeq]

Comment on lines +416 to +430

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These feel unnecessary.

/** The log level for the SDK. */
logLevel: this.logLevel,
expiringAuthTokenRefreshPeriod: this.expiringAuthTokenRefreshPeriod,
Expand Down
Loading