@inngageregistry/inngage-react 3.3.0-alpha.1 → 3.3.0-alpha.10

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": "3.3.0-alpha.1",
3
+ "version": "3.3.0-alpha.10",
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",
@@ -28,7 +28,7 @@
28
28
  "react-native-localize": "^2.2.6",
29
29
  "react-native-permissions": "^3.8.0",
30
30
  "react-native-push-notification": "^8.1.1",
31
- "react-native-reanimated-carousel": "^3.5.1"
31
+ "react-native-snap-carousel": "^3.9.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/jest": "^29.5.1",
package/src/Inngage.ts CHANGED
@@ -3,9 +3,10 @@ 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 { eventRequest, formatDate, subscriptionRequestAdapter } from "./utils";
6
+ import { addUserDataRequest, eventRequest, formatDate, subscriptionRequestAdapter } from "./utils";
7
7
  import notificationsListener, { notificationsListenerProps } from "./notificationsListener";
8
- import { subscriptionApi, eventsApi } from "./services/inngage";
8
+ import { subscriptionApi, eventsApi, addUserDataApi } from "./services/inngage";
9
+ import { InngageProperties } from './models/inngage_properties';
9
10
 
10
11
  import RNPermissions, { NotificationOption, RESULTS } from 'react-native-permissions';
11
12
 
@@ -92,6 +93,7 @@ const Inngage = {
92
93
  return { subscribed: false };
93
94
  }
94
95
  },
96
+
95
97
  Subscribe: async ({
96
98
  appToken,
97
99
  dev,
@@ -101,6 +103,9 @@ const Inngage = {
101
103
  phoneNumber,
102
104
  email,
103
105
  }: SubscriptionProps) => {
106
+ InngageProperties.appToken = appToken;
107
+ InngageProperties.identifier = friendlyIdentifier!;
108
+
104
109
  try {
105
110
  const respToken = await getFirebaseAccess()
106
111
 
@@ -177,6 +182,23 @@ const Inngage = {
177
182
  console.error(e)
178
183
  return { subscribed: false };
179
184
  }
185
+ },
186
+
187
+ async addUserData(customFields: any): Promise<any> {
188
+ const rawRequest = {
189
+ fieldsRequest: {
190
+ app_token: InngageProperties.appToken,
191
+ identifier: InngageProperties.identifier,
192
+ custom_field: customFields ?? {}
193
+ }
194
+ };
195
+
196
+ const request = addUserDataRequest(rawRequest);
197
+ try {
198
+ return await addUserDataApi(request);
199
+ } catch (e) {
200
+ console.error(e);
201
+ }
180
202
  }
181
203
  }
182
204
 
@@ -10,11 +10,10 @@ import {
10
10
  ImageBackground,
11
11
  Linking,
12
12
  Dimensions,
13
- Button,
14
13
  } from "react-native";
15
14
  import DeviceInfo from "react-native-device-info";
16
15
  import AsyncStorage from '@react-native-async-storage/async-storage';
17
- // import Carousel, { Pagination } from 'react-native-snap-carousel';
16
+ import Carousel, { Pagination } from 'react-native-snap-carousel';
18
17
 
19
18
  import { showAlertLink, isEmpty } from "../utils";
20
19
  import { linkInApp } from "../notificationsListener";
@@ -22,7 +21,6 @@ import { styleInapp, styleItem } from './styles';
22
21
 
23
22
  const SLIDER_WIDTH = Dimensions.get('window').width;
24
23
  const SLIDER_HEIGHT = Dimensions.get('window').height;
25
-
26
24
  const ITEM_WIDTH = Math.round(SLIDER_WIDTH * 0.8);
27
25
  const ITEM_HEIGHT = Math.round(SLIDER_HEIGHT * 0.8);
28
26
 
@@ -43,15 +41,11 @@ export const Inapp = (props: InappProps) => {
43
41
  const [visible, setVisible] = useState(true)
44
42
  const [bgImage, setbgImage] = useState<any>(undefined) // TODO, need a placeholder
45
43
 
46
- // const CarouselRef = useRef<Carousel<any>>(null);
44
+ const CarouselRef = useRef<Carousel<any>>(null);
47
45
  const ScrollRef1 = useRef<ScrollView>(null);
48
46
  const ScrollRef2 = useRef<ScrollView>(null);
49
47
  const ScrollRef3 = useRef<ScrollView>(null);
50
48
 
51
- // const inAppData = JSON.parse(data[0].data.additional_data)
52
- // const styles = styleInapp({ inAppData, SLIDER_WIDTH })
53
- // const itemStyles = styleItem({ inAppData })
54
-
55
49
 
56
50
  interface _renderItemProps {
57
51
  item: any;
@@ -70,7 +64,7 @@ export const Inapp = (props: InappProps) => {
70
64
  }
71
65
  }
72
66
 
73
- // const itemStyles = styleItem({ msg, checkBG })
67
+ const itemStyles = styleItem({ msg, checkBG })
74
68
 
75
69
  const chooseRef = () => {
76
70
  if (index == 0) {
@@ -85,46 +79,46 @@ export const Inapp = (props: InappProps) => {
85
79
  }
86
80
 
87
81
 
88
- // const pagination = ref => {
89
- // return (
90
- // <Pagination
91
- // dotsLength={arrayImgs.length}
92
- // activeDotIndex={indIm}
93
- // containerStyle={{ height: 2, padding: 0, margin: 0 }}
94
- // renderDots={(activeIndex, total, context) => {
95
- // let dots: any = []
96
- // var size = 0
97
- // for (let i = 0; i < total; i++) {
98
- // if (activeIndex == i) {
99
- // size = 13
100
- // } else {
101
- // size = 8
102
- // }
103
- // dots.push(
104
- // <TouchableOpacity
105
- // onPress={() => {
106
- // ref.current.scrollTo({ x: i * 220, y: 0, animated: true })
107
- // if (i * 220 === 0) {
108
- // setIndImg(0)
109
- // } else if (i * 220 === 220) {
110
- // setIndImg(1)
111
- // } else if (i * 220 === 440) {
112
- // setIndImg(2)
113
- // }
114
- // }}
115
- // key={i.toString()}
116
- // // style={[itemStyles.dot, { width: size, height: size }]}
117
- // />
118
- // )
119
- // }
120
- // return (
121
- // dots
122
- // )
123
- // }
124
- // }
125
- // />
126
- // );
127
- // }
82
+ const pagination = ref => {
83
+ return (
84
+ <Pagination
85
+ dotsLength={arrayImgs.length}
86
+ activeDotIndex={indIm}
87
+ containerStyle={{ height: 2, padding: 0, margin: 0 }}
88
+ renderDots={(activeIndex, total, context) => {
89
+ let dots: any = []
90
+ var size = 0
91
+ for (let i = 0; i < total; i++) {
92
+ if (activeIndex == i) {
93
+ size = 13
94
+ } else {
95
+ size = 8
96
+ }
97
+ dots.push(
98
+ <TouchableOpacity
99
+ onPress={() => {
100
+ ref.current.scrollTo({ x: i * 220, y: 0, animated: true })
101
+ if (i * 220 === 0) {
102
+ setIndImg(0)
103
+ } else if (i * 220 === 220) {
104
+ setIndImg(1)
105
+ } else if (i * 220 === 440) {
106
+ setIndImg(2)
107
+ }
108
+ }}
109
+ key={i.toString()}
110
+ style={[itemStyles.dot, { width: size, height: size }]}
111
+ />
112
+ )
113
+ }
114
+ return (
115
+ dots
116
+ )
117
+ }
118
+ }
119
+ />
120
+ );
121
+ }
128
122
 
129
123
  const handleButton = (title: string, body: string, url: string, type: string) => {
130
124
  if (type === '' || url === '') {
@@ -171,11 +165,10 @@ export const Inapp = (props: InappProps) => {
171
165
  )
172
166
  }
173
167
  }
174
-
175
- // return (
176
- // <View style={[itemStyles.body]}>
177
- // <Text style={[itemStyles.title, props.titleStyle]}>{msg.title}</Text>
178
- {/* <ScrollView
168
+ return (
169
+ <View style={[itemStyles.body]}>
170
+ <Text style={[itemStyles.title, props.titleStyle]}>{msg.title}</Text>
171
+ <ScrollView
179
172
  ref={chooseRef()}
180
173
  horizontal
181
174
  snapToInterval={220}
@@ -207,26 +200,22 @@ export const Inapp = (props: InappProps) => {
207
200
  msg.rich_content.carousel == true ?
208
201
  pagination(chooseRef()) : null
209
202
 
210
- } */}
211
- /* <Text style={[itemStyles.bodyText, props.bodyStyle]}>{msg.body}</Text>
212
- <View style={{ flexDirection: "row", marginBottom: 0, justifyContent: 'center' }}>
213
- {msg.btn_left_txt != null
214
- ? <TouchableOpacity onPress={() => handleButton(msg.title, msg.body, msg.btn_left_action_link, msg.btn_left_action_type)} style={[itemStyles.btn_left, props.buttonLeftStyle]}>
215
- <View>
216
- <Text style={[itemStyles.btn_left_title, props.buttonTitleLeftStyle]}>{msg.btn_left_txt}</Text>
217
- </View>
218
- </TouchableOpacity>
219
- : null}
220
- {msg.btn_left_txt != null
221
- ? <TouchableOpacity onPress={() => handleButton(msg.title, msg.body, msg.btn_right_action_link, msg.btn_right_action_type)} style={[itemStyles.btn_right, props.buttonRightStyle]}>
222
- <View>
223
- <Text style={[itemStyles.btn_right_title, props.buttonTitleRightStyle]}>{msg.btn_right_txt}</Text>
224
- </View>
225
- </TouchableOpacity>
226
- : null}
227
- </View> */
228
- // </View>
229
- // );
203
+ }
204
+ <Text style={[itemStyles.bodyText, props.bodyStyle]}>{msg.body}</Text>
205
+ <View style={{ flexDirection: "row", marginBottom: 0, justifyContent: 'center' }}>
206
+ <TouchableOpacity onPress={() => handleButton(msg.title, msg.body, msg.btn_left_action_link, msg.btn_left_action_type)} style={[itemStyles.btn_left, props.buttonLeftStyle]}>
207
+ <View>
208
+ <Text style={[itemStyles.btn_left_title, props.buttonTitleLeftStyle]}>{msg.btn_left_txt}</Text>
209
+ </View>
210
+ </TouchableOpacity>
211
+ <TouchableOpacity onPress={() => handleButton(msg.title, msg.body, msg.btn_right_action_link, msg.btn_right_action_type)} style={[itemStyles.btn_right, props.buttonRightStyle]}>
212
+ <View>
213
+ <Text style={[itemStyles.btn_right_title, props.buttonTitleRightStyle]}>{msg.btn_right_txt}</Text>
214
+ </View>
215
+ </TouchableOpacity>
216
+ </View>
217
+ </View>
218
+ );
230
219
  }
231
220
 
232
221
  useEffect(() => {
@@ -279,65 +268,26 @@ export const Inapp = (props: InappProps) => {
279
268
  }
280
269
 
281
270
  if (data.length > 0) {
282
- const inAppData = JSON.parse(data[0].data.additional_data)
283
- const styles = styleInapp({ inAppData, SLIDER_WIDTH })
284
- const itemStyles = styleItem({ inAppData })
285
-
286
- const { width: screenWidth } = Dimensions.get('window')
287
-
288
- const sliderWidth = screenWidth;
289
- const itemWidth = screenWidth * 8;
290
-
291
- const _renderItemCarrousel = ({ item }) => {
292
- <Image style={itemStyles.img} source={{ uri: inAppData.rich_content[item] }}></Image>
293
- }
294
-
295
- console.log(inAppData.rich_content)
296
271
  return (
297
- <View style={styles.centeredView}>
272
+ <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
298
273
  <Modal
299
274
  animationType='slide'
300
275
  visible={visible}
301
276
  transparent={true}
302
- >
303
- <View style={styles.centeredView}>
304
- <View style={styles.styleContainer}>
305
- {/* <Carousel
306
- layout='default'
307
- data={data}
308
- renderItem={_renderItemCarrousel}
309
- sliderWidth={sliderWidth}
310
- itemWidth={itemWidth}
311
- /> */}
312
- <Text style={itemStyles.title}>{inAppData.title}</Text>
313
- <Text style={itemStyles.bodyText}>{inAppData.body}</Text>
314
- <View style={itemStyles.containerButtons}>
315
- <TouchableOpacity
316
- onPress={() => { }}
317
- style={[itemStyles.btn_left, props.buttonLeftStyle]}>
318
- <View>
319
- <Text style={[itemStyles.btn_left_title, props.buttonTitleLeftStyle]}>{inAppData.btn_left_txt}</Text>
320
- </View>
321
- </TouchableOpacity>
322
- <TouchableOpacity onPress={() => { }} style={[itemStyles.btn_right, props.buttonRightStyle]}>
323
- <View>
324
- <Text style={[itemStyles.btn_right_title, props.buttonTitleRightStyle]}>{inAppData.btn_right_txt}</Text>
325
- </View>
326
- </TouchableOpacity>
327
- </View>
328
-
329
- {/* <ImageBackground style={{ width: '100%', alignItems: 'center', justifyContent: 'center' }} resizeMode='cover' imageStyle={{ borderRadius: 10, alignSelf: 'stretch', height: 480 }} source={bgImage}> */}
330
- <TouchableHighlight
331
- onPress={() => handleClose()}
332
- underlayColor='#cccccc'
333
- style={styles.closeButton}
334
- >
335
- <Text style={{ fontWeight: 'bold', color: '#ffffff' }}>
336
- X
337
- </Text>
338
- </TouchableHighlight>
339
-
340
- {/* <Carousel
277
+ style={{ justifyContent: 'center', alignItems: 'center', backgroundColor: 'blue' }}
278
+ ><View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
279
+ <View style={[styles.styleContainer, props.styleContainer]}>
280
+ <ImageBackground style={{ width: '100%', alignItems: 'center', justifyContent: 'center' }} resizeMode='cover' imageStyle={{ borderRadius: 10, alignSelf: 'stretch', height: 480 }} source={bgImage}>
281
+ <TouchableHighlight
282
+ onPress={() => handleClose()}
283
+ underlayColor='#cccccc'
284
+ style={styles.closeButton}
285
+ >
286
+ <Text style={{ fontWeight: 'bold', color: '#ffffff' }}>
287
+ X
288
+ </Text>
289
+ </TouchableHighlight>
290
+ <Carousel
341
291
  vertical
342
292
  ref={CarouselRef}
343
293
  layout={'default'}
@@ -359,16 +309,16 @@ export const Inapp = (props: InappProps) => {
359
309
  renderItem={({ item, index }) => _renderItem({ item, index })}
360
310
  sliderHeight={500}
361
311
  itemHeight={500}
362
- /> */}
363
- {/* </ImageBackground> */}
312
+ />
313
+ </ImageBackground>
364
314
  </View>
365
315
  </View>
366
316
  </Modal>
367
- </View >
317
+ </View>
368
318
  );
369
319
  } else {
370
320
  return null
371
321
  }
372
322
  };
373
323
 
374
-
324
+ const styles = styleInapp({ SLIDER_WIDTH })
@@ -2,30 +2,24 @@ import { StyleSheet } from "react-native";
2
2
 
3
3
 
4
4
  interface styleInappProps {
5
- inAppData: any;
6
5
  SLIDER_WIDTH: number;
7
6
  }
8
- export const styleInapp = ({ inAppData, SLIDER_WIDTH }: styleInappProps) => StyleSheet.create({
9
- centeredView: {
10
- flex: 1,
11
- justifyContent: 'center',
12
- alignItems: 'center',
13
- },
7
+ export const styleInapp = ({ SLIDER_WIDTH }: styleInappProps) => StyleSheet.create({
14
8
  carouselContainer: {
15
9
  width: "100%",
16
10
  marginTop: 10,
17
11
  },
18
12
  styleContainer: {
19
- alignItems: 'center',
20
- backgroundColor: inAppData.background_color || 'white',
13
+ backgroundColor: 'white',
21
14
  elevation: 10,
22
15
  borderRadius: 10,
23
16
  width: SLIDER_WIDTH * 0.8,
17
+ height: 480,
18
+ },
19
+ counter: {
20
+ alignSelf: 'center',
21
+ marginVertical: 10
24
22
  },
25
- // counter: {
26
- // alignSelf: 'center',
27
- // marginVertical: 10
28
- // },
29
23
  closeButton: {
30
24
  position: 'absolute',
31
25
  alignSelf: 'flex-end',
@@ -42,66 +36,62 @@ export const styleInapp = ({ inAppData, SLIDER_WIDTH }: styleInappProps) => Styl
42
36
  });
43
37
 
44
38
  interface styleItemProps {
45
- inAppData: any,
39
+ msg: any,
40
+ checkBG: Function,
46
41
  }
47
- export const styleItem = ({ inAppData }: styleItemProps) => StyleSheet.create({
42
+ export const styleItem = ({ msg, checkBG }: styleItemProps) => StyleSheet.create({
48
43
  btn_left: {
49
- width: inAppData.btn_right != '' ? '50%' : '100%',
44
+ backgroundColor: msg.btn_left_bg_color || "#FFFFFF",
45
+ height: 40,
46
+ width: 100,
50
47
  alignItems: 'center',
51
- backgroundColor: inAppData.btn_left_bg_color || '#DDDDDD',
52
- padding: 10,
53
- elevation: 10,
54
- borderRadius: 5,
48
+ justifyContent: 'center',
49
+ marginRight: 20,
50
+ marginLeft: 10,
51
+ flex: 1
55
52
  },
56
53
  btn_right: {
57
- width: inAppData.btn_left != '' ? '50%' : '100%',
54
+ backgroundColor: msg.btn_right_bg_color || "#FFFFFF",
55
+ height: 40,
56
+ width: 100,
58
57
  alignItems: 'center',
59
- backgroundColor: inAppData.btn_right_bg_color || '#DDDDDD',
60
- padding: 10,
61
- elevation: 10,
62
- borderRadius: 5,
58
+ justifyContent: 'center',
59
+ marginRight: 10,
60
+ flex: 1
63
61
  },
64
62
  btn_left_title: {
65
- color: inAppData.btn_left_txt_color || "#000000"
63
+ color: msg.btn_left_txt_color || "#000000"
66
64
  },
67
65
  btn_right_title: {
68
- color: inAppData.btn_right_txt_color || "#000000"
66
+ color: msg.btn_right_txt_color || "#000000"
69
67
  },
70
68
  body: {
71
- backgroundColor: inAppData.background_color,
69
+ backgroundColor: checkBG(),
72
70
  width: '100%',
71
+ height: 450,
73
72
  alignSelf: 'center',
74
73
  alignItems: 'center',
75
74
  justifyContent: 'center',
76
75
  },
77
76
  bodyText: {
78
- color: inAppData.body_font_color || "#000000",
77
+ color: msg.body_font_color || "#000000",
79
78
  textAlign: 'justify',
80
79
  marginBottom: 10,
81
80
  fontSize: 15,
82
81
  marginHorizontal: 10
83
82
  },
84
83
  title: {
85
- color: inAppData.title_font_color || "#000000",
84
+ color: msg.title_font_color || "#000000",
86
85
  fontWeight: "bold",
87
86
  fontSize: 18,
88
- marginTop: 10
87
+ marginTop: 40
89
88
  },
90
89
  dot: {
91
- backgroundColor: inAppData.dot_color || "#FFFFFF",
90
+ backgroundColor: msg.dot_color || "#FFFFFF",
92
91
  borderRadius: 100,
93
92
  width: 8,
94
93
  height: 8,
95
94
  marginLeft: 5,
96
95
  elevation: 5,
97
- },
98
- containerButtons: {
99
- flexDirection: 'row',
100
- justifyContent: 'center',
101
- margin: 10
102
- },
103
- img: {
104
- width: '100%',
105
- height: 150
106
96
  }
107
- });
97
+ });
@@ -0,0 +1,8 @@
1
+ export class InngageProperties {
2
+ static identifier: string = '';
3
+ static appToken: string = '';
4
+ static phoneNumber: string = '';
5
+ static email: string = '';
6
+ static customFields: any = {};
7
+ static blockDeepLink: boolean = false;
8
+ }
@@ -30,7 +30,7 @@ export const linkInApp = (link: string) => {
30
30
  const openLinkByType = (type: string, url: string) => {
31
31
  const linkTypeHandlers: Record<string, () => void> = {
32
32
  deep: () => Linking.openURL(url),
33
- inapp: () => linkInApp(url),
33
+ // inapp: () => linkInApp(url),
34
34
  };
35
35
 
36
36
  const handler = linkTypeHandlers[type];
@@ -15,3 +15,8 @@ export const eventsApi = (request, dev = false) => {
15
15
  console.log('eventsApi', request)
16
16
  return fetchClient('POST', request, '/events/newEvent/', !!dev)
17
17
  }
18
+ export const addUserDataApi = (request, dev = false) => {
19
+ if (dev)
20
+ console.log('addUserData', request)
21
+ return fetchClient('POST', request, '/subscription/addCustomField', !!dev)
22
+ }
package/src/utils.ts CHANGED
@@ -44,6 +44,10 @@ export const eventRequest = (request) => {
44
44
  return { ...request }
45
45
  }
46
46
 
47
+ export const addUserDataRequest = (request) => {
48
+ return { ...request }
49
+ }
50
+
47
51
  export const isEmpty = (obj: any) => {
48
52
  for (var prop in obj) {
49
53
  if (obj.hasOwnProperty(prop)) {
package/tsconfig.json CHANGED
@@ -11,7 +11,10 @@
11
11
  "skipLibCheck": true,
12
12
  "moduleResolution": "node",
13
13
  "noImplicitAny": false,
14
- "lib": [ "es2015" ]
14
+ "lib": [
15
+ "dom",
16
+ "es2015"
17
+ ]
15
18
  },
16
19
  "include": [
17
20
  "**/*"
@@ -21,4 +24,4 @@
21
24
  "**/__tests__/*",
22
25
  "build"
23
26
  ]
24
- }
27
+ }
package/src/index.d.ts DELETED
@@ -1,53 +0,0 @@
1
- import React from 'react';
2
- import { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native';
3
-
4
- interface notificationsListenerProps {
5
- appToken: string;
6
- dev?: boolean;
7
- enableAlert: boolean;
8
- onNotificationOpenedApp?: any;
9
- }
10
-
11
- interface SubscriptionProps {
12
- appToken: string;
13
- enableAlert: boolean;
14
- dev?: boolean;
15
- friendlyIdentifier?: string;
16
- customFields?: any;
17
- customData?: any;
18
- phoneNumber?: string;
19
- email?: string;
20
- }
21
-
22
- interface SendEventProps {
23
- appToken: string;
24
- eventName: string;
25
- identifier?: string;
26
- registration?: string;
27
- conversionEvent?: boolean;
28
- conversionValue?: number;
29
- conversionNotId?: string;
30
- eventValues?: any;
31
- }
32
-
33
- declare const Inngage: {
34
- RegisterNotificationListener: (props: notificationsListenerProps) => Promise<any>;
35
- Subscribe: (props: SubscriptionProps) => Promise<any>;
36
- SendEvent: (props: SendEventProps) => Promise<any>;
37
- };
38
-
39
- interface InappProps {
40
- mediaStyle?: StyleProp<ImageStyle>;
41
- titleStyle?: StyleProp<TextStyle>;
42
- bodyStyle?: StyleProp<TextStyle>;
43
- buttonLeftStyle?: StyleProp<ViewStyle>;
44
- buttonRightStyle?: StyleProp<ViewStyle>;
45
- buttonTitleLeftStyle?: StyleProp<TextStyle>;
46
- buttonTitleRightStyle?: StyleProp<TextStyle>;
47
- styleContainer?: StyleProp<ViewStyle>;
48
- onClose?: 'clear';
49
- }
50
-
51
- export const Inapp: React.FC<InappProps>;
52
-
53
- export default Inngage;