Skip to content

Commit

Permalink
feat(in-app-messaging): Add modal UI component (#9712)
Browse files Browse the repository at this point in the history
* feat: modal ui for in-app messaging

* feat: adds landscape support for modal

* support ios orientations

* review changes: constants, util function for layout decision

* review fixes: styling and positioning

* added constant for modal border radius

* remove array style

* merge conflict resolved and tests fixed

* remove style array

* Updated snapshots for tests

* adjust text and button left alignment

* align the image to the center of the modal

* updated snapshots

* fix: center image and contain x icon box

* Updated snapshot

* fix: image centering

* fix: self closing view

* fix: remove empty view and use margin and fix layout without image

* fix: remove color overrides used for dev

* snapshots updated

* fix: review changes renaming variables and comments

* refactor: use InAppMessagingLayout type for layout prop in util function

* refactor: comment on the function params

Co-authored-by: Caleb Pollman <cpollman1@gmail.com>

* fix: spacing for the comments

* refactor: type name in function comments

Co-authored-by: Caleb Pollman <cpollman1@gmail.com>
  • Loading branch information
Samaritan1011001 and calebpollman authored May 10, 2022
1 parent 12fddc3 commit 7caa08c
Show file tree
Hide file tree
Showing 10 changed files with 593 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@

import { ImageStyle, StyleSheet, ViewStyle } from 'react-native';
import {
BANNER_ELEVATION,
BANNER_SHADOW_HEIGHT,
BANNER_SHADOW_OPACITY,
BANNER_SHADOW_RADIUS,
BANNER_SHADOW_WIDTH,
BORDER_RADIUS_BASE,
COLOR_BLACK,
COLOR_LIGHT_GREY,
Expand All @@ -27,6 +22,11 @@ import {
FONT_WEIGHT_BASE,
LINE_HEIGHT_BASE,
LINE_HEIGHT_LARGE,
MESSAGE_ELEVATION,
MESSAGE_SHADOW_HEIGHT,
MESSAGE_SHADOW_OPACITY,
MESSAGE_SHADOW_RADIUS,
MESSAGE_SHADOW_WIDTH,
SPACING_EXTRA_LARGE,
SPACING_LARGE,
SPACING_MEDIUM,
Expand Down Expand Up @@ -78,15 +78,15 @@ export const getStyles = (imageDimensions: ImageStyle, additionalStyle: { positi
},
container: {
backgroundColor: COLOR_WHITE,
elevation: BANNER_ELEVATION,
elevation: MESSAGE_ELEVATION,
margin: SPACING_EXTRA_LARGE,
shadowColor: COLOR_BLACK,
shadowOffset: {
width: BANNER_SHADOW_WIDTH,
height: BANNER_SHADOW_HEIGHT,
width: MESSAGE_SHADOW_WIDTH,
height: MESSAGE_SHADOW_HEIGHT,
},
shadowOpacity: BANNER_SHADOW_OPACITY,
shadowRadius: BANNER_SHADOW_RADIUS,
shadowOpacity: MESSAGE_SHADOW_OPACITY,
shadowRadius: MESSAGE_SHADOW_RADIUS,
},
contentContainer: {
flexDirection: 'row',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,86 @@
* and limitations under the License.
*/

import React from 'react';
import { Image, Text, View } from 'react-native';

import icons from '../../../icons';
import { IN_APP_MESSAGING } from '../../../AmplifyTestIDs';
import { Button, IconButton } from '../../ui';

import { ICON_BUTTON_HIT_SLOP, ICON_BUTTON_SIZE } from '../constants';
import { useMessageProps } from '../hooks';
import MessageWrapper from '../MessageWrapper';

import { getStyles } from './styles';
import { ModalMessageProps } from './types';

export default function ModalMessage(_: ModalMessageProps) {
return null;
export default function ModalMessage(props: ModalMessageProps) {
const { body, header, image, onClose, primaryButton, secondaryButton } = props;
const { hasButtons, hasPrimaryButton, hasRenderableImage, hasSecondaryButton, shouldRenderMessage, styles } =
useMessageProps(props, getStyles);

if (!shouldRenderMessage) {
return null;
}

return (
<MessageWrapper style={styles.componentWrapper}>
<View style={styles.container}>
<View style={styles.contentContainer}>
{hasRenderableImage && (
<View style={styles.imageContainer}>
<Image source={{ uri: image?.src }} style={styles.image} testID={IN_APP_MESSAGING.IMAGE} />
</View>
)}
<IconButton
color={styles.iconButton.iconColor}
hitSlop={ICON_BUTTON_HIT_SLOP}
onPress={onClose}
size={ICON_BUTTON_SIZE}
source={icons.close}
style={styles.iconButton.container}
testID={IN_APP_MESSAGING.CLOSE_BUTTON}
/>
</View>
<View style={styles.textContainer}>
{header?.content && (
<Text style={styles.header} testID={IN_APP_MESSAGING.HEADER}>
{header.content}
</Text>
)}
{body?.content && (
<Text style={styles.body} testID={IN_APP_MESSAGING.BODY}>
{body.content}
</Text>
)}
</View>

{hasButtons && (
<View style={styles.buttonsContainer}>
{hasSecondaryButton && (
<Button
onPress={secondaryButton.onPress}
style={styles.secondaryButton.container}
testID={IN_APP_MESSAGING.SECONDARY_BUTTON}
textStyle={styles.secondaryButton.text}
>
{secondaryButton.title}
</Button>
)}
{hasPrimaryButton && (
<Button
onPress={primaryButton.onPress}
style={styles.primaryButton.container}
testID={IN_APP_MESSAGING.PRIMARY_BUTTON}
textStyle={styles.primaryButton.text}
>
{primaryButton.title}
</Button>
)}
</View>
)}
</View>
</MessageWrapper>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
* Copyright 2017-2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
* and limitations under the License.
*/

import React from 'react';
import TestRenderer from 'react-test-renderer';

import { IN_APP_MESSAGING } from '../../../../AmplifyTestIDs';
import useMessageImage from '../../hooks/useMessageImage';
import { INITIAL_IMAGE_DIMENSIONS } from '../../hooks/useMessageImage/constants';

import ModalMessage from '../ModalMessage';

jest.mock('../../hooks/useMessageImage');
jest.mock('../../MessageWrapper', () => 'MessageWrapper');

const mockUseMessageImage = useMessageImage as jest.Mock;
const onClose = jest.fn();
const onPress = jest.fn();

const baseProps = { layout: 'MODAL' as const, onClose };

describe('ModalMessage', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders a message as expected without an image', () => {
mockUseMessageImage.mockReturnValueOnce({
hasRenderableImage: false,
imageDimensions: INITIAL_IMAGE_DIMENSIONS,
isImageFetching: false,
});

const renderer = TestRenderer.create(<ModalMessage {...baseProps} />);

expect(renderer.toJSON()).toMatchSnapshot();
});

it('renders a message as expected with an image', () => {
mockUseMessageImage.mockReturnValueOnce({
hasRenderableImage: true,
imageDimensions: { height: 100, width: 100 },
isImageFetching: false,
});

const src = 'asset.png';
const props = { ...baseProps, image: { src } };

const renderer = TestRenderer.create(<ModalMessage {...props} />);

const image = renderer.root.findByProps({ testID: IN_APP_MESSAGING.IMAGE });

expect(image.props).toEqual(expect.objectContaining({ source: { uri: src } }));
expect(renderer.toJSON()).toMatchSnapshot();
});

it('returns null while an image is fetching', () => {
mockUseMessageImage.mockReturnValueOnce({
hasRenderableImage: false,
imageDimensions: INITIAL_IMAGE_DIMENSIONS,
isImageFetching: true,
});

const renderer = TestRenderer.create(<ModalMessage {...baseProps} />);

expect(renderer.toJSON()).toBeNull();
});

it.each([
['header', IN_APP_MESSAGING.HEADER, { content: 'header content' }, { children: 'header content' }],
['body', IN_APP_MESSAGING.BODY, { content: 'body content' }, { children: 'body content' }],
[
'primaryButton',
IN_APP_MESSAGING.PRIMARY_BUTTON,
{ onPress, title: 'primary button' },
{ children: 'primary button', onPress },
],
[
'secondaryButton',
IN_APP_MESSAGING.SECONDARY_BUTTON,
{ onPress, title: 'secondary button' },
{ children: 'secondary button', onPress },
],
])('correctly handles a %s prop', (key, testID, testProps, expectedProps) => {
mockUseMessageImage.mockReturnValueOnce({
hasRenderableImage: false,
imageDimensions: INITIAL_IMAGE_DIMENSIONS,
isImageFetching: false,
});

const props = { ...baseProps, [key]: testProps };

const renderer = TestRenderer.create(<ModalMessage {...props} />);
const testElement = renderer.root.findByProps({ testID });

expect(testElement.props).toEqual(expect.objectContaining(expectedProps));
});

it('calls onClose when the close button is pressed', () => {
mockUseMessageImage.mockReturnValueOnce({
hasRenderableImage: false,
imageDimensions: INITIAL_IMAGE_DIMENSIONS,
isImageFetching: false,
});

const renderer = TestRenderer.create(<ModalMessage {...baseProps} />);
const closeButton = renderer.root.findByProps({ testID: IN_APP_MESSAGING.CLOSE_BUTTON });

TestRenderer.act(() => {
(closeButton.props as { onPress: () => void }).onPress();
});

expect(onClose).toHaveBeenCalledTimes(1);
});
});
Loading

0 comments on commit 7caa08c

Please sign in to comment.