aesirx-analytics 2.2.5 → 2.2.6
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/README.md +38 -0
- package/dist/{Consent-ZWVGMKB7.js → Consent-U7ESRVW5.js} +124 -21
- package/dist/{ConsentCustom-P3FMYMS3.js → ConsentCustom-CSPWI25K.js} +192 -86
- package/dist/analytics.js +118 -115
- package/dist/{chunk-PHG7RB3K.js → chunk-7GRKFZZL.js} +125 -298
- package/dist/{chunk-JH54YDTD.js → chunk-N36VEWSG.js} +262 -1
- package/dist/index.js +63 -24
- package/package.json +1 -1
@@ -1116,10 +1116,271 @@ var AnalyticsContextProvider = ({ children }) => {
|
|
1116
1116
|
};
|
1117
1117
|
var AnalyticsContextProvider_default = AnalyticsContextProvider;
|
1118
1118
|
|
1119
|
+
// src/utils/consent.ts
|
1120
|
+
import { stringMessage } from "@concordium/react-components";
|
1121
|
+
import axios from "axios";
|
1122
|
+
var agreeConsents = async (endpoint, level, uuid, consent, wallet, signature, web3id, jwt, network = "concordium", gtagId, gtmId, layout) => {
|
1123
|
+
const url = `${endpoint}/consent/v1/level${level}/${uuid}`;
|
1124
|
+
const urlV2 = `${endpoint}/consent/v2/level${level}/${uuid}`;
|
1125
|
+
if (sessionStorage.getItem("consentGranted") !== "true") {
|
1126
|
+
gtagId && consentModeGrant(true, gtagId, layout);
|
1127
|
+
gtmId && consentModeGrant(false, gtmId, layout);
|
1128
|
+
}
|
1129
|
+
try {
|
1130
|
+
switch (level) {
|
1131
|
+
case 1:
|
1132
|
+
await axios.post(`${url}/${consent}`);
|
1133
|
+
break;
|
1134
|
+
case 2:
|
1135
|
+
await axios.post(
|
1136
|
+
`${url}`,
|
1137
|
+
{ consent: [1, 2] },
|
1138
|
+
{
|
1139
|
+
headers: {
|
1140
|
+
"Content-Type": "application/json",
|
1141
|
+
Authorization: "Bearer " + jwt
|
1142
|
+
}
|
1143
|
+
}
|
1144
|
+
);
|
1145
|
+
break;
|
1146
|
+
case 3:
|
1147
|
+
await axios.post(`${url}/${network}/${wallet}`, {
|
1148
|
+
signature,
|
1149
|
+
consent
|
1150
|
+
});
|
1151
|
+
break;
|
1152
|
+
case 4:
|
1153
|
+
await axios.post(
|
1154
|
+
`${urlV2}/${network}/${wallet}`,
|
1155
|
+
{
|
1156
|
+
signature,
|
1157
|
+
consent
|
1158
|
+
},
|
1159
|
+
{
|
1160
|
+
headers: {
|
1161
|
+
"Content-Type": "application/json",
|
1162
|
+
Authorization: "Bearer " + jwt
|
1163
|
+
}
|
1164
|
+
}
|
1165
|
+
);
|
1166
|
+
break;
|
1167
|
+
default:
|
1168
|
+
break;
|
1169
|
+
}
|
1170
|
+
} catch (error) {
|
1171
|
+
throw error;
|
1172
|
+
}
|
1173
|
+
};
|
1174
|
+
var consentModeGrant = async (isGtag, id, layout) => {
|
1175
|
+
if (layout !== "advance-consent-mode") {
|
1176
|
+
isGtag ? loadGtagScript(id) : loadGtmScript(id);
|
1177
|
+
}
|
1178
|
+
sessionStorage.setItem("consentGranted", "true");
|
1179
|
+
function gtag(p0, p1, p2) {
|
1180
|
+
dataLayer.push(arguments);
|
1181
|
+
}
|
1182
|
+
gtag("consent", "update", {
|
1183
|
+
ad_user_data: "granted",
|
1184
|
+
ad_personalization: "granted",
|
1185
|
+
ad_storage: "granted",
|
1186
|
+
analytics_storage: "granted"
|
1187
|
+
});
|
1188
|
+
};
|
1189
|
+
var loadGtagScript = (gtagId) => {
|
1190
|
+
const gtagScript = document.createElement("script");
|
1191
|
+
gtagScript.async = true;
|
1192
|
+
gtagScript.src = `https://www.googletagmanager.com/gtag/js?id=${gtagId}`;
|
1193
|
+
const firstScript = document.getElementsByTagName("script")[0];
|
1194
|
+
firstScript.parentNode.insertBefore(gtagScript, firstScript);
|
1195
|
+
};
|
1196
|
+
var loadGtmScript = (gtmId) => {
|
1197
|
+
const gtmScript = document.createElement("script");
|
1198
|
+
gtmScript.async = true;
|
1199
|
+
gtmScript.src = `https://www.googletagmanager.com/gtm.js?id=${gtmId}`;
|
1200
|
+
const firstScript = document.getElementsByTagName("script")[0];
|
1201
|
+
firstScript.parentNode.insertBefore(gtmScript, firstScript);
|
1202
|
+
};
|
1203
|
+
var getConsents = async (endpoint, uuid) => {
|
1204
|
+
try {
|
1205
|
+
const response = (await axios.get(`${endpoint}/visitor/v1/${uuid}`))?.data?.visitor_consents;
|
1206
|
+
return response;
|
1207
|
+
} catch (error) {
|
1208
|
+
throw error;
|
1209
|
+
}
|
1210
|
+
};
|
1211
|
+
var getSignature = async (endpoint, address, provider, text, network = "concordium") => {
|
1212
|
+
try {
|
1213
|
+
const nonce = await getNonce(endpoint, address, text, network);
|
1214
|
+
return getSignedNonce(nonce, address, provider);
|
1215
|
+
} catch (error) {
|
1216
|
+
throw error;
|
1217
|
+
}
|
1218
|
+
};
|
1219
|
+
var getNonce = async (endpoint, address, text, network = "concordium") => {
|
1220
|
+
try {
|
1221
|
+
const nonce = (await axios.post(`${endpoint}/wallet/v1/${network}/${address}/nonce`, { text }))?.data.nonce;
|
1222
|
+
return nonce;
|
1223
|
+
} catch (error) {
|
1224
|
+
throw error;
|
1225
|
+
}
|
1226
|
+
};
|
1227
|
+
var getSignedNonce = async (nonce, address, provider) => {
|
1228
|
+
const signature = await provider.signMessage(address, stringMessage(`${nonce}`));
|
1229
|
+
return Buffer.from(
|
1230
|
+
typeof signature === "object" && signature !== null ? JSON.stringify(signature) : signature,
|
1231
|
+
"utf-8"
|
1232
|
+
).toString("base64");
|
1233
|
+
};
|
1234
|
+
var revokeConsents = async (endpoint, level, uuid, wallet, signature, web3id, jwt, network = "concordium") => {
|
1235
|
+
const url = `${endpoint}/consent/v1/level${level}/revoke/${uuid}`;
|
1236
|
+
const urlV2 = `${endpoint}/consent/v2/level${level}/revoke/${uuid}`;
|
1237
|
+
sessionStorage.setItem("consentGranted", "false");
|
1238
|
+
try {
|
1239
|
+
switch (level) {
|
1240
|
+
case "2":
|
1241
|
+
await axios.put(`${url}`, null, {
|
1242
|
+
headers: {
|
1243
|
+
"Content-Type": "application/json",
|
1244
|
+
Authorization: "Bearer " + jwt
|
1245
|
+
}
|
1246
|
+
});
|
1247
|
+
break;
|
1248
|
+
case "3":
|
1249
|
+
await axios.put(`${url}/${network}/${wallet}`, {
|
1250
|
+
signature
|
1251
|
+
});
|
1252
|
+
break;
|
1253
|
+
case "4":
|
1254
|
+
await axios.put(
|
1255
|
+
`${urlV2}/${network}/${wallet}`,
|
1256
|
+
{
|
1257
|
+
signature
|
1258
|
+
},
|
1259
|
+
{
|
1260
|
+
headers: {
|
1261
|
+
"Content-Type": "application/json",
|
1262
|
+
Authorization: "Bearer " + jwt
|
1263
|
+
}
|
1264
|
+
}
|
1265
|
+
);
|
1266
|
+
break;
|
1267
|
+
default:
|
1268
|
+
break;
|
1269
|
+
}
|
1270
|
+
} catch (error) {
|
1271
|
+
throw error;
|
1272
|
+
}
|
1273
|
+
};
|
1274
|
+
var getMember = async (endpoint, accessToken) => {
|
1275
|
+
try {
|
1276
|
+
const member = await axios.get(
|
1277
|
+
`${endpoint}/index.php?webserviceClient=site&webserviceVersion=1.0.0&option=persona&api=hal&task=getTokenByUser`,
|
1278
|
+
{
|
1279
|
+
headers: {
|
1280
|
+
"Content-Type": "application/json",
|
1281
|
+
Authorization: "Bearer " + accessToken
|
1282
|
+
}
|
1283
|
+
}
|
1284
|
+
);
|
1285
|
+
if (member?.data?.result?.member_id) {
|
1286
|
+
const data = await axios.get(
|
1287
|
+
`${endpoint}/index.php?webserviceClient=site&webserviceVersion=1.0.0&option=member&api=hal&id=${member?.data?.result?.member_id}`,
|
1288
|
+
{
|
1289
|
+
headers: {
|
1290
|
+
"Content-Type": "application/json",
|
1291
|
+
Authorization: "Bearer " + accessToken
|
1292
|
+
}
|
1293
|
+
}
|
1294
|
+
);
|
1295
|
+
return data?.data;
|
1296
|
+
}
|
1297
|
+
} catch (error) {
|
1298
|
+
console.log("getMember", error);
|
1299
|
+
throw error;
|
1300
|
+
}
|
1301
|
+
};
|
1302
|
+
var getWalletNonce = async (endpoint, wallet, publicAddress) => {
|
1303
|
+
try {
|
1304
|
+
const reqAuthFormData = {
|
1305
|
+
publicAddress,
|
1306
|
+
wallet,
|
1307
|
+
text: `Login with nonce: {}`
|
1308
|
+
};
|
1309
|
+
const config = {
|
1310
|
+
method: "post",
|
1311
|
+
url: `${endpoint}/index.php?webserviceClient=site&webserviceVersion=1.0.0&option=member&task=getWalletNonce&api=hal`,
|
1312
|
+
headers: {
|
1313
|
+
"Content-Type": "application/json"
|
1314
|
+
},
|
1315
|
+
data: reqAuthFormData
|
1316
|
+
};
|
1317
|
+
const { data } = await axios(config);
|
1318
|
+
if (data.result) {
|
1319
|
+
return data.result;
|
1320
|
+
}
|
1321
|
+
throw false;
|
1322
|
+
} catch (error) {
|
1323
|
+
throw error;
|
1324
|
+
}
|
1325
|
+
};
|
1326
|
+
var verifySignature = async (endpoint, wallet, publicAddress, signature) => {
|
1327
|
+
try {
|
1328
|
+
const returnParams = new URLSearchParams(window.location.search)?.get("return");
|
1329
|
+
const reqAuthFormData = {
|
1330
|
+
wallet,
|
1331
|
+
publicAddress,
|
1332
|
+
signature
|
1333
|
+
};
|
1334
|
+
const config = {
|
1335
|
+
method: "post",
|
1336
|
+
url: `${endpoint}/index.php?webserviceClient=site&webserviceVersion=1.0.0&option=member&task=walletLogin&api=hal&return=${returnParams ?? null}`,
|
1337
|
+
headers: {
|
1338
|
+
"Content-Type": "application/json"
|
1339
|
+
},
|
1340
|
+
data: reqAuthFormData
|
1341
|
+
};
|
1342
|
+
const { data } = await axios(config);
|
1343
|
+
if (data?.result) {
|
1344
|
+
return data?.result;
|
1345
|
+
} else {
|
1346
|
+
throw false;
|
1347
|
+
}
|
1348
|
+
} catch (error) {
|
1349
|
+
console.log(error);
|
1350
|
+
throw error;
|
1351
|
+
}
|
1352
|
+
};
|
1353
|
+
var getConsentTemplate = async (domain) => {
|
1354
|
+
try {
|
1355
|
+
const endpointWeb3 = "https://web3id.backend.aesirx.io:8001";
|
1356
|
+
const data = await axios.get(`${endpointWeb3}/datastream/template/${domain}`, {
|
1357
|
+
headers: {
|
1358
|
+
"Content-Type": "application/json"
|
1359
|
+
}
|
1360
|
+
});
|
1361
|
+
if (data) {
|
1362
|
+
return data;
|
1363
|
+
}
|
1364
|
+
} catch (error) {
|
1365
|
+
console.log("error", error);
|
1366
|
+
}
|
1367
|
+
};
|
1368
|
+
|
1119
1369
|
export {
|
1120
1370
|
useTranslation,
|
1121
1371
|
AnalyticsContext,
|
1122
|
-
AnalyticsContextProvider_default
|
1372
|
+
AnalyticsContextProvider_default,
|
1373
|
+
agreeConsents,
|
1374
|
+
loadGtagScript,
|
1375
|
+
loadGtmScript,
|
1376
|
+
getConsents,
|
1377
|
+
getSignature,
|
1378
|
+
getNonce,
|
1379
|
+
revokeConsents,
|
1380
|
+
getMember,
|
1381
|
+
getWalletNonce,
|
1382
|
+
verifySignature,
|
1383
|
+
getConsentTemplate
|
1123
1384
|
};
|
1124
1385
|
/*
|
1125
1386
|
* @copyright Copyright (C) 2022 AesirX. All rights reserved.
|
package/dist/index.js
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
import {
|
2
2
|
AnalyticsContext,
|
3
|
-
AnalyticsContextProvider_default
|
4
|
-
|
3
|
+
AnalyticsContextProvider_default,
|
4
|
+
getConsentTemplate
|
5
|
+
} from "./chunk-N36VEWSG.js";
|
5
6
|
|
6
7
|
// src/AnalyticsNext/index.tsx
|
7
|
-
import React2 from "react";
|
8
|
+
import React2, { useEffect as useEffect2, useState as useState2 } from "react";
|
8
9
|
|
9
10
|
// src/AnalyticsNext/handle.tsx
|
10
11
|
import React, { useCallback, useEffect, useState } from "react";
|
@@ -381,10 +382,14 @@ var startTracker = async (endpoint, url, referer, user_agent, attributesVisit) =
|
|
381
382
|
});
|
382
383
|
if (window["aesirxTrackEcommerce"] === "true" && sessionStorage.getItem("aesirx-analytics-flow") !== (await responseStart)?.flow_uuid) {
|
383
384
|
sessionStorage.setItem("aesirx-analytics-flow", (await responseStart)?.flow_uuid);
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
385
|
+
try {
|
386
|
+
await trackerService(
|
387
|
+
rememberFlow(window.location.origin, (await responseStart)?.flow_uuid),
|
388
|
+
{}
|
389
|
+
);
|
390
|
+
} catch (error) {
|
391
|
+
console.log("Remember Flow Error", error);
|
392
|
+
}
|
388
393
|
}
|
389
394
|
return responseStart;
|
390
395
|
} catch (error) {
|
@@ -518,8 +523,8 @@ var handle_default = AnalyticsHandle;
|
|
518
523
|
|
519
524
|
// src/AnalyticsNext/index.tsx
|
520
525
|
import dynamic from "next/dynamic";
|
521
|
-
var ConsentComponent = dynamic(() => import("./Consent-
|
522
|
-
var ConsentComponentCustom = dynamic(() => import("./ConsentCustom-
|
526
|
+
var ConsentComponent = dynamic(() => import("./Consent-U7ESRVW5.js"), { ssr: false });
|
527
|
+
var ConsentComponentCustom = dynamic(() => import("./ConsentCustom-CSPWI25K.js"), { ssr: false });
|
523
528
|
var AnalyticsNext = ({
|
524
529
|
router,
|
525
530
|
attributes,
|
@@ -528,14 +533,28 @@ var AnalyticsNext = ({
|
|
528
533
|
isLoggedApp,
|
529
534
|
children
|
530
535
|
}) => {
|
531
|
-
|
536
|
+
const [layout, setLayout] = useState2(process.env.NEXT_PUBLIC_CONSENT_LAYOUT);
|
537
|
+
const [gtagId, setGtagId] = useState2(process.env.NEXT_PUBLIC_ANALYTICS_GTAG_ID);
|
538
|
+
const [gtmId, setGtmId] = useState2(process.env.NEXT_PUBLIC_ANALYTICS_GTM_ID);
|
539
|
+
useEffect2(() => {
|
540
|
+
const init = async () => {
|
541
|
+
const data = await getConsentTemplate(window.location.host);
|
542
|
+
setLayout(data?.data?.template ?? process.env.NEXT_PUBLIC_CONSENT_LAYOUT);
|
543
|
+
setGtagId(data?.data?.gtag_id ?? process.env.NEXT_PUBLIC_ANALYTICS_GTAG_ID);
|
544
|
+
setGtmId(data?.data?.gtm_id ?? process.env.NEXT_PUBLIC_ANALYTICS_GTM_ID);
|
545
|
+
};
|
546
|
+
init();
|
547
|
+
}, []);
|
548
|
+
return /* @__PURE__ */ React2.createElement(React2.Fragment, null, /* @__PURE__ */ React2.createElement(AnalyticsContextProvider_default, null, /* @__PURE__ */ React2.createElement(handle_default, { router, attributes }, children, process.env.NEXT_PUBLIC_DISABLE_ANALYTICS_CONSENT !== "true" && /* @__PURE__ */ React2.createElement(React2.Fragment, null, oldLayout || layout === "original" ? /* @__PURE__ */ React2.createElement(
|
532
549
|
ConsentComponent,
|
533
550
|
{
|
534
551
|
endpoint: process.env.NEXT_PUBLIC_ENDPOINT_ANALYTICS_URL,
|
535
552
|
networkEnv: process.env.NEXT_PUBLIC_CONCORDIUM_NETWORK,
|
536
553
|
aesirXEndpoint: process.env.NEXT_PUBLIC_ENDPOINT_URL ?? "https://api.aesirx.io",
|
537
554
|
loginApp,
|
538
|
-
isLoggedApp
|
555
|
+
isLoggedApp,
|
556
|
+
gtagId,
|
557
|
+
gtmId
|
539
558
|
}
|
540
559
|
) : /* @__PURE__ */ React2.createElement(
|
541
560
|
ConsentComponentCustom,
|
@@ -544,21 +563,24 @@ var AnalyticsNext = ({
|
|
544
563
|
networkEnv: process.env.NEXT_PUBLIC_CONCORDIUM_NETWORK,
|
545
564
|
aesirXEndpoint: process.env.NEXT_PUBLIC_ENDPOINT_URL ?? "https://api.aesirx.io",
|
546
565
|
loginApp,
|
547
|
-
isLoggedApp
|
566
|
+
isLoggedApp,
|
567
|
+
gtagId,
|
568
|
+
gtmId,
|
569
|
+
layout
|
548
570
|
}
|
549
571
|
)))));
|
550
572
|
};
|
551
573
|
var AnalyticsNext_default = AnalyticsNext;
|
552
574
|
|
553
575
|
// src/AnalyticsReact/index.tsx
|
554
|
-
import React4, { Suspense } from "react";
|
576
|
+
import React4, { Suspense, useEffect as useEffect4, useState as useState3 } from "react";
|
555
577
|
|
556
578
|
// src/AnalyticsReact/handle.tsx
|
557
|
-
import React3, { useEffect as
|
579
|
+
import React3, { useEffect as useEffect3 } from "react";
|
558
580
|
var AnalyticsHandle2 = ({ location, history, children }) => {
|
559
581
|
const AnalyticsStore = React3.useContext(AnalyticsContext);
|
560
582
|
const endPoint = process.env.REACT_APP_ENDPOINT_ANALYTICS_URL;
|
561
|
-
|
583
|
+
useEffect3(() => {
|
562
584
|
const init = async () => {
|
563
585
|
if (!AnalyticsStore.visitor_uuid) {
|
564
586
|
const referer = location.pathname ? location.pathname : "";
|
@@ -572,13 +594,13 @@ var AnalyticsHandle2 = ({ location, history, children }) => {
|
|
572
594
|
};
|
573
595
|
init();
|
574
596
|
}, [location.pathname, history]);
|
575
|
-
|
597
|
+
useEffect3(() => {
|
576
598
|
const init = async () => {
|
577
599
|
endTrackerVisibilityState(endPoint);
|
578
600
|
};
|
579
601
|
init();
|
580
602
|
}, []);
|
581
|
-
|
603
|
+
useEffect3(() => {
|
582
604
|
const init = async () => {
|
583
605
|
window["event_uuid"] = AnalyticsStore.event_uuid;
|
584
606
|
window["visitor_uuid"] = AnalyticsStore.visitor_uuid;
|
@@ -590,22 +612,39 @@ var AnalyticsHandle2 = ({ location, history, children }) => {
|
|
590
612
|
var handle_default2 = AnalyticsHandle2;
|
591
613
|
|
592
614
|
// src/AnalyticsReact/index.tsx
|
593
|
-
var ConsentComponent2 = React4.lazy(() => import("./Consent-
|
594
|
-
var ConsentComponentCustom2 = React4.lazy(() => import("./ConsentCustom-
|
615
|
+
var ConsentComponent2 = React4.lazy(() => import("./Consent-U7ESRVW5.js"));
|
616
|
+
var ConsentComponentCustom2 = React4.lazy(() => import("./ConsentCustom-CSPWI25K.js"));
|
595
617
|
var AnalyticsReact = ({ location, history, oldLayout = false, children }) => {
|
596
|
-
|
618
|
+
const [layout, setLayout] = useState3(process.env.REACT_APP_CONSENT_LAYOUT);
|
619
|
+
const [gtagId, setGtagId] = useState3(process.env.REACT_APP_ANALYTICS_GTAG_ID);
|
620
|
+
const [gtmId, setGtmId] = useState3(process.env.REACT_APP_ANALYTICS_GTM_ID);
|
621
|
+
useEffect4(() => {
|
622
|
+
const init = async () => {
|
623
|
+
const data = await getConsentTemplate(window.location.host);
|
624
|
+
setLayout(data?.data?.template ?? process.env.REACT_APP_CONSENT_LAYOUT);
|
625
|
+
setGtagId(data?.data?.gtag_id ?? process.env.REACT_APP_ANALYTICS_GTAG_ID);
|
626
|
+
setGtmId(data?.data?.gtm_id ?? process.env.REACT_APP_ANALYTICS_GTM_ID);
|
627
|
+
};
|
628
|
+
init();
|
629
|
+
}, []);
|
630
|
+
return /* @__PURE__ */ React4.createElement(AnalyticsContextProvider_default, null, /* @__PURE__ */ React4.createElement(handle_default2, { location, history }, children, process.env.REACT_APP_DISABLE_ANALYTICS_CONSENT !== "true" && /* @__PURE__ */ React4.createElement(Suspense, { fallback: /* @__PURE__ */ React4.createElement(React4.Fragment, null) }, oldLayout || layout === "original" ? /* @__PURE__ */ React4.createElement(
|
597
631
|
ConsentComponent2,
|
598
632
|
{
|
599
633
|
endpoint: process.env.REACT_APP_ENDPOINT_ANALYTICS_URL,
|
600
634
|
networkEnv: process.env.REACT_APP_CONCORDIUM_NETWORK,
|
601
|
-
aesirXEndpoint: process.env.REACT_APP_ENDPOINT_URL ?? "https://api.aesirx.io"
|
635
|
+
aesirXEndpoint: process.env.REACT_APP_ENDPOINT_URL ?? "https://api.aesirx.io",
|
636
|
+
gtagId,
|
637
|
+
gtmId
|
602
638
|
}
|
603
639
|
) : /* @__PURE__ */ React4.createElement(
|
604
640
|
ConsentComponentCustom2,
|
605
641
|
{
|
606
|
-
endpoint: process.env.
|
607
|
-
networkEnv: process.env.
|
608
|
-
aesirXEndpoint: process.env.
|
642
|
+
endpoint: process.env.REACT_APP_ENDPOINT_ANALYTICS_URL,
|
643
|
+
networkEnv: process.env.REACT_APP_CONCORDIUM_NETWORK,
|
644
|
+
aesirXEndpoint: process.env.REACT_APP_ENDPOINT_URL ?? "https://api.aesirx.io",
|
645
|
+
gtagId,
|
646
|
+
gtmId,
|
647
|
+
layout
|
609
648
|
}
|
610
649
|
))));
|
611
650
|
};
|