Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(in-app-messaging): Add modal UI component #9712

Merged
merged 28 commits into from
May 10, 2022
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
bf1ed0d
feat: modal ui for in-app messaging
Samaritan1011001 Mar 17, 2022
ee9d51b
feat: adds landscape support for modal
Samaritan1011001 Mar 17, 2022
084dc8e
support ios orientations
Samaritan1011001 Mar 17, 2022
de4931a
review changes: constants, util function for layout decision
Samaritan1011001 Mar 17, 2022
460710f
review fixes: styling and positioning
Samaritan1011001 Mar 22, 2022
20a15f1
added constant for modal border radius
Samaritan1011001 Mar 22, 2022
13dc4a3
mereg conflicts resolved
Samaritan1011001 Mar 24, 2022
468d5ae
remove array style
Samaritan1011001 Mar 24, 2022
d8437f3
merge conflict resolved and tests fixed
Samaritan1011001 Mar 25, 2022
ac86442
merge conflict resolved and tests fixed
Samaritan1011001 Mar 25, 2022
a66c553
remove style array
Samaritan1011001 Mar 25, 2022
011d2cc
snap merge conflicts resolved
Samaritan1011001 Mar 25, 2022
3dd96b6
Updated snapshots for tests
Samaritan1011001 Mar 25, 2022
24a581c
adjust text and button left alignment
Samaritan1011001 Mar 31, 2022
e08fe7e
align the image to the center of the modal
Samaritan1011001 Mar 31, 2022
604b22d
updated snapshots
Samaritan1011001 Mar 31, 2022
5aba4da
fix: center image and contain x icon box
Samaritan1011001 May 3, 2022
4034a2c
Updated snapshot
Samaritan1011001 May 3, 2022
5342e25
fix: image centering
Samaritan1011001 May 5, 2022
b698502
fix: self closing view
Samaritan1011001 May 5, 2022
e78ff02
fix: remove empty view and use margin and fix layout without image
Samaritan1011001 May 6, 2022
ab5ca41
fix: remove color overrides used for dev
Samaritan1011001 May 6, 2022
ab9df2b
snapshots updated
Samaritan1011001 May 6, 2022
a491716
fix: review changes renaming variables and comments
Samaritan1011001 May 9, 2022
6259e58
refactor: use InAppMessagingLayout type for layout prop in util function
Samaritan1011001 May 9, 2022
4ac4da9
refactor: comment on the function params
Samaritan1011001 May 9, 2022
1b6b60d
fix: spacing for the comments
Samaritan1011001 May 9, 2022
d758c0c
refactor: type name in function comments
Samaritan1011001 May 9, 2022
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"resolutions": {
"@babel/types": "7.12.10",
"@types/babel__traverse": "7.0.8",
"@babel/traverse": "7.17.9",
"@types/react": "16.9.10",
"terser": "4.6.7",
"npm-packlist": "1.1.12"
Expand Down
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