From cb914374263262c84c6337dddca1f17fb7dd204a Mon Sep 17 00:00:00 2001 From: Alex Lizarraga <31565793+buddyeorl@users.noreply.github.com> Date: Thu, 18 Apr 2024 12:23:45 -0700 Subject: [PATCH] fix(message overrides): extract message content with Platform-Specific Overrides (#12917) * extract message content with Platform-Specific Overrides * added default platform when getClientInfo returns an undefined platform * mocking getClientInfo shouldn't override mock utils * moved logic to extractContent fn, updated tests * exposed internal type InAppMessageButton * added empty line * return immediately if os is falsy * fixed lint warning * pinpoint internal type exports, mergeOverride test util, simplified data object * removed unnecessary spread operator * refactored merging fn * prettier * flatten test structure * minor update * minor update * updated merge override fns signature and associated tests * extracted getButtonConfig fn and refactored to add data existence checks. * removed unnecessary optional chaining operators * fixed lint issues --------- Co-authored-by: ManojNB --- .../core/src/awsClients/pinpoint/index.ts | 8 +- packages/core/src/libraryUtils.ts | 2 +- .../pinpoint/utils/helpers.native.test.ts | 73 +++++++++++++++ .../providers/pinpoint/utils/helpers.test.ts | 60 ++++++++++++- .../notifications/__tests__/testUtils/data.ts | 90 ++++++++++++++++++- .../mergeInAppMessageWithOverrides.ts | 59 ++++++++++++ .../providers/pinpoint/utils/helpers.ts | 57 +++++++++++- .../src/inAppMessaging/types/message.ts | 2 + 8 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.native.test.ts create mode 100644 packages/notifications/__tests__/testUtils/mergeInAppMessageWithOverrides.ts diff --git a/packages/core/src/awsClients/pinpoint/index.ts b/packages/core/src/awsClients/pinpoint/index.ts index 95bfc0951b3..85477b4e6e8 100644 --- a/packages/core/src/awsClients/pinpoint/index.ts +++ b/packages/core/src/awsClients/pinpoint/index.ts @@ -12,4 +12,10 @@ export { UpdateEndpointInput, UpdateEndpointOutput, } from './updateEndpoint'; -export { Event, InAppMessageCampaign, EventsBatch } from './types'; +export { + Event, + InAppMessageCampaign, + EventsBatch, + InAppMessageButton, + OverrideButtonConfiguration, +} from './types'; diff --git a/packages/core/src/libraryUtils.ts b/packages/core/src/libraryUtils.ts index a11eb0cf1c4..623fb12b11e 100644 --- a/packages/core/src/libraryUtils.ts +++ b/packages/core/src/libraryUtils.ts @@ -25,7 +25,7 @@ export { LegacyConfig } from './singleton/types'; export { ADD_OAUTH_LISTENER } from './singleton/constants'; export { amplifyUuid } from './utils/amplifyUuid'; export { AmplifyUrl, AmplifyUrlSearchParams } from './utils/amplifyUrl'; - +export { getClientInfo } from './utils'; // Auth utilities export { decodeJWT, diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.native.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.native.test.ts new file mode 100644 index 00000000000..fcf9449ce53 --- /dev/null +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.native.test.ts @@ -0,0 +1,73 @@ +/** + * @jest-environment node + */ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + extractContent, + mapOSPlatform, +} from '../../../../../src/inAppMessaging/providers/pinpoint/utils/helpers'; + +import { + nonBrowserConfigTestCases, + pinpointInAppMessage, + extractedContent, + nativeButtonOverrides, +} from '../../../../testUtils/data'; +import { mergeExpectedContentWithExpectedOverride, mergeInAppMessageWithOverrides } from '../../../../testUtils/mergeInAppMessageWithOverrides'; + +jest.mock('@aws-amplify/core'); + +jest.mock('@aws-amplify/core/internals/utils', () => { + const originalModule = jest.requireActual( + '@aws-amplify/core/internals/utils', + ); + return { + ...originalModule, + getClientInfo: jest.fn(), // Setup as a Jest mock function without implementation + }; +}); + +describe('InAppMessaging Provider Utils (running natively)', () => { + describe('mapOSPlatform method', () => { + nonBrowserConfigTestCases.forEach(({ os, expectedPlatform }) => { + test(`correctly maps OS "${os}" to ConfigPlatformType "${expectedPlatform}"`, () => { + const result = mapOSPlatform(os); + expect(result).toBe(expectedPlatform); + }); + }); + }); + + describe('extractContent with overrides', () => { + nativeButtonOverrides.forEach( + ({ buttonOverrides, configPlatform, mappedPlatform }) => { + const message = mergeInAppMessageWithOverrides( + pinpointInAppMessage, + mappedPlatform, + buttonOverrides, + ); + const expectedContent = mergeExpectedContentWithExpectedOverride( + extractedContent[0], + buttonOverrides, + ); + + test(`correctly extracts content for ${configPlatform}`, () => { + const utils = require('@aws-amplify/core/internals/utils'); + // Dynamically override the mock for getClientInfo + utils.getClientInfo.mockImplementation(() => ({ + platform: configPlatform, + })); + + const [firstContent] = extractContent(message); + expect(firstContent.primaryButton).toStrictEqual( + expectedContent.primaryButton, + ); + expect(firstContent.secondaryButton).toStrictEqual( + expectedContent.secondaryButton, + ); + }); + }, + ); + }); +}); diff --git a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.test.ts b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.test.ts index 62013e6cf5f..c4ea19ab1f9 100644 --- a/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.test.ts +++ b/packages/notifications/__tests__/inAppMessaging/providers/pinpoint/utils/helpers.test.ts @@ -10,6 +10,7 @@ import { extractMetadata, getStartOfDay, isBeforeEndDate, + mapOSPlatform, matchesAttributes, matchesEventType, matchesMetrics, @@ -19,13 +20,29 @@ import { extractedContent, extractedMetadata, pinpointInAppMessage, + browserConfigTestCases, + browserButtonOverrides, } from '../../../../testUtils/data'; -import { InAppMessagingEvent } from '../../../../../src/inAppMessaging/types'; +import { InAppMessagingEvent } from '../../../../../src/inAppMessaging/types'; +import { + mergeExpectedContentWithExpectedOverride, + mergeInAppMessageWithOverrides, +} from '../../../../testUtils/mergeInAppMessageWithOverrides'; jest.mock('@aws-amplify/core'); jest.mock('@aws-amplify/core/internals/providers/pinpoint'); jest.mock('../../../../../src/inAppMessaging/providers/pinpoint/utils'); +jest.mock('@aws-amplify/core/internals/utils', () => { + const originalModule = jest.requireActual( + '@aws-amplify/core/internals/utils', + ); + return { + ...originalModule, + getClientInfo: jest.fn(), // Setup as a Jest mock function without implementation + }; +}); + const HOUR_IN_MS = 1000 * 60 * 60; describe('InAppMessaging Provider Utils', () => { @@ -271,4 +288,45 @@ describe('InAppMessaging Provider Utils', () => { expect(extractMetadata(message)).toStrictEqual(extractedMetadata); }); + + describe('mapOSPlatform method (running in a browser)', () => { + browserConfigTestCases.forEach(({ os, expectedPlatform }) => { + test(`correctly maps OS "${os}" to ConfigPlatformType "${expectedPlatform}"`, () => { + const result = mapOSPlatform(os); + expect(result).toBe(expectedPlatform); + }); + }); + }); + + describe('extractContent with overrides (running in a browser)', () => { + browserButtonOverrides.forEach( + ({ buttonOverrides, configPlatform, mappedPlatform }) => { + const message = mergeInAppMessageWithOverrides( + pinpointInAppMessage, + mappedPlatform, + buttonOverrides, + ); + const expectedContent = mergeExpectedContentWithExpectedOverride( + extractedContent[0], + buttonOverrides, + ); + + test(`correctly extracts content for ${configPlatform}`, () => { + const utils = require('@aws-amplify/core/internals/utils'); + // Dynamically override the mock for getClientInfo + utils.getClientInfo.mockImplementation(() => ({ + platform: configPlatform, + })); + + const [firstContent] = extractContent(message); + expect(firstContent.primaryButton).toStrictEqual( + expectedContent.primaryButton, + ); + expect(firstContent.secondaryButton).toStrictEqual( + expectedContent.secondaryButton, + ); + }); + }, + ); + }); }); diff --git a/packages/notifications/__tests__/testUtils/data.ts b/packages/notifications/__tests__/testUtils/data.ts index c9986055675..6a3fddf37a5 100644 --- a/packages/notifications/__tests__/testUtils/data.ts +++ b/packages/notifications/__tests__/testUtils/data.ts @@ -2,12 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 import { PinpointAnalyticsEvent } from '@aws-amplify/core/internals/providers/pinpoint'; -import type { InAppMessageCampaign as PinpointInAppMessage } from '@aws-amplify/core/internals/aws-clients/pinpoint'; +import { + type InAppMessageCampaign as PinpointInAppMessage, + OverrideButtonConfiguration, +} from '@aws-amplify/core/internals/aws-clients/pinpoint'; import { InAppMessage, + InAppMessageContent, InAppMessagingEvent, } from '../../src/inAppMessaging/types'; import { PushNotificationMessage } from '../../src/pushNotifications'; +import { ButtonConfigPlatform } from '../../src/inAppMessaging/types/message'; export const credentials = { credentials: { @@ -183,7 +188,7 @@ export const pinpointInAppMessage: PinpointInAppMessage = { TreatmentId: 'T1', }; -export const extractedContent = [ +export const extractedContent: InAppMessageContent[] = [ { body: { content: 'Body content', @@ -210,6 +215,67 @@ export const extractedContent = [ }, ]; +export const nativeButtonOverrides: { + configPlatform: 'ios' | 'android'; + mappedPlatform: ButtonConfigPlatform; + buttonOverrides: { + primaryButton: OverrideButtonConfiguration; + secondaryButton: OverrideButtonConfiguration; + }; +}[] = [ + { + configPlatform: 'android', + mappedPlatform: 'Android', + buttonOverrides: { + primaryButton: { + ButtonAction: 'DEEP_LINK', + Link: 'android-app://primaryButtonLink', + }, + secondaryButton: { + ButtonAction: 'LINK', + Link: 'android-app://secondaryButtonLink', + }, + }, + }, + { + configPlatform: 'ios', + mappedPlatform: 'IOS', + buttonOverrides: { + primaryButton: { + ButtonAction: 'DEEP_LINK', + Link: 'ios-app://primaryButtonLink', + }, + secondaryButton: { + ButtonAction: 'LINK', + Link: 'ios-app://secondaryButtonLink', + }, + }, + }, +]; +export const browserButtonOverrides: { + configPlatform: 'web'; + mappedPlatform: ButtonConfigPlatform; + buttonOverrides: { + primaryButton: OverrideButtonConfiguration; + secondaryButton: OverrideButtonConfiguration; + }; +}[] = [ + { + configPlatform: 'web', + mappedPlatform: 'Web', + buttonOverrides: { + primaryButton: { + ButtonAction: 'LINK', + Link: 'https://webPrimaryButtonLink.com', + }, + secondaryButton: { + ButtonAction: 'LINK', + Link: 'https://webSecondaryButtonLink.com', + }, + }, + }, +]; + export const extractedMetadata = { customData: { foo: 'bar' }, endDate: '2021-01-01T00:00:00Z', @@ -295,3 +361,23 @@ export const completionHandlerId = 'completion-handler-id'; export const userAgentValue = 'user-agent-value'; export const channelType = 'APNS_SANDBOX'; + +export const browserConfigTestCases = [ + { os: 'android', expectedPlatform: 'Web' }, + { os: 'ios', expectedPlatform: 'Web' }, + { os: 'windows', expectedPlatform: 'Web' }, + { os: 'macos', expectedPlatform: 'Web' }, + { os: 'linux', expectedPlatform: 'Web' }, + { os: 'unix', expectedPlatform: 'Web' }, + { os: 'unknown', expectedPlatform: 'Web' }, +]; + +export const nonBrowserConfigTestCases = [ + { os: 'android', expectedPlatform: 'Android' }, + { os: 'ios', expectedPlatform: 'IOS' }, + { os: 'windows', expectedPlatform: 'DefaultConfig' }, + { os: 'macos', expectedPlatform: 'DefaultConfig' }, + { os: 'linux', expectedPlatform: 'DefaultConfig' }, + { os: 'unix', expectedPlatform: 'DefaultConfig' }, + { os: 'unknown', expectedPlatform: 'DefaultConfig' }, +]; diff --git a/packages/notifications/__tests__/testUtils/mergeInAppMessageWithOverrides.ts b/packages/notifications/__tests__/testUtils/mergeInAppMessageWithOverrides.ts new file mode 100644 index 00000000000..09f90f03b8d --- /dev/null +++ b/packages/notifications/__tests__/testUtils/mergeInAppMessageWithOverrides.ts @@ -0,0 +1,59 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { cloneDeep } from 'lodash'; +import { + InAppMessageCampaign, + OverrideButtonConfiguration, +} from '@aws-amplify/core/internals/aws-clients/pinpoint'; +import { + ButtonConfigPlatform, + InAppMessageButton, + InAppMessageContent, +} from '../../src/inAppMessaging/types/message'; + +export const mergeInAppMessageWithOverrides = ( + pinpointInAppMessage: InAppMessageCampaign, + mappedPlatform: ButtonConfigPlatform, + buttonOverrides?: { + primaryButton: OverrideButtonConfiguration; + secondaryButton: OverrideButtonConfiguration; + }, +): InAppMessageCampaign => { + const message = cloneDeep(pinpointInAppMessage); + if (message?.InAppMessage?.Content) { + message.InAppMessage.Content[0] = { + ...message.InAppMessage.Content[0], + PrimaryBtn: { + ...message.InAppMessage.Content[0].PrimaryBtn, + [mappedPlatform]: buttonOverrides?.primaryButton, + }, + SecondaryBtn: { + ...message.InAppMessage.Content[0].SecondaryBtn, + [mappedPlatform]: buttonOverrides?.secondaryButton, + }, + }; + } + return message; +}; + +export const mergeExpectedContentWithExpectedOverride = ( + inAppMessage: InAppMessageContent, + expectedButtonConfig: { + primaryButton: OverrideButtonConfiguration; + secondaryButton: OverrideButtonConfiguration; + }, +): InAppMessageContent => { + let expectedContent = cloneDeep(inAppMessage); + expectedContent.primaryButton = { + ...expectedContent.primaryButton, + action: expectedButtonConfig.primaryButton.ButtonAction, + url: expectedButtonConfig.primaryButton.Link, + } as InAppMessageButton; + expectedContent.secondaryButton = { + ...expectedContent.secondaryButton, + action: expectedButtonConfig.secondaryButton.ButtonAction, + url: expectedButtonConfig.secondaryButton.Link, + } as InAppMessageButton; + return expectedContent; +}; \ No newline at end of file diff --git a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts index dcd5c0adf23..2d1cda76680 100644 --- a/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts +++ b/packages/notifications/src/inAppMessaging/providers/pinpoint/utils/helpers.ts @@ -2,8 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 import { ConsoleLogger } from '@aws-amplify/core'; -import { InAppMessagingAction } from '@aws-amplify/core/internals/utils'; -import type { InAppMessageCampaign as PinpointInAppMessage } from '@aws-amplify/core/internals/aws-clients/pinpoint'; +import { + InAppMessagingAction, + getClientInfo, +} from '@aws-amplify/core/internals/utils'; +import type { + InAppMessageButton, + InAppMessageCampaign as PinpointInAppMessage, +} from '@aws-amplify/core/internals/aws-clients/pinpoint'; import isEmpty from 'lodash/isEmpty.js'; import { record as recordCore } from '@aws-amplify/core/internals/providers/pinpoint'; @@ -16,6 +22,7 @@ import { InAppMessagingEvent, } from '../../../types'; import { MetricsComparator, PinpointMessageEvent } from '../types'; +import { ButtonConfigPlatform } from '../../../types/message'; import { resolveConfig } from './resolveConfig'; import { resolveCredentials } from './resolveCredentials'; @@ -251,6 +258,9 @@ export const interpretLayout = ( export const extractContent = ({ InAppMessage: message, }: PinpointInAppMessage): InAppMessageContent[] => { + const clientInfo = getClientInfo(); + const configPlatform = mapOSPlatform(clientInfo?.platform); + return ( message?.Content?.map(content => { const { @@ -261,8 +271,13 @@ export const extractContent = ({ PrimaryBtn, SecondaryBtn, } = content; - const defaultPrimaryButton = PrimaryBtn?.DefaultConfig; - const defaultSecondaryButton = SecondaryBtn?.DefaultConfig; + + const defaultPrimaryButton = getButtonConfig(configPlatform, PrimaryBtn); + const defaultSecondaryButton = getButtonConfig( + configPlatform, + SecondaryBtn, + ); + const extractedContent: InAppMessageContent = {}; if (BackgroundColor) { extractedContent.container = { @@ -341,3 +356,37 @@ export const extractMetadata = ({ priority: Priority, treatmentId: TreatmentId, }); + +export const mapOSPlatform = (os?: string): ButtonConfigPlatform => { + if (!os) return 'DefaultConfig'; + // Check if running in a web browser + if (typeof window !== 'undefined' && typeof window.document !== 'undefined') { + return 'Web'; + } + // Native environment checks + switch (os) { + case 'android': + return 'Android'; + case 'ios': + return 'IOS'; + default: + return 'DefaultConfig'; + } +}; + +const getButtonConfig = ( + configPlatform: ButtonConfigPlatform, + button?: InAppMessageButton, +): InAppMessageButton['DefaultConfig'] | undefined => { + if (!button?.DefaultConfig) { + return; + } + if (!configPlatform || !button?.[configPlatform]) { + return button?.DefaultConfig; + } + + return { + ...button.DefaultConfig, + ...button[configPlatform], + }; +}; diff --git a/packages/notifications/src/inAppMessaging/types/message.ts b/packages/notifications/src/inAppMessaging/types/message.ts index 0e84cacf9f9..5bb37ea2ca8 100644 --- a/packages/notifications/src/inAppMessaging/types/message.ts +++ b/packages/notifications/src/inAppMessaging/types/message.ts @@ -13,6 +13,8 @@ export type InAppMessageAction = 'CLOSE' | 'DEEP_LINK' | 'LINK'; export type InAppMessageTextAlign = 'center' | 'left' | 'right'; +export type ButtonConfigPlatform = 'Android' | 'IOS' | 'Web' | 'DefaultConfig'; + interface InAppMessageContainer { style?: InAppMessageStyle; }