-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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 BannerMessage UI component #9124
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
/* | ||
* Copyright 2017-2021 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 { Image, Text, View } from 'react-native'; | ||
import isEmpty from 'lodash/isEmpty'; | ||
|
||
import icons from '../../../icons'; | ||
import { useInAppMessageButtonStyle, useInAppMessageImage } from '../../hooks'; | ||
import { Button, IconButton } from '../../ui'; | ||
|
||
import { ICON_BUTTON_HIT_SLOP, ICON_BUTTON_SIZE } from '../constants'; | ||
import MessageWrapper from '../MessageWrapper'; | ||
import { styles } from './styles'; | ||
import { BannerMessageProps } from './types'; | ||
|
||
export default function BannerMessage({ | ||
body, | ||
container, | ||
header, | ||
image, | ||
layout, | ||
onClose, | ||
position, | ||
primaryButton, | ||
secondaryButton, | ||
style, | ||
}: BannerMessageProps) { | ||
const { imageStyle, shouldDelayMessageRendering, shouldRenderImage } = useInAppMessageImage(image, layout); | ||
|
||
const messageButtonStyle = { primaryButton: primaryButton?.style, secondaryButton: secondaryButton?.style }; | ||
const { primaryButtonStyle, secondaryButtonStyle } = useInAppMessageButtonStyle({ | ||
baseButtonStyle: styles, | ||
messageButtonStyle, | ||
overrideButtonStyle: style, | ||
}); | ||
|
||
const hasPrimaryButton = !isEmpty(primaryButton); | ||
const hasSecondaryButton = !isEmpty(secondaryButton); | ||
|
||
return shouldDelayMessageRendering ? null : ( | ||
<MessageWrapper> | ||
<View style={[styles.positionContainer, styles[position]]}> | ||
<View style={[styles.container, container?.style, style?.container]}> | ||
<View style={styles.contentContainer}> | ||
{shouldRenderImage && ( | ||
<View style={styles.imageContainer}> | ||
<Image source={{ uri: image?.src }} style={imageStyle} /> | ||
</View> | ||
)} | ||
<View style={styles.textContainer}> | ||
{header?.content && <Text style={[styles.header, header.style, style?.header]}>{header.content}</Text>} | ||
{body?.content && <Text style={[styles.message, body.style, style?.message]}>{body.content}</Text>} | ||
</View> | ||
<IconButton | ||
color={style?.closeIconColor} | ||
hitSlop={ICON_BUTTON_HIT_SLOP} | ||
onPress={onClose} | ||
size={ICON_BUTTON_SIZE} | ||
source={icons.close} | ||
style={[styles.iconButton, style?.closeIconButton]} | ||
/> | ||
</View> | ||
{(hasPrimaryButton || hasSecondaryButton) && ( | ||
<View style={styles.buttonsContainer}> | ||
{hasSecondaryButton && ( | ||
<Button | ||
onPress={secondaryButton.onPress} | ||
style={secondaryButtonStyle.container} | ||
textStyle={secondaryButtonStyle.text} | ||
> | ||
{secondaryButton.title} | ||
</Button> | ||
)} | ||
{hasPrimaryButton && ( | ||
<Button | ||
onPress={primaryButton.onPress} | ||
style={primaryButtonStyle.container} | ||
textStyle={primaryButtonStyle.text} | ||
> | ||
{primaryButton.title} | ||
</Button> | ||
)} | ||
</View> | ||
)} | ||
</View> | ||
</View> | ||
</MessageWrapper> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export { default } from './BannerMessage'; | ||
export { BannerMessageProps } from './types'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
/* | ||
* Copyright 2017-2021 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 { StyleSheet } from 'react-native'; | ||
|
||
import { getLineHeight } from '../utils'; | ||
import { | ||
BANNER_ELEVATION, | ||
BANNER_SHADOW_HEIGHT, | ||
BANNER_SHADOW_OPACITY, | ||
BANNER_SHADOW_RADIUS, | ||
BANNER_SHADOW_WIDTH, | ||
BLACK, | ||
BORDER_RADIUS_BASE, | ||
FONT_SIZE_BASE, | ||
FONT_SIZE_LARGE, | ||
FONT_WEIGHT_BASE, | ||
LIGHT_GREY, | ||
SPACING_EXTRA_LARGE, | ||
SPACING_LARGE, | ||
SPACING_MEDIUM, | ||
SPACING_SMALL, | ||
WHITE, | ||
} from '../constants'; | ||
import { BannerMessageStyle } from './types'; | ||
|
||
export const styles: BannerMessageStyle = StyleSheet.create({ | ||
// position style | ||
positionContainer: { | ||
flex: 1, | ||
backgroundColor: 'transparent', | ||
}, | ||
bottom: { | ||
justifyContent: 'flex-end', | ||
}, | ||
middle: { | ||
justifyContent: 'center', | ||
}, | ||
top: { | ||
justifyContent: 'flex-start', | ||
}, | ||
|
||
// shared style | ||
buttonContainer: { | ||
backgroundColor: LIGHT_GREY, | ||
borderRadius: BORDER_RADIUS_BASE, | ||
flex: 1, | ||
margin: SPACING_MEDIUM, | ||
padding: SPACING_LARGE, | ||
}, | ||
buttonsContainer: { | ||
flexDirection: 'row', | ||
justifyContent: 'center', | ||
paddingHorizontal: SPACING_SMALL, | ||
}, | ||
buttonText: { | ||
fontSize: FONT_SIZE_BASE, | ||
fontWeight: FONT_WEIGHT_BASE, | ||
lineHeight: getLineHeight(FONT_SIZE_BASE), | ||
textAlign: 'center', | ||
}, | ||
container: { | ||
backgroundColor: WHITE, | ||
elevation: BANNER_ELEVATION, | ||
margin: SPACING_EXTRA_LARGE, | ||
shadowColor: BLACK, | ||
shadowOffset: { | ||
width: BANNER_SHADOW_WIDTH, | ||
height: BANNER_SHADOW_HEIGHT, | ||
}, | ||
shadowOpacity: BANNER_SHADOW_OPACITY, | ||
shadowRadius: BANNER_SHADOW_RADIUS, | ||
}, | ||
contentContainer: { | ||
flexDirection: 'row', | ||
padding: SPACING_LARGE, | ||
}, | ||
header: { | ||
fontSize: FONT_SIZE_LARGE, | ||
fontWeight: FONT_WEIGHT_BASE, | ||
lineHeight: getLineHeight(FONT_SIZE_LARGE), | ||
}, | ||
iconButton: { | ||
alignSelf: 'flex-start', | ||
marginLeft: 'auto', | ||
}, | ||
imageContainer: { | ||
justifyContent: 'center', | ||
}, | ||
message: { | ||
fontSize: FONT_SIZE_BASE, | ||
fontWeight: FONT_WEIGHT_BASE, | ||
lineHeight: getLineHeight(FONT_SIZE_BASE), | ||
}, | ||
textContainer: { | ||
marginHorizontal: SPACING_SMALL, | ||
paddingLeft: SPACING_MEDIUM, | ||
flex: 1, | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,29 @@ | |
* and limitations under the License. | ||
*/ | ||
|
||
import { TextStyle, ViewStyle } from 'react-native'; | ||
import { InAppMessageComponentPosition, InAppMessageComponentBaseProps } from '../types'; | ||
|
||
export interface BannerMessageProps extends InAppMessageComponentBaseProps { | ||
position: InAppMessageComponentPosition; | ||
} | ||
|
||
export interface BannerMessageStyle { | ||
// position specific style | ||
bottom: ViewStyle; | ||
middle: ViewStyle; | ||
top: ViewStyle; | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the grouping approach you did here :) |
||
// component style | ||
buttonContainer: ViewStyle; | ||
buttonsContainer: ViewStyle; | ||
buttonText: TextStyle; | ||
container: ViewStyle; | ||
contentContainer: ViewStyle; | ||
header: TextStyle; | ||
iconButton: ViewStyle; | ||
imageContainer: ViewStyle; | ||
message: TextStyle; | ||
positionContainer: ViewStyle; | ||
textContainer: ViewStyle; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright 2017-2021 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 { SafeAreaView } from 'react-native'; | ||
|
||
import { styles } from './styles'; | ||
import { MessageWrapperProps } from './types'; | ||
|
||
export default function MessageWrapper({ children, style }: MessageWrapperProps) { | ||
return <SafeAreaView style={[styles.safeArea, style]}>{children}</SafeAreaView>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export { default } from './MessageWrapper'; | ||
export { MessageWrapperProps, MessageWrapperStyle } from './types'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* Copyright 2017-2021 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 { StyleSheet } from 'react-native'; | ||
import { MessageWrapperStyle } from './types'; | ||
|
||
export const styles: MessageWrapperStyle = StyleSheet.create({ | ||
safeArea: { | ||
...StyleSheet.absoluteFillObject, | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/* | ||
* Copyright 2017-2021 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 { ReactNode } from 'react'; | ||
import { StyleProp, ViewStyle } from 'react-native'; | ||
|
||
export interface MessageWrapperProps { | ||
children: ReactNode; | ||
style?: StyleProp<ViewStyle>; | ||
} | ||
|
||
export interface MessageWrapperStyle { | ||
safeArea: ViewStyle; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* Copyright 2017-2021 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. | ||
*/ | ||
|
||
/** | ||
* Style constants either match or approximate the values used in the Pinpoint console preview. | ||
* Some values, such as spacing, are slightly different to allow for a more mobile friendly UX | ||
*/ | ||
|
||
// color | ||
export const BLACK = '#000'; | ||
export const LIGHT_GREY = '#e8e8e8'; | ||
export const WHITE = '#fff'; | ||
Comment on lines
+19
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I'm not yet familiar with the React Native codebase's patterns, but in the future, it could be good to prefix the color |
||
|
||
// spacing | ||
export const SPACING_SMALL = 4; | ||
export const SPACING_MEDIUM = 8; | ||
export const SPACING_LARGE = 12; | ||
export const SPACING_EXTRA_LARGE = 16; | ||
|
||
// border radius | ||
export const BORDER_RADIUS_BASE = 4; | ||
|
||
// font | ||
export const FONT_SIZE_BASE = 16; | ||
export const FONT_SIZE_LARGE = 18; | ||
|
||
export const FONT_WEIGHT_BASE = '400'; | ||
|
||
export const LINE_HEIGHT_MULTIPLIER = 1.5; | ||
|
||
// icon | ||
export const ICON_BUTTON_SIZE = 20; | ||
export const ICON_BUTTON_HIT_SLOP = 10; | ||
|
||
// component specific constants | ||
|
||
// BannerMessage | ||
// iOS shadow values | ||
export const BANNER_SHADOW_HEIGHT = 2; | ||
export const BANNER_SHADOW_WIDTH = 2; | ||
export const BANNER_SHADOW_OPACITY = 0.1; | ||
export const BANNER_SHADOW_RADIUS = 2; | ||
|
||
// android shadow values | ||
export const BANNER_ELEVATION = 3; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This is not blocking but I wonder if
styles
should bedefaultStyle
or something? I keep finding myself having to remind myself what the difference betweenstyles
andstyle
is. As a reader of foreign code, I would have no idea why they're different things.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imo default styling as
styles
is a pretty standard convention in React Native. That said i do think all the styling applied in these components is not the easiest to parse 😟There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good point too since static StyleSheets are typically assigned to a
styles
variable. It was just a thought