├── src
├── components
│ └── index.js
├── index.js
├── views
│ ├── index.js
│ └── DefaultPopup.js
└── utils
│ └── index.js
├── .eslintignore
├── .gitignore
├── .babelrc
├── .editorconfig
├── types.d.ts
├── .eslintrc.js
├── LICENSE
├── package.json
└── README.md
/src/components/index.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react-native"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { DefaultPopup } from './views';
2 |
3 | export default DefaultPopup;
4 |
--------------------------------------------------------------------------------
/src/views/index.js:
--------------------------------------------------------------------------------
1 | import DefaultPopup from './DefaultPopup';
2 |
3 | export {
4 | DefaultPopup
5 | };
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 | quote_type = single
11 |
--------------------------------------------------------------------------------
/types.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | declare module "react-native-push-notification-popup" {
5 | import { ReactElement, Component } from "react";
6 | import { StyleProp, ViewStyle, ImageSourcePropType } from "react-native";
7 |
8 | interface ContentOptionsBase {
9 | appIconSource?: ImageSourcePropType;
10 | appTitle?: string;
11 | timeText?: string;
12 | title?: string;
13 | body?: string;
14 | }
15 |
16 | interface ShowOptions extends ContentOptionsBase {
17 | onPress?: () => void;
18 | slideOutTime?: number;
19 | }
20 |
21 | interface PushNotificationPopupProps {
22 | renderPopupContent?: (options: ContentOptionsBase) => ReactElement;
23 | shouldChildHandleResponderStart?: boolean;
24 | shouldChildHandleResponderMove?: boolean;
25 | isSkipStatusBarPadding?: boolean;
26 | }
27 |
28 | export default class ReactNativePushNotificationPopup extends Component {
29 | public show(options: ShowOptions): void;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": ["eslint:recommended", "plugin:react/recommended"],
8 | "parser": "babel-eslint",
9 | "parserOptions": {
10 | "ecmaVersion": 2017,
11 | "ecmaFeatures": {
12 | "experimentalObjectRestSpread": true,
13 | "jsx": true
14 | },
15 | "sourceType": "module"
16 | },
17 | "plugins": [
18 | "react"
19 | ],
20 | "rules": {
21 | "indent": [
22 | "error",
23 | 2,
24 | { "flatTernaryExpressions": true, "SwitchCase": 1 }
25 | ],
26 | "linebreak-style": [
27 | "error",
28 | "unix"
29 | ],
30 | "no-console": 0,
31 | "quotes": [
32 | "error",
33 | "single"
34 | ],
35 | "semi": [
36 | "error",
37 | "always"
38 | ],
39 | "no-unused-vars": ["error", { "args": "none" }]
40 | }
41 | };
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Carson Wah
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-push-notification-popup",
3 | "version": "1.7.0",
4 | "description": "React Native Push Notification Popup Component",
5 | "main": "src/index.js",
6 | "types": "./types.d.ts",
7 | "scripts": {
8 | "lint": "eslint --fix --ext js src"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/carsonwah/react-native-push-notification-popup.git"
13 | },
14 | "keywords": [
15 | "react",
16 | "react-native",
17 | "react-component",
18 | "react-native-component",
19 | "push-notification",
20 | "ios",
21 | "android",
22 | "modal",
23 | "popup",
24 | "dialog"
25 | ],
26 | "author": "Carson Wah",
27 | "license": "MIT",
28 | "bugs": {
29 | "url": "https://github.com/carsonwah/react-native-push-notification-popup/issues"
30 | },
31 | "homepage": "https://github.com/carsonwah/react-native-push-notification-popup#readme",
32 | "devDependencies": {
33 | "babel-eslint": "^8.2.3",
34 | "babel-preset-react-native": "^4.0.0",
35 | "eslint": "^4.19.1",
36 | "eslint-plugin-react": "^7.7.0",
37 | "react": "^16.8.6",
38 | "react-native": "^0.59.8"
39 | },
40 | "dependencies": {
41 | "prop-types": "^15.7.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import { Dimensions, Platform, StatusBar } from 'react-native';
2 |
3 | // Code borrowed from https://github.com/ovr/react-native-status-bar-height
4 | const STATUSBAR_DEFAULT_HEIGHT = 20;
5 | const STATUSBAR_X_HEIGHT = 44;
6 | const STATUSBAR_IP12_HEIGHT = 47;
7 | const STATUSBAR_IP12MAX_HEIGHT = 47;
8 | const STATUSBAR_IP14PRO_HEIGHT = 54;
9 | const STATUSBAR_IP14PROMAX_HEIGHT = 54;
10 |
11 | const X_WIDTH = 375;
12 | const X_HEIGHT = 812;
13 |
14 | const XSMAX_WIDTH = 414;
15 | const XSMAX_HEIGHT = 896;
16 |
17 | const IP12_WIDTH = 390;
18 | const IP12_HEIGHT = 844;
19 |
20 | const IP12MAX_WIDTH = 428;
21 | const IP12MAX_HEIGHT = 926;
22 |
23 | const IP14PRO_WIDTH = 393;
24 | const IP14PRO_HEIGHT = 852;
25 |
26 | const IP14PROMAX_WIDTH = 430;
27 | const IP14PROMAX_HEIGHT = 932;
28 |
29 | const { height: W_HEIGHT, width: W_WIDTH } = Dimensions.get('window');
30 |
31 | let statusBarHeight = STATUSBAR_DEFAULT_HEIGHT;
32 |
33 | if (Platform.OS === 'ios' && !Platform.isPad && !Platform.isTVOS) {
34 | if (W_WIDTH === X_WIDTH && W_HEIGHT === X_HEIGHT) {
35 | statusBarHeight = STATUSBAR_X_HEIGHT;
36 | } else if (W_WIDTH === XSMAX_WIDTH && W_HEIGHT === XSMAX_HEIGHT) {
37 | statusBarHeight = STATUSBAR_X_HEIGHT;
38 | } else if (W_WIDTH === IP12_WIDTH && W_HEIGHT === IP12_HEIGHT) {
39 | statusBarHeight = STATUSBAR_IP12_HEIGHT;
40 | } else if (W_WIDTH === IP12MAX_WIDTH && W_HEIGHT === IP12MAX_HEIGHT) {
41 | statusBarHeight = STATUSBAR_IP12MAX_HEIGHT;
42 | } else if (W_WIDTH === IP14PRO_WIDTH && W_HEIGHT === IP14PRO_HEIGHT) {
43 | statusBarHeight = STATUSBAR_IP14PRO_HEIGHT;
44 | } else if (W_WIDTH === IP14PROMAX_WIDTH && W_HEIGHT === IP14PROMAX_HEIGHT) {
45 | statusBarHeight = STATUSBAR_IP14PROMAX_HEIGHT;
46 | }
47 | }
48 |
49 | export function getStatusBarHeight() {
50 | return Platform.select({
51 | ios: statusBarHeight,
52 | android: StatusBar.currentHeight,
53 | default: 0,
54 | });
55 | }
56 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native Push Notification Popup
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 |  
10 |
11 | ## Features
12 |
13 | - Support "pan" gesture
14 | - Support "onPress" gesture feedback
15 | - Written in pure-JS using official react-native `Animation` package
16 | - *Which means it supports all Expo/CRNA apps*
17 | - Support iPhone X, XS, Max (yeah that notch)
18 | - Support Android native "elevation"
19 |
20 | ## Motivations
21 |
22 | [Blog post](https://medium.com/@carsonwah/show-push-notification-popup-in-react-native-19db965a5603)
23 |
24 | 1. In some apps, you may just want to display reminders to user, without going through those troublesome push notification setups
25 | 2. Expo/CNRA apps [cannot display push notification while app is in foreground](https://docs.expo.io/versions/v27.0.0/guides/push-notifications#notification-handling-timing)
26 | 3. Even if you eject, you still need to configure [iOS](https://stackoverflow.com/questions/14872088/get-push-notification-while-app-in-foreground-ios) and [Android](https://stackoverflow.com/questions/38451235/how-to-handle-the-fire-base-notification-when-app-is-in-foreground) separately with native codes
27 |
28 | This package is here to help. Just show your own notification popup to your users!
29 |
30 | ## Installation
31 |
32 | ```bash
33 | # yarn, recommended
34 | yarn add react-native-push-notification-popup
35 |
36 | # or npm
37 | npm install react-native-push-notification-popup --save
38 | ```
39 |
40 | ## Usage
41 |
42 | ### Declare Component
43 |
44 | Put it in a wrapper component. (Maybe where you handle your incoming push notifications)
45 |
46 | ```javascript
47 | import NotificationPopup from 'react-native-push-notification-popup';
48 |
49 | class MyComponent extends React.Component {
50 | render() {
51 | return (
52 |
53 |
54 | this.popup = ref} />
55 |
56 | );
57 | }
58 | // ...
59 | ```
60 |
61 | > **IMPORTANT**: Remember to put it on the **bottom of other components**, because React render from back to front in order of declaration. We do not use `zIndex` becuase it is [problematic on Android](https://github.com/carsonwah/react-native-push-notification-popup/issues/21).
62 |
63 | #### Optional: Customize your popup
64 |
65 | ```javascript
66 | // Render function
67 | const renderCustomPopup = ({ appIconSource, appTitle, timeText, title, body }) => (
68 |
69 | {title}
70 | {body}
71 |
73 | );
74 |
75 | class MyComponent extends React.Component {
76 | render() {
77 | return (
78 |
79 | this.popup = ref}
81 | renderPopupContent={renderCustomPopup}
82 | shouldChildHandleResponderStart={true}
83 | shouldChildHandleResponderMove={true}
84 | isSkipStatusBarPadding={true} />
85 |
86 | );
87 | }
88 | // ...
89 | ```
90 |
91 | ### Show it!
92 |
93 | ```javascript
94 | componentDidMount() {
95 | this.popup.show({
96 | onPress: function() {console.log('Pressed')},
97 | appIconSource: require('./assets/icon.jpg'),
98 | appTitle: 'Some App',
99 | timeText: 'Now',
100 | title: 'Hello World',
101 | body: 'This is a sample message.\nTesting emoji 😀',
102 | slideOutTime: 5000
103 | });
104 | }
105 | ```
106 |
107 | ### Props
108 |
109 | | Param | Type | Default | Description |
110 | | --- | --- | --- | --- |
111 | | **`renderPopupContent`** | function
`(options?: { appIconSource?: ImageSourcePropType; appTitle?: string; timeText?: string; title?: string;body?: string; }) => React.ReactElement` | null | Render your own custom popup body (Optional) |
112 | | **`shouldChildHandleResponderStart`** | boolean | false | By default, parent popup will prevent bubbling event to child. This should be set to true if you have button inside your custom popup that wants to receive the event. |
113 | | **`shouldChildHandleResponderMove`** | boolean | false | By default, parent popup will prevent bubbling event to child. This should be set to true if you have button inside your custom popup that wants to receive the event. |
114 | | **`isSkipStatusBarPadding`** | boolean | false | Set this to true if your app is an Android app with non-translucent StatusBar. ([See #35](https://github.com/carsonwah/react-native-push-notification-popup/issues/35)) |
115 |
116 | ### Methods
117 |
118 | #### .show()
119 |
120 | | Param | Type | Default | Description |
121 | | --- | --- | --- | --- |
122 | | **`onPress`** | Function | null | Callback to be called when user press the popup |
123 | | **`appIconSource`** | [Image source](https://facebook.github.io/react-native/docs/image.html#source) | null | Icon on the upper left |
124 | | **`appTitle`** | String | '' | Usually your app name, but you can also customize it |
125 | | **`timeText`** | String | '' | Text on the upper right |
126 | | **`title`** | String | '' | Message title |
127 | | **`body`** | String | '' | Message body (support multi-line) |
128 | | **`slideOutTime`** | Number | 4000 | Time until notification slides out |
129 |
130 | ## Star History
131 |
132 | [](https://star-history.com/#carsonwah/react-native-push-notification-popup&Date)
133 |
134 | ## Roadmap
135 |
136 | - [ ] Add testing
137 | - [ ] Add example/ project
138 | - [ ] Support showing it globally
139 | - [ ] Customizing props: speed, duration, etc
140 | - [ ] Support image on the right-side
141 | - [ ] Android material design style
142 | - [ ] Other types of popup, e.g. without app icon
143 | - [ ] More usage examples
144 | - [ ] Identify peerDependencies on react-native
145 |
146 | ## Contributing
147 |
148 | ### Using demo project
149 |
150 | - Refer to [this demo project](https://github.com/carsonwah/react-native-push-notification-popup-demo-local) for local debugging
151 |
152 | ### General Steps
153 |
154 | 1. Clone this repo
155 | 2. Run `yarn --production`
156 | 1. *(Installing dependencies without --production will include devDependencies (e.g. react-native), which causes crashes)*
157 | 3. Create a react-native project next to it
158 | 4. Add dependency to package.json
159 | 1. `"react-native-push-notification-popup": "file:../react-native-push-notification-popup"`
160 | 5. Try it
161 | 6. Re-run `yarn --production` whenever there is any code change
162 |
163 | ### Linting
164 |
165 | 1. Run `yarn` (Install devDependencies)
166 | 2. Run `yarn run lint`
167 |
168 | ## License
169 |
170 | [MIT License](https://opensource.org/licenses/mit-license.html). © Carson Wah 2018
171 |
172 |
173 |
--------------------------------------------------------------------------------
/src/views/DefaultPopup.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import { Animated, View, Text, Image, Dimensions, StyleSheet, PanResponder, TouchableWithoutFeedback } from 'react-native';
4 |
5 | import { getStatusBarHeight } from '../utils';
6 |
7 | const { width: deviceWidth } = Dimensions.get('window');
8 |
9 | const CONTAINER_MARGIN_TOP_WITHOUT_STATUS_BAR = 10; // Add padding to prevent touching edge of the screen
10 | const CONTAINER_MARGIN_TOP_WITH_STATUS_BAR = getStatusBarHeight() + CONTAINER_MARGIN_TOP_WITHOUT_STATUS_BAR;
11 |
12 | const slideOffsetYToTranslatePixelMapping = {
13 | inputRange: [0, 1],
14 | outputRange: [-150, 0]
15 | };
16 |
17 | const HORIZONTAL_MARGIN = 8; // left/right margin to screen edge
18 |
19 | const getAnimatedContainerStyle = ({containerSlideOffsetY, containerDragOffsetY, containerScale, isSkipStatusBarPadding}) => {
20 | // Map 0-1 value to translateY value
21 | const slideInAnimationStyle = {
22 | transform: [
23 | {translateY: containerSlideOffsetY.interpolate(slideOffsetYToTranslatePixelMapping)},
24 | {translateY: containerDragOffsetY},
25 | {scale: containerScale},
26 | ],
27 | };
28 |
29 | // Combine with original container style
30 | const animatedContainerStyle = [
31 | styles.popupContainer,
32 | isSkipStatusBarPadding ? { top: CONTAINER_MARGIN_TOP_WITHOUT_STATUS_BAR } : { top: CONTAINER_MARGIN_TOP_WITH_STATUS_BAR },
33 | slideInAnimationStyle,
34 | ];
35 |
36 | return animatedContainerStyle;
37 | };
38 |
39 | export default class DefaultPopup extends Component {
40 |
41 | static propTypes = {
42 | renderPopupContent: PropTypes.func,
43 | shouldChildHandleResponderStart: PropTypes.bool,
44 | shouldChildHandleResponderMove: PropTypes.bool,
45 | isSkipStatusBarPadding: PropTypes.bool,
46 | }
47 |
48 | constructor(props) {
49 | super(props);
50 | this.state = {
51 | show: false,
52 |
53 | /*
54 | Slide-in Animation
55 | Use value 0 - 1 to control the whole animation
56 | Then map it to actual behaviour in style in render
57 | */
58 | containerSlideOffsetY: new Animated.Value(0),
59 | slideOutTimer: null,
60 |
61 | // Drag Gesture
62 | containerDragOffsetY: new Animated.Value(0),
63 |
64 | // onPress Feedback
65 | containerScale: new Animated.Value(1), // Directly set a scale
66 |
67 | onPressAndSlideOut: null,
68 | appIconSource: null,
69 | appTitle: null,
70 | timeText: null,
71 | title: null,
72 | body: null,
73 | slideOutTime: null,
74 | };
75 | this._panResponder = PanResponder.create({
76 | onStartShouldSetPanResponder: (e, gestureState) => true,
77 | onStartShouldSetPanResponderCapture: (e, gestureState) => props.shouldChildHandleResponderStart ? false : true, // Capture child event
78 | onMoveShouldSetPanResponder: (e, gestureState) => true,
79 | onMoveShouldSetPanResponderCapture: (e, gestureState) => props.shouldChildHandleResponderMove ? false : true, // Capture child event
80 | onPanResponderGrant: this._onPanResponderGrant,
81 | onPanResponderMove: this._onPanResponderMove,
82 | onPanResponderRelease: this._onPanResponderRelease,
83 | });
84 | }
85 |
86 | _onPanResponderGrant = (e, gestureState) => {
87 | // console.log('_onPanResponderGrant', gestureState); // DEBUG
88 | this.onPressInFeedback();
89 | }
90 |
91 | // https://facebook.github.io/react-native/docs/animations.html#tracking-gestures
92 | _onPanResponderMove = (e, gestureState) => {
93 | // console.log('_onPanResponderMove', gestureState); // DEBUG
94 | const { containerDragOffsetY } = this.state;
95 | // Prevent dragging down too much
96 | const newDragOffset = gestureState.dy < 100 ? gestureState.dy : 100; // TODO: customize
97 | containerDragOffsetY.setValue(newDragOffset);
98 | }
99 |
100 | _onPanResponderRelease = (e, gestureState) => {
101 | // console.log('_onPanResponderRelease', gestureState); // DEBUG
102 | const { onPressAndSlideOut, containerDragOffsetY } = this.state;
103 |
104 | // Present feedback
105 | this.onPressOutFeedback();
106 |
107 | // Check if it is onPress
108 | // Currently tolerate +-2 movement
109 | // Note that "move around, back to original position, release" still triggers onPress
110 | if (gestureState.dy <= 2 && gestureState.dy >= -2 && gestureState.dx <= 2 && gestureState.dx >= -2) {
111 | onPressAndSlideOut();
112 | }
113 |
114 | // Check if it is leaving the screen
115 | if (containerDragOffsetY._value < -30) { // TODO: turn into constant
116 | // 1. If leaving screen -> slide out
117 | this.slideOutAndDismiss(200);
118 | } else {
119 | // 2. If not leaving screen -> slide back to original position
120 | this.clearTimerIfExist();
121 | Animated.timing(containerDragOffsetY, { toValue: 0, duration: 200, useNativeDriver: false })
122 | .start(({finished}) => {
123 | // Reset a new countdown
124 | this.countdownToSlideOut();
125 | });
126 | }
127 | }
128 |
129 | renderPopupContent = () => {
130 | const { appIconSource, appTitle, timeText, title, body } = this.state;
131 | const { renderPopupContent } = this.props;
132 | if (renderPopupContent) {
133 | return renderPopupContent({ appIconSource, appTitle, timeText, title, body });
134 | }
135 |
136 | return (
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | {appTitle || ''}
145 |
146 |
147 |
148 |
149 | {timeText || ''}
150 |
151 |
152 |
153 |
154 |
155 | {title || ''}
156 |
157 |
158 | {body || ''}
159 |
160 |
161 |
162 | );
163 | }
164 |
165 | render() {
166 | const {
167 | show,
168 | containerSlideOffsetY, containerDragOffsetY, containerScale,
169 | onPressAndSlideOut,
170 | } = this.state;
171 | const { isSkipStatusBarPadding } = this.props;
172 |
173 | if (!show) {
174 | return null;
175 | }
176 |
177 | return (
178 |
181 |
182 |
183 | {this.renderPopupContent()}
184 |
185 |
186 |
187 | );
188 | }
189 |
190 | onPressInFeedback = () => {
191 | // console.log('PressIn!'); // DEBUG
192 | // Show feedback as soon as user press down
193 | const { containerScale } = this.state;
194 | Animated.spring(containerScale, { toValue: 0.95, friction: 8, useNativeDriver: false })
195 | .start();
196 | }
197 |
198 | onPressOutFeedback = () => {
199 | // console.log('PressOut!'); // DEBUG
200 | // Show feedback as soon as user press down
201 | const { containerScale } = this.state;
202 | Animated.spring(containerScale, { toValue: 1, friction: 8, useNativeDriver: false })
203 | .start();
204 | }
205 |
206 | createOnPressWithCallback = (callback) => {
207 | return () => {
208 | // slide out
209 | this.slideOutAndDismiss(200);
210 |
211 | // Run callback
212 | if (callback) callback();
213 | };
214 | }
215 |
216 | clearTimerIfExist = () => {
217 | const { slideOutTimer } = this.state;
218 | if (slideOutTimer) clearTimeout(slideOutTimer);
219 | }
220 |
221 | slideIn = (duration) => {
222 | // Animate "this.state.containerSlideOffsetY"
223 | const { containerSlideOffsetY } = this.state; // Using the new one is fine
224 | Animated.timing(containerSlideOffsetY, { toValue: 1, duration: duration || 400, useNativeDriver: false }) // TODO: customize
225 | .start(({finished}) => {
226 | this.countdownToSlideOut();
227 | });
228 | }
229 |
230 | countdownToSlideOut = () => {
231 | const slideOutTimer = setTimeout(() => {
232 | this.slideOutAndDismiss();
233 | }, this.state.slideOutTime);
234 | this.setState({ slideOutTimer });
235 | }
236 |
237 | slideOutAndDismiss = (duration) => {
238 | const { containerSlideOffsetY } = this.state;
239 |
240 | // Reset animation to 0 && show it && animate
241 | Animated.timing(containerSlideOffsetY, { toValue: 0, duration: duration || 400, useNativeDriver: false }) // TODO: customize
242 | .start(({finished}) => {
243 | // Reset everything and hide the popup
244 | this.setState({ show: false });
245 | });
246 | }
247 |
248 | // Public method
249 | show = (messageConfig) => {
250 | this.clearTimerIfExist();
251 |
252 | // Put message configs into state && show popup
253 | const _messageConfig = messageConfig || {};
254 | const {
255 | onPress: onPressCallback,
256 | appIconSource,
257 | appTitle,
258 | timeText,
259 | title,
260 | body,
261 | slideOutTime
262 | } = _messageConfig;
263 | const onPressAndSlideOut = this.createOnPressWithCallback(onPressCallback);
264 | this.setState({
265 | show: true,
266 | containerSlideOffsetY: new Animated.Value(0),
267 | slideOutTimer: null,
268 | containerDragOffsetY: new Animated.Value(0),
269 | containerScale: new Animated.Value(1),
270 | onPressAndSlideOut,
271 | appIconSource,
272 | appTitle,
273 | timeText,
274 | title,
275 | body,
276 | slideOutTime: typeof slideOutTime !== 'number' ? 4000 : slideOutTime
277 | }, this.slideIn);
278 | }
279 | }
280 |
281 | const styles = StyleSheet.create({
282 | popupContainer: {
283 | position: 'absolute',
284 | width: deviceWidth - (HORIZONTAL_MARGIN * 2),
285 | left: HORIZONTAL_MARGIN,
286 | right: HORIZONTAL_MARGIN,
287 | // top: CONTAINER_MARGIN_TOP, // Refactored as dynamic style
288 | },
289 |
290 | popupContentContainer: {
291 | backgroundColor: 'white', // TEMP
292 | borderRadius: 12,
293 | minHeight: 86,
294 | // === Shadows ===
295 | // Android
296 | elevation: 2,
297 | // iOS
298 | shadowColor: '#000000',
299 | shadowOpacity: 0.5,
300 | shadowRadius: 3,
301 | shadowOffset: {
302 | height: 1,
303 | width: 0,
304 | },
305 | },
306 |
307 | popupHeaderContainer: {
308 | height: 32,
309 | backgroundColor: '#F1F1F1', // TEMP
310 | borderTopLeftRadius: 12,
311 | borderTopRightRadius: 12,
312 | paddingVertical: 6,
313 | flexDirection: 'row',
314 | justifyContent: 'space-between',
315 | alignItems: 'center',
316 | },
317 | headerIconContainer: {
318 | height: 20,
319 | width: 20,
320 | marginLeft: 12,
321 | marginRight: 8,
322 | borderRadius: 4,
323 | },
324 | headerIcon: {
325 | height: 20,
326 | width: 20,
327 | resizeMode: 'contain',
328 | },
329 | headerTextContainer: {
330 | flex: 1,
331 | },
332 | headerText: {
333 | fontSize: 13,
334 | color: '#808080',
335 | lineHeight: 20,
336 | },
337 | headerTimeContainer: {
338 | marginHorizontal: 16,
339 | },
340 | headerTime: {
341 | fontSize: 12,
342 | color: '#808080',
343 | lineHeight: 14,
344 | },
345 | contentContainer: {
346 | width: '100%',
347 | paddingTop: 8,
348 | paddingBottom: 10,
349 | paddingHorizontal: 16,
350 | },
351 | contentTitleContainer: {
352 | },
353 | contentTitle: {
354 | fontSize: 15,
355 | lineHeight: 18,
356 | color: 'black',
357 | },
358 | contentTextContainer: {
359 | },
360 | contentText: {
361 | fontSize: 12,
362 | lineHeight: 14,
363 | color: '#808080',
364 | marginTop: 5,
365 | },
366 | });
367 |
--------------------------------------------------------------------------------