@textback/notification-widget 2.0.1-84986 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. package/.eslintrc.js +291 -291
  2. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  3. package/.idea/misc.xml +6 -0
  4. package/.idea/modules.xml +8 -0
  5. package/.idea/notificationwidget.iml +9 -0
  6. package/.idea/vcs.xml +6 -0
  7. package/build/index.js +3 -3
  8. package/build/sdk.js +2 -2
  9. package/package.json +70 -70
  10. package/promote_tag.sh +1 -1
  11. package/readme.md +569 -569
  12. package/server.js +1 -1
  13. package/src/libraries/ai.1.0.11.js +4088 -4088
  14. package/src/libraries/localization/locales/index.js +8 -8
  15. package/src/libraries/localization/text.js +9 -9
  16. package/src/libraries/t.js +82 -82
  17. package/src/sdk/channels/channel.js +30 -30
  18. package/src/sdk/channels/facebook.js +13 -13
  19. package/src/sdk/channels/skype.js +12 -12
  20. package/src/sdk/channels/telegram.js +18 -18
  21. package/src/sdk/channels/viber.js +12 -12
  22. package/src/sdk/channels/vk-modal/vk-modal.html +17 -17
  23. package/src/sdk/channels/vk-modal/vk-modal.js +25 -25
  24. package/src/sdk/channels/vk-modal/vk-modal.scss +116 -116
  25. package/src/sdk/channels/vk.js +195 -195
  26. package/src/sdk/events/observer.js +46 -46
  27. package/src/sdk/index.js +5 -5
  28. package/src/sdk/sdk.js +67 -67
  29. package/src/sdk/utils/apiErrorHandler.js +11 -11
  30. package/src/sdk/utils/appInsights.js +88 -88
  31. package/src/sdk/utils/browserInfo.js +8 -8
  32. package/src/sdk/utils/constants.js +17 -17
  33. package/src/sdk/utils/cookies.js +67 -67
  34. package/src/sdk/utils/find.js +7 -7
  35. package/src/sdk/utils/loadConfig.js +20 -20
  36. package/src/sdk/utils/loadDeepLink.js +48 -48
  37. package/src/sdk/utils/loadScript.js +25 -25
  38. package/src/sdk/utils/loadSubscriptions.js +6 -6
  39. package/src/sdk/utils/parseQueryString.js +33 -33
  40. package/src/sdk/utils/windowHelper.js +25 -25
  41. package/src/sdk/widget/widget.js +192 -192
  42. package/src/widget/components/tb-notification-button/index.js +34 -34
  43. package/src/widget/components/tb-notification-button/styles.scss +657 -657
  44. package/src/widget/components/tb-notification-widget/normalize.scss +395 -395
  45. package/src/widget/components/tb-nw-wahunter/styles.scss +471 -471
  46. package/src/widget/config.js +5 -5
  47. package/src/widget/icons/icon_chat_window.svg +1 -1
  48. package/src/widget/icons/icon_close.svg +1 -1
  49. package/src/widget/icons/icon_facebook.svg +7 -7
  50. package/src/widget/icons/icon_facebook_circle.svg +7 -7
  51. package/src/widget/icons/icon_instagram_circle.svg +95 -95
  52. package/src/widget/icons/icon_skype.svg +44 -44
  53. package/src/widget/icons/icon_skype_circle.svg +46 -46
  54. package/src/widget/icons/icon_skype_new.svg +113 -113
  55. package/src/widget/icons/icon_tg.svg +25 -25
  56. package/src/widget/icons/icon_tg_circle.svg +17 -17
  57. package/src/widget/icons/icon_viber.svg +75 -75
  58. package/src/widget/icons/icon_viber_circle.svg +67 -67
  59. package/src/widget/icons/icon_viber_new.svg +102 -102
  60. package/src/widget/icons/icon_vk.svg +14 -14
  61. package/src/widget/icons/icon_vk_circle.svg +16 -16
  62. package/src/widget/icons/icon_whatsapp.svg +147 -147
  63. package/src/widget/icons/icon_whatsapp_circle.svg +3 -3
  64. package/src/widget/icons/icon_whatsapp_new.svg +127 -127
  65. package/src/widget/icons/icon_whatsappb.svg +147 -147
  66. package/src/widget/icons/icon_whatsappb_circle.svg +3 -3
  67. package/src/widget/icons/icon_whatsappb_new.svg +127 -127
  68. package/src/widget/icons/paper-plane-arrow.svg +3 -3
  69. package/src/widget/icons/tb-logo.svg +21 -21
  70. package/src/widget/index.js +28 -28
  71. package/src/widget/locales/cs.js +42 -42
  72. package/src/widget/locales/en.js +42 -42
  73. package/src/widget/locales/ro.js +41 -41
  74. package/src/widget/utils/cookiesEx.js +41 -41
  75. package/src/widget/utils/stringifyAttributes.js +19 -19
  76. package/src/widget/utils/widgetsStorage.js +28 -28
  77. package/src/widget/widget.entry.js +3 -3
  78. package/tests/gf.html +35 -35
  79. package/tests/gf.js +21 -21
  80. package/tests/index.js +61 -61
  81. package/views/examples.ejs +3 -3
  82. package/views/sdk.html +274 -274
  83. package/webpack.common.js +72 -72
  84. package/webpack.dev.js +15 -15
  85. package/webpack.prod.js +10 -10
@@ -1,116 +1,116 @@
1
- .tb-modal-open {
2
- overflow: hidden;
3
-
4
- .tb-modal {
5
- overflow-x: hidden;
6
- overflow-y: auto;
7
-
8
- display: block;
9
- }
10
- }
11
-
12
- .tb-modal {
13
- position: fixed;
14
- top: 0;
15
- right: 0;
16
- bottom: 0;
17
- left: 0;
18
- display: none;
19
- background-color: rgba(0, 0, 0, .7);
20
- z-index: 1072;
21
- height: 100%;
22
- width: 100%;
23
- text-align: center;
24
-
25
- * {
26
- box-sizing: border-box;
27
- }
28
-
29
- .tb-modal-dialog {
30
- height: 100%;
31
- width: 100%;
32
-
33
- &:before {
34
- display: inline-block;
35
- vertical-align: middle;
36
- content: " ";
37
- height: 100%;
38
- }
39
-
40
- .tb-modal-content {
41
- display: inline-block;
42
- position: relative;
43
- padding: 25px 25px 35px;
44
- background-color: #fff;
45
-
46
- width: 310px;
47
-
48
- .tb-modal-content_text {
49
- display: flex;
50
- justify-content: center;
51
- }
52
-
53
- .tb-close {
54
- position: absolute;
55
- top: 5px;
56
- right: 12px;
57
-
58
- border: none;
59
- outline: 0;
60
- padding: 0;
61
- background-color: transparent;
62
-
63
- font-size: 24px;
64
- font-weight: bold;
65
- color: #000;
66
- opacity: 0.7;
67
-
68
- &:hover {
69
- opacity: 1;
70
- cursor: pointer;
71
- }
72
- }
73
- }
74
- }
75
- }
76
-
77
- .tb-vk-button {
78
- margin-top: 15px;
79
- display: inline-block;
80
- width: 230px;
81
- position: relative;
82
- padding-left: 40px;
83
-
84
- .tb-vk-icon {
85
- width: 30px;
86
- height: 30px;
87
- position: absolute;
88
- top: 0px;
89
- left: 0px;
90
-
91
- > svg path {
92
- fill: #45668e;
93
- }
94
- }
95
-
96
- .tb-vk-button-container {
97
-
98
- }
99
- }
100
-
101
- @media only screen and (min-device-width: 736px) {
102
- .tb-modal .tb-modal-dialog .tb-modal-content{
103
- width: 500px;
104
- }
105
-
106
- .tb-vk-button {
107
- width: 245px;
108
- padding-left: 55px;
109
-
110
- .tb-vk-icon {
111
- width: 40px;
112
- height: 40px;
113
- top: -5px;
114
- }
115
- }
116
- }
1
+ .tb-modal-open {
2
+ overflow: hidden;
3
+
4
+ .tb-modal {
5
+ overflow-x: hidden;
6
+ overflow-y: auto;
7
+
8
+ display: block;
9
+ }
10
+ }
11
+
12
+ .tb-modal {
13
+ position: fixed;
14
+ top: 0;
15
+ right: 0;
16
+ bottom: 0;
17
+ left: 0;
18
+ display: none;
19
+ background-color: rgba(0, 0, 0, .7);
20
+ z-index: 1072;
21
+ height: 100%;
22
+ width: 100%;
23
+ text-align: center;
24
+
25
+ * {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ .tb-modal-dialog {
30
+ height: 100%;
31
+ width: 100%;
32
+
33
+ &:before {
34
+ display: inline-block;
35
+ vertical-align: middle;
36
+ content: " ";
37
+ height: 100%;
38
+ }
39
+
40
+ .tb-modal-content {
41
+ display: inline-block;
42
+ position: relative;
43
+ padding: 25px 25px 35px;
44
+ background-color: #fff;
45
+
46
+ width: 310px;
47
+
48
+ .tb-modal-content_text {
49
+ display: flex;
50
+ justify-content: center;
51
+ }
52
+
53
+ .tb-close {
54
+ position: absolute;
55
+ top: 5px;
56
+ right: 12px;
57
+
58
+ border: none;
59
+ outline: 0;
60
+ padding: 0;
61
+ background-color: transparent;
62
+
63
+ font-size: 24px;
64
+ font-weight: bold;
65
+ color: #000;
66
+ opacity: 0.7;
67
+
68
+ &:hover {
69
+ opacity: 1;
70
+ cursor: pointer;
71
+ }
72
+ }
73
+ }
74
+ }
75
+ }
76
+
77
+ .tb-vk-button {
78
+ margin-top: 15px;
79
+ display: inline-block;
80
+ width: 230px;
81
+ position: relative;
82
+ padding-left: 40px;
83
+
84
+ .tb-vk-icon {
85
+ width: 30px;
86
+ height: 30px;
87
+ position: absolute;
88
+ top: 0px;
89
+ left: 0px;
90
+
91
+ > svg path {
92
+ fill: #45668e;
93
+ }
94
+ }
95
+
96
+ .tb-vk-button-container {
97
+
98
+ }
99
+ }
100
+
101
+ @media only screen and (min-device-width: 736px) {
102
+ .tb-modal .tb-modal-dialog .tb-modal-content{
103
+ width: 500px;
104
+ }
105
+
106
+ .tb-vk-button {
107
+ width: 245px;
108
+ padding-left: 55px;
109
+
110
+ .tb-vk-icon {
111
+ width: 40px;
112
+ height: 40px;
113
+ top: -5px;
114
+ }
115
+ }
116
+ }
@@ -1,195 +1,195 @@
1
- import UUID from 'uuid-js';
2
-
3
- import loadScript from '../utils/loadScript.js';
4
- import apiErrorHandler from '../utils/apiErrorHandler.js';
5
-
6
- import Channel from './channel.js';
7
- import VKModal from './vk-modal/vk-modal.js';
8
- import constants from "../utils/constants";
9
-
10
- const VK_API_VERSION = '5.131';
11
- const LOGIN_TIMEOUT = 4096;
12
-
13
- function loadVkLib(){
14
- return new Promise((resolve, reject) => {
15
- if (window.VK && window.VK.Auth) {
16
- resolve('vk cache');
17
- }
18
- else {
19
- loadScript('//vk.com/js/api/openapi.js')
20
- .then(function () {
21
- resolve('vk load');
22
- }, function (err) {
23
- reject(err);
24
- });
25
- }
26
- });
27
- }
28
-
29
- function initVK(apiId) {
30
- return new Promise((resolve, reject) => {
31
- if (apiId) {
32
- if (!window.VK._apiId) {
33
- window.VK.init({apiId});
34
- }
35
- window.VK.Auth.getLoginStatus(resolve);
36
- } else {
37
- reject('noVKApiId');
38
- }
39
- });
40
- }
41
-
42
- function handleVKLoadingError(error) {
43
- console.error('Failed to load VK openapi.js. VK channel will be unavailable');
44
- return null;
45
- }
46
-
47
- // isVkAppUsed :: Object -> Boolean
48
- const isVkAppUsed = config => {
49
- return config.useVkApp
50
- ? config.useVkApp
51
- : false;
52
- };
53
-
54
- export default class VKChannel extends Channel {
55
- constructor(channelData = {}, deeplink, widget) {
56
- super(channelData, deeplink, widget);
57
-
58
- this.isDoubleCheck = widget.config.isDoubleCheck;
59
- this.vkApiId = this.widget.config.vkApiId;
60
- this.initPromise = null;
61
- this.vkPluginContainerId = null;
62
- this.authorized = false;
63
- this.landingUrl = this.config.additionalProperties ? this.config.additionalProperties.landingUrl : undefined;
64
-
65
- if (this.enabled) {
66
- this.vkPluginContainerId = UUID.create(4).hex;
67
-
68
- if (!this.widget.useVKApi) {
69
- this.initPromise = loadVkLib()
70
- .catch(err => this.__handleVKLoadingError(err));
71
- } else {
72
- this.initPromise = loadVkLib()
73
- .then(() => initVK(this.vkApiId))
74
- .then(() => {
75
- this.authorized = !!(window.VK.Auth.getSession());
76
- })
77
- .catch(err => this.__handleVKLoadingError(err));
78
- }
79
- }
80
- }
81
-
82
- subscribe() {
83
- super.subscribe();
84
- return this.landingUrl ?
85
- window.open(this.landingUrl, '_blank')
86
- :
87
- (isVkAppUsed(this.widget.config)
88
- ? window.open(`https://vk.com/app${this.widget.config.vkApiId}_-${this.config.id}#r=subscribe_${this.deeplink}&accountId=${this.widget.config.accountId}&channelId=${this.channelId}`, 'vk')
89
- : this.subscribeViaPlugin());
90
- }
91
-
92
- subscribeViaApi() {
93
- let promise = null;
94
-
95
- if (!this.authorized) {
96
- promise = this.__login().then( res => this.__allowMessagesFromGroup() );
97
- } else {
98
- promise = this.__allowMessagesFromGroup();
99
- }
100
- return promise;
101
- }
102
-
103
- __allowMessagesFromGroup () {
104
- window.VK.Api.call(
105
- 'messages.allowMessagesFromGroup',
106
- {
107
- group_id: this.id,
108
- v: VK_API_VERSION,
109
- },
110
- res => {
111
- const vkSession = window.VK.Auth.getSession();
112
- const vkUserId = vkSession.mid;
113
-
114
- if (res.response === 1) {
115
- this.__textbackSubscribe(vkUserId);
116
- } else {
117
- console.log('VK res error:', res);
118
- }
119
- }
120
- );
121
- }
122
-
123
-
124
- __login () {
125
- return new Promise(resolve => {
126
- window.VK.Auth.login(res => {
127
- resolve(res);
128
- }, LOGIN_TIMEOUT);
129
- });
130
- }
131
-
132
- __textbackSubscribe (vkUserId, channelWidgetIdx) {
133
- //we shall parse deeplink from server webhook. This method looks like artifact from age without webhhoks.
134
- if(!this.isDoubleCheck) return null;
135
-
136
- return fetch(`${this.widget.initialConfig.apiPath}/endUserNotifications/subscriptions`, {
137
- method: 'POST',
138
- headers: {
139
- 'Content-Type': 'application/json'
140
- },
141
- body: JSON.stringify({
142
- 'accountId': this.widget.config.accountId,
143
- 'insecureContext': this.widget.insecureContext,
144
- 'secureContextToken': this.widget.secureContextToken,
145
- 'address': {
146
- 'channel': 'vk',
147
- 'channelId': this.config.channelId,
148
- 'accountId': this.widget.config.accountId,
149
- 'remoteAddress': vkUserId,
150
- 'chatId': vkUserId
151
- },
152
- deepLinkId: this.deeplink.replace('subscribe_',''),
153
- widgetId: this.widget.id,
154
- 'user': {
155
- 'id': this.widget.widgetUserId
156
- }
157
- })
158
- }).then(apiErrorHandler);
159
- }
160
-
161
- __handleVKLoadingError(err) {
162
- super.reportError('Failed to load VK openapi.js. VK channel will be unavailable.', err);
163
- return null;
164
- }
165
-
166
- subscribeViaPlugin() {
167
- const modal = new VKModal(this.vkPluginContainerId);
168
- this.renderVKSubscriptionButton();
169
-
170
- modal.onClose = () => {
171
- if (this.vkSubscribeHandler) {
172
- window.VK.Observer.unsubscribe('widgets.allowMessagesFromCommunity.allowed', this.vkSubscribeHandler);
173
- this.vkSubscribeHandler = null;
174
- }
175
- };
176
- }
177
-
178
- renderVKSubscriptionButton(height = 30, containerId = this.vkPluginContainerId) {
179
- if (this.vkSubscribeHandler) {
180
- window.VK.Observer.unsubscribe('widgets.allowMessagesFromCommunity.allowed', this.vkSubscribeHandler);
181
- this.vkSubscribeHandler = null;
182
- }
183
-
184
- const options = {
185
- height,
186
- key: this.deeplink
187
- };
188
-
189
- window.VK.Widgets.AllowMessagesFromCommunity(containerId, options, this.id);
190
- if (!this.config.disableAllowMessageEvents) {
191
- this.vkSubscribeHandler = this.__textbackSubscribe.bind(this);
192
- window.VK.Observer.subscribe('widgets.allowMessagesFromCommunity.allowed', this.vkSubscribeHandler);
193
- }
194
- }
195
- }
1
+ import UUID from 'uuid-js';
2
+
3
+ import loadScript from '../utils/loadScript.js';
4
+ import apiErrorHandler from '../utils/apiErrorHandler.js';
5
+
6
+ import Channel from './channel.js';
7
+ import VKModal from './vk-modal/vk-modal.js';
8
+ import constants from "../utils/constants";
9
+
10
+ const VK_API_VERSION = '5.131';
11
+ const LOGIN_TIMEOUT = 4096;
12
+
13
+ function loadVkLib(){
14
+ return new Promise((resolve, reject) => {
15
+ if (window.VK && window.VK.Auth) {
16
+ resolve('vk cache');
17
+ }
18
+ else {
19
+ loadScript('//vk.com/js/api/openapi.js')
20
+ .then(function () {
21
+ resolve('vk load');
22
+ }, function (err) {
23
+ reject(err);
24
+ });
25
+ }
26
+ });
27
+ }
28
+
29
+ function initVK(apiId) {
30
+ return new Promise((resolve, reject) => {
31
+ if (apiId) {
32
+ if (!window.VK._apiId) {
33
+ window.VK.init({apiId});
34
+ }
35
+ window.VK.Auth.getLoginStatus(resolve);
36
+ } else {
37
+ reject('noVKApiId');
38
+ }
39
+ });
40
+ }
41
+
42
+ function handleVKLoadingError(error) {
43
+ console.error('Failed to load VK openapi.js. VK channel will be unavailable');
44
+ return null;
45
+ }
46
+
47
+ // isVkAppUsed :: Object -> Boolean
48
+ const isVkAppUsed = config => {
49
+ return config.useVkApp
50
+ ? config.useVkApp
51
+ : false;
52
+ };
53
+
54
+ export default class VKChannel extends Channel {
55
+ constructor(channelData = {}, deeplink, widget) {
56
+ super(channelData, deeplink, widget);
57
+
58
+ this.isDoubleCheck = widget.config.isDoubleCheck;
59
+ this.vkApiId = this.widget.config.vkApiId;
60
+ this.initPromise = null;
61
+ this.vkPluginContainerId = null;
62
+ this.authorized = false;
63
+ this.landingUrl = this.config.additionalProperties ? this.config.additionalProperties.landingUrl : undefined;
64
+
65
+ if (this.enabled) {
66
+ this.vkPluginContainerId = UUID.create(4).hex;
67
+
68
+ if (!this.widget.useVKApi) {
69
+ this.initPromise = loadVkLib()
70
+ .catch(err => this.__handleVKLoadingError(err));
71
+ } else {
72
+ this.initPromise = loadVkLib()
73
+ .then(() => initVK(this.vkApiId))
74
+ .then(() => {
75
+ this.authorized = !!(window.VK.Auth.getSession());
76
+ })
77
+ .catch(err => this.__handleVKLoadingError(err));
78
+ }
79
+ }
80
+ }
81
+
82
+ subscribe() {
83
+ super.subscribe();
84
+ return this.landingUrl ?
85
+ window.open(this.landingUrl, '_blank')
86
+ :
87
+ (isVkAppUsed(this.widget.config)
88
+ ? window.open(`https://vk.com/app${this.widget.config.vkApiId}_-${this.config.id}#r=subscribe_${this.deeplink}&accountId=${this.widget.config.accountId}&channelId=${this.channelId}`, 'vk')
89
+ : this.subscribeViaPlugin());
90
+ }
91
+
92
+ subscribeViaApi() {
93
+ let promise = null;
94
+
95
+ if (!this.authorized) {
96
+ promise = this.__login().then( res => this.__allowMessagesFromGroup() );
97
+ } else {
98
+ promise = this.__allowMessagesFromGroup();
99
+ }
100
+ return promise;
101
+ }
102
+
103
+ __allowMessagesFromGroup () {
104
+ window.VK.Api.call(
105
+ 'messages.allowMessagesFromGroup',
106
+ {
107
+ group_id: this.id,
108
+ v: VK_API_VERSION,
109
+ },
110
+ res => {
111
+ const vkSession = window.VK.Auth.getSession();
112
+ const vkUserId = vkSession.mid;
113
+
114
+ if (res.response === 1) {
115
+ this.__textbackSubscribe(vkUserId);
116
+ } else {
117
+ console.log('VK res error:', res);
118
+ }
119
+ }
120
+ );
121
+ }
122
+
123
+
124
+ __login () {
125
+ return new Promise(resolve => {
126
+ window.VK.Auth.login(res => {
127
+ resolve(res);
128
+ }, LOGIN_TIMEOUT);
129
+ });
130
+ }
131
+
132
+ __textbackSubscribe (vkUserId, channelWidgetIdx) {
133
+ //we shall parse deeplink from server webhook. This method looks like artifact from age without webhhoks.
134
+ if(!this.isDoubleCheck) return null;
135
+
136
+ return fetch(`${this.widget.initialConfig.apiPath}/endUserNotifications/subscriptions`, {
137
+ method: 'POST',
138
+ headers: {
139
+ 'Content-Type': 'application/json'
140
+ },
141
+ body: JSON.stringify({
142
+ 'accountId': this.widget.config.accountId,
143
+ 'insecureContext': this.widget.insecureContext,
144
+ 'secureContextToken': this.widget.secureContextToken,
145
+ 'address': {
146
+ 'channel': 'vk',
147
+ 'channelId': this.config.channelId,
148
+ 'accountId': this.widget.config.accountId,
149
+ 'remoteAddress': vkUserId,
150
+ 'chatId': vkUserId
151
+ },
152
+ deepLinkId: this.deeplink.replace('subscribe_',''),
153
+ widgetId: this.widget.id,
154
+ 'user': {
155
+ 'id': this.widget.widgetUserId
156
+ }
157
+ })
158
+ }).then(apiErrorHandler);
159
+ }
160
+
161
+ __handleVKLoadingError(err) {
162
+ super.reportError('Failed to load VK openapi.js. VK channel will be unavailable.', err);
163
+ return null;
164
+ }
165
+
166
+ subscribeViaPlugin() {
167
+ const modal = new VKModal(this.vkPluginContainerId);
168
+ this.renderVKSubscriptionButton();
169
+
170
+ modal.onClose = () => {
171
+ if (this.vkSubscribeHandler) {
172
+ window.VK.Observer.unsubscribe('widgets.allowMessagesFromCommunity.allowed', this.vkSubscribeHandler);
173
+ this.vkSubscribeHandler = null;
174
+ }
175
+ };
176
+ }
177
+
178
+ renderVKSubscriptionButton(height = 30, containerId = this.vkPluginContainerId) {
179
+ if (this.vkSubscribeHandler) {
180
+ window.VK.Observer.unsubscribe('widgets.allowMessagesFromCommunity.allowed', this.vkSubscribeHandler);
181
+ this.vkSubscribeHandler = null;
182
+ }
183
+
184
+ const options = {
185
+ height,
186
+ key: this.deeplink
187
+ };
188
+
189
+ window.VK.Widgets.AllowMessagesFromCommunity(containerId, options, this.id);
190
+ if (!this.config.disableAllowMessageEvents) {
191
+ this.vkSubscribeHandler = this.__textbackSubscribe.bind(this);
192
+ window.VK.Observer.subscribe('widgets.allowMessagesFromCommunity.allowed', this.vkSubscribeHandler);
193
+ }
194
+ }
195
+ }