authscape 1.0.708 → 1.0.712

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.
@@ -1,551 +1,246 @@
1
- import React, {useState, useRef, useEffect} from 'react';
2
- import { ToastContainer, toast } from 'react-toastify';
3
- import { ThemeProvider } from '@mui/material/styles';
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { ToastContainer, toast } from "react-toastify";
3
+ import { ThemeProvider } from "@mui/material/styles";
4
4
  import Head from "next/head";
5
- import { useSearchParams, usePathname } from 'next/navigation';
6
- import axios from 'axios';
5
+ import { useSearchParams, usePathname } from "next/navigation";
6
+ import axios from "axios";
7
7
  import querystring from "query-string";
8
- import Router from 'next/router';
9
- import GA4React from 'ga-4-react';
10
- import { create } from 'zustand'
11
- import { clarity } from 'react-microsoft-clarity';
12
- import { authService } from 'authscape';
13
-
14
- export function AuthScapeApp ({Component, layout, loadingLayout, pageProps, muiTheme = {}, store={}, enforceLoggedIn = false, enableAuth = true, enableConsentDialog = false}) {
15
-
16
- const [frontEndLoadedState, setFrontEndLoadedState] = useState(false);
17
- const [concentState, setConcentState] = useState(false);
18
-
19
- const [isLoadingShow, setIsLoadingShow] = useState(false);
20
-
21
- const [signedInUserState, setSignedInUserState] = useState(null);
22
-
23
- const loadingAuth = useRef(false);
24
- const frontEndLoaded = useRef(false);
25
- const signedInUser = useRef(null);
26
- const queryCodeUsed = useRef(null);
27
-
28
- const ga4React = useRef(null);
29
-
30
- const searchParams = useSearchParams();
31
- const queryRef = searchParams.get('ref');
32
- const queryCode = searchParams.get('code');
33
-
34
- const pathname = usePathname();
35
-
36
- const handleConsentAccepted = (prefs) => {
37
- console.log("Consent accepted:", prefs);
38
- setConcentState(prefs);
39
- };
40
-
41
- const handleConsentRejected = () => {
42
- console.log("Consent rejected");
43
- setConcentState(null);
44
- // disable analytics scripts, etc.
45
- };
46
-
47
- const signInValidator = async (queryCode) => {
48
-
49
- if (queryCodeUsed.current != queryCode)
50
- {
51
- queryCodeUsed.current = queryCode;
52
- }
53
- else
54
- {
55
- return;
56
- }
57
-
58
- let codeVerifier = window.localStorage.getItem("verifier");
59
- if (queryCode != null && codeVerifier != null)
60
- {
61
- const headers = {'Content-Type': 'application/x-www-form-urlencoded'}
62
-
63
- let queryString = querystring.stringify({
64
- code: queryCode,
65
- grant_type: "authorization_code",
66
- redirect_uri: window.location.origin + "/signin-oidc",
67
- client_id: process.env.client_id,
68
- client_secret: process.env.client_secret,
69
- code_verifier: codeVerifier
70
- });
71
-
72
- try
73
- {
74
- let response = await axios.post(process.env.authorityUri + '/connect/token', queryString, {
75
- headers: headers
76
- });
77
-
78
- let domainHost = window.location.hostname.split('.').slice(-2).join('.');
79
- window.localStorage.removeItem("verifier");
80
-
81
- await setCookie('access_token', response.data.access_token, {
82
- maxAge: 60 * 60 * 24 * 365, // 1 year,
83
- path: '/',
84
- domain: domainHost,
85
- secure: true
86
- });
87
-
88
- await setCookie('expires_in', response.data.expires_in, {
89
- maxAge: 60 * 60 * 24 * 365, // 1 year,
90
- path: '/',
91
- domain: domainHost,
92
- secure: true
93
- });
94
-
95
- await setCookie('refresh_token', response.data.refresh_token, {
96
- maxAge: 60 * 60 * 24 * 365, // 1 year,
97
- path: '/',
98
- domain: domainHost,
99
- secure: true
100
- });
101
-
102
- // await setCookie(null, "access_token", response.data.access_token,
103
- // {
104
- // maxAge: 2147483647,
105
- // path: '/',
106
- // domain: domainHost,
107
- // secure: true
108
- // });
109
-
110
- // await setCookie(null, "expires_in", response.data.expires_in,
111
- // {
112
- // maxAge: 2147483647,
113
- // path: '/',
114
- // domain: domainHost,
115
- // secure: true
116
- // });
117
-
118
- // await setCookie(null, "refresh_token", response.data.refresh_token,
119
- // {
120
- // maxAge: 2147483647,
121
- // path: '/',
122
- // domain: domainHost,
123
- // secure: true
124
- // });
125
-
126
-
127
- let redirectUri = localStorage.getItem("redirectUri")
128
- localStorage.clear();
129
- if (redirectUri != null)
130
- {
131
- window.location.href = redirectUri;
132
- }
133
- else
134
- {
135
- window.location.href = "/";
136
- }
137
- }
138
- catch(exp)
139
- {
140
- //alert(exp)
141
- }
142
- }
8
+ import Router from "next/router";
9
+ import GA4React from "ga-4-react";
10
+ import { create } from "zustand";
11
+ import { clarity } from "react-microsoft-clarity";
12
+ import { authService } from "authscape";
13
+
14
+ export function AuthScapeApp({
15
+ Component,
16
+ layout,
17
+ loadingLayout,
18
+ pageProps,
19
+ muiTheme = {},
20
+ store = {},
21
+ enforceLoggedIn = false,
22
+ enableAuth = true,
23
+ }) {
24
+ const [frontEndLoadedState, setFrontEndLoadedState] = useState(false);
25
+ const [isLoadingShow, setIsLoadingShow] = useState(false);
26
+ const [signedInUserState, setSignedInUserState] = useState(null);
27
+
28
+ const loadingAuth = useRef(false);
29
+ const signedInUser = useRef(null);
30
+ const queryCodeUsed = useRef(null);
31
+ const ga4React = useRef(null);
32
+
33
+ const searchParams = useSearchParams();
34
+ const queryCode = searchParams.get("code");
35
+ const pathname = usePathname();
36
+
37
+ // ---------- PKCE Sign-in ----------
38
+ const signInValidator = async (queryCode) => {
39
+ if (queryCodeUsed.current === queryCode) return;
40
+ queryCodeUsed.current = queryCode;
41
+
42
+ if (typeof window === "undefined") return;
43
+
44
+ const codeVerifier = window.localStorage.getItem("verifier");
45
+ if (!queryCode || !codeVerifier) return;
46
+
47
+ const headers = { "Content-Type": "application/x-www-form-urlencoded" };
48
+
49
+ const queryString = querystring.stringify({
50
+ code: queryCode,
51
+ grant_type: "authorization_code",
52
+ redirect_uri: window.location.origin + "/signin-oidc",
53
+ client_id: process.env.client_id,
54
+ client_secret: process.env.client_secret,
55
+ code_verifier: codeVerifier,
56
+ });
57
+
58
+ try {
59
+ const response = await axios.post(
60
+ process.env.authorityUri + "/connect/token",
61
+ queryString,
62
+ { headers }
63
+ );
64
+
65
+ const domainHost = window.location.hostname
66
+ .split(".")
67
+ .slice(-2)
68
+ .join(".");
69
+
70
+ window.localStorage.removeItem("verifier");
71
+
72
+ await setCookie("access_token", response.data.access_token, {
73
+ maxAge: 60 * 60 * 24 * 365,
74
+ path: "/",
75
+ domain: domainHost,
76
+ secure: true,
77
+ });
78
+ await setCookie("expires_in", response.data.expires_in, {
79
+ maxAge: 60 * 60 * 24 * 365,
80
+ path: "/",
81
+ domain: domainHost,
82
+ secure: true,
83
+ });
84
+ await setCookie("refresh_token", response.data.refresh_token, {
85
+ maxAge: 60 * 60 * 24 * 365,
86
+ path: "/",
87
+ domain: domainHost,
88
+ secure: true,
89
+ });
90
+
91
+ const redirectUri = localStorage.getItem("redirectUri");
92
+ localStorage.clear();
93
+ window.location.href = redirectUri || "/";
94
+ } catch (exp) {
95
+ console.error("PKCE sign-in failed", exp);
143
96
  }
144
-
145
- async function initGA(G) {
146
- if (!GA4React.isInitialized() && G && process.browser) {
147
- ga4React.current = new GA4React(G, { debug_mode: !process.env.production });
148
-
149
- try {
150
-
151
- await ga4React.current.initialize();
152
-
153
- } catch (error) {
154
- console.error(error);
155
- }
156
- }
97
+ };
98
+
99
+ // ---------- GA + Clarity ----------
100
+ async function initGA(G) {
101
+ if (
102
+ typeof window !== "undefined" &&
103
+ !GA4React.isInitialized() &&
104
+ G
105
+ ) {
106
+ ga4React.current = new GA4React(G, {
107
+ debug_mode: !process.env.production,
108
+ });
109
+ try {
110
+ await ga4React.current.initialize();
111
+ } catch (error) {
112
+ console.error(error);
113
+ }
157
114
  }
115
+ }
158
116
 
159
- const logEvent = (category, action, label) => {
160
-
161
- if (ga4React != null && ga4React.current != null && ga4React != "")
162
- {
163
- ga4React.current.event(action, label, category);
164
- }
165
-
166
- if (process.env.enableDatabaseAnalytics == "true")
167
- {
168
- let userId = null;
169
- let locationId = null;
170
- let companyId = null;
171
-
172
- var host = window.location.protocol + "//" + window.location.host;
173
-
174
- if (signedInUser.current != null)
175
- {
176
- userId = signedInUser.current.id;
177
- locationId = signedInUser.current.locationId;
178
- companyId = signedInUser.current.companyId;
179
- }
180
-
181
- apiService().post("/Analytics/Event", {
182
- userId: userId,
183
- locationId: locationId,
184
- companyId: companyId,
185
- uri: window.location.pathname,
186
- category: category,
187
- action: action,
188
- label: label,
189
- host: host
190
- });
191
- }
192
- }
193
-
194
- const databaseDrivenPageView = (pathName) => {
195
-
196
- if (process.env.enableDatabaseAnalytics == "true")
197
- {
198
- let userId = null;
199
- let locationId = null;
200
- let companyId = null;
201
-
202
- var host = window.location.protocol + "//" + window.location.host;
203
-
204
- if (signedInUser.current != null)
205
- {
206
- userId = signedInUser.current.id;
207
- locationId = signedInUser.current.locationId;
208
- companyId = signedInUser.current.companyId;
209
- }
210
-
211
- if (pathName == "/signin-oidc")
212
- {
213
- return;
214
- }
215
-
216
- apiService().post("/Analytics/PageView", {
217
- userId: userId,
218
- locationId: locationId,
219
- companyId: companyId,
220
- uri: pathName,
221
- host: host
222
- });
223
-
224
- }
117
+ const logEvent = (category, action, label) => {
118
+ if (ga4React.current) {
119
+ ga4React.current.event(action, label, category);
225
120
  }
226
-
227
- useEffect(() => {
228
-
229
- let enableAnalytics = false;
230
- const stored = localStorage.getItem("gdpr-consent");
231
- if (stored) {
232
- const prefs = JSON.parse(stored);
233
- if (prefs.analytics)
234
- {
235
- enableAnalytics = true;
236
- }
237
- }
238
-
239
- if ((frontEndLoadedState && !enableConsentDialog) || (frontEndLoadedState && enableConsentDialog && enableAnalytics))
240
- {
241
-
242
- console.log("Analytics are working now!");
243
-
244
- if (pageProps.googleAnalytics4Code != null)
245
- {
246
- initGA(pageProps.googleAnalytics4Code);
247
- }
248
- else if (process.env.googleAnalytics4 != "")
249
- {
250
- initGA(process.env.googleAnalytics4);
251
- }
252
-
253
- if (pageProps.microsoftClarityCode != null)
254
- {
255
- clarity.init(pageProps.microsoftClarityCode);
256
-
257
- if (signedInUser.current != null && clarity.hasStarted())
258
- {
259
- clarity.identify('USER_ID', { userProperty: signedInUser.current.id.toString() });
260
- }
261
- }
262
- else if (process.env.microsoftClarityTrackingCode != "") // if there isn't a private label tracking code use the one built in the app
263
- {
264
- clarity.init(process.env.microsoftClarityTrackingCode);
265
-
266
- if (signedInUser.current != null && clarity.hasStarted())
267
- {
268
- clarity.identify('USER_ID', { userProperty: signedInUser.current.id.toString() });
269
- }
270
- }
271
-
272
- databaseDrivenPageView(window.location.pathname);
273
- Router.events.on('routeChangeComplete', () => {
274
-
275
- if (ga4React != null && ga4React != "")
276
- {
277
- try
278
- {
279
- ga4React.current.pageview(window.location.pathname);
280
- }
281
- catch(exp) {}
282
- }
283
-
284
- databaseDrivenPageView(window.location.pathname);
285
- });
286
-
287
- if (pageProps.hubspotTrackingCode != null && pageProps.hubspotTrackingCode != "")
288
- {
289
- const script = document.createElement("script");
290
- script.src = pageProps.hubspotTrackingCode;
291
- script.async = true;
292
- script.defer = true;
293
- script.id = "hs-script-loader";
294
- document.body.appendChild(script);
295
- }
296
- }
297
- else
298
- {
299
- console.log("No Analytics for you!");
300
- }
301
-
302
- }, [frontEndLoadedState, concentState]);
303
-
304
- const validateUserSignedIn = async () => {
305
-
306
- loadingAuth.current = true;
307
-
308
- if (enableAuth)
309
- {
310
- let usr = await apiService().GetCurrentUser();
311
- if (usr != null)
312
- {
313
- signedInUser.current = usr;
314
- }
315
- }
316
-
121
+ };
122
+
123
+ const databaseDrivenPageView = (pathName) => {
124
+ if (process.env.enableDatabaseAnalytics !== "true") return;
125
+ if (typeof window === "undefined") return;
126
+ if (pathName === "/signin-oidc") return;
127
+
128
+ const host = window.location.protocol + "//" + window.location.host;
129
+
130
+ apiService().post("/Analytics/PageView", {
131
+ userId: signedInUser.current?.id,
132
+ locationId: signedInUser.current?.locationId,
133
+ companyId: signedInUser.current?.companyId,
134
+ uri: pathName,
135
+ host,
136
+ });
137
+ };
138
+
139
+ // ---------- Auth Init ----------
140
+ useEffect(() => {
141
+ if (queryCode) {
142
+ signInValidator(queryCode);
143
+ } else if (!loadingAuth.current) {
144
+ loadingAuth.current = true;
145
+ if (enableAuth) {
146
+ apiService().GetCurrentUser().then((usr) => {
147
+ signedInUser.current = usr;
148
+ setSignedInUserState(usr);
149
+ setFrontEndLoadedState(true);
150
+ });
151
+ } else {
317
152
  setFrontEndLoadedState(true);
318
- frontEndLoaded.current = true;
319
- setSignedInUserState(signedInUser.current);
320
- }
321
-
322
- if (queryCode != null)
323
- {
324
- signInValidator(queryCode);
325
- }
326
- else
327
- {
328
- if (!loadingAuth.current)
329
- {
330
- validateUserSignedIn();
331
- }
332
- }
333
-
334
- useEffect(() => {
335
-
336
- if (signedInUserState == null && enforceLoggedIn && pathname != "/signin-oidc" && frontEndLoadedState == true)
337
- {
338
- authService().login();
339
- }
340
-
341
- }, [signedInUserState, enforceLoggedIn, frontEndLoadedState]);
342
-
343
-
344
- const setIsLoading = (isLoading) => {
345
- setIsLoadingShow(isLoading);
346
- }
347
-
348
- const logPurchase = (transactionId, amount, tax, items) => {
349
-
350
- if (ga4React != null && ga4React != "")
351
- {
352
- ga4React.current.gtag("event", "purchase", {
353
- transaction_id: transactionId,
354
- value: amount,
355
- tax: tax,
356
- currency: "USD",
357
- items: items
358
- });
359
- }
360
- }
361
-
362
- const setToastMessage = (message, options = null) => {
363
-
364
- if (options != null)
365
- {
366
- toast(message, options);
367
- }
368
- else
369
- {
370
- toast(message);
371
- }
372
- }
373
-
374
- const setInfoToastMessage = (message, options = null) => {
375
- if (options != null)
376
- {
377
- toast.info(message, options);
378
- }
379
- else
380
- {
381
- toast.info(message);
382
- }
153
+ }
383
154
  }
155
+ }, [queryCode, enableAuth]);
384
156
 
385
- const setSuccessToastMessage = (message, options = null) => {
386
- if (options != null)
387
- {
388
- toast.success(message, options);
389
- }
390
- else
391
- {
392
- toast.success(message);
393
- }
394
- }
157
+ // ---------- Analytics Init ----------
158
+ useEffect(() => {
159
+ if (!frontEndLoadedState || typeof window === "undefined") return;
395
160
 
396
- const setWarnToastMessage = (message, options = null) => {
397
- if (options != null)
398
- {
399
- toast.warn(message, options);
400
- }
401
- else
402
- {
403
- toast.warn(message);
404
- }
161
+ if (pageProps.googleAnalytics4Code) {
162
+ initGA(pageProps.googleAnalytics4Code);
163
+ } else if (process.env.googleAnalytics4) {
164
+ initGA(process.env.googleAnalytics4);
405
165
  }
406
166
 
407
- const setErrorToastMessage = (message, options = null) => {
408
- if (options != null)
409
- {
410
- toast.error(message, options);
411
- }
412
- else
413
- {
414
- toast.error(message);
415
- }
167
+ if (pageProps.microsoftClarityCode) {
168
+ clarity.init(pageProps.microsoftClarityCode);
169
+ } else if (process.env.microsoftClarityTrackingCode) {
170
+ clarity.init(process.env.microsoftClarityTrackingCode);
416
171
  }
417
-
418
-
419
- const GetSignedInUser = () => {
420
-
421
- if (signedInUser != null)
422
- {
423
- let _signedInUser = signedInUser.current;
424
-
425
- if (_signedInUser != null)
426
- {
427
- _signedInUser.hasRole = function(name) {
428
-
429
- if (_signedInUser.roles != null)
430
- {
431
- if (_signedInUser.roles.find(r => r.name === name) != null)
432
- {
433
- return true;
434
- }
435
- else
436
- {
437
- return false;
438
- }
439
- }
440
- };
441
-
442
- _signedInUser.hasRoleId = function(id) {
443
-
444
- if (_signedInUser.roles != null)
445
- {
446
- if (_signedInUser.roles.find(r => r.id === id) != null)
447
- {
448
- return true;
449
- }
450
- else
451
- {
452
- return false;
453
- }
454
- }
455
- };
456
-
457
- _signedInUser.hasPermission = function(name) {
458
-
459
- if (_signedInUser.permissions != null)
460
- {
461
- if (_signedInUser.permissions.find(r => r === name) != null)
462
- {
463
- return true;
464
- }
465
- else
466
- {
467
- return false;
468
- }
469
- }
470
- };
471
- }
472
172
 
473
- return _signedInUser;
474
- }
475
- else
476
- {
477
- return null;
478
- }
173
+ databaseDrivenPageView(window.location.pathname);
174
+
175
+ Router.events.on("routeChangeComplete", (url) => {
176
+ ga4React.current?.pageview(url);
177
+ databaseDrivenPageView(url);
178
+ });
179
+ }, [frontEndLoadedState]);
180
+
181
+ // ---------- Enforce Login ----------
182
+ useEffect(() => {
183
+ if (
184
+ enforceLoggedIn &&
185
+ pathname !== "/signin-oidc" &&
186
+ frontEndLoadedState &&
187
+ !signedInUserState
188
+ ) {
189
+ authService().login();
479
190
  }
480
-
481
-
482
- const useStore = create((set) => (store));
483
-
484
- return (
485
- <>
486
- <Head>
487
- <meta name="viewport" content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86"></meta>
488
-
489
- {/* {(pageProps != null && pageProps.oemCompanyId != null) ?
490
- <>
491
- <link
492
- href={process.env.apiUri + "/api/PrivateLabel/GetDataFromRecord?oemCompanyId=" + pageProps.oemCompanyId}
493
- rel="stylesheet"
494
- />
495
- </>
496
- :
497
- <>
498
- <link rel="icon" href="/favicon.ico" />
499
- </>
500
- } */}
501
-
502
- </Head>
503
-
504
- {enableConsentDialog &&
505
- <GDPRConsentDialog
506
- onAccept={handleConsentAccepted}
507
- onReject={handleConsentRejected}
508
- enableAnalytics={true}
509
- enableMarketing={false}
510
- // additionalPrivacyFeatures={[
511
- // {id: "danceParty", title: "Dance Party", description: "Hello world this is about the feature", checked: true },
512
- // {id: "frog", title: "Able to see Frogs", description: "Frogs will appear on your screen", checked: true }
513
- // ]}
514
- />
515
- }
516
-
517
- <ThemeProvider theme={muiTheme}>
518
- {frontEndLoadedState != null && frontEndLoadedState && pathname != "/signin-oidc" &&
519
- <>
520
- {layout != null && layout({
521
- children: <Component {...pageProps} currentUser={GetSignedInUser()} loadedUser={frontEndLoadedState} setIsLoading={setIsLoading} logEvent={logEvent} logPurchase={logPurchase} store={useStore} setToastMessage={setToastMessage} setInfoToastMessage={setInfoToastMessage} setSuccessToastMessage={setSuccessToastMessage} setWarnToastMessage={setWarnToastMessage} setErrorToastMessage={setErrorToastMessage} />,
522
- currentUser: GetSignedInUser(),
523
- logEvent: logEvent,
524
- setIsLoading: setIsLoading,
525
- toast: toast,
526
- store: useStore,
527
- setToastMessage: setToastMessage,
528
- pageProps: pageProps,
529
- setInfoToastMessage: setInfoToastMessage,
530
- setSuccessToastMessage: setSuccessToastMessage,
531
- setWarnToastMessage: setWarnToastMessage,
532
- setErrorToastMessage: setErrorToastMessage
533
- })}
534
-
535
- {layout == null &&
536
- <Component {...pageProps} currentUser={GetSignedInUser()} loadedUser={frontEndLoadedState} setIsLoading={setIsLoading} logEvent={logEvent} logPurchase={logPurchase} store={useStore} setToastMessage={setToastMessage} setInfoToastMessage={setInfoToastMessage} setSuccessToastMessage={setSuccessToastMessage} setWarnToastMessage={setWarnToastMessage} setErrorToastMessage={setErrorToastMessage} />
537
- }
538
- </>
539
- }
540
- <ToastContainer />
541
-
542
- </ThemeProvider>
543
-
544
- {loadingLayout &&
545
- <>
546
- {loadingLayout(isLoadingShow)}
547
- </>
548
- }
549
- </>
550
- )
551
- }
191
+ }, [signedInUserState, enforceLoggedIn, frontEndLoadedState, pathname]);
192
+
193
+ const GetSignedInUser = () => signedInUser.current;
194
+ const useStore = create((set) => store);
195
+
196
+ // ---------- Render ----------
197
+ const pageContent = layout
198
+ ? layout({
199
+ children: (
200
+ <Component
201
+ {...pageProps}
202
+ currentUser={GetSignedInUser()}
203
+ loadedUser={frontEndLoadedState}
204
+ setIsLoading={setIsLoadingShow}
205
+ logEvent={logEvent}
206
+ store={useStore}
207
+ toast={toast}
208
+ />
209
+ ),
210
+ currentUser: GetSignedInUser(),
211
+ setIsLoading: setIsLoadingShow,
212
+ logEvent,
213
+ toast,
214
+ store: useStore,
215
+ pageProps,
216
+ })
217
+ : (
218
+ <Component
219
+ {...pageProps}
220
+ currentUser={GetSignedInUser()}
221
+ loadedUser={frontEndLoadedState}
222
+ setIsLoading={setIsLoadingShow}
223
+ logEvent={logEvent}
224
+ store={useStore}
225
+ toast={toast}
226
+ />
227
+ );
228
+
229
+ return (
230
+ <>
231
+ <Head>
232
+ <meta
233
+ name="viewport"
234
+ content="width=device-width, initial-scale=0.86, maximum-scale=5.0, minimum-scale=0.86"
235
+ />
236
+ </Head>
237
+
238
+ <ThemeProvider theme={muiTheme}>
239
+ {pageContent}
240
+ <ToastContainer />
241
+ </ThemeProvider>
242
+
243
+ {loadingLayout && loadingLayout(isLoadingShow)}
244
+ </>
245
+ );
246
+ }