-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(inApp): functional dispatchEvent & setConflictHandler APIs (#12231)
* chore: truncate comments and typo --------- Co-authored-by: Aaron S <[email protected]> Co-authored-by: Jim Blanchard <[email protected]>
- Loading branch information
1 parent
db28490
commit f5bcc3a
Showing
19 changed files
with
874 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
...ages/notifications/__tests__/inAppMessaging/providers/pinpoint/apis/dispatchEvent.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { defaultStorage } from '@aws-amplify/core'; | ||
import { dispatchEvent } from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; | ||
import { processInAppMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; | ||
import { | ||
inAppMessages, | ||
simpleInAppMessages, | ||
simpleInAppMessagingEvent, | ||
} from '../../../../../__mocks__/data'; | ||
import { InAppMessagingError } from '../../../../../src/inAppMessaging/errors'; | ||
import { notifyEventListeners } from '../../../../../src/common/eventListeners'; | ||
|
||
jest.mock('@aws-amplify/core'); | ||
jest.mock('@aws-amplify/core/internals/utils'); | ||
jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); | ||
jest.mock('../../../../../src/common/eventListeners'); | ||
|
||
const mockDefaultStorage = defaultStorage as jest.Mocked<typeof defaultStorage>; | ||
const mockNotifyEventListeners = notifyEventListeners as jest.Mock; | ||
const mockProcessInAppMessages = processInAppMessages as jest.Mock; | ||
|
||
describe('dispatchEvent', () => { | ||
beforeEach(() => { | ||
mockDefaultStorage.setItem.mockClear(); | ||
mockNotifyEventListeners.mockClear(); | ||
}); | ||
test('gets in-app messages from store and notifies listeners', async () => { | ||
const [message] = inAppMessages; | ||
mockDefaultStorage.getItem.mockResolvedValueOnce( | ||
JSON.stringify(simpleInAppMessages) | ||
); | ||
mockProcessInAppMessages.mockReturnValueOnce([message]); | ||
await dispatchEvent(simpleInAppMessagingEvent); | ||
expect(mockProcessInAppMessages).toBeCalledWith( | ||
simpleInAppMessages, | ||
simpleInAppMessagingEvent | ||
); | ||
expect(mockNotifyEventListeners).toBeCalledWith('messageReceived', message); | ||
}); | ||
|
||
test('handles conflicts through default conflict handler', async () => { | ||
mockDefaultStorage.getItem.mockResolvedValueOnce( | ||
JSON.stringify(simpleInAppMessages) | ||
); | ||
mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); | ||
await dispatchEvent(simpleInAppMessagingEvent); | ||
expect(mockProcessInAppMessages).toBeCalledWith( | ||
simpleInAppMessages, | ||
simpleInAppMessagingEvent | ||
); | ||
expect(mockNotifyEventListeners).toBeCalledWith( | ||
'messageReceived', | ||
inAppMessages[4] | ||
); | ||
}); | ||
|
||
test('does not notify listeners if no messages are returned', async () => { | ||
mockProcessInAppMessages.mockReturnValueOnce([]); | ||
mockDefaultStorage.getItem.mockResolvedValueOnce( | ||
JSON.stringify(simpleInAppMessages) | ||
); | ||
|
||
await dispatchEvent(simpleInAppMessagingEvent); | ||
|
||
expect(mockNotifyEventListeners).not.toBeCalled(); | ||
}); | ||
|
||
test('logs error if storage retrieval fails', async () => { | ||
mockDefaultStorage.getItem.mockRejectedValueOnce(Error); | ||
await expect( | ||
dispatchEvent(simpleInAppMessagingEvent) | ||
).rejects.toStrictEqual(expect.any(InAppMessagingError)); | ||
}); | ||
}); |
54 changes: 54 additions & 0 deletions
54
...notifications/__tests__/inAppMessaging/providers/pinpoint/apis/setConflictHandler.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { defaultStorage } from '@aws-amplify/core'; | ||
import { | ||
dispatchEvent, | ||
setConflictHandler, | ||
} from '../../../../../src/inAppMessaging/providers/pinpoint/apis'; | ||
import { processInAppMessages } from '../../../../../src/inAppMessaging/providers/pinpoint/utils'; | ||
import { | ||
closestExpiryMessage, | ||
customHandledMessage, | ||
inAppMessages, | ||
simpleInAppMessagingEvent, | ||
} from '../../../../../__mocks__/data'; | ||
import { notifyEventListeners } from '../../../../../src/common/eventListeners'; | ||
|
||
jest.mock('@aws-amplify/core'); | ||
jest.mock('@aws-amplify/core/internals/utils'); | ||
jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); | ||
jest.mock('../../../../../src/common/eventListeners'); | ||
|
||
const mockDefaultStorage = defaultStorage as jest.Mocked<typeof defaultStorage>; | ||
const mockNotifyEventListeners = notifyEventListeners as jest.Mock; | ||
const mockProcessInAppMessages = processInAppMessages as jest.Mock; | ||
|
||
describe('Conflict handling', () => { | ||
beforeEach(() => { | ||
mockDefaultStorage.setItem.mockClear(); | ||
mockNotifyEventListeners.mockClear(); | ||
}); | ||
test('has a default implementation', async () => { | ||
mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); | ||
await dispatchEvent(simpleInAppMessagingEvent); | ||
expect(mockNotifyEventListeners).toBeCalledWith( | ||
'messageReceived', | ||
closestExpiryMessage | ||
); | ||
}); | ||
|
||
test('can be customized through setConflictHandler', async () => { | ||
const customConflictHandler = messages => | ||
messages.find(message => message.id === 'custom-handled'); | ||
mockProcessInAppMessages.mockReturnValueOnce(inAppMessages); | ||
|
||
setConflictHandler(customConflictHandler); | ||
await dispatchEvent(simpleInAppMessagingEvent); | ||
|
||
expect(mockNotifyEventListeners).toBeCalledWith( | ||
'messageReceived', | ||
customHandledMessage | ||
); | ||
}); | ||
}); |
59 changes: 59 additions & 0 deletions
59
packages/notifications/__tests__/inAppMessaging/utils/processInAppMessages.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { | ||
pinpointInAppMessage, | ||
simpleInAppMessagingEvent, | ||
} from '../../../__mocks__/data'; | ||
import { processInAppMessages } from '../../../src/inAppMessaging/providers/pinpoint/utils/processInAppMessages'; | ||
import { cloneDeep } from 'lodash'; | ||
import { | ||
isBeforeEndDate, | ||
matchesAttributes, | ||
matchesEventType, | ||
matchesMetrics, | ||
} from '../../../src/inAppMessaging/providers/pinpoint/utils/helpers'; | ||
|
||
jest.mock('@aws-amplify/core'); | ||
jest.mock('@aws-amplify/core/internals/utils'); | ||
jest.mock('../../../src/inAppMessaging/providers/pinpoint/utils/helpers'); | ||
|
||
const mockIsBeforeEndDate = isBeforeEndDate as jest.Mock; | ||
const mockMatchesAttributes = matchesAttributes as jest.Mock; | ||
const mockMatchesEventType = matchesEventType as jest.Mock; | ||
const mockMatchesMetrics = matchesMetrics as jest.Mock; | ||
|
||
// TODO(V6): Add tests for session cap etc | ||
describe('processInAppMessages', () => { | ||
const messages = [ | ||
cloneDeep(pinpointInAppMessage), | ||
{ ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-2', Priority: 3 }, | ||
{ ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-3', Priority: 1 }, | ||
{ ...cloneDeep(pinpointInAppMessage), CampaignId: 'uuid-4', Priority: 2 }, | ||
]; | ||
beforeEach(() => { | ||
mockMatchesEventType.mockReturnValue(true); | ||
mockMatchesAttributes.mockReturnValue(true); | ||
mockMatchesMetrics.mockReturnValue(true); | ||
mockIsBeforeEndDate.mockReturnValue(true); | ||
}); | ||
|
||
test('filters in-app messages from Pinpoint by criteria', async () => { | ||
mockMatchesEventType.mockReturnValueOnce(false); | ||
mockMatchesAttributes.mockReturnValueOnce(false); | ||
mockMatchesMetrics.mockReturnValueOnce(false); | ||
const [result] = await processInAppMessages( | ||
messages, | ||
simpleInAppMessagingEvent | ||
); | ||
expect(result.id).toBe('uuid-4'); | ||
}); | ||
|
||
test('filters in-app messages from Pinpoint by criteria', async () => { | ||
const [result] = await processInAppMessages( | ||
messages, | ||
simpleInAppMessagingEvent | ||
); | ||
expect(result.id).toBe('uuid-3'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export { identifyUser, syncMessages } from './providers/pinpoint'; | ||
export { | ||
identifyUser, | ||
syncMessages, | ||
dispatchEvent, | ||
setConflictHandler, | ||
} from './providers/pinpoint'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,9 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
export { identifyUser, syncMessages } from './pinpoint/apis'; | ||
export { | ||
identifyUser, | ||
syncMessages, | ||
dispatchEvent, | ||
setConflictHandler, | ||
} from './pinpoint/apis'; |
59 changes: 59 additions & 0 deletions
59
packages/notifications/src/inAppMessaging/providers/pinpoint/apis/dispatchEvent.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { | ||
PINPOINT_KEY_PREFIX, | ||
STORAGE_KEY_SUFFIX, | ||
processInAppMessages, | ||
} from '../utils'; | ||
import { InAppMessage } from '../../../types'; | ||
import flatten from 'lodash/flatten'; | ||
import { defaultStorage } from '@aws-amplify/core'; | ||
import { notifyEventListeners } from '../../../../common'; | ||
import { assertServiceError } from '../../../errors'; | ||
import { DispatchEventInput } from '../types'; | ||
import { syncMessages } from './syncMessages'; | ||
import { conflictHandler, setConflictHandler } from './setConflictHandler'; | ||
|
||
/** | ||
* Triggers an In-App message to be displayed. Use this after your campaigns have been synced to the device using | ||
* {@link syncMessages}. Based on the messages synced and the event passed to this API, it triggers the display | ||
* of the In-App message that meets the criteria. | ||
* To change the conflict handler, use the {@link setConflictHandler} API. | ||
* | ||
* @param DispatchEventInput The input object that holds the event to be dispatched. | ||
* | ||
* @throws service exceptions - Thrown when the underlying Pinpoint service returns an error. | ||
* | ||
* @returns A promise that will resolve when the operation is complete. | ||
* | ||
* @example | ||
* ```ts | ||
* // Sync message before disptaching an event | ||
* await syncMessages(); | ||
* | ||
* // Dispatch an event | ||
* await dispatchEvent({ name: "test_event" }); | ||
* ``` | ||
*/ | ||
export async function dispatchEvent(input: DispatchEventInput): Promise<void> { | ||
try { | ||
const key = `${PINPOINT_KEY_PREFIX}${STORAGE_KEY_SUFFIX}`; | ||
const cachedMessages = await defaultStorage.getItem(key); | ||
const messages: InAppMessage[] = await processInAppMessages( | ||
cachedMessages ? JSON.parse(cachedMessages) : [], | ||
input | ||
); | ||
const flattenedMessages = flatten(messages); | ||
|
||
if (flattenedMessages.length > 0) { | ||
notifyEventListeners( | ||
'messageReceived', | ||
conflictHandler(flattenedMessages) | ||
); | ||
} | ||
} catch (error) { | ||
assertServiceError(error); | ||
throw error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
packages/notifications/src/inAppMessaging/providers/pinpoint/apis/setConflictHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
import { InAppMessage } from '../../../types'; | ||
import { InAppMessageConflictHandler, SetConflictHandlerInput } from '../types'; | ||
|
||
export let conflictHandler: InAppMessageConflictHandler = | ||
defaultConflictHandler; | ||
|
||
/** | ||
* Set a conflict handler that will be used to resolve conflicts that may emerge | ||
* when matching events with synced messages. | ||
* @remark | ||
* The conflict handler is not persisted between sessions | ||
* and needs to be called before dispatching an event to have any effect. | ||
* | ||
* @param SetConflictHandlerInput: The input object that holds the conflict handler to be used. | ||
* | ||
* | ||
* @example | ||
* ```ts | ||
* // Sync messages before dispatching an event | ||
* await syncMessages(); | ||
* | ||
* // Example custom conflict handler | ||
* const myConflictHandler = (messages) => { | ||
* // Return a random message | ||
* const randomIndex = Math.floor(Math.random() * messages.length); | ||
* return messages[randomIndex]; | ||
* }; | ||
* | ||
* // Set the conflict handler | ||
* setConflictHandler(myConflictHandler); | ||
* | ||
* // Dispatch an event | ||
* await dispatchEvent({ name: "test_event" }); | ||
* ``` | ||
*/ | ||
export function setConflictHandler(input: SetConflictHandlerInput): void { | ||
conflictHandler = input; | ||
} | ||
|
||
function defaultConflictHandler(messages: InAppMessage[]): InAppMessage { | ||
// default behavior is to return the message closest to expiry | ||
// this function assumes that messages processed by providers already filters out expired messages | ||
const sorted = messages.sort((a, b) => { | ||
const endDateA = a.metadata?.endDate; | ||
const endDateB = b.metadata?.endDate; | ||
// if both message end dates are falsy or have the same date string, treat them as equal | ||
if (endDateA === endDateB) { | ||
return 0; | ||
} | ||
// if only message A has an end date, treat it as closer to expiry | ||
if (endDateA && !endDateB) { | ||
return -1; | ||
} | ||
// if only message B has an end date, treat it as closer to expiry | ||
if (!endDateA && endDateB) { | ||
return 1; | ||
} | ||
// otherwise, compare them | ||
return new Date(endDateA) < new Date(endDateB) ? -1 : 1; | ||
}); | ||
// always return the top sorted | ||
return sorted[0]; | ||
} |
Oops, something went wrong.