@inngageregistry/inngage-react 4.0.0-alpha.1 → 4.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inngageregistry/inngage-react",
3
- "version": "4.0.0-alpha.01",
3
+ "version": "4.0.0-alpha.2",
4
4
  "description": "Inngage Plugin for React Native applications for marketing campaign optimization using Push Notification.",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.d.ts",
package/src/Inngage.ts CHANGED
@@ -3,7 +3,7 @@ import DeviceInfo from "react-native-device-info";
3
3
  import * as RNLocalize from "react-native-localize";
4
4
  import AsyncStorage from '@react-native-async-storage/async-storage';
5
5
 
6
- import { addUserDataRequest, eventRequest, formatDate } from "./utils";
6
+ import { formatDate } from "./utils";
7
7
  import { InngageNotificationMessage } from "./firebase/notifications_listener";
8
8
  import { InngageProperties } from './models/inngage_properties';
9
9
 
@@ -130,7 +130,6 @@ class Inngage {
130
130
  const appUpdatedIn = formatDate(lastUpdateTime);
131
131
 
132
132
  const subscription = {
133
- ...SUBSCRIBE_REQUEST,
134
133
  registerSubscriberRequest: {
135
134
  app_token: appToken,
136
135
  identifier: friendlyIdentifier,
@@ -166,8 +165,7 @@ class Inngage {
166
165
 
167
166
  const registration = await getFirebaseAccess();
168
167
 
169
- const request = eventRequest({
170
- ...EVENT_REQUEST,
168
+ const request = {
171
169
  newEventRequest: {
172
170
  appToken: InngageProperties.appToken,
173
171
  identifier: InngageProperties.identifier,
@@ -178,7 +176,7 @@ class Inngage {
178
176
  conversionNotId,
179
177
  eventValues,
180
178
  },
181
- });
179
+ };
182
180
 
183
181
  return inngage.apiService.sendEvent(request);
184
182
  }
@@ -186,14 +184,13 @@ class Inngage {
186
184
  static async addUserData(customFields: any): Promise<any> {
187
185
  const inngage = Inngage.getInstance();
188
186
 
189
- const request = addUserDataRequest({
190
- ...USER_DATA_REQUEST,
187
+ const request = {
191
188
  fieldsRequest: {
192
189
  appToken: InngageProperties.appToken,
193
190
  identifier: InngageProperties.identifier,
194
191
  customField: customFields,
195
192
  },
196
- });
193
+ };
197
194
 
198
195
  return inngage.apiService.addUserData(request);
199
196
  }
@@ -1,35 +1,94 @@
1
- import React from 'react';
1
+ import * as React from "react";
2
2
  import {
3
- Modal,
4
3
  View,
5
- StyleSheet,
6
4
  Text,
7
5
  TouchableOpacity,
8
6
  ImageBackground,
9
- Platform,
10
- PlatformColor,
11
7
  } from "react-native";
12
8
 
13
- export function InAppContainer(data: any): JSX.Element {
14
- const styles = buildStyles(data)
9
+ import AsyncStorage from "@react-native-async-storage/async-storage";
10
+
11
+ import { buildStyles } from "./styles";
12
+ import { Modal } from "../components/modal";
13
+
14
+ interface InAppData {
15
+ inapp_message: boolean
16
+ title: string
17
+ body: string
18
+ title_font_color: string
19
+ body_font_color: string
20
+ background_color: string
21
+ btn_left_txt_color: string
22
+ btn_left_bg_color: string
23
+ btn_right_txt_color: string
24
+ btn_right_bg_color: string
25
+ background_image: string
26
+ btn_left_text: string
27
+ btn_left_action_type: string
28
+ btn_left_action_link: string
29
+ btn_right_text: string
30
+ btn_right_action_type: string
31
+ btn_right_action_link: string
32
+ rick_content: string
33
+ inpression: string
34
+ bg_img_action_type: string
35
+ bg_img_action_link: string
36
+ dot_color: string
37
+ }
38
+
39
+ export function InAppContainer(): JSX.Element {
40
+ const [data, setData] = React.useState<InAppData>();
41
+ const [visible, setVisible] = React.useState(false);
42
+
43
+ React.useEffect(() => {
44
+ const fetchAdditionalData = async () => {
45
+ try {
46
+ const data = await AsyncStorage.getItem('inapp');
47
+ if (data) {
48
+ const parsedData = JSON.parse(data);
49
+
50
+ setVisible(true);
51
+ setData(parsedData);
52
+ }
53
+ } catch (error) {
54
+ console.error('Error retrieving additionalData from AsyncStorage:', error);
55
+ }
56
+ };
57
+
58
+ fetchAdditionalData();
59
+ }, []);
60
+
61
+ const styles = buildStyles(data);
62
+
63
+ const handleDismissInApp = async () => {
64
+ await AsyncStorage.removeItem('inapp');
65
+ setVisible(false);
66
+ }
67
+
68
+ if (data != null)
69
+ console.log('data in app', data)
70
+
15
71
  return (
16
72
  <Modal
17
73
  renderToHardwareTextureAndroid={true}
18
74
  transparent={true}
19
- visible={true}>
20
- <ImageBackground style={styles.content} source={{ uri: "" }}>
75
+ visible={visible}>
76
+ <ImageBackground style={styles.content} source={{ uri: data?.background_image }}>
77
+ <TouchableOpacity style={styles.closeButton} onPress={handleDismissInApp}>
78
+ <Text>X</Text>
79
+ </TouchableOpacity>
21
80
  <View style={styles.messageData}>
22
- <Text style={styles.title}>data.title</Text>
23
- <Text style={styles.body}>Corpo</Text>
81
+ <Text style={styles.title}>{data?.title}</Text>
82
+ <Text style={styles.body}>{data?.body}</Text>
24
83
  </View>
25
84
  <View style={styles.footer}>
26
85
  <TouchableOpacity
27
- style={styles.button}>
28
- <Text style={styles.textButton}>Teste</Text>
86
+ style={styles.buttonLeft}>
87
+ <Text style={styles.textButton}>{data?.btn_left_text}</Text>
29
88
  </TouchableOpacity>
30
89
  <TouchableOpacity
31
- style={styles.button}>
32
- <Text style={styles.textButton}>Teste</Text>
90
+ style={styles.buttonRight}>
91
+ <Text style={styles.textButton}>{data?.btn_right_text}</Text>
33
92
  </TouchableOpacity>
34
93
  </View>
35
94
  </ImageBackground>
@@ -37,81 +96,7 @@ export function InAppContainer(data: any): JSX.Element {
37
96
  )
38
97
  }
39
98
 
40
- const buildStyles = ({ data }) => StyleSheet.create({
41
- content: Platform.select({
42
- android: {
43
- backgroundColor: PlatformColor("?attr/colorBackgroundFloating"),
44
- flexDirection: "column",
45
- borderRadius: 3,
46
- padding: 16,
47
- margin: 16,
48
- overflow: "hidden",
49
- elevation: 10,
50
- minWidth: 300,
51
- alignItems: "center"
52
- },
53
- default: {}
54
- }),
55
- messageData: Platform.select({
56
- android: {
57
- marginTop: 12
58
- },
59
- default: {}
60
- }),
61
- footer: {
62
- flexDirection: "row",
63
- ...Platform.select({
64
- android: {
65
- alignItems: "center",
66
- justifyContent: "flex-end",
67
- marginTop: 4,
68
- },
69
- default: {},
70
- }),
71
- },
72
- title: Platform.select({
73
- android: {
74
- color: PlatformColor(
75
- `@android:color/${"primary_text_light"}`
76
- ),
77
- fontWeight: "500",
78
- fontSize: 18,
79
- },
80
-
81
- default: {},
82
- }),
83
- body: Platform.select({
84
- android: {
85
- color: PlatformColor(
86
- `@android:color/${"secondary_text_light"}`
87
- ),
88
- fontSize: 16,
89
- marginTop: 10,
90
- marginBottom: 10,
91
- },
92
- default: {},
93
- }),
94
- button: Platform.select({
95
- android: {
96
- margin: 2,
97
- flex: 1,
98
- backgroundColor: "red",
99
- justifyContent: "center",
100
- alignItems: "center",
101
- },
102
- default: {},
103
- }),
104
- textButton: Platform.select({
105
- android: {
106
- color: PlatformColor(
107
- `@android:color/${"secondary_text_light"}`
108
- ),
109
- textAlign: "center",
110
- backgroundColor: "transparent",
111
- padding: 8,
112
- fontSize: 14,
113
- textTransform: "uppercase",
114
- },
115
- default: {},
116
- }),
117
- })
99
+
100
+
101
+
102
+
@@ -0,0 +1,221 @@
1
+ import * as React from "react";
2
+ import {
3
+ Animated, Easing,
4
+ Modal as ReactNativeModal,
5
+ ModalProps as ReactNativeModalProps,
6
+ Platform,
7
+ StyleProp,
8
+ StyleSheet,
9
+ TouchableWithoutFeedback,
10
+ ViewStyle
11
+ } from "react-native";
12
+ import { Component } from "react";
13
+
14
+ const MODAL_ANIM_DURATION = 300;
15
+ const MODAL_BACKDROP_OPACITY = 0.3;
16
+
17
+ const CONTENT_ANIMATION_IN = Platform.select({
18
+ ios: {
19
+ opacity: {
20
+ inputRange: [0, 1],
21
+ outputRange: [0, 1],
22
+ },
23
+ scale: {
24
+ inputRange: [0, 0.5, 1],
25
+ outputRange: [1.2, 1.1, 1],
26
+ },
27
+ },
28
+ android: {
29
+ opacity: {
30
+ inputRange: [0, 0.5, 1],
31
+ outputRange: [0, 1, 1],
32
+ },
33
+ scale: {
34
+ inputRange: [0, 1],
35
+ outputRange: [0.3, 1],
36
+ },
37
+ },
38
+ default: {
39
+ opacity: {
40
+ inputRange: [0, 0.5, 1],
41
+ outputRange: [0, 1, 1],
42
+ },
43
+ scale: {
44
+ inputRange: [0, 1],
45
+ outputRange: [0.3, 1],
46
+ },
47
+ },
48
+ });
49
+
50
+ const CONTENT_ANIMATION_OUT = Platform.select({
51
+ default: {
52
+ opacity: {
53
+ inputRange: [0, 1],
54
+ outputRange: [0, 1],
55
+ },
56
+ },
57
+ });
58
+
59
+ export interface ModalProps extends ReactNativeModalProps {
60
+ onBackdropPress?: () => void;
61
+ onHide?: () => void;
62
+ visible?: boolean;
63
+ contentStyle?: StyleProp<ViewStyle>;
64
+ useNativeDriver?: boolean;
65
+ }
66
+
67
+ interface ModalState {
68
+ visible: boolean;
69
+ currentAnimation: "none" | "in" | "out";
70
+ }
71
+
72
+ export class Modal extends Component<ModalProps, ModalState> {
73
+ static defaultProps: Partial<ModalProps> = {
74
+ onBackdropPress: () => null,
75
+ onHide: () => null,
76
+ visible: false,
77
+ useNativeDriver: false,
78
+ };
79
+
80
+ state: ModalState = {
81
+ visible: Boolean(this.props.visible),
82
+ currentAnimation: "none",
83
+ };
84
+
85
+ animVal = new Animated.Value(0);
86
+ _isMounted = false;
87
+
88
+ componentDidMount() {
89
+ this._isMounted = true;
90
+ if (this.state.visible) {
91
+ this.show();
92
+ }
93
+ }
94
+
95
+ componentWillUnmount() {
96
+ this._isMounted = false;
97
+ }
98
+
99
+ componentDidUpdate(prevProps: ModalProps) {
100
+ if (this.props.visible && !prevProps.visible) {
101
+ this.show();
102
+ } else if (!this.props.visible && prevProps.visible) {
103
+ this.hide();
104
+ }
105
+ }
106
+
107
+ show = () => {
108
+ this.setState({ visible: true, currentAnimation: "in" }, () => {
109
+ Animated.timing(this.animVal, {
110
+ easing: Easing.inOut(Easing.quad),
111
+ useNativeDriver: Boolean(this.props.useNativeDriver),
112
+ duration: MODAL_ANIM_DURATION,
113
+ toValue: 1,
114
+ }).start(() => {
115
+ this.setState({ currentAnimation: "none" });
116
+ });
117
+ });
118
+ };
119
+
120
+ hide = () => {
121
+ this.setState({ currentAnimation: "out" }, () => {
122
+ Animated.timing(this.animVal, {
123
+ easing: Easing.inOut(Easing.quad),
124
+ useNativeDriver: Boolean(this.props.useNativeDriver),
125
+ duration: MODAL_ANIM_DURATION,
126
+ toValue: 0,
127
+ }).start(() => {
128
+ if (this._isMounted) {
129
+ this.setState({ currentAnimation: "none" });
130
+ this.setState({ visible: false }, this.props.onHide);
131
+ }
132
+ });
133
+ });
134
+ };
135
+
136
+ render() {
137
+ const {
138
+ children,
139
+ onBackdropPress,
140
+ contentStyle,
141
+ ...otherProps
142
+ } = this.props;
143
+ const { currentAnimation, visible } = this.state;
144
+
145
+ const backdropAnimatedStyle = {
146
+ opacity: this.animVal.interpolate({
147
+ inputRange: [0, 1],
148
+ outputRange: [0, MODAL_BACKDROP_OPACITY],
149
+ }),
150
+ };
151
+
152
+ const contentAnimatedStyle =
153
+ currentAnimation === "in"
154
+ ? {
155
+ opacity: this.animVal.interpolate({
156
+ inputRange: CONTENT_ANIMATION_IN.opacity.inputRange,
157
+ outputRange: CONTENT_ANIMATION_IN.opacity.outputRange,
158
+ extrapolate: "clamp",
159
+ }),
160
+ transform: [
161
+ {
162
+ scale: this.animVal.interpolate({
163
+ inputRange: CONTENT_ANIMATION_IN.scale.inputRange,
164
+ outputRange: CONTENT_ANIMATION_IN.scale.outputRange,
165
+ extrapolate: "clamp",
166
+ }),
167
+ },
168
+ ],
169
+ }
170
+ : {
171
+ opacity: this.animVal.interpolate({
172
+ inputRange: CONTENT_ANIMATION_OUT.opacity.inputRange,
173
+ outputRange: CONTENT_ANIMATION_OUT.opacity.outputRange,
174
+ extrapolate: "clamp",
175
+ }),
176
+ };
177
+
178
+ return (
179
+ <ReactNativeModal
180
+ transparent
181
+ animationType="none"
182
+ {...otherProps}
183
+ visible={visible}
184
+ >
185
+ <TouchableWithoutFeedback onPress={onBackdropPress}>
186
+ <Animated.View style={[styles.backdrop, backdropAnimatedStyle]} />
187
+ </TouchableWithoutFeedback>
188
+ {visible && (
189
+ <Animated.View
190
+ style={[styles.content, contentAnimatedStyle, contentStyle]}
191
+ pointerEvents="box-none"
192
+ needsOffscreenAlphaCompositing={["in", "out"].includes(
193
+ currentAnimation
194
+ )}
195
+ >
196
+ {children}
197
+ </Animated.View>
198
+ )}
199
+ </ReactNativeModal>
200
+ );
201
+ }
202
+ }
203
+
204
+ const styles = StyleSheet.create({
205
+ backdrop: {
206
+ position: "absolute",
207
+ top: 0,
208
+ bottom: 0,
209
+ left: 0,
210
+ right: 0,
211
+ backgroundColor: "black",
212
+ opacity: 0,
213
+ },
214
+ content: {
215
+ flex: 1,
216
+ alignItems: "center",
217
+ justifyContent: "center",
218
+ },
219
+ });
220
+
221
+ export default Modal;
@@ -1,97 +1,97 @@
1
- import { StyleSheet } from "react-native";
1
+ import { Platform, PlatformColor, StyleSheet } from "react-native";
2
2
 
3
3
 
4
- interface styleInappProps {
5
- SLIDER_WIDTH: number;
6
- }
7
- export const styleInapp = ({ SLIDER_WIDTH }: styleInappProps) => StyleSheet.create({
8
- carouselContainer: {
9
- width: "100%",
10
- marginTop: 10,
11
- },
12
- styleContainer: {
13
- backgroundColor: 'white',
14
- elevation: 10,
15
- borderRadius: 10,
16
- width: SLIDER_WIDTH * 0.8,
17
- height: 480,
18
- },
19
- counter: {
20
- alignSelf: 'center',
21
- marginVertical: 10
22
- },
4
+ export const buildStyles = (data: any) => StyleSheet.create({
23
5
  closeButton: {
24
6
  position: 'absolute',
25
- alignSelf: 'flex-end',
26
- right: 20,
27
- top: 20,
28
- width: 30,
29
- height: 30,
30
- borderRadius: 20,
31
- backgroundColor: '#00000020',
32
- justifyContent: 'center',
33
- alignItems: 'center',
34
- zIndex: 90,
35
- },
36
- });
37
-
38
- interface styleItemProps {
39
- msg: any,
40
- checkBG: Function,
41
- }
42
- export const styleItem = ({ msg, checkBG }: styleItemProps) => StyleSheet.create({
43
- btn_left: {
44
- backgroundColor: msg.btn_left_bg_color || "#FFFFFF",
45
- height: 40,
46
- width: 100,
47
- alignItems: 'center',
48
- justifyContent: 'center',
49
- marginRight: 20,
50
- marginLeft: 10,
51
- flex: 1
52
- },
53
- btn_right: {
54
- backgroundColor: msg.btn_right_bg_color || "#FFFFFF",
55
- height: 40,
56
- width: 100,
57
- alignItems: 'center',
58
- justifyContent: 'center',
59
- marginRight: 10,
60
- flex: 1
7
+ top: 10,
8
+ right: 10,
9
+ zIndex: 10,
61
10
  },
62
- btn_left_title: {
63
- color: msg.btn_left_txt_color || "#000000"
11
+ content: Platform.select({
12
+ android: {
13
+ backgroundColor: data?.background_color,
14
+ flexDirection: "column",
15
+ borderRadius: 3,
16
+ padding: 16,
17
+ margin: 16,
18
+ overflow: "hidden",
19
+ elevation: 10,
20
+ minWidth: 300,
21
+ alignItems: "center"
22
+ },
23
+ default: {}
24
+ }),
25
+ messageData: Platform.select({
26
+ android: {
27
+ marginTop: 12
28
+ },
29
+ default: {}
30
+ }),
31
+ footer: {
32
+ flexDirection: "row",
33
+ ...Platform.select({
34
+ android: {
35
+ alignItems: "center",
36
+ justifyContent: "flex-end",
37
+ marginTop: 4,
38
+ },
39
+ default: {},
40
+ }),
64
41
  },
65
- btn_right_title: {
66
- color: msg.btn_right_txt_color || "#000000"
67
- },
68
- body: {
69
- backgroundColor: checkBG(),
70
- width: '100%',
71
- height: 450,
72
- alignSelf: 'center',
73
- alignItems: 'center',
74
- justifyContent: 'center',
75
- },
76
- bodyText: {
77
- color: msg.body_font_color || "#000000",
78
- textAlign: 'justify',
79
- marginBottom: 10,
80
- fontSize: 15,
81
- marginHorizontal: 10
82
- },
83
- title: {
84
- color: msg.title_font_color || "#000000",
85
- fontWeight: "bold",
86
- fontSize: 18,
87
- marginTop: 40
88
- },
89
- dot: {
90
- backgroundColor: msg.dot_color || "#FFFFFF",
91
- borderRadius: 100,
92
- width: 8,
93
- height: 8,
94
- marginLeft: 5,
95
- elevation: 5,
96
- }
97
- });
42
+ title: Platform.select({
43
+ android: {
44
+ color: PlatformColor(
45
+ `@android:color/${"primary_text_light"}`
46
+ ),
47
+ fontWeight: "500",
48
+ fontSize: 18,
49
+ },
50
+
51
+ default: {},
52
+ }),
53
+ body: Platform.select({
54
+ android: {
55
+ color: PlatformColor(
56
+ `@android:color/${"secondary_text_light"}`
57
+ ),
58
+ fontSize: 16,
59
+ marginTop: 10,
60
+ marginBottom: 10,
61
+ },
62
+ default: {},
63
+ }),
64
+ buttonLeft: Platform.select({
65
+ android: {
66
+ margin: 2,
67
+ flex: 1,
68
+ backgroundColor: data?.btn_left_bg_color,
69
+ justifyContent: "center",
70
+ alignItems: "center",
71
+ },
72
+ default: {},
73
+ }),
74
+ buttonRight: Platform.select({
75
+ android: {
76
+ margin: 2,
77
+ flex: 1,
78
+ backgroundColor: data?.btn_right_bg_color,
79
+ justifyContent: "center",
80
+ alignItems: "center",
81
+ },
82
+ default: {},
83
+ }),
84
+ textButton: Platform.select({
85
+ android: {
86
+ color: PlatformColor(
87
+ `@android:color/${"secondary_text_light"}`
88
+ ),
89
+ textAlign: "center",
90
+ backgroundColor: "transparent",
91
+ padding: 8,
92
+ fontSize: 14,
93
+ textTransform: "uppercase",
94
+ },
95
+ default: {},
96
+ }),
97
+ })
@@ -34,21 +34,27 @@ export const InngageNotificationMessage = (firebaseListenCallback?: any) => {
34
34
  })
35
35
  useEffect(() => {
36
36
  const handleMessage = async (remoteMessage: any) => {
37
+ const notificationData = remoteMessage.data;
38
+
37
39
  if (InngageProperties.getDebugMode())
38
40
  console.log('Remote message received in foreground: ', remoteMessage);
39
41
 
40
42
  if (firebaseListenCallback != null && remoteMessage != null)
41
- firebaseListenCallback(remoteMessage.data)
43
+ firebaseListenCallback(notificationData)
42
44
 
43
45
  PushNotification.localNotification({
44
46
  autoCancel: true,
45
47
  bigPictureUrl: remoteMessage.notification?.android?.imageUrl,
46
48
  largeIconUrl: remoteMessage.notification?.android?.imageUrl,
47
- title: remoteMessage.data?.title,
48
- message: remoteMessage.data?.body,
49
+ title: notificationData?.title,
50
+ message: notificationData?.body,
49
51
  vibration: 300,
50
52
  channelId: "channel_id",
51
53
  });
54
+
55
+ if (notificationData.additional_data != null)
56
+ await AsyncStorage.setItem('inapp', notificationData.additional_data);
57
+
52
58
  };
53
59
 
54
60
  return messaging().onMessage(handleMessage);
@@ -56,12 +62,17 @@ export const InngageNotificationMessage = (firebaseListenCallback?: any) => {
56
62
 
57
63
  useEffect(() => {
58
64
  const backgroundHandler = async (remoteMessage: any) => {
65
+ const notificationData = remoteMessage.data;
66
+
59
67
  if (InngageProperties.getDebugMode())
60
68
  console.log('Remote message received in background: ', remoteMessage);
61
69
 
62
70
  if (remoteMessage != null)
63
71
  firebaseListenCallback(remoteMessage.data)
64
72
 
73
+ if (notificationData.additional_data != null)
74
+ await AsyncStorage.setItem('inapp', notificationData.additional_data);
75
+
65
76
  messaging().onNotificationOpenedApp(remoteMessage => {
66
77
  handleNotification(remoteMessage)
67
78
  })
@@ -72,6 +83,7 @@ export const InngageNotificationMessage = (firebaseListenCallback?: any) => {
72
83
 
73
84
  useEffect(() => {
74
85
  messaging().getInitialNotification().then(async (value) => {
86
+
75
87
  if (value !== null)
76
88
  handleUniqueRemoteMessage(value);
77
89
  });
package/src/utils.ts CHANGED
@@ -24,30 +24,6 @@ export const showAlertLink = (title: string, body: string, appName: string, link
24
24
  })
25
25
  }
26
26
 
27
- export const subscriptionRequestAdapter = (sdkRequest, useCustomData, customData) => {
28
- if (useCustomData) {
29
- return {
30
- registerSubscriberRequest: {
31
- ...sdkRequest.registerSubscriberRequest,
32
- custom_field: customData
33
- }
34
- }
35
- }
36
- return {
37
- registerSubscriberRequest: {
38
- ...sdkRequest.registerSubscriberRequest
39
- }
40
- };
41
- }
42
-
43
- export const eventRequest = (request) => {
44
- return { ...request }
45
- }
46
-
47
- export const addUserDataRequest = (request) => {
48
- return { ...request }
49
- }
50
-
51
27
  export const isEmpty = (obj: any) => {
52
28
  for (var prop in obj) {
53
29
  if (obj.hasOwnProperty(prop)) {