@inngageregistry/inngage-react 4.0.0-beta.2 → 4.0.0

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.
Files changed (42) hide show
  1. package/README.md +60 -60
  2. package/dist/Inngage.d.ts +28 -0
  3. package/dist/Inngage.js +149 -0
  4. package/dist/api/api.d.ts +4 -0
  5. package/{src/api/api.ts → dist/api/api.js} +22 -28
  6. package/dist/api/handler.d.ts +1 -0
  7. package/dist/api/handler.js +44 -0
  8. package/dist/components/in_app.d.ts +6 -0
  9. package/dist/components/in_app.js +92 -0
  10. package/dist/components/modal.d.ts +26 -0
  11. package/dist/components/modal.js +158 -0
  12. package/dist/components/styles.d.ts +88 -0
  13. package/dist/components/styles.js +86 -0
  14. package/dist/firebase/notifications_listener.d.ts +6 -0
  15. package/dist/firebase/notifications_listener.js +170 -0
  16. package/{src/index.ts → dist/index.d.ts} +6 -11
  17. package/dist/index.js +6 -0
  18. package/dist/models/inngage_properties.d.ts +11 -0
  19. package/dist/models/inngage_properties.js +13 -0
  20. package/dist/models/requests.d.ts +40 -0
  21. package/{src/models/requests.ts → dist/models/requests.js} +40 -42
  22. package/dist/services/api_services.d.ts +7 -0
  23. package/{src/services/api_services.ts → dist/services/api_services.js} +25 -30
  24. package/dist/services/handler.d.ts +1 -0
  25. package/dist/services/handler.js +38 -0
  26. package/dist/services/inngage.d.ts +4 -0
  27. package/dist/services/inngage.js +22 -0
  28. package/dist/utils.d.ts +4 -0
  29. package/dist/utils.js +25 -0
  30. package/package.json +79 -40
  31. package/.editorconfig +0 -6
  32. package/src/Inngage.ts +0 -193
  33. package/src/api/handler.ts +0 -53
  34. package/src/components/in_app.tsx +0 -192
  35. package/src/components/modal.tsx +0 -221
  36. package/src/components/styles.ts +0 -88
  37. package/src/firebase/notifications_listener.ts +0 -182
  38. package/src/models/inngage_properties.ts +0 -13
  39. package/src/services/handler.ts +0 -41
  40. package/src/services/inngage.ts +0 -23
  41. package/src/utils.ts +0 -35
  42. package/tsconfig.json +0 -27
@@ -1,192 +0,0 @@
1
- import * as React from "react";
2
- import {
3
- StyleSheet,
4
- Image,
5
- View,
6
- Text,
7
- TouchableOpacity,
8
- ImageBackground,
9
- Dimensions,
10
- Linking,
11
- } from "react-native";
12
-
13
- import AsyncStorage from "@react-native-async-storage/async-storage";
14
- import Carousel from 'react-native-snap-carousel';
15
-
16
- import { buildStyles } from "./styles";
17
- import { Modal } from "../components/modal";
18
- import { InngageProperties } from "../models/inngage_properties";
19
-
20
- import InAppBrowser from 'react-native-inappbrowser-reborn';
21
-
22
- interface InAppData {
23
- inapp_message: boolean
24
- title: string
25
- body: string
26
- title_font_color: string
27
- body_font_color: string
28
- background_color: string
29
- btn_left_txt_color: string
30
- btn_left_bg_color: string
31
- btn_right_txt_color: string
32
- btn_right_bg_color: string
33
- background_image: string
34
- btn_left_txt: string
35
- btn_left_action_type: string
36
- btn_left_action_link: string
37
- btn_right_txt: string
38
- btn_right_action_type: string
39
- btn_right_action_link: string
40
- rich_content: string
41
- inpression: string
42
- bg_img_action_type: string
43
- bg_img_action_link: string
44
- dot_color: string
45
- }
46
-
47
- interface InAppRichContent {
48
- carousel: boolean
49
- img1: string
50
- img2: string
51
- img3: string
52
- img4: string
53
- img5: string
54
- }
55
-
56
- interface InAppProps {
57
- onDismiss: () => void;
58
- }
59
-
60
- export function InApp({ onDismiss }: InAppProps): JSX.Element {
61
- const { width: screenWidth } = Dimensions.get('window');
62
-
63
- const sliderWidth = screenWidth;
64
- const itemWidth = screenWidth * 0.9;
65
-
66
- const [data, setData] = React.useState<InAppData>();
67
- const [richContent, setRichContent] = React.useState<InAppRichContent>();
68
- const [visible, setVisible] = React.useState(false);
69
-
70
- React.useEffect(() => {
71
- const fetchAdditionalData = async () => {
72
- try {
73
- console.log('in app message')
74
- const data = await AsyncStorage.getItem('inapp');
75
- console.log(data)
76
- if (data) {
77
- const parsedData = JSON.parse(data);
78
- const richContentData = parsedData.rich_content;
79
-
80
- setVisible(true);
81
- setData(parsedData);
82
- setRichContent(richContentData);
83
- }
84
- } catch (error) {
85
- console.error('Error retrieving additionalData from AsyncStorage:', error);
86
- }
87
- };
88
-
89
- fetchAdditionalData();
90
- }, []);
91
-
92
- const styles = buildStyles(data, screenWidth * 0.9);
93
-
94
- const handleDismissInApp = async () => {
95
- await AsyncStorage.removeItem('inapp');
96
- setVisible(false);
97
- onDismiss();
98
- }
99
-
100
- const handleClick = async (link, type) => {
101
- if (type === 'inapp') {
102
- try {
103
- if (await InAppBrowser.isAvailable()) {
104
- await InAppBrowser.open(link, {
105
- dismissButtonStyle: 'close',
106
- preferredBarTintColor: '#453AA4',
107
- preferredControlTintColor: 'white',
108
- enableDefaultShare: true,
109
- enableBarCollapsing: true,
110
- });
111
- } else {
112
- Linking.openURL(link);
113
- }
114
- } catch (error) {
115
- console.error(error);
116
- }
117
- } else if (type === 'deep') {
118
- Linking.openURL(link).catch((err) =>
119
- console.error('Erro ao abrir o link:', err)
120
- );
121
- }
122
- };
123
-
124
- const imageUrls: string[] = [
125
- richContent?.img1 ?? '',
126
- richContent?.img2 ?? '',
127
- richContent?.img3 ?? '',
128
- richContent?.img4 ?? '',
129
- richContent?.img5 ?? ''
130
- ];
131
-
132
- const stylesCarousel = StyleSheet.create({
133
- itemContainer: {
134
- justifyContent: 'center',
135
- height: 250
136
- },
137
- itemImg: {
138
- height: 250,
139
- },
140
- });
141
-
142
- const _renderItem = ({ item, index }) => {
143
- return (
144
- <View style={stylesCarousel.itemContainer}>
145
- <Image style={[stylesCarousel.itemImg]} source={{ uri: item }} />
146
- </View>
147
- )
148
- }
149
-
150
- if (InngageProperties.getDebugMode() && data != null) {
151
- console.log('INNGAGE - Data In App:', data)
152
- console.log('INNGAGE - Data Rich Content:', richContent)
153
- }
154
-
155
- return (
156
- <Modal
157
- onBackdropPress={handleDismissInApp}
158
- renderToHardwareTextureAndroid={true}
159
- transparent={true}
160
- visible={visible}>
161
- <ImageBackground style={styles.content} source={{ uri: data?.background_image }}>
162
- {richContent?.carousel ? <Carousel
163
- layout={"default"}
164
- data={imageUrls}
165
- sliderWidth={sliderWidth}
166
- itemWidth={itemWidth}
167
- renderItem={_renderItem} /> : null}
168
- <TouchableOpacity style={styles.closeButton} onPress={handleDismissInApp}>
169
- <Text style={styles.textButton}>X</Text>
170
- </TouchableOpacity>
171
- <View style={styles.messageData}>
172
- <Text style={styles.title}>{data?.title}</Text>
173
- <Text style={styles.body}>{data?.body}</Text>
174
- </View>
175
- <View style={styles.footer}>
176
- {data?.btn_left_txt != null ?
177
- <TouchableOpacity
178
- style={styles.buttonLeft}
179
- onPress={() => handleClick(data?.btn_left_action_link, data?.btn_left_action_type)}>
180
- <Text style={styles.textButtonLeft}>{data?.btn_left_txt}</Text>
181
- </TouchableOpacity> : null}
182
- {data?.btn_right_txt != null ?
183
- <TouchableOpacity
184
- style={styles.buttonRight}
185
- onPress={() => handleClick(data?.btn_right_action_link, data?.btn_right_action_type)}>
186
- <Text style={styles.textButtonRight}>{data?.btn_right_txt}</Text>
187
- </TouchableOpacity> : null}
188
- </View>
189
- </ImageBackground>
190
- </Modal>
191
- )
192
- }
@@ -1,221 +0,0 @@
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,88 +0,0 @@
1
- import { PlatformColor, StyleSheet } from "react-native";
2
-
3
-
4
- export const buildStyles = (data: any, screen: any) => StyleSheet.create({
5
- closeButton: {
6
- width: 30,
7
- height: 30,
8
- justifyContent: 'center',
9
- alignItems: 'center',
10
- backgroundColor: 'transparent', // Cor do botão
11
- borderRadius: 25, // Torna o botão circular
12
- shadowOffset: { width: 0, height: 2 },
13
- shadowOpacity: 0.2,
14
- shadowRadius: 4,
15
- position: 'absolute',
16
- top: 10,
17
- right: 10,
18
- zIndex: 10,
19
- },
20
- textButton: {
21
- color: '#000', // Cor do texto "X"
22
- fontSize: 16, // Tamanho do "X"
23
- fontWeight: 'bold', // Deixa o "X" em negrito
24
- },
25
- content: {
26
- backgroundColor: data?.background_color,
27
- flexDirection: "column",
28
- borderRadius: 20,
29
- paddingBottom: 16,
30
- paddingRight: 16,
31
- paddingLeft: 16,
32
- margin: 16,
33
- overflow: "hidden",
34
- elevation: 10,
35
- width: screen,
36
- alignItems: "center"
37
- },
38
- messageData: {
39
- marginTop: 12,
40
- alignItems: "center",
41
- justifyContent: "center",
42
- },
43
- footer: {
44
- flexDirection: "row",
45
- alignItems: "center",
46
- justifyContent: "flex-end",
47
- marginTop: 4,
48
- },
49
- title: {
50
- color: data?.title_font_color,
51
- fontWeight: "500",
52
- fontSize: 18,
53
- },
54
- body: {
55
- color: data?.body_font_color,
56
- fontSize: 16,
57
- marginTop: 10,
58
- marginBottom: 10,
59
- },
60
- buttonLeft: {
61
- margin: 2,
62
- flex: 1,
63
- backgroundColor: data?.btn_left_bg_color,
64
- justifyContent: "center",
65
- alignItems: "center",
66
- },
67
- buttonRight: {
68
- margin: 2,
69
- flex: 1,
70
- backgroundColor: data?.btn_right_bg_color,
71
- justifyContent: "center",
72
- alignItems: "center",
73
- },
74
- textButtonLeft: {
75
- color: data?.btn_left_txt_color,
76
- textAlign: "center",
77
- backgroundColor: "transparent",
78
- padding: 8,
79
- fontSize: 14,
80
- },
81
- textButtonRight: {
82
- color: data?.btn_right_txt_color,
83
- textAlign: "center",
84
- backgroundColor: "transparent",
85
- padding: 8,
86
- fontSize: 14,
87
- },
88
- })
@@ -1,182 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
-
3
- import AsyncStorage from '@react-native-async-storage/async-storage';
4
-
5
- import messaging from '@react-native-firebase/messaging';
6
- import PushNotificationIOS from "@react-native-community/push-notification-ios";
7
- import PushNotification, { Importance } from 'react-native-push-notification'
8
-
9
- import { InngageProperties } from '../models/inngage_properties';
10
- import * as ApiService from '../services/api_services'
11
- import { AppState, Linking } from 'react-native';
12
- import InAppBrowser from 'react-native-inappbrowser-reborn';
13
-
14
- export const messagingHeadlessTask = () => {
15
- return messaging().setBackgroundMessageHandler(async remoteMessage => {
16
- if (InngageProperties.getDebugMode())
17
- console.log('INNGAGE BACKGROUND AND CLOSED DATA: ', remoteMessage)
18
-
19
- if (remoteMessage?.data?.additional_data != null) {
20
- await AsyncStorage.setItem('inapp', remoteMessage?.data?.additional_data);
21
- }
22
-
23
- return Promise.resolve();
24
- });
25
- }
26
-
27
- export const useInAppHandler = () => {
28
- const [showInApp, setShowInApp] = useState(false);
29
-
30
- useEffect(() => {
31
- const checkInAppData = async () => {
32
- const inAppData = await AsyncStorage.getItem('inapp');
33
- if (inAppData) {
34
- setShowInApp(true);
35
- }
36
- };
37
-
38
- checkInAppData();
39
-
40
- const subscription = AppState.addEventListener('change', nextAppState => {
41
- if (nextAppState === 'active') {
42
- checkInAppData();
43
- }
44
- });
45
-
46
- return () => {
47
- subscription.remove();
48
- };
49
- }, []);
50
-
51
- return { showInApp, setShowInApp };
52
- }
53
-
54
- export const InngageNotificationMessage = (firebaseListenCallback?: any) => {
55
- const [notificationMessage, setNotificationMessage] = useState(null);
56
-
57
- useEffect(() => {
58
- PushNotification.configure({
59
- onAction: function (data: any) {
60
- console.log(data)
61
- },
62
- onNotification: function (data: any) {
63
- if (data.foreground) {
64
- if (notificationMessage) {
65
- handleNotification(notificationMessage)
66
- }
67
- }
68
- data.finish(PushNotificationIOS.FetchResult.NoData);
69
- },
70
- popInitialNotification: true,
71
- requestPermissions: true
72
- })
73
- PushNotification.createChannel({
74
- channelId: 'channel_id',
75
- channelName: 'default',
76
- importance: Importance.HIGH,
77
- playSound: true,
78
- soundName: 'default',
79
- vibrate: true
80
- }, (created: any) => {
81
- if (created) {
82
- console.log('Channel created');
83
- }
84
- });
85
- })
86
- useEffect(() => {
87
- const handleMessage = async (remoteMessage: any) => {
88
- const notificationData = remoteMessage.data;
89
-
90
- if (notificationData.additional_data != null)
91
- await AsyncStorage.setItem('inapp', notificationData.additional_data);
92
-
93
- if (InngageProperties.getDebugMode())
94
- console.log('Remote message received in foreground: ', remoteMessage);
95
-
96
- if (firebaseListenCallback != null && remoteMessage != null)
97
- firebaseListenCallback(notificationData)
98
-
99
- setNotificationMessage(remoteMessage);
100
-
101
- PushNotification.localNotification({
102
- autoCancel: true,
103
- bigPictureUrl: remoteMessage.notification?.android?.imageUrl,
104
- largeIconUrl: remoteMessage.notification?.android?.imageUrl,
105
- title: notificationData?.title,
106
- message: notificationData?.body,
107
- vibration: 300,
108
- channelId: "channel_id",
109
- });
110
- };
111
-
112
- return messaging().onMessage(handleMessage);
113
- }, []);
114
-
115
- useEffect(() => {
116
- messaging().onNotificationOpenedApp(remoteMessage => {
117
- if (remoteMessage != null)
118
- firebaseListenCallback(remoteMessage.data)
119
-
120
- handleNotification(remoteMessage)
121
- })
122
- }, []);
123
-
124
- useEffect(() => {
125
- messaging().getInitialNotification().then(async (value) => {
126
- if (value !== null)
127
- handleUniqueRemoteMessage(value);
128
- });
129
- }, [])
130
-
131
- const handleUniqueRemoteMessage = async (
132
- remoteMessage: { messageId?: string }) => {
133
- try {
134
- console.log('oi')
135
- const lastRemoteMessageId = await AsyncStorage.getItem('LAST_REMOTE_MESSAGE_ID');
136
- const newRemoteMessageId = remoteMessage?.messageId;
137
-
138
- if (newRemoteMessageId && lastRemoteMessageId !== newRemoteMessageId) {
139
- await AsyncStorage.setItem('LAST_REMOTE_MESSAGE_ID', newRemoteMessageId);
140
- handleNotification(remoteMessage)
141
- }
142
- } catch (e) {
143
- console.error(e);
144
- }
145
- };
146
-
147
- async function handleNotification(remoteMessage) {
148
- const notId = remoteMessage.data?.notId;
149
- const request = {
150
- notificationRequest: {
151
- id: notId,
152
- app_token: InngageProperties.appToken,
153
- }
154
- };
155
-
156
- if (remoteMessage.data?.url) {
157
- if (remoteMessage.data?.type === 'inapp') {
158
- try {
159
- if (await InAppBrowser.isAvailable()) {
160
- await InAppBrowser.open(remoteMessage.data?.url, {
161
- dismissButtonStyle: 'close',
162
- preferredBarTintColor: '#453AA4',
163
- preferredControlTintColor: 'white',
164
- enableDefaultShare: true,
165
- enableBarCollapsing: true,
166
- });
167
- } else {
168
- Linking.openURL(remoteMessage.data?.url);
169
- }
170
- } catch (error) {
171
- console.error(error);
172
- }
173
- } else if (remoteMessage.data?.type === 'deep') {
174
- Linking.openURL(remoteMessage.data?.url).catch((err) =>
175
- console.error('Erro ao abrir o link:', err)
176
- );
177
- }
178
- }
179
-
180
- await ApiService.sendNotification(request);
181
- }
182
- }
@@ -1,13 +0,0 @@
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
- static debugMode: boolean = false;
9
- static firebaseToken: string = '';
10
- static getDebugMode(): boolean {
11
- return InngageProperties.debugMode;
12
- }
13
- }