PageSourceSearch

https://my.jcb.co.jp/apl/renew/passkey/nnlappsdk-9.2.0/js/utils.js

js jcb.co.jp collected 2026-10-02 02:13:20 UTC 182,553 bytes, 5,150 lines download raw bytes

1// Common classes used both in adaptive and regular SDKs.
2
3// Create global object if needed.
4window.nnl = window.nnl || {};
5
6// The flag indicates that this is a Cordova App (in cordova document URL starts with FILE)
7nnl.isCordovaApp = (document.URL.indexOf('http://') === -1) && (document.URL.indexOf('https://') === -1);
8
9// DefaultUIConfig represents the default configuration for the UI.
10const DefaultUIConfig = Object.freeze({
11	reasonCodeMap: {
12		passkey_on_new_device: "New device detected!",
13		key_protection_software: "Additional verification is required."
14	}
15});
16
17/**
18 * @classdesc Promise wrapper that can be externally setteled.
19 * @class
20 * @ignore
21 */
22const PromiseWrapper = function() {
23	const _this = this;
24	/**
25	 * Resolves the actual Promise object.
26	 * @method PromiseWrapper#resolve
27	 * @param {any} value The value to resolve the Promise with.
28	 */
29	this.resolve = null;
30
31	/**
32	 * Rejects the actual Promise object.
33	 * @method PromiseWrapper#reject
34	 * @param {any} value The value to reject the Promise with.
35	 */
36	this.reject = null;
37
38	const mPromise = new Promise(function(resolve, reject) {
39		_this.resolve = resolve;
40		_this.reject = reject;
41	});
42
43	/**
44	 * Returns the actual Promise object.
45	 */
46	this.get = function() {
47		return mPromise;
48	}
49
50	Object.freeze(this);
51}
52
53/**
54 * Defines type of fido operation
55 * @enum {string}
56 */
57const FidoOperationType = {
58	REG: 'REG',
59	AUTH: 'AUTH',
60	LIST_REG: 'LIST_REG',
61	RENAME_REG: 'RENAME_REG',
62	DELETE_REG: 'DELETE_REG',
63	LIST_OOB_AUTH: 'LIST_OOB_AUTH'
64}
65Object.freeze(FidoOperationType);
66
67/**
68 * Defines state of ongoing adaptive operation which should be used to determine next step on adaptive process.
69 * @enum {string}
70 */
71const OperationState = {
72	/**
73	 * should continue with init.
74	 */
75	OP_INIT: 'OP_INIT',
76	/**
77	 * should continue with update.
78	 */
79	OP_UPDATE: 'OP_UPDATE',
80	/**
81	 * should complete with success result.
82	 */
83	OP_CANCEL: 'OP_CANCEL',
84	/**
85	 * Should cancel entire adaptive process.
86	 */
87	OP_SUCCESS: 'OP_SUCCESS',
88	/**
89	 * Should lock the method until lock period is passed.
90	 */
91	OP_LOCKED: 'OP_LOCKED'
92}
93Object.freeze(OperationState);
94
95/**
96 *  Defines type of ongoing adaptive operation. {@link MethodUI} implementations use this to show appropriate UI.
97 *  @enum {string}
98 */
99const OperationType = {
100	/**
101	 * Indicates registration operation.
102	 */
103    REGISTRATION: 'REGISTRATION',
104	/**
105	 * Indicates authentication operation.
106	 */
107    AUTHENTICATION: 'AUTHENTICATION'
108}
109Object.freeze(OperationType);
110
111/**
112 * Specifies the type of QR code to generate.
113 * @enum {string}
114 */
115const QRType = {
116	/**
117	 * The QR code will be generated for mobile apps only.
118	 * The QR code will be specific for the relying party (the OOB data will not include the operation endpoint).
119	 */
120	APP_RP_SPECIFIC: 'APP_RP_SPECIFIC',
121	/**
122	 * The QR code will be generated for mobile apps only.
123	 * The QR code will be generated for any relying party (the OOB data will include the operation endpoint).
124	 */
125	APP_ANY_RP: 'APP_ANY_RP',
126	/**
127	 * The QR code will be generated for mobile apps and browsers.
128	 * The QR code will be specific for the relying party (the OOB data will not include the operation endpoint).
129	 */
130	UNIVERSAL_RP_SPECIFIC: 'UNIVERSAL_RP_SPECIFIC',
131	/**
132	 * The QR code will be generated for mobile apps and browsers.
133	 * The QR code will be generated for any relying party (the OOB data will include the operation endpoint).
134	 */
135	UNIVERSAL_ANY_RP: 'UNIVERSAL_ANY_RP'
136}
137Object.freeze(QRType);
138
139/**
140 * Adaptive method types.
141 * @type {Object}
142 * @name AdaptiveType
143 * @property {string} EMAIL Indicates method that uses Email OTP.
144 * @property {string} SMS Indicates method that uses SMS OTP.
145 * @property {string} FIDO Indicates method that uses FIDO protocol.
146 * @property {string} FIDO_OOB Indicates method that uses OOB.
147 * @property {string} EXTERNAL_AUTH Indicates method that uses External Authentication.
148 */
149const AdaptiveType = new (function() {
150	// Adaptive types.
151	this.EMAIL = 'Email OTP';
152	this.SMS = 'SMS OTP';
153	this.FIDO = 'FIDO Auth';
154	this.FIDO_OOB = 'FIDO OOB Auth';
155	this.EXTERNAL_AUTH = 'External Auth';
156	this.PHOTO_ID = 'Photo ID';
157
158	// Adaptive names.
159	this.names = {};
160	this.names[this.EMAIL] = 'OTP Using Email';
161	this.names[this.SMS] = 'OTP Using SMS';
162	this.names[this.FIDO] = '';
163	this.names[this.FIDO_OOB] = '';
164	this.names[this.EXTERNAL_AUTH] = '';
165	this.names[this.PHOTO_ID] = 'Using Photo ID';
166
167	Object.freeze(this);
168})();
169
170
171/**
172 * Adaptive method types.
173 * @type {Object}
174 * @name AuthMethod
175 */
176const AuthMethod = {
177	FIDO2_AUTH:'FIDO2-AUTH',
178	UAF_AUTH: 'UAF-AUTH',
179	OOB_PUSH: 'OOB-PUSH'
180}
181Object.freeze(AuthMethod);
182
183/**
184 * @enum {string}
185 */
186const MethodState = {
187	SUCCEEDED:"SUCCEEDED",
188	AWAITING_USER_ACTION:"AWAITING_USER_ACTION",
189	PENDING:"PENDING",
190	IN_PROGRESS:"IN_PROGRESS",
191	FAILED:"FAILED"
192}
193Object.freeze(MethodState);
194
195/**
196 * @enum {string}
197 * Indicates which subsystem of the SDK has produced the error.
198 */
199const SubSystem = {
200	/** The error was produced by the UAF subsystem */
201	UAF: 'UAF',
202	/** The error was produced by the FIDO2 subsystem */
203	FIDO2: 'FIDO2',
204	/** The error was produced by a non FIDO subsystem */
205	NON_FIDO: 'NON_FIDO',
206	/** The error was produced by the Adaptive Engine subsystem */
207	ADAPTIVE_ENGINE: 'ADAPTIVE_ENGINE'
208}
209Object.freeze(SubSystem);
210
211
212const QUICK_AUTH_DATA_FIDO2_KEY = 'dataFido2';
213const SECURITY_KEY_DEFAULT_NAME = 'Security Key';
214
215/**
216 * @enum {string}
217 * Indicates adaptive operation type in {@link AdaptiveResult}.
218 */
219const AdaptiveOperation = {
220	/** The result is for authentication operation */
221	AUTH: 'AUTH',
222	/** The result is for suggest register operation */
223	SUGGEST_REG: 'SUGGEST_REG',
224	/** The result is for sign up operation */
225	SIGN_UP: 'SIGN_UP'
226
227}
228Object.freeze(AdaptiveOperation);
229
230// Levels and tags definitions for the UI configuration.
231const UIConfigTags = Object.freeze({
232    TAG_LEVEL_SIGN_IN_VIEW: "sign_in_view",
233    TAG_LEVEL_USER_NAME_VIEW: "user_name_view",
234    TAG_LEVEL_METHODS_VIEW: "methods_view",
235    TAG_LEVEL_REGISTRATION_VIEW: "registration_view",
236    TAG_LEVEL_SIGN_UP: "sign_up_view",
237    TAG_LEVEL_MANAGE_REGISTRATION_VIEW: "manage_regs_view",
238    TAG_LEVEL_FIDO_VIEW: "fido_view",
239    TAG_LEVEL_ADAPTIVE_VIEW: "adaptive_view",
240    TAG_LEVEL_SUGGEST_REG_VIEW: "suggest_reg_view",
241    TAG_LEVEL_PENDING_AUTHS_VIEW: "pending_auths_view",
242	TAG_LEVEL_OOB_VIEW: "oob_view",
243
244    TAG_LABEL_METHOD_TITLE_MOBILE_DEVICE: "method_title_mobile_device",
245    TAG_LABEL_METHOD_TITLE_FIDO: "method_title_fido",
246    TAG_LABEL_METHOD_TITLE_EMAIL: "method_title_email",
247    TAG_LABEL_METHOD_TITLE_SMS: "method_title_sms",
248    TAG_LABEL_METHOD_TITLE_EMAIL_OR_SMS: "method_title_email_or_sms",
249
250    TAG_LABEL_SIGN_IN_EMPTY_USERNAME: "sign_in_empty_username",
251    TAG_LABEL_SIGN_IN_EMPTY_PASSWORD: "sign_in_empty_password",
252
253    TAG_LABEL_METHOD_SIGN_IN_TITLE_PREFIX: "method_sign_in_title_prefix",
254    TAG_LABEL_METHOD_TRANSACTION_TITLE_PREFIX: "method_transaction_title_prefix",
255
256    TAG_LABEL_METHOD_REGISTER_TITLE_PREFIX: "method_register_title_prefix",
257
258    TAG_LABEL_REGISTER_EMAIL_TITLE: "register_email_title",
259    TAG_LABEL_REGISTER_PHONE_TITLE: "register_phone_title",
260
261    TAG_LABEL_PENDING_AUTH_REMAINING_TIME_PREFIX: "pending_auth_remaining_time_prefix",
262    TAG_LABEL_PENDING_AUTH_PUSH_STATUS_PREFIX: "pending_auth_push_status_prefix",
263
264    TAG_LABEL_REG_PLATFORM_AUTH_TITLE: "register_platform_auth_title",
265    TAG_LABEL_REG_SECURITY_KEY_TITLE: "register_security_key_title",
266    TAG_LABEL_PASSKEY_NAME_ON_ICLOUD_KEYCHAIN: "passkey_name_on_icloud_keychain",
267    TAG_LABEL_PASSKEY_NAME_ON_CHROME_ON_MAC: "passkey_name_on_chrome_on_mac",
268    TAG_LABEL_PASSKEY_NAME_WINDOWS_HELLO: "passkey_name_windows_hello",
269    TAG_LABEL_PASSKEY_NAME_ON_GOOGLE_PW_MANAGER: "passkey_name_on_google_password_manager",
270    TAG_LABEL_PASSKEY_NAME_LOCAL_ANDROID: "passkey_name_on_local_android",
271    TAG_LABEL_REG_SECURITY_KEY_SUCCESS_MESSAGE: "register_security_key_success_message",
272    TAG_LABEL_SECURITY_KEY_NAME: "security_key_name",
273
274    TAG_IMAGE_PLATFORM_AUTHENTICATOR_REG_ICON: "platform_authenticator_reg_icon",
275    TAG_IMAGE_SECURITY_KEY_REG_ICON: "security_key_reg_icon",
276
277    TAG_LABEL_AUTHENTICATOR_USED_JUST_NOW: "used_just_now_label",
278    TAG_LABEL_AUTHENTICATOR_LAST_USED_FORMAT: "last_used_authenticator_format",
279    TAG_LABEL_AUTHENTICATOR_USED_SECOND: "second",
280    TAG_LABEL_AUTHENTICATOR_USED_SECONDS: "seconds",
281    TAG_LABEL_AUTHENTICATOR_USED_MINUTE: "minute",
282    TAG_LABEL_AUTHENTICATOR_USED_MINUTES: "minutes",
283    TAG_LABEL_AUTHENTICATOR_USED_HOUR: "hour",
284    TAG_LABEL_AUTHENTICATOR_USED_HOURS: "hours",
285    TAG_LABEL_AUTHENTICATOR_USED_DAY: "day",
286    TAG_LABEL_AUTHENTICATOR_USED_DAYS: "days",
287    TAG_LABEL_AUTHENTICATOR_USED_WEEK: "week",
288    TAG_LABEL_AUTHENTICATOR_USED_WEEKS: "weeks",
289    TAG_LABEL_AUTHENTICATOR_USED_MONTH: "month",
290    TAG_LABEL_AUTHENTICATOR_USED_MONTHS: "months",
291    TAG_LABEL_AUTHENTICATOR_USED_YEAR: "year",
292    TAG_LABEL_AUTHENTICATOR_USED_YEARS: "years",
293
294	TAG_LABEL_OOB_TIME_REMAINING: "oob_time_remaining",
295
296    // Error tags.
297    TAG_ERROR_SIGN_IN_INCORRECT_IDENTIFIER: "sign_in_incorrect_identifier",
298    TAG_ERROR_NOT_ENROLLED: "error_not_enrolled",
299    TAG_ERROR_REG_NO_MATCH: "error_reg_no_match",
300    TAG_ERROR_AUTH_NO_MATCH: "error_auth_no_match",
301    TAG_ERROR_CONNECTION: "error_connection",
302    TAG_ERROR_WRONG_ATTEMPTS: "error_wrong_attempts",
303    TAG_ERROR_SESSION_EXPIRED: "error_session_expired",
304    TAG_ERROR_KEY_DISAPPEARED_PERMANENTLY: "error_key_disappeared_permanently",
305    TAG_ERROR_NOT_INSTALLED: "error_not_installed",
306    TAG_ERROR_ALREADY_REGISTERED: "error_already_registered",
307    TAG_ERROR_DEFAULT: "error_default",
308    TAG_ERROR_SIGN_UP_SESSION_EXPIRED: "sign_up_session_expired",
309    TAG_ERROR_USER_NOT_RESPONSIVE: "error_user_not_responsive"
310});
311
312/**
313 * @enum {string}
314 * Defines the dark mode behavior for built in SDK views.
315 */
316const NNLDarkMode = {
317	/** Dark mode is always disabled. */
318	OFF: 'off',
319	/** Dark mode is always enabled. */
320	ON: 'on',
321	/** Dark mode is enabled or disabled based on browser/platform settings. */
322	AUTOMATIC: 'automatic'
323}
324
325/**
326 * Defines keys for UI customization configuration JSON object.
327 * @class
328 * @ignore
329 */
330const JsonWrapperKey = Object.freeze({
331	level: 'level',
332	styles: 'styles',
333	strings: 'strings',
334	images: 'images',
335	views: 'views'
336});
337
338/**
339 * Wraps the UI customization configuration and provides methods for searching for configuration values.
340 * @class
341 * @ignore
342 * @param {any} value - The JSON value to wrap.
343 * @param {JsonWrapper} [parent] - The parent wrapper when wrapping a child item.
344 */
345const JsonWrapper = function(value, parent) {
346	const _this = this;
347	const mValue = typeof value === 'object' ? NNLUtils.cloneObject(value) : value;
348	const mParent = parent;
349
350	/**
351	 * Recursively merges the provided wrapper into this one.
352	 * @param {JsonWrapper} wrapperToBeApplied the wrapper to merge items from.
353	 */
354	this.mergeJsonObjectsRecursively = function(wrapperToBeApplied) {
355		wrapperToBeApplied.getAllKeys().forEach(function(key) {
356			const subWrapperExisting = _this.get(key);
357			const subWrapperToBeApplied = wrapperToBeApplied.get(key);
358
359			if(subWrapperExisting != null && subWrapperToBeApplied != null && subWrapperToBeApplied.isJsonObject()) {
360				subWrapperExisting.mergeJsonObjectsRecursively(subWrapperToBeApplied);
361				addValue(key, subWrapperExisting);
362			} else {
363				addValue(key, subWrapperToBeApplied);
364			}
365		});
366	}
367
368	/**
369	 * Returns wrapper item for given key.
370	 * @param {string} key the key to get wrapper for.
371	 * @returns {JsonWrapper} the wrapper item for given key or null if item for given key does not exist.
372	 */
373	this.get = function(key) {
374		if(mValue[key] != null) {
375			return new JsonWrapper(mValue[key], _this);
376		}
377
378		return null;
379	}
380
381	/**
382	 * Returns the current wrapped value.
383	 * @returns {any} the wrapped value.
384	 */
385	this.getValue = function() {
386		return mValue;
387	}
388
389	/**
390	 * Returns the list of keys of the current wrapepd object.
391	 * @returns {string[]} list of keys.
392	 */
393	this.getAllKeys = function() {
394		return Object.keys(mValue);
395	}
396
397	/**
398	 * Checks whether the current wrapped value is a JSON object or not.
399	 * @returns {boolean} true if the wrapped value is JSON object, false otherwise.
400	 */
401	this.isJsonObject = function() {
402		return typeof mValue === 'object';
403	}
404
405	/**
406	 * Inserts the wrapped value of the provided wrapper into the current wrapepd object for given key.
407	 * @param {string} key the key of the value being inserted
408	 * @param {JsonWrapper} wrapper the wrapper to get the value from to insert.
409	 */
410	function addValue(key, wrapper) {
411		mValue[key] = wrapper.getValue();
412	}
413
414	/**
415	 * Searches for configuration object for specified sub property inside the specified property starting from this level and going up.
416	 * @param {string} property - The property to search configuration in.
417	 * @param {string} subProperty - The sub property to search configuration for.
418	 * @returns {object} The configuration object if found, null otherwise.
419	 * @private
420	 */
421	this.searchUp = function(property, subProperty) {
422		if(subProperty == null) {
423			return null;
424		}
425
426		const element = searchLevel(property, subProperty);
427		if(element == null && mParent != null) {
428			return mParent.searchUp(property, subProperty);
429		}
430
431		return element;
432	}
433
434	/**
435	 * Searches for configuration object for specified sub property inside the specified property starting from this level
436	 * and going down by the specified list of levels. Returns the configuration found in the lowest level.
437	 * @param {string[]} levels - The list of levels where the configuration should be searched.
438	 * @param {string} property - The property to search configuration in.
439	 * @param {string} subProperty - The sub property to search configuration for.
440	 * @returns {object} The configuration object if found, null otherwise.
441	 * @private
442	 */
443	this.searchDown = function(levels, property, subProperty) {
444		if(levels != null && levels.length > 0) {
445			const wrapper = _this.getLevel(levels[0]);
446			if(wrapper != null) {
447				const lowerElement = wrapper.searchDown(levels.slice(1), property, subProperty);
448				if(lowerElement != null) {
449					return lowerElement;
450				}
451			}
452		}
453
454		return searchLevel(property, subProperty);
455	}
456
457	/**
458	 * Returns the configuration wrapper for the specified level.
459	 * @param {string} level - The level to get the configuration wrapper for.
460	 * @returns {JsonWrapper} The configuration wrapper if found, null otherwise.
461	 * @private
462	 */
463	this.getLevel = function(level) {
464		if(level == null) {
465			return null;
466		}
467
468		const views = mValue[JsonWrapperKey.views];
469		// If the "views" prop is not found, return null to continue the search.
470		if (views == null) {
471			return null;
472		}
473
474		const keys = Object.keys(views);
475		for(let index in keys) {
476			const key = keys[index];
477			if(typeof views[key] === 'object') {
478				let levelConfig = new JsonWrapper(views[key], _this);
479				if(key === level) {
480					return levelConfig;
481				} else {
482					levelConfig = levelConfig.getLevel(level);
483					if(levelConfig != null) {
484						return levelConfig;
485					}
486				}
487			}
488		}
489	}
490
491	// Searches configuration on the current level.
492	function searchLevel(property, subProperty) {
493		// Get the object for specified property.
494		let element = mValue[property];
495
496		// If a configuration for specified property was found,
497		// retrieve configuration for specified sub property.
498		if(element != null) {
499			element = element[subProperty];
500		}
501
502		return element;
503	}
504
505	Object.freeze(this);
506}
507Object.freeze(JsonWrapper);
508
509/**
510 * Wraps an HTML element to apply customization according the "data-tag" attribute.
511 * @class
512 * @param {HTMLElement} view - The view to apply the customization to.
513 */
514const ViewWrapper = function(view) {
515	const _this = this;
516	const mView = view;
517	const mTags = {};
518
519	// Parse the "data-tag" attribute value if specified.
520	if(mView.dataset.tag != null) {
521		mView.dataset.tag.split(';').forEach(function(tag) {
522			const parts = tag.split(':');
523			let key = '';
524			let value = '';
525			if(parts.length === 2) {
526				key = parts[0].trim();
527				value = parts[1].trim();
528			}
529
530			if(key !== '' && value !== '') {
531				mTags[key] = value;
532			} else {
533				console.error('The tag value "' + tag + '" is not valid for view', mView);
534			}
535		});
536	}
537
538	/**
539	 * Loads the UI customization configuration and starts applying it.
540	 */
541	this.onShow = function() {
542		const configJsonWrapper = NNLUiConfig.getJsonWrapper();
543		if(configJsonWrapper != null) {
544			_this.customize(configJsonWrapper);
545		}
546
547		// Enable or disable dark mode based on configuration (called after customize() to be able to apply images).
548		NNLUiConfig.setDarkMode();
549	}
550
551	/**
552	 * Starts applying customization from the specified configuration to this view and its children
553	 * @private
554	 * @param {JsonWrapper} wrapper - The customization configuration
555	 */
556	this.customize = function(wrapper) {
557		// If a level tag is specified, try to retrieve and use configuration for that level.
558		const levelWrapper = wrapper.getLevel(mTags[JsonWrapperKey.level]);
559		if(levelWrapper != null) {
560			wrapper = levelWrapper;
561		}
562
563		const styleJson = wrapper.searchUp(JsonWrapperKey.styles, mTags[JsonWrapperKey.styles]);
564		if(styleJson != null) {
565			applyStyles(styleJson);
566		}
567
568		const text = wrapper.searchUp(JsonWrapperKey.strings, mTags[JsonWrapperKey.strings]);
569		if(text != null) {
570			applyText(text);
571		}
572
573		const lightImage = wrapper.searchUp(JsonWrapperKey.images, mTags[JsonWrapperKey.images]);
574		const darkImage = wrapper.searchUp(JsonWrapperKey.images, 'dark_' + mTags[JsonWrapperKey.images]);
575		applyImage(lightImage, darkImage);
576
577		// Apply configuration to the child elements too.
578		for(let index = 0; index < mView.children.length; ++index) {
579			(new ViewWrapper(mView.children[index])).customize(wrapper);
580		}
581	}
582
583	function applyStyles(styleJson) {
584		// The the current view is the SDK defined custom toggle, apply the the values as CSS properties.
585		if(mTags[JsonWrapperKey.styles] === 'toggle') {
586			if(styleJson.thumb_enable_color != null) {
587				mView.style.setProperty('--light-thumb-enabled-color', convertColor(styleJson.thumb_enable_color));
588			}
589			if(styleJson.dark_thumb_enable_color != null) {
590				mView.style.setProperty('--dark-thumb-enabled-color', convertColor(styleJson.dark_thumb_enable_color));
591			}
592
593			if(styleJson.thumb_disable_color != null) {
594				mView.style.setProperty('--light-thumb-disabled-color', convertColor(styleJson.thumb_disable_color));
595			}
596			if(styleJson.dark_thumb_disable_color != null) {
597				mView.style.setProperty('--dark-thumb-disabled-color', convertColor(styleJson.dark_thumb_disable_color));
598			}
599
600			if(styleJson.track_enable_color != null) {
601				mView.style.setProperty('--light-track-enabled-color', convertColor(styleJson.track_enable_color));
602			}
603			if(styleJson.dark_track_enable_color != null) {
604				mView.style.setProperty('--dark-track-enabled-color', convertColor(styleJson.dark_track_enable_color));
605			}
606
607			if(styleJson.track_disable_color != null) {
608				mView.style.setProperty('--light-track-disabled-color', convertColor(styleJson.track_disable_color));
609			}
610			if(styleJson.dark_track_disable_color != null) {
611				mView.style.setProperty('--dark-track-disabled-color', convertColor(styleJson.dark_track_disable_color));
612			}
613
614			if(styleJson.border_color != null) {
615				mView.style.setProperty('--light-border-color', convertColor(styleJson.border_color));
616			}
617			if(styleJson.dark_border_color != null) {
618				mView.style.setProperty('--dark-border-color', convertColor(styleJson.dark_border_color));
619			}
620
621			if(styleJson.border_size != null) {
622				mView.style.setProperty('--toggle-border-width', styleJson.border_size + 'px');
623			}
624
625			if(styleJson.corner_radius != null) {
626				mView.style.setProperty('--toggle-border-radius', styleJson.corner_radius + 'px');
627			}
628		} else if (mTags[JsonWrapperKey.styles] === 'progress_bar') {
629			if(styleJson.progress_bar_color != null) {
630				mView.style.setProperty('--light-progress-color', convertColor(styleJson.progress_bar_color));
631			}
632			if(styleJson.dark_progress_bar_color != null) {
633				mView.style.setProperty('--dark-progress-color', convertColor(styleJson.dark_progress_bar_color));
634			}
635		} else {
636			if(styleJson.background_color != null) {
637				mView.style.setProperty('--light-background-color', convertColor(styleJson.background_color));
638			}
639			if(styleJson.dark_background_color != null) {
640				mView.style.setProperty('--dark-background-color', convertColor(styleJson.dark_background_color));
641			}
642
643			if(styleJson.disable_color != null) {
644				const disabledState = getColorAndOpacity(styleJson.disable_color);
645				mView.style.setProperty('--light-disabled-background-color', disabledState.color);
646				mView.style.setProperty('--light-disabled-opacity', disabledState.o
646pacity);
647			}
648			if(styleJson.dark_disable_color != null) {
649				const disabledState = getColorAndOpacity(styleJson.dark_disable_color);
650				mView.style.setProperty('--dark-disabled-background-color', disabledState.color);
651				mView.style.setProperty('--dark-disabled-opacity', disabledState.opacity);
652			}
653
654			if(styleJson.text_color != null) {
655				mView.style.setProperty('--light-text-color', convertColor(styleJson.text_color));
656			}
657			if(styleJson.dark_text_color != null) {
658				mView.style.setProperty('--dark-text-color', convertColor(styleJson.dark_text_color));
659			}
660
661			if(styleJson.border_color != null) {
662				mView.style.setProperty('--light-border-color', convertColor(styleJson.border_color));
663			}
664			if(styleJson.dark_border_color != null) {
665				mView.style.setProperty('--dark-border-color', convertColor(styleJson.dark_border_color));
666			}
667
668			if(styleJson.border_size != null) {
669				mView.style.borderWidth = styleJson.border_size + 'px';
670			}
671
672			if(styleJson.text_alignment != null) {
673				mView.style.textAlign = styleJson.text_alignment;
674				mView.dataset.textAlign = styleJson.text_alignment;
675			}
676
677			if(styleJson.corner_radius != null) {
678				mView.style.borderRadius = styleJson.corner_radius + 'px';
679			}
680		}
681	}
682
683	// Sets the given text as a placeholder for input elements and as display text for other elements.
684	function applyText(text) {
685		if(mView instanceof HTMLInputElement) {
686			mView.placeholder = text;
687		} else {
688			mView.innerText = text;
689		}
690	}
691
692	// Sets the given light and dark images as custom properties to the view.
693	function applyImage(lightImage, darkImage) {
694		if(lightImage != null) {
695			if(lightImage !== '') {
696				mView.dataset.lightImage = NNLUtils.getAbsoluteImageURL(lightImage);
697				mView.dataset.darkImage = mView.dataset.lightImage;
698			} else {
699				// If image source is set to empty value, it should be hidden.
700				mView.style.display = 'none';
701				// In that case ignore dark image as well.
702				darkImage = null;
703			}
704		}
705		if(darkImage != null) {
706			mView.dataset.darkImage = NNLUtils.getAbsoluteImageURL(darkImage);
707		}
708	}
709
710	function convertColor(colorString) {
711		// The #AARRGGBB should be converted to #RRGGBBAA
712		if(colorString.length === 9) {
713			return '#' + colorString.slice(3) + colorString.slice(1, 3);
714		}
715
716		// #ARGB should be converted to #RGBA
717		if(colorString.length === 5) {
718			return '#' + colorString.slice(2) + colorString.slice(1, 2);
719		}
720
721		return colorString;
722	}
723
724	function getColorAndOpacity(colorString) {
725		let opacity = null;
726
727		if(colorString.length == 9) {
728			// Parse from double digit hex.
729			opacity = parseInt(colorString.slice(1, 3), 16);
730			colorString = '#' + colorString.slice(3);
731		} else if(colorString.length == 5) {
732			opacity = colorString.slice(1, 2);
733			// Make a double digit hex, then parse.
734			opacity = parseInt(opacity + opacity, 16);
735			colorString = '#' + colorString.slice(2);
736		}
737
738		if(opacity == null || isNaN(opacity)) {
739			opacity = 1;
740		} else {
741			opacity = (opacity/255).toFixed(2);
742		}
743
744		return {
745			color: colorString,
746			opacity: opacity
747		}
748	}
749
750	function getImageURL(imageURL) {
751		return 'url("' + NNLUtils.getAbsoluteImageURL(imageURL) + '")';
752	}
753
754	Object.freeze(this);
755}
756
757/**
758* Defines the WebAuthn user mediation for Conditional UI.
759* @type {string}
760*/
761const USER_MEDIATION_CONDITIONAL = "conditional";
762
763/**
764 * @classdesc A singleton factory class to create and provide {@link MethodUI} implementations for particular method types.
765 * @interface
766 */
767const MethodUIFactory = (function() {
768	var mInstance = null;
769	/**
770	 * This method implementation should create {@link MethodUI} instance for the given type.
771	 * <br/>The implementation can leverage the default implementation of {@link MethodUI} for the following types.
772	 * Method type                     | Default implementation
773	 * ------------------------------- | ----------------------
774	 * {@link AdaptiveType FIDO_OOB}   | OOBMethodUI({@link Extras extras})
775	 * {@link AdaptiveType EMAIL, SMS} | OtpMethodUI({@link Extras extras})
776	 * See the example below for details.
777	 *
778	 * @example <caption>Using custom UI with default implementation of {@link MethodUI}.</caption>
779	 * // Get the default factory in order to forward calls to it when needed.
780	 * var defaultFactory =  MethodUIFactory.getInstance();
781	 * // Create instance of custom factory and set it as current.
782	 * MethodUIFactory.setInstance(new CustomMethodUIFactory());
783	 * var CustomMethodUIFactory = function() {
784	 *     this.createMethodUI = function(methodType, extras, sessionData) {
785	 *         // For OOB, E-mail OTP and SMS OTP method types create instance of default MethodUI with custom UI implementation.
786	 *         if(methodType === AdaptiveType.FIDO_OOB) {
787	 *             // CustomOobUI implements {@link IOobLiveData} interface.
788	 *             var oobMethodUI = new OOBMethodUI(extras, sessionData);
789	 *             var oobLiveData = new CustomOobUI(oobMethodUI);
790	 *             oobMethodUI.setLiveData(oobLiveData);
791	 *             return oobMethodUI;
792	 *         }
793	 *
794	 *         if(methodType === AdaptiveType.EMAIL || methodType === AdaptiveType.SMS) {
795	 *             // CustomOtpUI implements {@link IOtpLiveData} interface.
796	 *             var otpMethodUI = new OtpMethodUI(extras, sessionData);
797	 *             var otpLiveData = new CustomOtpUI(otpMethodUI);
798	 *             otpMethodUI.setLiveData(otpLiveData);
799	 *             return otpMethodUI;
800	 *         }
801	 *
802	 *         // Forward call to default factory for other method types.
803	 *         return defaultFactory.createMethodUI(methodType, extras, sessionData);
804	 *     }
805	 *
806	 *     // Forward call to the default factory for the other functions.
807	 *     this.isSupported = function(methodType) {
808	 *         return defaultFactory.isSupported(methodType);
809	 *     }
810	 *
811	 *     this.getInstructions = function(methodType) {
812	 *         return defaultFactory.getInstructions(methodType);
813	 *     }
814	 *
815	 *     this.getGroups = function() {
816	 *         return defaultFactory.getGroups();
817	 *     }
818	 *
819	 *     this.getTitle = function(methodGroup, opType) {
820	 *         return defaultFactory.getTitle(methodGroup, opType);
821	 *     }
822	 *
823 	 *     this.getIcon = function(methodGroup) {
824	 *         return defaultFactory.getIcon(methodGroup);
825	 *     }
826	 * }
827	 *
828	 * @method MethodUIFactory#createMethodUI
829	 * @param {AdaptiveType} methodType The type of desired MethodUI. 
830     * @param {Extras} [extras] Extra options.
831	 * @param {SessionData} [sessionData] Object containing session information as well as custom properties set by API Server plugins.
832     * @returns {MethodUI} MethouUI class instance of given type.
833	 */
834
835	/**
836	 * This method implementation should return a boolean value indicating whether
837	 * the factory supports the given method type or not.
838	 * @method MethodUIFactory#isSupported
839	 * @param {AdaptiveType} methodType Method type to check.
840	 * @returns {boolean} true if method is supported or false otherwise.
841	 */
842
843 	/**
844	 * This method implementation should return an instruction message for given method type.
845	 * @method MethodUIFactory#getInstructions
846	 * @param {AdaptiveType} methodType Method type to get instruction for.
847	 * @returns {string}
847 Instructions string for method type.
848	 */
849
850	/**
851	 * This method implementation should return the list of supported authentication method groups.
852     * Authentication methods can be grouped if they utilize the same UI.
853	 * @method MethodUIFactory#getGroups
854	 * @returns {AdaptiveMethod[][]} The list of method groups.
855	 */
856
857	/**
858	 * This method implementation should return the title of a group of authentication methods
859	 * for given operation.
860	 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get title for.
861	 * @param {OpType} opType The operation to get title for.
862	 * @method MethodUIFactory#getTitle
863	 * @returns {string} The authentication methods' group title.
864	 */
865
866 	/**
867	 * This method implementation should return the icon URL for a group of authentication methods.
868	 * The URL can be either an http URL or a data URL.
869	 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get icon URL for.
870	 * @method MethodUIFactory#getIcon
871	 * @returns {string} The authentication method group's icon URL, null if not specified.
872	 */
873
874	return {
875		 /**
876	     * Returns the MethodUIFactory instance.
877	     * If no instance was set using setInstance method, creates and returns {@link DefaultMethodUIFactory} instance
878	     * @returns {MethodUIFactory} Stored instance of MethodUIFactory.
879	     * @memberof MethodUIFactory
880	     */
881		getInstance: function() {
882			if(mInstance === null) {
883				mInstance = new DefaultMethodUIFactory();
884			}
885
886			return mInstance;
887		},
888		/**
889	     * Sets the MethodUIFactory instance.
890	     * @param {MethodUIFactory} instance The new instance.
891	     * @memberof MethodUIFactory
892	     */
893		setInstance: function(instance) {
894			NNLUtils.validateArgument('instance', instance, 'object', null, false);
895			mInstance = instance;
896		}
897	};
898})();
899
900/**
901 * @classdesc Default implementation of MethodUIFactory.
902 * @class
903 * @implements MethodUIFactory
904 */
905var DefaultMethodUIFactory = function() {
906	/**
907	 * Creates and returns default {@link MethodUI} instance for given type.
908	 * @param {AdaptiveType} methodType The type of desired MethodUI. 
909     * @param {Extras} [extras] Extra options.
910	 * @param {SessionData} [sessionData] Object containing session information as well as custom properties set by API Server plugins.
911     * @returns {MethodUI} MethodUI class instance of given type.
912	 */
913	this.createMethodUI = function(methodType, extras, sessionData) {
914		if(methodType === AdaptiveType.EMAIL || methodType === AdaptiveType.SMS) {
915			var otpMethodUI = new OtpMethodUI(extras, sessionData);
916			var otpLiveData = new DefaultOtpUI(otpMethodUI);
917			otpMethodUI.setLiveData(otpLiveData);
918			return otpMethodUI;
919		}
920
921		if(methodType === AdaptiveType.FIDO) {
922			return new FIDOMethodUI(extras, sessionData);
923		}
924
925		if(methodType === AdaptiveType.FIDO_OOB) {
926			var oobMethodUI = new OOBMethodUI(extras, sessionData);
927			var oobLiveData = new DefaultOobUI(oobMethodUI);
928			oobMethodUI.setLiveData(oobLiveData);
929			return oobMethodUI;
930		}
931
932		throw {outcome: Outcome.NO_MATCH};
933	}
934
935	/**
936	 * Indicates whether the given method type is supported by the default factory or not.
937	 * @param {AdaptiveType} methodType Method type to check.
938	 * @returns {boolean} true if method is supported or false otherwise.
939	 */
940	this.isSupported = function(methodType) {
941		switch(methodType) {
942		case AdaptiveType.EMAIL:
943		case AdaptiveType.SMS:
944		case AdaptiveType.FIDO_OOB:
945			return true;
946
947		case AdaptiveType.FIDO:
948			// If FIDO2 is not supported in a web browser, then "FIDO Auth" method is not supported.
949			return (nnl.isCordovaApp || window.PublicKeyCredential != null);
950
951		default:
952			return false;
953		}
954	}
955
956	/**
957	 * Returns instruction message for given method type.
958	 * @param {AdaptiveType} methodType Method type to get instruction for.
959	 * @returns {string} Instructions string for method type.
960	 */
961	this.getInstructions = function(methodType) {
962		switch(methodType) {
963		case AdaptiveType.EMAIL:
964            return NNLUiConfig.getText(
965                [UIConfigTags.TAG_LEVEL_REGISTRATION_VIEW, UIConfigTags.TAG_LEVEL_ADAPTIVE_VIEW],
966                UIConfigTags.TAG_LABEL_REGISTER_EMAIL_TITLE,
967                'Add your email address to help secure your account'
968            );
969		case AdaptiveType.SMS:
970            return NNLUiConfig.getText(
971                [UIConfigTags.TAG_LEVEL_REGISTRATION_VIEW, UIConfigTags.TAG_LEVEL_ADAPTIVE_VIEW],
972                UIConfigTags.TAG_LABEL_REGISTER_PHONE_TITLE,
973                'Add your phone number to help secure your account'
974            );
975
976		default:
977			return '';
978		}
979	}
980
981	/**
982	 * The function returns the list of supported authentication method groups.
983     * Authentication methods can be grouped if they utilize the same UI.
984	 * @returns {AdaptiveMethod[][]} The list of method groups.
985	 */
986	this.getGroups = function() {
987		return [
988			[
989				{type: AdaptiveType.EMAIL, name: AdaptiveType.names[AdaptiveType.EMAIL]},
990				{type: AdaptiveType.SMS, name: AdaptiveType.names[AdaptiveType.SMS]},
991	 		]
992		];
993	}
994
995	/**
996	 * This method returns the title of a group of authentication methods for given operation type.
997	 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get title for.
998	 * @param {OpType} opType The operation type to get title for.
999	 * @returns {string} The authentication methods' group title.
1000	 */
1001	this.getTitle = function(methodGroup, opType) {
1002		if(methodGroup == null || methodGroup.length === 0) {
1003			console.error('Empty method group' + opType);
1004			throw {outcome: Outcome.FAILURE};
1005		}
1006
1007		if(methodGroup[0].type === AdaptiveType.FIDO) {
1008			const policyConfig = NNLUtils.getPolicyConfig(methodGroup[0].name);
1009			if(policyConfig != null) {
1010				return policyConfig.title;
1011			}
1012		}
1013
1014		// First set prefix based on operation type.
1015        let defaultMethodFormat = null;
1016        let methodFormatTag = null;
1017        let methodFormatLevels = null;
1018        let methodsLevel = [UIConfigTags.TAG_LEVEL_SIGN_IN_VIEW];
1019		switch(opType) {
1020            case OpType.REG:
1021                methodsLevel = [UIConfigTags.TAG_LEVEL_SUGGEST_REG_VIEW];
1022				break;
1023
1024			case OpType.AUTH:
1025                defaultMethodFormat = 'Sign in with {0}';
1026                methodFormatTag = UIConfigTags.TAG_LABEL_METHOD_SIGN_IN_TITLE_PREFIX;
1027                methodFormatLevels = [UIConfigTags.TAG_LEVEL_SIGN_IN_VIEW, UIConfigTags.TAG_LEVEL_METHODS_VIEW];
1028                break;
1029
1030			case OpType.TRANS:
1031                defaultMethodFormat = 'Authorize with {0}';
1032                methodFormatTag = UIConfigTags.TAG_LABEL_METHOD_TRANSACTION_TITLE_PREFIX;
1033                methodFormatLevels = [UIConfigTags.TAG_LEVEL_SIGN_IN_VIEW, UIConfigTags.TAG_LEVEL_METHODS_VIEW];
1034				break;
1035
1036			default:
1037				console.error('Unsupported operation type' + opType);
1038				throw {outcome: Outcome.FAILURE};
1039		}
1040
1041		// Then set title based on group type and size.
1042		var groupTitle = '';
1043		// Check the type of the first method in the group.
1044		switch(methodGroup[0].type) {
1045			// As grouping it not supported for FIDO and FIDO_OOB methods, they should be the only
1046			// method in the group, so checking the type of only the first method is enough.
1047			case AdaptiveType.FIDO:
1048				groupTitle = NNLUiConfig.getText(
1049                    methodsLevel,
1050                    UIConfigTags.TAG_LABEL_METHOD_TITLE_FIDO,
1051                    'FIDO Auth'
1052                );
1053				break;
1054
1055			case AdaptiveType.FIDO_OOB:
1056				groupTitle = NNLUiConfig.getText(
1057                    methodsLevel,
1058                    UIConfigTags.TAG_LABEL_METHOD_TITLE_MOBILE_DEVICE,
1059                    'Mobile Device'
1060                );
1061				break;
1062
1063			// The SMS and EMAIL methods can be grouped together, so for both cases we should check if
1064			// the other one is in the group too (group size is greater than 1).
1065			case AdaptiveType.EMAIL:
1066			case AdaptiveType.SMS:
1067				if(methodGroup.length > 1) {
1068					groupTitle = NNLUiConfig.getText(
1069                        methodsLevel,
1070                        UIConfigTags.TAG_LABEL_METHOD_TITLE_EMAIL_OR_SMS,
1071                        'Email or SMS'
1072                    );
1073				} else if(methodGroup[0].type === AdaptiveType.EMAIL) {
1074					groupTitle = NNLUiConfig.getText(
1075                        methodsLevel,
1076                        UIConfigTags.TAG_LABEL_METHOD_TITLE_EMAIL,
1077                        'Email'
1078                    );
1079				} else {
1080					groupTitle = NNLUiConfig.getText(
1081                        methodsLevel,
1082                        UIConfigTags.TAG_LABEL_METHOD_TITLE_SMS,
1083                        'SMS'
1084                    );
1085				}
1086				break;
1087
1088			default:
1089				console.error('Unsupported method type ' + methodGroup[0].type);
1090				throw {outcome: Outcome.FAILURE};
1091		}
1092
1093		if (defaultMethodFormat == null &&
1094			methodFormatLevels == null &&
1095			methodFormatTag == null) {
1096
1097			return groupTitle;
1098		}
1099
1100		return NNLUiConfig.getText(
1101            methodFormatLevels,
1102            methodFormatTag,
1103            defaultMethodFormat,
1104            groupTitle
1105        );
1106	}
1107	
1108	/**
1109	 * This method returns the icon URL for a group of authentication methods.
1110	 * Looks for the icon in the {@link AppSdkConfig|UI customization configuration or AppSdkConfig.policyMap}.
1111	 * @param {AdaptiveMethod[]} methodGroup The group of authentication methods to get icon URL for.
1112	 * @returns {string} The authentication method group's icon URL, null if not specified.
1113	 */
1114	this.getIcon = function(methodGroup) {
1115				
1116        if(methodGroup == null || methodGroup.length === 0) {
1117			console.error('Empty method group');
1118			throw {outcome: Outcome.FAILURE};
1119		}
1120
1121		if(methodGroup[0].type === AdaptiveType.FIDO) {
1122			const policyConfig = NNLUtils.getPolicyConfig(methodGroup[0].name);
1123			if(policyConfig != null) {
1124				return policyConfig.icon;
1125			}
1126		}
1127
1128        return null;
1129    }
1130
1131	Object.freeze(this);
1132}
1133
1134/**
1135 * This interface defines methods for displaying OOB operation related UI.
1136 * @interface IOobLiveData
1137 */
1138
1139/**
1140 * This class contains information about started OOB operation.
1141 * @property {QRType} qrType Type of the QR code contained in the qrImage property.
1142 * @property {string} qrImage Base64 PNG image data of QR code.
1143 * @property {boolean}
1143 pushSent Indicates whether the operation was started by sending a push notification or not.
1144 * @property {number} remainingTimeMillis The operation expiration time in milliseconds.
1145 * @property {string} rawData The raw QR data received from the server.
1146 */
1147 var OobInformation = function() {
1148	this.qrType = '';
1149	this.qrImage = '';
1150	this.pushSent = false;
1151	this.remainingTimeMillis = 0;
1152	this.rawData = null;
1153}
1154
1155/**
1156  * This method will be called when an OOB operation has started.
1157  * @method IOobLiveData#onOperationStarted
1158  * @param {OperationType} operation The OOB operation type.
1159  * @param {OobInformation} oobInformation Structure containing operation related information.
1160  */
1161
1162/**
1163 * This method will be called when the user has scanned the QR code or has opened the push notification.
1164 * @method IOobLiveData#onOobReceived
1165 * @param {object} deviceInfo Object containing information about the user's device
1166 * @param {string} deviceInfo.info The name of the device.
1167 */
1168
1169/**
1170 * This method will be called when the OOB operation is completed.
1171 * @method IOobLiveData#onOperationCompleted
1172 * @param {Outcome} outcome The operation outcome.
1173 * @param {string} [message] Error message.
1174 */
1175
1176/**
1177 * This method will be called to notify that the UI should be closed
1178 * @method IOobLiveData#onFinish
1179 */
1180
1181/**
1182 * This interface defines a method for displaying transaction UI
1183 * @interface ITransactionUI
1184 */
1185
1186/**
1187 * This method implementation should display a Transaction Authorization UI
1188 * @method ITransactionUI#openUI
1189 * @param {string} transactionContent base64 encoded transaction content
1190 * @param {string} contentType The MIME-Type of the content, e.g. "text/plain"
1191 * @returns {Promise} A promise that will be resolved with {Outcome:SUCCESS} or {Outcome:CANCELED} indicating user's choice.
1192 */
1193
1194/**
1195 * Default implementation of ITransactionUI.
1196 * @implements {ITransactionUI}
1197 */
1198const DefaultTransactionUI = function() {
1199	// The stored timer for expiration
1200	let mTimer = null;
1201
1202	// The amount of time with milliseconds for the "mTimer" expiration
1203	let mExpirationTime = 3 * 60 * 1000;
1204
1205	var popup, authorizeBtn, declineBtn;
1206
1207	var isReady = NNLUtils.loadHTML('transaction-ui.html', 'nnl-transaction-ui-section')
1208		.then(function() {
1209			document.querySelector('#nnl-transaction-text').innerHTML = '';
1210			popup = document.getElementById('nnl-transaction-ui');
1211			authorizeBtn = document.getElementById('nnl-transaction-authorize');
1212			declineBtn = document.getElementById('nnl-transaction-decline');
1213		});
1214
1215	// Reference to the "resolve" function of the Promise returned by openUI. 
1216	var returnResult;
1217
1218	// Authorize button click handler.
1219	function onAuthorize() {
1220		if (mTimer) {
1221			clearTimeout(mTimer);
1222			mTimer = null;
1223		}
1224
1225		hideUI();
1226		returnResult(Outcome.SUCCESS);
1227	}
1228
1229	// Decline button click handler.
1230	function onDecline() {
1231		if (mTimer) {
1232			clearTimeout(mTimer);
1233			mTimer = null;
1234		}
1235
1236		hideUI();
1237		returnResult(Outcome.CANCELED);
1238	}
1239
1240	// Timer expiration handler.
1241	function onTimeOut() {
1242		hideUI();
1243		returnResult(Outcome.CANCELED);
1244	}
1245	
1246	function hideUI() {
1247		// Hide the UI and remove click handlers. 
1248		NNLUtils.showModal(popup, false);
1249		authorizeBtn.removeEventListener('click', onAuthorize);
1250		declineBtn.removeEventListener('click', onDecline);
1251	}
1252
1253	this.openUI = function(transactionText) {
1254		return isReady.then(function() {
1255			// Add click handlers
1256			authorizeBtn.addEventListener('click', onAuthorize);
1257			declineBtn.addEventListener('click', onDecline);
1258			// Setting the expiration timer
1259			mTimer = setTimeout(onTimeOut, mExpirationTime);
1260			// Display UI
1261			var decodedText = NNLBase64.toString(transactionText);
1262			document.querySelector('#nnl-transaction-text').innerText = decodedText;
1263
1264			new ViewWrapper(popup).onShow();
1265			NNLUtils.showModal(popup, true);
1266
1267			return new Promise(function(resolve) {
1268				returnResult = resolve;
1269			});
1270		});
1271	}
1272
1273	Object.freeze(this);
1274}
1275
1276/**
1277 * @classdesc A singleton factory class to create and provide {@link ITransactionUI} implementations.
1278 * @interface
1279 */
1280const TransactionUIFactory = (function() {
1281	var mInstance = null;
1282
1283	return {
1284		/**
1285		 * Returns the TransactionUIFactory instance.
1286		 * If no instance was set using setInstance method, creates and returns DefaultTransactionUIFactory instance
1287		 * @returns {TransactionUIFactory} Stored instance of TransactionUIFactory.
1288		 * @memberof TransactionUIFactory
1289		 */
1290		getInstance: function() {
1291			if(mInstance === null) {
1292				mInstance = new DefaultTransactionUIFactory();
1293			}
1294
1295			return mInstance;
1296		},
1297		/**
1298		 * Sets the TransactionUIFactory instance.
1299		 * @param {TransactionUIFactory} instance The new instance.
1300		 * @memberof TransactionUIFactory
1301		 */
1302		setInstance: function(instance) {
1303			NNLUtils.validateArgument('instance', instance, 'object', null, false);
1304			mInstance = instance;
1305		}
1306	};
1307})();
1308Object.freeze(TransactionUIFactory);
1309
1310/**
1311 * This method implementation should create and return {@link ITransactionUI} implementation.
1312 * @method TransactionUIFactory#createTransactionUIInstance
1313 * @returns {ITransactionUI} The created instance.
1314 */
1315
1316/**
1317 * @classdesc Default implementation of TransactionUIFactory.
1318 * @class
1319 * @implements TransactionUIFactory
1320 */
1321const DefaultTransactionUIFactory = function() {
1322	this.createTransactionUIInstance = function() {
1323		return new DefaultTransactionUI();
1324	}
1325
1326	Object.freeze(this);
1327}
1328
1329/**
1330 * This interface defines methods for displaying a prompt to ask the user to try
1331 * FIDO authentication in cases when the user has a registration but it is unclear if it is
1332 * registered on the same device where the authentication is being performed.
1333 * @interface ITryFidoUi
1334 */
1335
1336/**
1337 * The implementation should ask the user to try FIDO authentication as it may be available on the current device.
1338 * @method ITryFidoUi#openUI
1339 * @returns {Promise} A promise that should always resolve either with {@link Outcome|Outcome.SUCCESS} when the user
1340 * agrees to try FIDO authentication or with {@link Outcome|Outcome.CANCELED} when the user declines the prompt.
1341 */
1342
1343/**
1344 * This interface defines a method for creating {@link ITryFidoUi} instance.
1345 * @interface ITryFidoUiFactory
1346 */
1347
1348/**
1349 * The implementation should create and return {@link ITryFidoUi} instance.
1350 * This function will be called in case when the user's registration is most likely
1351 * available on the current device for authentication (e.g. a synced passkey).
1352 * @method ITryFidoUiFactory#createTryFidoUi
1353 * @returns {ITryFidoUi} The {@link ITryFidoUi} instance.
1354 */
1355
1356/**
1357 * The implementation should create and return {@link ITryFidoUi} instance.
1358 * This function will be called in case when the user's registration is most likely
1359 * available on another device (e.g. a passkey created on mobile device by scanning a QR code).
1360 * @method ITryFidoUiFactory#createTryHybridUi
1361 * @returns {ITryFidoUi} The {@link ITryFidoUi} instance.
1362 */
1363
1364/**
1365 * A singleton factory class to create and provide {@link ITryFidoUiFactory} implementations.
1366 * @class
1367 * @hideconstructor
1368 */
1369const TryFidoUiFactory = new (function() {
1370	var mInstance = null;
1371
1372	/**
1373	 * Returns {@link ITryFidoUiFactory} instance.
1374	 * If no instance was set using the setInstance method, creates and returns {@link DefaultTryFidoUiFactory} instance.
1375	 * @alias TryFidoUiFactory.getInstance
1376	 * @returns {ITryFidoUiFactory} The factory instance to use.
1377	 */
1378	this.getInstance = function() {
1379		if(mInstance == null) {
1380			mInstance = new DefaultTryFidoUiFactory();
1381		}
1382
1383		return mInstance;
1384	}
1385
1386	/**
1387	 * Sets the {@link ITryFidoUiFactory} instance.
1388	 * @alias TryFidoUiFactory.setInstance
1389	 * @param {ITryFidoUiFactory} instance The new instance.
1390	 */
1391	this.setInstance = function(instance) {
1392		NNLUtils.validateArgument('instance', instance, 'object', null, false);
1393		mInstance = instance;
1394	}
1395
1396	Object.freeze(this);
1397})();
1398
1399/**
1400 * Default implementation of {@link ITryFidoUiFactory}.
1401 * @class
1402 * @implements ITryFidoUiFactory
1403 */
1404function DefaultTryFidoUiFactory() {
1405	/**
1406	 * Creates an instance of {@link DefaultTryFidoUi} for the case when the user's
1407	 * registration is most likely available on the current device for
1408	 * authentication (e.g. a synced passkey).
1409	 */
1410	this.createTryFidoUi = function() {
1411		return new DefaultTryFidoUi(false);
1412	}
1413
1414	/**
1415	 * Creates an instance of {@link DefaultTryFidoUi} for the case when the user's
1416	 * registration is most likely available on another device (e.g. a passkey created
1417	 * on mobile device by scanning a QR code).
1418	 */
1419	this.createTryHybridUi = function() {
1420		return new DefaultTryFidoUi(true);
1421	}
1422}
1423
1424/**
1425 * Default implementation of {@link ITryFidoUi}.
1426 * Handles both Try FIDO and Try Hybrid cases.
1427 * @class
1428 * @implements {ITryFidoUi}
1429 * @param {boolean} isHybridMode indicates whether the prompt should be
1430 * for the current device (`false`) or another device (`true`).
1431 */
1432const DefaultTryFidoUi = function(isHybridMode) {
1433	const mIsReady = NNLUtils.loadHTML('try-fido-ui.html', 'nnl-try-fido-ui-section');
1434	let mDialog, mAuthorizeBtn, mDeclineBtn, mPromise;
1435
1436	// Authorize button click handler.
1437	function onAuthorize() {
1438		hideUI();
1439		mPromise.resolve(Outcome.SUCCESS);
1440	}
1441
1442	// Decline button click handler.
1443	function onDecline() {
1444		hideUI();
1445		mPromise.resolve(Outcome.CANCELED);
1446	}
1447
1448	function hideUI() {
1449		// Hide the UI and remove click handlers. 
1450		NNLUtils.showModal(mDialog, false);
1451		mAuthorizeBtn.removeEventListener('click', onAuthorize);
1452		mDeclineBtn.removeEventListener('click', onDecline);
1453	}
1454
1455	/**
1456	 * Asks the user to try FIDO authentication indicating that a registration
1457	 * may be available on the current or another device.
1458	 * @returns {Promise} A promise that resolves with {@link Outcome|Outcome.SUCCESS} when the user
1459	 * agrees to try FIDO authentication or with {@link Outcome|Outcome.CANCELED} when the user declines the prompt.
1460	 */
1461	this.openUI = function() {
1462		mPromise = new PromiseWrapper();
1463
1464		mIsReady.then(function() {
1465			mDialog = document.querySelector(isHybridMode ? '#nnl-try-hybrid-ui' : '#nnl-try-fido-ui');
1466			mAuthorizeBtn = mDialog.querySelector('.nnl-try-fido-authorize');
1467			mDeclineBtn = mDialog.querySelector('.nnl-try-fido-decline');
1468
1469			// Add click handlers
1470			mAuthorizeBtn.addEventListener('click', onAuthorize);
1471			mDeclineBtn.addEventListener('click', onDecline);
1472
1473			new ViewWrapper(mDialog).onShow();
1474			NNLUtils.showModal(mDialog, true);
1475		});
1476
1477		return mPromise.get();
1478	}
1479
1480	Object.freeze(this);
1481}
1482
1483/**
1484 * This interface defines methods to get operation results information
1485 * @interface IOperationResultListener
1486 */
1487
1488/**
1489 * This method implementation handles operations result on complete
1490 * @method IOperationResultListener#onComplete
1491 * @param {FidoOperationType} operationType The type of operation
1492 * @param {OperationData} operationData The operation data object that contains the result data of completed operation
1493 * @param {Extras} extras Extra options specified for the operation
1494 * @returns {Promise|undefined} If the implementation is doing an asynchronous job, it can return a Promise that should
1495 * be resolved when the job is done (it should not be rejected).
1496 */
1497
1498/**
1499 * OperationData object provided in {@link IOperationResultListener#onComplete|IOperationResultListener.onComplete} method.
1500 * <br>The following table illustrates which properties will be set depending on operation.
1501 * {@link FidoOperationType}    | Properties
1502 * ---------------------------- | --------------------------------------------
1503 * FidoOperationType.REG        | protocolFamily, additionalInfo, authenticatorAttachment
1504 * FidoOperationType.AUTH       | userName, protocolFamily, additionalInfo
1505 * FidoOperationType.LIST_REG   | registrations
1506 * FidoOperationType.DELETE_REG | handle
1507 * FidoOperationType.RENAME_REG | handle
1508 *
1509 * @type {object}
1510 * @name OperationData
1511 * @property {string} [protocolFamily] Either "uaf" or "web".
1512 * @property {object} [additionalInfo] Additional information from Server.
1513 * @property {string} [authenticatorAttachment] Either "platform", "cross-platform" or "unknown". Present only when protocolFamily is "web".
1514 * @property {FidoRegistration[]} [registrations] A list of user's active registered FIDO authenticators.
1515 * @property {object} [handle] Handle of the deleted or renamed authenticator. Will be null when all authenticators were deleted.
1516 * @property {string} [userName] The userName of the user that performed the operation.
1517 * @property {object} [quickData] Data returned from server for quick Authentication.
1518 */
1519
1520/**
1521 * This method implementation handles operations result on failure
1522 * @method IOperationResultListener#onFailure
1523 * @param {FidoOperationType} operationType The operation type
1524 * @param {Error} error The result object containing failure information
1525 * @returns {Promise|undefined} If the implementation is doing an asynchronous job, it can return a Promise that should
1526 * be resolved when the job is done (it should not be rejected).
1527 */
1528
1529/**
1530 * The error object provided in {@link IOperationResultListener#onFailure|IOperationResultListener.onFailure} method.
1531 * @type {object}
1532 * @name Error
1533 * @property {Outcome} outcome Result code returned after processing a FIDO request (see {@link Outcome} object for possible values).
1534 * @property {Object} [additionalInfo] Additional info returned from the server
1535 * @property {Object} [additionalErrorInfo] Additional error information from API Server.
1536 * @property {number} [serverStatusCode] The statusCode from the server.
1537 */
1538
1539/**
1540 * This method implementation handles session data updates.
1541 * @method IOperationResultListener#onSessionDataUpdated
1542 * @param {SessionData} sessionData The updated session data. Should be null if session is expired.
1543 */
1544
1545/**
1546 * @class
1547 * @hideconstructor
1548 * @classdesc
1549 * A singleton that implements {@link IOperationResultListener} interface to cache data about users and their
1550 * registrations after successful operations. Also provides methods for accessing the cached data.
1551 * @implements {IOperationResultListener}
1552 */
1553const UserDataCache = new (function () {
1554	var _this = this;
1555	const USER_DATA_KEY_PREFIX = 'nnl.userDataCache.';
1556	const ACTIVE_USER_KEY = 'nnl.activeUser';
1557
1558	/**
1559	 * After each successful operation updates cached user data.
1560	 * @override
1561	 */
1562	this.onComplete = function(operationType, operationData, extras) {
1563		return AppSdkInfo.getDeviceId().then(function(thisDeviceID) {
1564			switch(operationType) {
1565				case FidoOperationType.REG:
1566				case FidoOperationType.AUTH:
1567					return addRegistration(operationData, thisDeviceID, extras);
1568				case FidoOperationType.LIST_REG:
1569					return updateRegistrations(operationData, thisDeviceID);
1570				case FidoOperationType.DELETE_REG:
1571					return removeRegistrations(operationData);
1572				default:
1573					break;
1574			}
1575		});
1576	}
1577
1578	/**
1579	 * Does nothing.
1580	 * @override
1581	 */
1582	this.onFailure = function(operationType, error) {}
1583
1584	/**
1585	 * Returns the authenticated user's userName previously stored by calling {@link UserDataCache#setActiveUser|setActiveUser}.
1586	 * @returns {string} active user's userName
1587	 */
1588	this.getActiveUser = function() {
1589		return NNLStorage.getItem(ACTIVE_USER_KEY);
1590	}
1591
1592	/**
1593	 * Stores currently active user's userName in localStorage and loads the cached data for that user.
1594	 * If the userName parameter is provided, stores it as the authenticated user's userName.
1595	 * <br>If the userName parameter is not provided or is null, removes previously stored authenticated user's userName.
1596	 * @param {string} [userName] Authenticated user's userName.
1597	 * @returns {Promise}
1597 A promise that will be resolved when the active user's cached data is loaded. 
1598	 */
1599	this.setActiveUser = function(userName) {
1600		if(userName == null) {
1601			NNLStorage.removeItem(ACTIVE_USER_KEY);
1602			return Promise.resolve();
1603		}
1604
1605		// Get existing active user before updating it.
1606		var currentUserName = _this.getActiveUser();
1607
1608		// If the same user was active before, no need to do anything.
1609		if(currentUserName === userName) {
1610			return Promise.resolve(_this.getUserData());
1611		}
1612
1613		// If the user was changed, set the new user as active and fetch its cached data.
1614		NNLStorage.setItem(ACTIVE_USER_KEY, userName);
1615		return _this.fetchUserData(userName);
1616	}
1617
1618	/**
1619	 * Retrieves the cached data from cookie storage for the specified user.
1620	 * @param {string} userName User name to retrieve data for.
1621	 * @returns {Promise} A promise that will be resolved with the cached user data (see the description for {@link UserDataCache#getUserData|getUserData}).
1622	 */
1623	this.fetchUserData = function(userName) {
1624		return NNLStorage.fetchItem(USER_DATA_KEY_PREFIX + userName).then(parseData);
1625	} 
1626
1627	/**
1628	 * Returns a JSON object containing cached data for the currently active user (specified by {@link  UserDataCache#setActiveUser|setActiveUser} call).
1629	 * <br>The built in properties in the returned object are listed below.
1630	 * <br>The object also will contain any custom properties added using the {@link UserDataCache#setUserData|setUserData} method.
1631	 *
1632	 * Property                | Type   | Description
1633	 * ----------------------- | ------ | -------------
1634	 * fido2CredentialIds      | Array  | List of objects with handle and attachment type of local FIDO2 registrations.
1635	 * fido2CredentialIds[].id | String | The handle for FIDO2 registration.
1636	 * fido2CredentialIds[].authenticatorAttachment | String | The attachment type of FIDO2 registration. Either "platform", "cross-platform" or "unknown".
1637	 * uafKeyIds               | Array  | List of handles of local UAF registrations.
1638	 * secondDeviceId          | String | The device ID of the second device used during OOB registration.
1639	 * pushHandle              | String | The push handle for the second device.
1640	 *
1641	 * @returns {object} JSON object with user's data.
1642	 */
1643	this.getUserData = function() {
1644		var userName = _this.getActiveUser();
1645		if(userName == null) {
1646			return null;
1647		}
1648
1649		return parseData(NNLStorage.getItem(USER_DATA_KEY_PREFIX + userName));
1650	}
1651
1652	this.getQuickAuthData = function (userName, keyName) {
1653		let data = NNLStorage.getItem(USER_DATA_KEY_PREFIX + userName);
1654		if (data == null) {
1655			return null;
1656		}
1657
1658		let userData = parseData(data);
1659		if (userData.quickAuth == null) {
1660			return null;
1661		}
1662
1663		return userData.quickAuth[keyName != null ? keyName : QUICK_AUTH_DATA_FIDO2_KEY];
1664	}
1665
1666	/**
1667	 * Stores the specified data in the cache for the specified user.
1668	 * <br>Note that the data will be stored as is, and the next call to {@link UserDataCache#getUserData|getUserData}
1669	 * will return this object. In order to add a custom property to the cached data,
1670	 * first call {@link UserDataCache#getUserData|getUserData}, add the new property to the returned data, then call this
1671	 * method to store it.
1672	 * <br> If the data parameter is not specified, is null or is empty, then the cached data for the specified user will be deleted.
1673	 * @param {string} userName User name to set data for.
1674	 * @param {Object} [data] user's data to be cached.
1675	 */
1676	this.setUserData = function(userName, data) {
1677		if(userName == null) {
1678			return null;
1679		}
1680
1681		// If data is not provided or is empty object, delete the existing data.
1682		// Otherwise override the existing data.
1683		if(NNLUtils.isObjectEmpty(data)) {
1684			NNLStorage.removeItem(USER_DATA_KEY_PREFIX + userName);
1685		} else {
1686			NNLStorage.setItem(USER_DATA_KEY_PREFIX + userName, JSON.stringify(data), true);
1687		}
1688	}
1689
1690	/**
1691	 * Removes the quickData (or quickData related to the credentialId of the provided handle) from the userData.
1692	 *
1693	 * @param userData The userData from where to delete the quickData.
1694	 * @param quickData The quick data to  optional if the handle is provided.
1695	 * @param handle
1696	 */
1697	function removeQuickData(userData, quickData, handle) {
1698		let currUserQuickCredId = getUserQuickCredentialId(userData);
1699		if (handle != null && handle !== '') {
1700			let credId = extractCredentialIdFromHandle(handle);
1701			if (credId === currUserQuickCredId) {
1702				delete userData.quickAuth;
1703			}
1704		} else if (quickData != null && quickData.credentialId === currUserQuickCredId ) {
1705			delete userData.quickAuth;
1706		}
1707	}
1708
1709	function getUserQuickCredentialId(userData) {
1710		if (userData != null &&
1711			userData.quickAuth != null &&
1712			userData.quickAuth.dataFido2 != null &&
1713			userData.quickAuth.dataFido2.credentialId != null &&
1714			userData.quickAuth.dataFido2.credentialId !== "") {
1715
1716			return userData.quickAuth.dataFido2.credentialId;
1717		}
1718
1719		return null;
1720	}
1721
1722	/**
1723	 * Extracts the credentialId from the provided handle.
1724	 *
1725	 * @param handle the handle from which to extract the credentialId.
1726	 * @returns {string} The extracted credentialId.
1727	 * @private
1728	 */
1729	function extractCredentialIdFromHandle(handle) {
1730		let handleParts = JSON.parse(NNLBase64.toString(handle));
1731		return handleParts[2];
1732	}
1733
1734	function parseData(userData) {
1735		// Parse existing user data or create a default empty object.
1736		userData = userData || '{}';
1737		userData = JSON.parse(userData);
1738		// Make sure that "fido2CredentialIds" and "uafKeyIds" are defined.
1739		userData.fido2CredentialIds = userData.fido2CredentialIds || [];
1740		userData.uafKeyIds = userData.uafKeyIds || [];
1741
1742		return userData;
1743	}
1744
1745	function addRegistration(operationData, thisDeviceID, extras) {
1746		// In case of authentication the userName is supplied in the operationData.
1747		// In case of registration the app is expected to set the active user name before starting the registration.
1748		var userName = operationData.userName;
1749		var activeUser = _this.getActiveUser();
1750		// If user is missing or data is not available, nothing can be added in cache
1751		if((userName == null && activeUser == null) || NNLUtils.isObjectEmpty(operationData.additionalInfo)) {
1752			return;
1753		}
1754
1755		return Promise.resolve()
1756		.then(function() {
1757			// If active user was not set, fetch the cached data for the user specified in operationData.
1758			if(activeUser == null) {
1759				return _this.fetchUserData(userName);
1760			}
1761
1762			// Otherwise use active user's userName and get already updated data.
1763			userName = activeUser;
1764			return _this.getUserData();
1765		})
1766		.then(function(userData) {
1767			// If the operation was done on another device, then it is an OOB operation 
1768			if(operationData.additionalInfo.device != null && operationData.additionalInfo.device.id !== thisDeviceID) {
1769				userData.secondDeviceId = operationData.additionalInfo.device.id;
1770				if(operationData.additionalInfo.push != null && operationData.additionalInfo.push.pushHandle != null) {
1771					userData.pushHandle = operationData.additionalInfo.push.pushHandle;
1772				} 
1773			} else {
1774				// Get the list of authenticators used during the operation.
1775				var authenticatorsResult = operationData.additionalInfo.authenticatorsResult || [];
1776				if(authenticatorsResult.length > 0) {
1777					if(operationData.protocolFamily === 'web') {
1778						// Add the new credentials ID to the list if it does not exist.
1779						var found = userData.fido2CredentialIds.some(function(item) {
1780							if(item.id === authenticatorsResult[0].handle) {
1781								// If the attachment type of the existing credential is not known, override it with
1782								// attachment type from operation data which is either the same or has the correct value.
1783								if(item.authenticatorAttachment == null || item.authenticatorAttachment === 'unknown') {
1784									item.authenticatorAttachment = operationData.authenticatorAttachment;
1785								}
1786								return true;
1787							}
1788
1789							return false;
1790						});
1791
1792						if(!found) {
1793							userData.fido2CredentialIds.push({
1794								id: authenticatorsResult[0].handle,
1795								authenticatorAttachment: operationData.authenticatorAttachment
1796							});
1797						}
1798
1799						if (operationData.quickAuthData != null) {
1800							updateQuickData(userData, operationData.quickAuthData, extras[AppSdk.EXTRA_KEY_QUICK_AUTH_DATA_KEY_NAME]);
1801						}
1802					} else if(operationData.protocolFamily === 'uaf') {
1803						// Add the new keyID to the list if it does not exist.
1804						if(userData.uafKeyIds.indexOf(authenticatorsResult[0].handle) === -1) {
1805							userData.uafKeyIds.push(authenticatorsResult[0].handle);
1806						}
1807					}
1808				}
1809			}
1810	
1811			_this.setUserData(userName, userData);
1812		});
1813	}
1814
1815	function updateQuickData(userData, quickData, customKey) {
1816		if (quickData == null) {
1817			return;
1818		}
1819		let handle = userData.fido2CredentialIds[userData.fido2CredentialIds.length - 1].id;
1820		quickData.credentialId = extractCredentialIdFromHandle(handle);
1821
1822		let quickDataKey = QUICK_AUTH_DATA_FIDO2_KEY;
1823		if (customKey != null) {
1824			quickDataKey = customKey;
1825		}
1826
1827		userData.quickAuth = {};
1828        userData.quickAuth[quickDataKey] = quickData;
1829	}
1830
1831	function removeRegistrations(operationData) {
1832		var userName = _this.getActiveUser();
1833		// If the operation data contains a "userName" and a "quickData",
1834		// then this is a request to remove the quick data.
1835		let isQuickDataRemoval = operationData.userName != null && operationData.quickData;
1836		if (isQuickDataRemoval) {
1837			userName = operationData.userName;
1838		}
1839
1840		if(userName === null) {
1841			return;
1842		}
1843
1844		// Load data for active user.
1845		var userData = _this.getUserData();
1846		if (isQuickDataRemoval && userData == null) {
1847			// If the user data is not set but this is request for quickData removal, then get the data from the cache.
1848			userData = parseData(NNLStorage.getItem(USER_DATA_KEY_PREFIX + userName));
1849		}
1850
1851		// In case of deleting single registration remove the deleted handle from list of cached handles.
1852		if(operationData.handle != null) {
1853			// As the protocol is not available, filter lists for both protocols.
1854			userData.fido2CredentialIds = userData.fido2CredentialIds.filter(function(item) {
1855				return item.id !== operationData.handle;
1856			});
1857			userData.uafKeyIds = userData.uafKeyIds.filter(function(handle) {
1858				return handle !== operationData.handle;
1859			});
1860
1861			removeQuickData(userData, null, operationData.handle);
1862		} else if (isQuickDataRemoval) {
1863			removeQuickData(userData, operationData.quickData, null);
1864		} else {
1865			// In case of deleting all registration or performing purge, just delete user data.
1866			userData = null;
1867		}
1868
1869		// Save the data for the user.
1870		_this.setUserData(userName, userData);
1871	}
1872
1873	function updateRegistrations(operationData, thisDeviceID) {
1874		var userName = _this.getActiveUser();
1875		if(userName === null) {
1876			return;
1877		}
1878
1879		// Load data for active user.
1880		var userData = _this.getUserData();
1881
1882		// In case of list registrations operations update the lists of cached handles
1883		// by intersecting them with available handles.
1884		var credentialIds = [];
1885		var keyIds = [];
1886		// delete oob information. 
1887		delete userData.secondDeviceId;
1888		delete userData.pushHandle;
1889		var lastUsedOOBTimeStamp = 0;
1890
1891		// Loop over the registrations.
1892		operationData.registrations.forEach(function(registration) {
1893			if(registration.protocol === 'fido2') {
1894				var found = credentialIds.some(function(item) {
1895					return item.id === registration.handle;
1896				});
1897
1898				// Try to add the registration if it is not already in the new list.
1899				if(!found) {
1900					var existingHandles = userData.fido2CredentialIds.map(function(item) {
1901						return item.id;
1902					});
1903	
1904					// If the registration info is in the current cache, keep it.
1905					var index = existingHandles.indexOf(registration.handle);
1906					if(index !== -1) {
1907						credentialIds.push(userData.fido2CredentialIds[index]);
1908					} else if(registration.device === thisDeviceID) {
1909						// If a fido2 registration is for this device, add it.
1910						credentialIds.push({
1911							id: registration.handle,
1912							authenticatorAttachment: 'unknown'
1913						});
1914					}
1915				}
1916			} else if(registration.protocol === 'uaf') {
1917				if(userData.uafKeyIds.indexOf(registration.handle) !== -1) {
1918					keyIds.push(registration.handle);
1919				}
1920				// If a fido2 registration is for this device and is not in the list, add it.
1921				if(registration.device === thisDeviceID && keyIds.indexOf(registration.handle) === -1) {
1922					keyIds.push(registration.handle);
1923				}
1924			}
1925
1926			// Save the OOB information of the last used registered mobile device other than this device.
1927			// Check for mobile device is needed because after authenticating with FIDO2 on a new browser
1928			// a new registration entry is added in server with new device id. Because of that this condition
1929			// without check for mobile device becomes true, and incorrect OOB information will be saved.
1930			if(registration.device !== thisDeviceID && isMobileDevice(registration.deviceType)
1931								&& registration.lastUsedTimeStamp > lastUsedOOBTimeStamp) {
1932				// remember timestamp and store device id.
1933				lastUsedOOBTimeStamp = registration.lastUsedTimeStamp;
1934				userData.secondDeviceId = registration.device;
1935				// If this registration has push handle store it,
1936				// otherwise delete push handle stored from previous device.
1937				if(registration.push != null) {
1938					userData.pushHandle = registration.push.pushHandle;
1939				} else {
1940					delete userData.pushHandle;
1941				}
1942			}
1943		});
1944
1945		// Override localCredentials with filtered list.
1946		userData.fido2CredentialIds = credentialIds;
1947		userData.uafKeyIds = keyIds;
1948		// Save data for user.
1949		_this.setUserData(userName, userData);
1950	}
1951
1952	function isMobileDevice(deviceType) {
1953		return deviceType != null && deviceType.match(/^(?:ios|android)/i) != null;
1954	}
1955
1956	function convertStorageFormatIfNeeded() {
1957		// If the old data does not exist, just return.
1958		if(localStorage.getItem('userDataCache') == null) {
1959			return;
1960		}
1961
1962		// Parse the old data and delete it and related cookies before adding new data
1963		// in order not to excess the cookie size limit.
1964		var oldUserData = JSON.parse(localStorage.getItem('userDataCache'));
1965		NNLStorage.removeItem('userDataCache');
1966
1967		// Loop over the stored users and create separate entry for each one.
1968		Object.keys(oldUserData).forEach(function(userName) {
1969			var oldData = oldUserData[userName];
1970			var newData = {};
1971
1972			// If there is a stored "fido2" object, convert it to "fido2CredentialIds" array
1973			// while also converting the array items' format.
1974			if(oldData.fido2 != null && oldData.fido2.credentialIds != null) {
1975				newData.fido2CredentialIds = oldData.fido2.credentialIds.map(function(item) {
1976					// If an item is already object, return it as is.
1977					if(typeof item === 'object') {
1978						return item;
1979					}
1980
1981					// Otherwise return object with unknown attachment type.
1982					return {
1983						id: item,
1984						authenticatorAttachment: 'unknown'
1985					}
1986				});
1987			}
1988			// Delete the "fido2" object from the old data.
1989			delete oldData.fido2;
1990
1991			// If there is a stored "uaf" object, convert it to "uafKeyIds" array.
1992			if(oldData.uaf != null && oldData.uaf.keyIds != null) {
1993				newData.uafKeyIds = oldData.uaf.keyIds;
1994			}
1995			// Delete the "uaf" object from the old data.
1996			delete oldData.uaf;
1997
1998			// If there is a stored "oob" object, copy its content to "secondDeviceId" and "pushHandle" fields.
1999			if(oldData.oob != null) {
2000				if(oldData.oob.device != null) {
2001					newData.secondDeviceId = oldData.oob.device;
2002				}
2003				if(oldData.oob.pushHandle != null) {
2004					newData.pushHandle = oldData.oob.pushHandle;
2005				}
2006			}
2007			// Delete the "oob" object from the old data.
2008			delete oldData.oob;
2009
2010			// Delete timestamp as it is not needed anymore.
2011			delete oldData.lastUpdatedTimestamp;
2012
2013			// If there are other keys remaining in the old data
2014			// (custom items stored by the app) copy them as is.
2015			Object.keys(oldData).forEach(function(key) {
2016				newData[key] = oldData[key];
2017			});
2018
2019			// Store the new data with new key in the storage.
2020			NNLStorage.setItem(USER_DATA_KEY_PREFIX + userName, JSON.stringify(newData), true);
2021		});
2022	}
2023
2024	return {
2025		/**
2026		 * Returns the single instance.
2027		 * @memberof UserDataCache
2028		 * @returns {UserDataCache}
2029		 */
2030		getInstance: function() {
2031			convertStorageFormatIfNeeded();
2032			return _this;
2033		}
2034	}
2035})();
2036Object.freeze(UserDataCache);
2037
2038/**
2039 * @class
2040 * @hideconstructor
2041 * @classdesc A singleton factory class to create and provide {@link IOperationResultListener} implementations.
2042 */
2043const OperationResultListener = (function() {
2044	var mInstance = null;
2045	var mListeners = [];
2046
2047	function impl() {
2048		this.onComplete = function(operationType, operationData, extras) {
2049			// Chain of promises to wait on before completing the operation.
2050			var chain = Promise.resolve();
2051			mListeners.forEach(function(listener) {
2052				if(typeof listener.onComplete === 'function') {
2053					chain = chain.then(function() {
2054						// If an implementation does not return a Promise, than it is assumed
2055						// to be synchronous and finished its job and the next listener can be called,
2056						// otherwise the returned Promise will be added to the chain and the next listener
2057						// will be called when that Promise is resolved.
2058						return listener.onComplete(operationType, operationData, extras);
2059					});
2060				}
2061			});
2062
2063			// Complete operation after all implementations are finished their job.
2064			return chain;
2065		}
2066
2067		this.onFailure = function(operationType, error) {
2068			// Chain of promises to wait on before completing the operation.
2069			var chain = Promise.resolve();
2070			mListeners.forEach(function(listener) {
2071				if(typeof listener.onFailure === 'function') {
2072					chain = chain.then(function() {
2073						// If an implementation does not return a Promise, than it is assumed
2074						// to be synchronous and finished its job and the next listener can be called,
2075						// otherwise the returned Promise will be added to the chain and the next listener
2076						// will be called when that Promise is resolved.
2077						return listener.onFailure(operationType, error);
2078					});
2079				}
2080			});
2081
2082			// Complete operation after all implementations are finished their job.
2083			return chain;
2084		}
2085
2086		this.onSessionDataUpdated = function(sessionData) {
2087			mListeners.forEach(function(listener) {
2088				if(typeof listener.onSessionDataUpdated === 'function') {
2089					listener.onSessionDataUpdated(NNLUtils.cloneObject(sessionData));
2090				}
2091			});
2092		}
2093	}
2094
2095	return {
2096		/**
2097		 * Returns the IOperationResultListener instance.
2098		 * @returns {IOperationResultListener} Stored instance of IOperationResultListener.
2099		 * @memberof OperationResultListener
2100		 */
2101		getInstance: function() {
2102			if(mInstance === null) {
2103				mInstance = new impl();
2104				OperationResultListener.addInstance(UserDataCache.getInstance());
2105			}
2106
2107			return mInstance;
2108		},
2109		/**
2110		 * Adds a new IOperationResultListener instance.
2111		 * @param {IOperationResultListener} instance The new instance.
2112		 * @memberof OperationResultListener
2113		 */
2114		addInstance: function(instance) {
2115			mListeners.push(instance);
2116		},
2117		/**
2118		 * Removes a previously added IOperationResultListener instance.
2119		 * @param {IOperationResultListener} instance The instance to remove.
2120		 * @memberof OperationResultListener
2121		 */
2122		removeInstance: function(instance) {
2123			var index = mListeners.indexOf(instance);
2124			if(index !== -1) {
2125				mListeners.splice(index, 1);
2126			}
2127		}
2128	};
2129})();
2130Object.freeze(OperationResultListener);
2131
2132const NNLUtils = new (function() {
2133	const IFRAME_ID = 'nnl_iframe';
2134
2135	/**
2136	 * ID of the "location" extension.
2137	 */
2138	const LOCATION_EXT_ID =  'noknok.uaf.location';
2139
2140	/**
2141	 * ID of the "metrics" extension.
2142	 */
2143	const METRICS_EXT_ID = 'noknok.metrics';
2144
2145	// References to the NNL iframe tag and its origin.
2146	var nnlIframe;
2147	var nnlIframeOrigin;
2148
2149	var SDK_URL = null;
2150	if(document.currentScript) {
2151		SDK_URL = document.currentScript.src;
2152	} else {
2153		// document.currentScript is not supported on IE, try to get url from a stack trace.
2154		try {
2155			throw new Error();
2156		} catch(err) {
2157			// Find the second match for the "at" string to get file src url from stack.
2158			// Specifically works with the format of stack traces in IE.
2159			var stackDetails = (/.*at [^(]*\((.*):(.+):(.+)\)$/ig).exec(err.stack);
2160			SDK_URL = stackDetails && stackDetails[1];
2161		}
2162	}
2163	SDK_URL = SDK_URL && SDK_URL.replace('js/utils.js', '');
2164
2165	// Returns a promise that will be resolved when document loading is complete.
2166	function documentReady() {
2167		return new Promise(function(resolve) {
2168			if(document.readyState === 'loading') {
2169				// If the document is still loading, wait for it to complete.
2170				document.addEventListener('DOMContentLoaded', resolve);
2171			} else {
2172				// Otherwise resolve right away.
2173				resolve();
2174			}
2175		});
2176	}
2177
2178	// Returns a promise that will be resolved when iframe is loaded and rejected when loading fails.
2179	function loadIframe() {
2180		return new Promise(function(resolve, reject) {
2181			nnlIframe = document.getElementById(IFRAME_ID);
2182			// If the iframe is not present in the document, then create a new element and load the iframe.
2183			// The promise will be resolved on successful load and rejected on error.
2184			if(nnlIframe === null) {
2185				nnlIframe = document.createElement('iframe');
2186//				nnlIframe.src = NNLUtils.getResourceURL('', 'iframe.jsp');
2187				nnlIframe.src = window.location.origin + '/iss-pc/jsp/passkey/nnlappsdk-9.2.0/iframe.jsp';
2188				nnlIframe.id = IFRAME_ID;
2189				nnlIframe.name = IFRAME_ID;
2190				nnlIframe.style = 'display:none';
2191				nnlIframe.onload = resolve;
2192				nnlIframe.onerror = reject;
2193				document.body.appendChild(nnlIframe);
2194			} else {
2195				// If the iframe is already in the page, resolve the promise.
2196				resolve();
2197			}
2198			nnlIframeOrigin = NNLUtils.getOrigin(nnlIframe.src);
2199		});
2200	}
2201
2202	// Returns a promose that will be resolved when a ready event is received from the iframe.
2203	function connectToIframe() {
2204		return new Promise(function(resolve) {
2205			function readyCallback(event) {
2206				// Once iframe has reported for duty, remove event listener and resolve the promise.
2207				if(event.origin === nnlIframeOrigin && event.data && event.data.type === 'nnl.iframeReady') {
2208					console.log('Connection with the NNL iframe is established.');
2209					window.removeEventListener('message', readyCallback);
2210
2211					nnlIframe.contentWindow.postMessage({type: 'nnl.ready'}, nnlIframeOrigin);
2212					resolve();
2213				}
2214			}
2215
2216			// Start listening from message.
2217			window.addEventListener('message', readyCallback, false);
2218		});
2219	}
2220
2221	// Load and connect to the iframe and store the resulting promise.
2222	var nnlIframeLoaded = documentReady()
2223	.then(loadIframe)
2224	.then(connectToIframe);
2225
2226	// This promise will be created first time initialize is called, and just returned in subsequent calls
2227	// without doing the initialization calls (to prevent simultaneous loadScript calls for the same script).
2228	var initPromise = null;
2229
2230	// Returns a promise that will be resolved when the iframe is loaded and ready for communication
2231	// and all common dependencies are loaded. Will be rejected otherwise.
2232	this.initialize = function() {
2233		if(initPromise == null) {
2234			initPromise = nnlIframeLoaded.then(function() {
2235				// The resources loaded here are not referenced globally (before an SDK API call)
2236				// so it is safe to load them asynchronously in parallel.
2237				return Promise.all([
2238					NNLUtils.loadCSS('nnlsdk.css'),
2239					NNLUtils.loadHTML('utils.html', 'nnl-utils-section'),
2240					NNLUtils.loadScript('metrics.js'),
2241					NNLUtils.loadScript('fido-client.js'),
2242					NNLUtils.loadScript('outcome.js'),
2243					NNLUtils.loadScript('common-utils.js')
2244				]);
2245			});
2246		}
2247
2248		return initPromise;
2249	}
2250
2251	this.loadScript = function(scriptName, scriptFolder) {
2252		scriptFolder = scriptFolder || 'js';
2253		return new Promise(function(resolve, reject) {
2254			var existing = document.querySelector('script[src*="' + scriptName + '"]');
2255			if(existing === null) {
2256				var script = document.createElement('script');
2257				script.src = NNLUtils.getResourceURL(scriptFolder, scriptName);
2258				script.onload = resolve;
2259				script.onerror = function() {
2260					// If the script fails to load, remove the element to try again next time. 
2261					script.remove();
2262					reject();
2263				}
2264				document.head.appendChild(script);
2265			} else {
2266				resolve();
2267			}
2268		});
2269	}
2270
2271	this.loadCSS = function(cssName) {
2272		return new Promise(function(resolve, reject) {
2273			var existing = document.querySelector('link[href*="' + cssName + '"]');
2274			if(existing === null) {
2275				var link = document.createElement('link');
2276				link.rel = 'stylesheet';
2277				link.type = 'text/css';
2278				link.href = NNLUtils.getResourceURL('css', cssName);
2279				link.onload = resolve;
2280				link.onerror = function() {
2281					// If the CSS fails to load, remove the element to try again next time.
2282					link.remove();
2283					reject();
2284				}
2285				document.head.appendChild(link);
2286			} else {
2287				resolve();
2288			}
2289		});
2290	}
2291
2292	this.loadHTML = function(htmlName, elementID) {
2293		return NNLUtils.loadResource(NNLUtils.getResourceURL('html', htmlName), 'html', true)
2294		.then(function(content) {
2295			let div = document.querySelector('#' + elementID);
2296			if(div == null) {
2297				div = document.createElement('div');
2298				div.id = elementID;
2299				document.body.appendChild(div);
2300			}
2301
2302			div.innerHTML = content;
2303		});
2304	}
2305
2306	this.loadResource = function(resourceURL, resourceType, useIframe) {
2307		return Promise.resolve()
2308		.then(function() {
2309			if(useIframe) {
2310				return loadResourceInIframe(resourceURL, resourceType);
2311			}
2312
2313			return loadResourceDirect(resourceURL, resourceType);
2314		})
2315		.then(function(httpResponse) {
2316			// on cordova the status is 0, so need to check if there is an actual response
2317			if(httpResponse.status === 200 || (nnl.isCordovaApp && httpResponse.status === 0 && !isResourceEmpty(httpResponse.content, resourceType))) {
2318				return httpResponse.content;
2319			}
2320
2321			return Promise.reject(new Error('Request to "' + resourceURL + '" has failed with status code ' + httpResponse.status));
2322		});
2323	}
2324
2325	// Loads resource through iframe.
2326	// Returns a Promise that always resolves with an object containing status and content fields.
2327	function loadResourceInIframe(resourceURL, resourceType) {
2328		return new Promise(function(resolve) {
2329			var data = {
2330	    		type: 'nnl.loadresource',
2331	    		resourceURL: resourceURL,
2332			    resourceType: resourceType
2333		    }
2334
2335			// Create a message channel to receive the response through it.
2336			var channel = new MessageChannel();
2337			// Listen for response on this end of the channel.
2338			channel.port1.onmessage = function(event) {
2339				resolve(event.data);
2340			}
2341
2342		    nnlIframeLoaded.then(function() {
2343				// Transfer the other end of the channel to iframe.
2344				nnlIframe.contentWindow.postMessage(data, nnlIframeOrigin, [channel.port2]);
2345			});
2346		});
2347	}
2348
2349	// Loads resource with XMLHttpRequest in top page context.
2350	// Returns a Promise that always resolves with an object containing status and content fields.
2351	function loadResourceDirect(resourceURL, resourceType) {
2352		return new Promise(function(resolve) {
2353			const req = new XMLHttpRequest();
2354			if(resourceType != null) {
2355				req.responseType = resourceType;
2356			}
2357			req.open('GET', resourceURL, true);
2358			req.onload = function() {
2359				resolve({
2360					status: req.status,
2361					content: req.response
2362				});
2363			}
2364			req.onerror = req.onload;
2365			req.send();
2366		});
2367	}
2368
2369	function isResourceEmpty(content, resourceType) {
2370		if(content == null) {
2371			return true;
2372		}
2373
2374		if(resourceType === 'json') {
2375			return NNLUtils.isObjectEmpty(content);
2376		}
2377
2378		return content === '';
2379	}
2380
2381	this.sendRestMessage = function(endPoint, message) {
2382		return new Promise(function(resolve) {
2383			var data = {
2384	    		type: 'nnl.restmessage',
2385	    		endPoint: endPoint,
2386			    message: message,
2387			    ajaxCallConnectionTimeout: AppSdkConfig.ajaxCallConnectionTimeout
2388		    }
2389
2390			// Create a message channel to receive the response through it.
2391			var channel = new MessageChannel();
2392			// Listen for response on this end of the channel.
2393			channel.port1.onmessage = function(event) {
2394				resolve(event.data);
2395			}
2396
2397		    NNLUtils.initialize().then(function() {
2398				// Transfer the other end of the channel to iframe.
2399				nnlIframe.contentWindow.postMessage(data, nnlIframeOrigin, [channel.port2]);
2400			});
2401		});
2402	}
2403
2404	this.getOrigin = function(url) {
2405		if(url === undefined || url === null) {
2406			return "*";
2407		}
2408
2409		var parts = url.split("/");
2410		return parts[0] + "//" + parts[2];
2411	}
2412
2413	/**
2414	 *	This method returns location extension object in following format: 
2415							{
2416								"id" : "noknok.uaf.location",
2417								"data" : "<location_params>",
2418								"fail_if_unknown" : false 
2419							}
2420		@param opType extension operation type
2421		@param extensions requested extensions list
2422	 */
2423	this.createLocationExtension = function(opType, extensions) {
2424		var sendExtension = false;
2425 
2426		// Get extension mode value for given opType from AppSdkConfig config object
2427		switch(AppSdkConfig.sendLocation[opType]) {
2428			case ExtensionMode.NEVER:
2429			   // never send extension
2430			 	break;
2431			case ExtensionMode.REQUESTED:
2432				// send extension only if it is included in extensions list (exists in server response)
2433				sendExtension = isExtRequested(LOCATION_EXT_ID, extensions);
2434				break;
2435			case ExtensionMode.ALWAYS:
2436			 	// always send extension 
2437				sendExtension = true;
2438				break;
2439			default:
2440				break;
2441		}
2442
2443		if (sendExtension) {
2444			// get location params from local storage if available return location extension.
2445			const locationParams = NNLStorage.getItem("locationParams");
2446			// update local storage
2447			getAndUpdateLocationParams();
2448
2449			if(locationParams) {
2450				return Promise.resolve({
2451					"id" : LOCATION_EXT_ID,
2452					"data" : locationParams,
2453					"fail_if_unknown" : false
2454				});
2455			}
2456		}
2457		return Promise.reject();
2458	};
2459
2460	this.createMetricsExtension = function(opType, extensions) {
2461		var sendExtension = false;
2462
2463		// Get extension mode value for given opType from AppSdkConfig config object
2464		switch(AppSdkConfig.sendMetrics[opType]) {
2465			case ExtensionMode.NEVER:
2466				// never send extension
2467				break;
2468			case ExtensionMode.REQUESTED:
2469				// send extension only if it is included in extensions list (exists in server response)
2470				sendExtension = isExtRequested(METRICS_EXT_ID, extensions);
2471				break;
2472			case ExtensionMode.ALWAYS:
2473				// always send extension
2474				sendExtension = true;
2475				break;
2476			default:
2477				break;
2478		}
2479
2480		if (sendExtension) {
2481			// get the metrics report
2482			const metrics = Tabulator.getInstance().getReport();
2483			if(metrics) {
2484				return Promise.resolve({
2485					"id" : METRICS_EXT_ID,
2486					"data" : metrics,
2487					"fail_if_unknown" : false
2488				});
2489			}
2490		}
2491		return Promise.reject();
2492	};
2493
2494	/**
2495	 * Checks whether the extension is requested by the server.
2496	 * @param {string} extId the extension ID to check.
2497	 * @param {Array} extensions the array of extensions.
2498	 * @returns {boolean} is extension requested by the server.
2499	 */
2500	function isExtRequested(extId, extensions) {
2501		return extensions && extensions.reduce(function(acc, ext) {return ext.id === extId}, false)
2502	}
2503
2504	/**
2505	 * Creates a deep clone of the specified object.
2506	 */
2507	this.cloneObject = function(obj) {
2508		// Object.assign() does not do a deep clone, so use stringify/parse approach.
2509		return JSON.parse(JSON.stringify(obj || {}));
2510	}
2511
2512	// Check if the given object is empty
2513	this.isObjectEmpty = function(obj){
2514		if(obj == null) {
2515			return true;
2516		}
2517
2518		for(var prop in obj) {
2519			if(Object.prototype.hasOwnProperty.call(obj, prop)) {
2520				return false;
2521			}
2522		}
2523
2524		return JSON.stringify(obj) === JSON.stringify({});
2525	}
2526
2527	// Removes event listeners of given element by recreating it.
2528	// Returns reference to the newly created element.
2529	this.removeEventListeners = function(element) {
2530		const copy = element.cloneNode(true);
2531		element.parentNode.replaceChild(copy, element);
2532		return copy;
2533	}
2534
2535	// Checks whether provided data is email or not.
2536	this.isEmail = function(data) {
2537		return (/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/).test(data);
2538	}
2539
2540	// Checks whether provided data is phone number or not.
2541	this.isPhoneNumber = function(data) {
2542		return (/^\+?[0-9]+$/).test(data);
2543	}
2544
2545	this.getInitMessage = function(opType, extras, supportedProtocols) {
2546		var rpData = NNLUtils.createRpData(extras);
2547		return nnl.FIDOClient.init(rpData, opType, supportedProtocols);
2548	}
2549
2550	// Creates RP data structure from extras.
2551	this.createRpData = function(extras) {
2552		var rpData = {
2553			checkPolicy: false
2554		}
2555
2556		// Copy available data from extras
2557		if(extras != null) {
2558			// If this is a web OOB operation, copy the OOB data.
2559			if(extras.webOob) {
2560				rpData.oobData = extras.oobData;
2561			}
2562
2563			// If application has specified support for QR code scanning, copy that flag.
2564			if(extras.qrSupported != null) {
2565				rpData.qrSupported = extras.qrSupported;
2566			}
2567
2568			// If application has specified a custom username, copy it.
2569			if(extras.userName != null) {
2570				rpData.userName = extras.userName;
2571			}
2572
2573			// If application has specified a custom user display name, copy it.
2574			if(extras.userDisplayName != null) {
2575				rpData.userDisplayName = extras.userDisplayName;
2576			}
2577
2578			// If the application has specified the SPC requirement level, copy it.
2579			if (extras[AppSdk.EXTRA_KEY_PAYMENT_SPC] != null) {
2580				rpData[AppSdk.EXTRA_KEY_PAYMENT_SPC] = extras[AppSdk.EXTRA_KEY_PAYMENT_SPC];
2581			}
2582
2583			// If the application has specified the user mediation, copy it.
2584			if (extras[AppSdk.EXTRA_KEY_USER_MEDIATION] != null) {
2585				rpData[AppSdk.EXTRA_KEY_USER_MEDIATION] = extras[AppSdk.EXTRA_KEY_USER_MEDIATION];
2586			}
2587
2588			// If the application has specified the flag for enabilng the quick auth, copy it.
2589			if (extras[AppSdk.EXTRA_KEY_QUICK_AUTH_ENABLED]) {
2590				rpData[AppSdk.EXTRA_KEY_QUICK_AUTH_ENABLED] = extras[AppSdk.EXTRA_KEY_QUICK_AUTH_ENABLED];
2591			}
2592
2593			// Copy extensions if availalbe.
2594			if(extras.extensions != null) {
2595				rpData.extensions = extras.extensions;
2596			}
2597		}
2598
2599		return rpData;
2600	}
2601
2602	// Creates OperationData structure based on the operation result.
2603	this.createOperationData = function(result, userName) {
2604		var operationData = {};
2605		operationData.additionalInfo = NNLUtils.cloneObject(result.additionalInfo);
2606
2607		// Extract the protocol from authenticator handle.
2608		if(operationData.additionalInfo.authenticatorsResult != null && operationData.additionalInfo.authenticatorsResult.length > 0) {
2609			var handle = operationData.additionalInfo.authenticatorsResult[0].handle;
2610			handle = NNLBase64.toString(handle);
2611			handle = JSON.parse(handle);
2612			// Strip version from protocol family.
2613			operationData.protocolFamily = handle[0].split('_')[0];
2614
2615			if(operationData.protocolFamily === 'web') {
2616				// The attachmentHints array can be returned from server after authentication
2617				var hints = operationData.additionalInfo.authenticatorsResult[0].attachmentHints;
2618				// The isPlatformAuthenticator flag can be set by FIDOClient after registration or authentication.   
2619				var isPlatformAuthenticator = result.isPlatformAuthenticator;
2620				// If the flag is not set, but server indicates that the "internal" transport was used,
2621				// then this was an authentication with platform authenticator.
2622				if(isPlatformAuthenticator == null && hints != null && hints.length > 0) {
2623					isPlatformAuthenticator = (hints.indexOf('internal') !== -1);
2624				}
2625
2626				if(isPlatformAuthenticator != null) {
2627					operationData.authenticatorAttachment = isPlatformAuthenticator ? 'platform' : 'cross-platform';
2628				} else {
2629					operationData.authenticatorAttachment = 'unknown';
2630				}
2631			}
2632		}
2633
2634		if(result.push != null) {
2635			operationData.additionalInfo.push = result.push;
2636		}
2637
2638		if(result.message != null) {
2639			let messageDecoded = JSON.parse(NNLBase64.toString(result.message));
2640			if (messageDecoded.server && messageDecoded.server.quickAuthData && messageDecoded.server.appID) {
2641				operationData.quickAuthData = messageDecoded.server.quickAuthData;
2642				operationData.quickAuthData.appId = messageDecoded.server.appID;
2643			}
2644		}
2645
2646		if(userName != null) {
2647			operationData.userName = userName;
2648		}
2649
2650		return operationData;
2651	}
2652
2653	// Creates Error object for OperationResultListener.onFailure from a Result or AdaptiveResult object.
2654	this.createErrorObject = function(result) {
2655		// Outcome is always present in a Result or AdaptiveResult object
2656		const error = {
2657			outcome: result.outcome
2658		}
2659
2660		// Result object contains statusCode, AdaptiveResult object contains serverStatusCode.
2661		if(result.statusCode != null) {
2662			error.serverStatusCode = result.statusCode;
2663		} else {
2664			error.serverStatusCode = result.serverStatusCode;
2665		}
2666
2667		// Copy optional fields if they are present.
2668		if(result.additionalInfo != null) {
2669			error.additionalInfo = result.additionalInfo;
2670		}
2671
2672		if(result.additionalErrorInfo != null) {
2673			error.additionalErrorInfo = result.additionalErrorInfo;
2674		}
2675
2676		return error;
2677	}
2678
2679	// Returns a policy configuration if a mapping for the given policy name exists.
2680	this.getPolicyConfig = function(policyName) {
2681		let policyConfig = null;
2682		// First look if UI config contains a policy map.
2683		const configJsonWrapper = NNLUiConfig.getJsonWrapper();
2684		if(configJsonWrapper != null) {
2685			policyConfig = configJsonWrapper.searchUp('policyMap', policyName);
2686		}
2687
2688		// For backward compatibility, also look up in the AppSdkConfig.
2689		if(policyConfig == null && AppSdkConfig.policyMap != null) {
2690			policyConfig = AppSdkConfig.policyMap[policyName];
2691		}
2692
2693		return policyConfig;
2694	}
2695
2696	this.getResourceURL = function(resourceFolder, resourceName) {
2697		return SDK_URL + resourceFolder + (resourceFolder !== '' ? '/' : '') + resourceName;
2698	}
2699
2700	this.getAbsoluteImageURL = function(imageURL) {
2701		// Construct an absolute URL from current page as base URL.
2702		let baseURL = document.location.href;
2703		// If base URL is specified by the app (by calling NNLUiConfig.downloadUiConfig), use that one.
2704		if(nnl.uiConfigBaseUrl != null) {
2705			baseURL = nnl.uiConfigBaseUrl + 'web/';
2706		}
2707		const absoluteURL = new URL(imageURL, baseURL);
2708		return absoluteURL.href;
2709	}
2710
2711	// The modal dialog built-in in the SDK that is currently open.
2712	// The showModal function below will set this to the modal when it is opened,
2713	// and set this to null when the modal is closed. 
2714	var mActiveModal = null;
2715
2716	// This event listener will be called anytime an element in the document gets the input focus.
2717	// If there is an opened dialog and it does not contain the element that got focus
2718	// then it will move focus to that dialog. This prevents selecting buttons on the background
2719	// by repeatedly pressing the "Tab" key.
2720	document.addEventListener('focusin', function(event) {
2721		if(mActiveModal != null && !mActiveModal.contains(event.target)) {
2722			mActiveModal.focus();
2723		}
2724	}, true);
2725
2726	/**
2727	 * Show or hides the given modal dialog (built-in in the SDK).
2728	 */
2729	this.showModal = function(modal, bShow) {
2730		if(bShow) {
2731			// Set the active modal.
2732			mActiveModal = modal;
2733			// Make the dialog focusable in order to keep focus on it.
2734			modal.setAttribute('tabindex', '-1');
2735
2736			modal.style.display = 'block';
2737			document.body.classList.add('nnl-dialog-opened');
2738		} else {
2739			modal.style.display = 'none';
2740			mActiveModal = null;
2741			document.body.classList.remove('nnl-dialog-opened');
2742		}
2743	}
2744
2745	/**
2746	 * Performs Try FIDO operation if the server request indicates that there is a
2747	 * registration which may be available on the current or another device
2748	 * via the "credentialAvailability" list.
2749	 * After user's confirmation sets "credentialAvailability" to an empty list
2750	 * to not show this prompt again for the current processing flow.
2751	 */
2752	this.tryFido = function(request) {
2753		const credentialAvailability = request.credentialAvailability || [];
2754		// For backward compatibility check the "regUsedOnThisDevice" flag.
2755		if(request.regUsedOnThisDevice != null) {
2756			credentialAvailability.push(request.regUsedOnThisDevice ? 'OnThisDevice ' : 'MaybeOnThisDevice');
2757		}
2758
2759		let tryFidoUI = null;
2760		// Check for credential availability by priority.
2761		// If the credential is available on the current device, then do not show a prompt.
2762		if(credentialAvailability.indexOf('OnThisDevice') !== -1) {
2763			return Promise.resolve();
2764		}
2765
2766		// If the credential may be available on the current device,
2767		// then show the Try FIDO prompt.
2768		if(credentialAvailability.indexOf('MaybeOnThisDevice') !== -1) {
2769			tryFidoUI = TryFidoUiFactory.getInstance().createTryFidoUi();
2770		} else {
2771			// If the credential is not available on the current device.
2772			// If the credential is a security key, do not show a prompt.
2773			if(credentialAvailability.indexOf('SecurityKey') !== -1) {
2774				return Promise.resolve();
2775			}
2776
2777			// If the credential is available on another device,
2778			// then show the Try Hybrid prompt.
2779			if(credentialAvailability.indexOf('Hybrid') !== -1) {
2780				tryFidoUI = TryFidoUiFactory.getInstance().createTryHybridUi();
2781			}
2782		}
2783
2784		// Show the prompt if needed.
2785		if(tryFidoUI != null) {
2786			ProgressIndicator.getInstance().suspend();
2787			return tryFidoUI.openUI()
2788			.then(function(outcome) {
2789				request.credentialAvailability = [];
2790
2791				ProgressIndicator.getInstance().resume();
2792				if(outcome !== Outcome.SUCCESS) {
2793					return Promise.reject({outcome: outcome});
2794				}
2795			});
2796		}
2797
2798		return Promise.resolve();
2799	}
2800
2801	/**
2802	 * Validates the provided value against the type and the instance type,
2803	 * using javascript typeof and instanceof operators respectively.
2804	 * @param {string} name The name of the parameter.
2805	 * @param {any} value The value to be validated.
2806	 * @param {string} type the string representative of the type of value.
2807	 * @param {any} instanceType the instance type that the value should be assignable to.
2808	 * @param {boolean} nullable the flag indicating whether the value can be null or undefined.
2809	 * @throws {INVALID_ARGUMENT_EXCEPTION} if the value is not of expected type, instance, or is null (if not nullable).
2810	 */
2811	this.validateArgument = function (name, value, type, instanceType, nullable) {
2812		// If the value is required and is null or undefined - throw an exception.
2813		if (!nullable && value == null) {
2814			throw new AppSdk.INVALID_ARGUMENT_EXCEPTION(name);
2815		}
2816
2817		// This is the case if the parameter is optional and is provided.
2818		if (value != null) {
2819			// Check the type of value against required one (if provided).
2820			if (type != null && typeof value !== type) {
2821				throw new AppSdk.INVALID_ARGUMENT_EXCEPTION(name);
2822			}
2823
2824			// Check the instance type of value against the required one (if provided).
2825			if (instanceType != null && !(value instanceof instanceType)) {
2826				throw new AppSdk.INVALID_ARGUMENT_EXCEPTION(name);
2827			}
2828		}
2829	}
2830
2831	/**
2832	 *  This method gets location params using Geolocation API and updates local storage
2833	 */
2834	var getAndUpdateLocationParams = function() {
2835
2836		if(!navigator.geolocation) {
2837			NNLStorage.setItem('locationParams', JSON.stringify({status:LocationStatus.SENSOR_UNAVAILABLE}));
2838			return;
2839		}
2840
2841		navigator.geolocation.getCurrentPosition (
2842			function(position) {
2843				NNLStorage.setItem('locationParams', JSON.stringify({
2844					status : LocationStatus.OK,
2845					longitude : position.coords.longitude,
2846					latitude : position.coords.latitude,
2847					accuracy : position.coords.accuracy
2848				}));
2849			},
2850			function(err) {
2851				console.log("ERROR: ", err.code, err.message);
2852				var statusCode = {
2853					// map browser geolocation API error codes to AppSDK codes
2854					1: LocationStatus.NO_APP_PERMISSION,
2855					2: LocationStatus.SIGNAL_UNAVAILABLE,
2856					3: LocationStatus.SIGNAL_UNAVAILABLE
2857				}[err.code] || LocationStatus.UNKNOWN;
2858
2859				NNLStorage.setItem('locationParams', JSON.stringify({status : statusCode}));
2860			},
2861			{timeout:5000}
2862		);
2863	}
2864	
2865	/**
2866	 *	Defines the location fetching status codes
2867	 */
2868	var LocationStatus = {
2869			OK : 0,
2870			UNKNOWN : 1,
2871			SENSOR_UNAVAILABLE : 2,
2872			SERVICES_NOT_INSTALLED : 3,
2873			SIGNAL_UNAVAILABLE : 4,
2874			NO_APP_PERMISSION : 5
2875	}
2876
2877	// Helper methods for loading libphonenumber and parsing with it.
2878	// It is the caller's responsibility to call load() and wait for it to resolve before calling parse().
2879	this.libphonenumber = Object.freeze({
2880		load: function() {
2881			return NNLUtils.loadScript('libphonenumber-max.js', 'webjars/libphonenumber-js/1.9.18/bundle');
2882		},
2883		parse: function(data, extras) {
2884			// Format the phone number according to current locale.
2885			const locale = (extras != null ? extras.currentlocale : null);
2886			const phoneNumber = libphonenumber.parsePhoneNumberFromString(data, locale);
2887			if(phoneNumber != null && phoneNumber.isValid()) {
2888				return phoneNumber.number;
2889			}
2890
2891			return null;
2892		}
2893	});
2894})();
2895Object.freeze(NNLUtils);
2896
2897// Parses a list of method groups to identify if there are methods with same type but different names.
2898// Provides a method to retrieve a list of objects containing a method group, its constructed title and icon.
2899const MethodInfoHelper = function(methodGroups, opType) {
2900	// Check if there are FIDO and FIDO_OOB methods with different names
2901	// in order to determine whether to show the names in the list or not.
2902	let hasDifferentFidoMethods = false;
2903	let hasDifferentFidoOOBMethods = false;
2904	let firstFidoMethodName = null;
2905	let firstFidoOOBMethodName = null;
2906
2907	// Loop over all groups and methods.
2908	methodGroups.forEach(function(group) {
2909		group.forEach(function(method) {
2910			// If difference is not found yet, check the method.
2911			if(method.type === AdaptiveType.FIDO && !hasDifferentFidoMethods) {
2912				// For the first occurrence just save the name.
2913				if(firstFidoMethodName == null) {
2914					firstFidoMethodName = method.name;
2915				} else {
2916					// On subsequent occurrence set the flag to true if the names do not match.
2917					hasDifferentFidoMethods = (firstFidoMethodName !== method.name);
2918				}
2919			}
2920
2921			// Repeat the same for OOB method.
2922			if(method.type === AdaptiveType.FIDO_OOB && !hasDifferentFidoOOBMethods) {
2923				if(firstFidoOOBMethodName == null) {
2924					firstFidoOOBMethodName = method.name;
2925				} else {
2926					hasDifferentFidoOOBMethods = (firstFidoOOBMethodName !== method.name);
2927				}
2928			}
2929		});
2930	});
2931
2932	// Returns a list of objects containing a method group, its constructed title and icon.
2933	this.getMethodInfoList = function() {
2934		const list = [];
2935		methodGroups.forEach(function(group) {
2936			let title = MethodUIFactory.getInstance().getTitle(group, opType);
2937			const icon = MethodUIFactory.getInstance().getIcon(group);
2938
2939			// Append method name for FIDO methods if there are multiple FIDO methods
2940			// of the same type but with different names.
2941			if(group[0].type === AdaptiveType.FIDO && !hasPolicyMap(group) && hasDifferentFidoMethods ||
2942			   group[0].type === AdaptiveType.FIDO_OOB && hasDifferentFidoOOBMethods) {
2943				title += ' (' + group[0].name + ')';
2944			}
2945
2946			list.push({
2947				title: title,
2948				icon: icon,
2949				group: group
2950			});
2951		});
2952
2953		return list;
2954	}
2955
2956	function hasPolicyMap(methodGroup) {
2957		if(methodGroup[0].type === AdaptiveType.FIDO) {
2958			const policyConfig = NNLUtils.getPolicyConfig(methodGroup[0].name);
2959			return policyConfig != null && policyConfig.title != null;
2960		}
2961		return false;
2962	}
2963
2964	Object.freeze(this);
2965}
2966
2967/**
2968 * Controls how extensions are processed.
2969 * @property {string} NEVER never produce the extension
2970 * @property {string} ALWAYS always produce the extension
2971 * @property {string} REQUESTED produce the extension if requested by the server.
2972 */
2973const ExtensionMode = {
2974	NEVER: "never",
2975	ALWAYS: "always",
2976	REQUESTED: "requested"
2977}
2978Object.freeze(ExtensionMode);
2979
2980/**
2981 * Defines operation type where the extensions should be included
2982 * @property {string} INIT include extension in INIT requests payload
2983 * @property {string} FINISH include extension in FINISH requests payload
2984 * @property {string} PROTOCOL include in protocol message
2985 */
2986const ExtensionOpType = {
2987	INIT : "init",
2988	FINISH : "finish",
2989	PROTOCOL : "protocol"
2990}
2991Object.freeze(ExtensionOpType);
2992
2993/**
2994 * ID of the "show transaction" extension.
2995 */
2996const SHOW_TRANSACTION_EXT_ID = 'noknok.showTransaction';
2997
2998/**
2999 * Defines functions for applying UI customization configurations.
3000 * @class
3001 * @hideconstructor
3002 */
3003const NNLUiConfig = new (function() {
3004	let mWrapper = null;
3005	const langtag = document.documentElement.lang || navigator.language;
3006	let mLocale;
3007	// If Intl.Locale is available, parse language and region with it.
3008	if(typeof Intl.Locale === 'function') {
3009		mLocale = new Intl.Locale(langtag);
3010	} else {
3011		// Fallback for old browsers.
3012		const langtagParts = langtag.split('-');
3013		// The language is always the first part. Remove it from array and store in the object.
3014		mLocale = {
3015			language: langtagParts.shift()
3016		}
3017		// The region code is the fist occurrence of a two letter part in the rest of the langtag.
3018		langtagParts.some(function(langtagPart) {
3019			if(/^\w{2}$/.test(langtagPart)) {
3020				// If a 2 letter part is found, store it in the object and stop iteration. 
3021				mLocale.region = langtagPart;
3022				return true;
3023			}
3024		});
3025	}
3026
3027	// Contains the list of configuration files eligible for download, in the order then should be applied.
3028	const mAllowedFiles = [];
3029	['default', 'web'].forEach(function(configType) {
3030		mAllowedFiles.push(configType + '/ui_cfg.json');
3031		mAllowedFiles.push(configType + '/ui_cfg_' + mLocale.language + '.json');
3032		if(mLocale.region != null) {
3033			mAllowedFiles.push(configType + '/ui_cfg_' + mLocale.language + '-' + mLocale.region.toLowerCase() + '.json');
3034		}
3035	});
3036
3037	/**
3038	 * Applies a UI customization configuration. This function can be called multiple times, every subsequent call
3039	 * will merge the provided configuration with existing one.
3040	 * @param {(object|string|URL)} config The UI customization configuration source.
3041	 * It can be the configuration object itself, a JSON string of the configuration object or a URL
3042	 * to load the configuration object from.
3043	 * @param {boolean} localized Flag indicating whether to automatically load configurations for current language/region or not.
3044	 * Used only when `config` is a URL ending with a file name. When this parameter is true, the function will first load
3045	 * the configuration from the URL, then will try to load language and region specific files.<br/>
3046	 * For example when the URL is `https://example.com/path/to/ui_configuration.json`, it will try to load the following two URLs as well:<br/>
3047	 * <code>
3048	 * https://example.com/path/to/ui_configuration<b>_languageCode</b>.json<br/>
3049	 * https://example.com/path/to/ui_configuration<b>_languageCode-regionCode</b>.json</br>
3050	 * </code>
3051	 * where `languageCode` and `regionCode` are two letter lower case language and region codes.
3052	 * @returns {Promise} A Promise that will be resolved when the configuration is successfully loaded
3053	 * and rejected if the provided string is not a valid JSON object or network error happens when loading the
3054	 * provided URL.
3055	 */
3056	this.addUiConfig = function(config, localized) {
3057		let isUrl = false;
3058		return Promise.resolve()
3059		.then(function() {
3060			// If an object is provided, use it as is.
3061			if(typeof config === 'object') {
3062				return Promise.resolve(NNLUtils.cloneObject(config));
3063			}
3064
3065			// Try to parse the provided string as JSON object.
3066			try {
3067				return Promise.resolve(JSON.parse(config));
3068			} catch(e) {}
3069
3070			// If parsing fails, treat the parameter as URL.
3071			isUrl = true;
3072			return NNLUtils.loadResource(config, 'json', false);
3073		})
3074		.then(applyConfiguration)
3075		.then(function() {
3076			// If localized versions are requested with URL, try to load them
3077			// if there is a filename at the end of the URL.
3078			if(isUrl && localized) {
3079				const lastSlash = config.lastIndexOf('/');
3080				if(lastSlash !== -1) {
3081					const baseUrl = config.substring(0, lastSlash + 1);
3082					const fileName = config.substring(lastSlash + 1);
3083					if(fileName !== '') {
3084						return loadLocalizations(baseUrl, fileName);
3085					}
3086				}
3087			}
3088		});
3089	}
3090
3091    /**
3092     * Downloads UI configuration files from the specified URL and applies the configurations.
3093     * @param {URL} url Base URL from which to download UI configuration files.
3094     * @returns {Promise} A Promise that will be resolved when all configuration files are successfully downloaded and
3095	 * applied, and rejected if a network error happens when downloading the files.
3096     */
3097	this.downloadUiConfig = function(url) {
3098		// Store the base URL to use it relative image paths when rendering them.
3099		// Also add a trailing slash if it is not there.
3100		nnl.uiConfigBaseUrl = url + (url.lastIndexOf('/') === url.length - 1 ? '' : '/');
3101
3102		return Promise.resolve()
3103		.then(function() {
3104			return NNLUtils.loadResource(nnl.uiConfigBaseUrl + 'toc.json', 'json', true);
3105		})
3106		.then(function(toc) {
3107			const downloads = [];
3108			// Loop over the allowed config files in order they should be applied.
3109			mAllowedFiles.forEach(function(configFile) {
3110				// If TOC contains an allowed file, download it and add to array to apply.
3111				if(toc.indexOf(configFile) !== -1) {
3112					downloads.push(NNLUtils.loadResource(nnl.uiConfigBaseUrl + configFile, 'json', true));
3113				}
3114			});
3115
3116			// Wait for all downloads to complete.
3117			return Promise.all(downloads);
3118		})
3119		.then(function(uiConfigs) {
3120			uiConfigs.forEach(applyConfiguration);
3121		});
3122	}
3123
3124	/**
3125	 * Returns the JsonWrapper instance.
3126	 * @private
3127	 * @returns {JsonWrapper} the wrapper instance.
3128	 */
3129	this.getJsonWrapper = function() {
3130		return mWrapper;
3131	}
3132
3133    /**
3134     * Retrieves a string based on UI customization settings or from app resources.
3135     *
3136     * @param {array} levels        An array of levels specifying the hierarchy to search for the property.
3137     * @param {string} tag          The tag identifying the string within the configuration file.
3138     * @param {string} defaultValue The default value if the configuration was not found.
3139     * @param {...any} formatArgs   Values that will be substituted into the return value.
3140     * @returns {string} retrieved string value, either from UI configuration or default.
3141     * @private
3142     */
3143    this.getText = function () {
3144        const args = [].slice.call(arguments);
3145        let levels = args.shift();
3146        let tag = args.shift();
3147        let defaultValue = args.shift();
3148        let formatArgs = [];
3149        if (args.length > 0) {
3150            formatArgs = args;
3151        }
3152
3153        let text = defaultValue;
3154        // If json wrapper is null, return the default value.
3155        if (mWrapper != null) {
3156            let configString = mWrapper.searchDown(levels, JsonWrapperKey.strings, tag);
3157            // If configured string is null or empty, return the default value.
3158            if (configString != null && configString.length !== 0) {
3159                text = configString;
3160            }
3161        }
3162
3163        // Format the text with provided arguments.
3164        return formatString(text, formatArgs);
3165    }
3166
3167    function formatString(format, args) {
3168        let result = format;
3169        args.forEach(function (elem, i) {
3170            result = result.replace("{" + i + "}", elem);
3171        });
3172        return result;
3173    }
3174
3175	function loadLocalizations(baseUrl, fileName) {
3176		let fileNameBase = fileName;
3177		let extension = '';
3178
3179		// Parse file name and extension.
3180		const lastDot = fileName.lastIndexOf('.');
3181		if(lastDot !== -1) {
3182			fileNameBase = fileName.substring(0, lastDot);
3183			extension = fileName.substring(lastDot);
3184		}
3185
3186		// Language code is always available, try to load configuration for that language.
3187		fileNameBase += ('_' + mLocale.language);
3188		return loadLocalization(baseUrl + fileNameBase + extension)
3189		.then(function() {
3190			// If region code is also available, try to load configuration for that region as well.
3191			if(mLocale.region != null) {
3192				fileNameBase += ('-' + mLocale.region.toLowerCase());
3193				return loadLocalization(baseUrl + fileNameBase + extension)
3194			}
3195		});
3196	}
3197
3198	function loadLocalization(url) {
3199		return NNLUtils.loadResource(url, 'json', false)
3200		.then(applyConfiguration)
3201		.catch(function(e){
3202			// Ignore errors when loading localized configurations
3203			// as the constructed URL may not exist. 
3204			console.log(e);
3205		});
3206	}
3207
3208	function applyConfiguration(uiConfig) {
3209		const wrapper = new JsonWrapper(uiConfig);
3210		if(mWrapper == null) {
3211			mWrapper = wrapper;
3212		} else {
3213			mWrapper.mergeJsonObjectsRecursively(wrapper);
3214		}
3215	}
3216
3217	applyConfiguration(DefaultUIConfig);
3218
3219	// A query to check if dark mode is enabled.
3220	const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)');
3221
3222	// Enables or disables dark mode based on configured behavior and system mode.
3223	this.setDarkMode = function() {
3224		let propName = 'lightImage';
3225		// If automatic handling is configured, enable or disable dark mode based on the query match.
3226		// If explicit mode is configured, enable or disable based on the configured value.
3227		if((AppSdkConfig.darkMode === NNLDarkMode.AUTOMATIC && darkModeQuery.matches) || AppSdkConfig.darkMode === NNLDarkMode.ON) {
3228			document.body.classList.add('nnl-dark-mode');
3229			propName = 'darkImage';
3230		} else {
3231			document.body.classList.remove('nnl-dark-mode');
3232		}
3233
3234		document.querySelectorAll('[data-tag*="images:"]').forEach(function(image) {
3235			if(image.dataset[propName] != null) {
3236				if(image instanceof HTMLImageElement) {
3237					image.src = image.dataset[propName];
3238				} else if(image instanceof HTMLLinkElement && image.rel === 'icon') {
3239					image.href = image.dataset[propName];
3240				} else {
3241					image.style.content = 'url("'  + image.dataset[propName] + '")';
3242				}
3243			}
3244		});
3245	}
3246
3247	// Listen for dark mode change events and enable or disable it accordingly.
3248	if(typeof darkModeQuery.addEventListener === 'function') {
3249		darkModeQuery.addEventListener('change', this.setDarkMode);
3250	}
3251})();
3252
3253/**
3254 * This object contains many configurable properties that determine some behaviors of the SDK.
3255 * @property {number}			adaptiveTimeout=70						- Adaptive operation timeout in seconds.
3256 * @property {number}			ajaxCallConnectionTimeout=15			- ajax call connection timeout in seconds.
3257 * @property {boolean}			serverNamingEnabled=true				- Enables / Disables server-side default naming for passkeys and security keys.
3258 * @property {boolean}			sendCredentialProviderInfo=true			- Enables / Disables sending the credential provider information to the server.
3259 * @property {Object}			sendLocation							- Controls the generation of the location extension.
3260 * @property {ExtensionMode}	sendLocation.init=NEVER					- to generate in init request payload.
3261 * @property {ExtensionMode}	sendLocation.finish=REQUESTED			- to generate in finish request payload.
3262 * @property {ExtensionMode}	sendLocation.protocol=NEVER				- to generate in protocol message.
3263 * @property {Object}			sendMetrics								- Controls the generation of the metrics extension.
3264 * @property {ExtensionMode}	sendMetrics.init=NEVER					- to generate in init request payload.
3265 * @property {ExtensionMode}	sendMetrics.finish=ALWAYS				- to generate in finish request payload.
3266 * @property {ExtensionMode}	sendMetrics.protocol=NEVER				- to generate in protocol message.
3267 * @property {Object}			oob										- Contains parameters for OOB operations.
3268 * @property {number}			oob.pollingInterval=1000				- Polling interval for OOB operation in milliseconds.
3269 * @property {Object}			otp										- OTP method anti hammering configuration.
3270 * @property {number}			otp.maxFalseAttempts=3					- Maximum number of failed attempts to start OTP authentication.
3271 * @property {number}			otp.probationPeriod=0					- The time period in seconds during which failed attempts are allowed.
3272 * @property {number}			otp.lockoutPeriod=10					- The time period in seconds during which the OTP authentication method will be locked.
3273 * @property {Object}			metricConfig							- Configures the metrics unit and range for historical data in JS App SDK calls.
3274 * @property {number}			metricConfig.range=2					- The total count of metrics to be stored.
3275 * @property {MetricTimeUnit}	metricConfig.unit=DAY					- The data capacity to be stored in each metric.
3276 * @property {Object}			applink									- Contains parameters for App Linking.
3277 * @property {string}			applink.linkUrlBase						- A URL used to launch the native mobile app.
3278 * @property {string}			applink.hmsDomain						- Domain name (without protocol) for HMS App Linking.
3279 * @property {string}			applink.hmsAndroidPackage				- The package name of the Android application to open with the HMS App Link.
3280 * @property {boolean}			applink.enableHmsAppLink=conditional	- Enables / Disables HMS App Linking. By default HMS App Linking is enabled on HMS devices.
3281 * @property {NNLDarkMode}		darkMode=OFF							- Specifies how the built in SDK views should handle dark mode. Should be set before triggering any view. 
3282 */
3283var AppSdkConfig = {
3284	adaptiveTimeout: 70,
3285	ajaxCallConnectionTimeout: 15,
3286	serverNamingEnabled: true,
3287	sendCredentialProviderInfo: true,
3288
3289	sendLocation: {
3290		init: ExtensionMode.NEVER,
3291		finish: ExtensionMode.REQUESTED,
3292		protocol: ExtensionMode.NEVER
3293	},
3294
3295	sendMetrics: {
3296		init: ExtensionMode.NEVER,
3297		finish: ExtensionMode.ALWAYS,
3298		protocol: ExtensionMode.NEVER
3299	},
3300
3301	oob: {
3302		regBaseUrl: "",
3303		authBaseUrl: "",
3304		pollingInterval: 1000
3305	},
3306
3307	otp: {
3308		maxFalseAttempts: 3,
3309		probationPeriod: 0,
3310		lockoutPeriod: 10
3311	},
3312
3313	metricConfig: {
3314		range: 2,
3315		unit: 'DAY'
3316	},
3317
3318	applink: {
3319		linkUrlBase: "",
3320		hmsDomain: "",
3321		hmsAndroidPackage: "",
3322		enableHmsAppLink: (navigator.userAgent.match('HMSCore') && !navigator.userAgent.match('GMSCore'))
3323	},
3324
3325	darkMode: NNLDarkMode.OFF
3326}
3327
3328/**
3329 * @hideconstructor
3330 * @class
3331 * Allows to specify retrieve various AppSdk related properties.
3332 * <br/>Supported properties are:
3333 * 
3334 * Property       | Description
3335 * -------------- | -----------
3336 * AppName        | A unique name identifying the web application.
3337 * AppDisplayName | User friendly application name.
3338 * DeviceID       | Device ID associated with the Browser.
3339 * SdkVersion     | NNL SDK version. Read only.
3340 */
3341const AppSdkInfo = new (function() {
3342	var appName = window.location.origin;
3343	var appDisplayName = window.location.origin;
3344
3345	/**
3346	 * Returns the version of App SDK.
3347	 * @return {string} NNL SDK version.
3348	 * @method AppSdkInfo.getSdkVersion
3349	 * @static
3350	 */
3351	this.getSdkVersion = function() {
3352		return '9.2.0.281';
3353	}
3354
3355	/**
3356	 * Sets custom application name.
3357	 * Note: The custom application name provided by the Web App is unique across all AppSdk instances
3358	 * and does not persist when page is closed or reloaded.
3359	 * @param {string} customAppName - The custom application name.
3360	 * @method AppSdkInfo.setAppName
3361	 * @static
3362	 */
3363	this.setAppName = function(customAppName) {
3364		appName = customAppName;
3365	}
3366
3367	/**
3368	 * Sets custom application display name.
3369	 * Note: The custom application display name provided by the Web App is unique across all AppSdk instances
3370	 * and does not persist when page is closed or reloaded.
3371	 * @param {string} customAppDisplayName - The custom application display name.
3372	 * @method AppSdkInfo.setAppDisplayName
3373	 * @static
3374	 */
3375	this.setAppDisplayName = function(customAppDisplayName) {
3376		appDisplayName = customAppDisplayName;
3377	}
3378	
3379	/**
3380	 * Sets the custom deviceId to be used instead of the default one.
3381	 * Note: The custom deviceId provided by the Web App is unique across all AppSdk instances
3382	 * and does not persist when page is closed or reloaded.
3383	 * @param {string} deviceId Custom deviceId which uniquely identifies the device.
3384	 * @method AppSdkInfo.setDeviceId
3385	 * @static
3386	 */
3387	this.setDeviceId = function(deviceId) {
3388		return NNLUtils.initialize().then(function() {
3389			return nnl.FIDOClient.setDeviceId(deviceId);
3390		});
3391	}
3392
3393	/**
3394	 * Retrieves the stored application name.
3395	 * Either default, or custom application name (if set by [setAppName]{@link AppSdkInfo.setAppName}) is returned.
3396	 * @returns {string} application name.
3397	 * @method AppSdkInfo.getAppName
3398	 * @static
3399	 */
3400	this.getAppName = function() {
3401		return appName;
3402	}
3403
3404	/**
3405	 * Retrieves the stored application display name.
3406	 * Either default, or custom application display name (if set by [setAppDisplayName]{@link AppSdkInfo.setAppDisplayName}) is returned.
3407	 * @returns {string} application display name.
3408	 * @method AppSdkInfo.getAppDisplayName
3409	 * @static
3410	 */
3411	this.getAppDisplayName = function() {
3412		return appDisplayName;
3413	}
3414	
3415	/**
3416	 * Returns the Device ID associated with the Browser.
3417	 * Either default, or custom Device ID (if set by [setDeviceId]{@link AppSdkInfo.setDeviceId}) is returned.
3418	 * @returns {Promise} A Promise that will be resolved with the device ID associated with the Browser.
3419	 * @method AppSdkInfo.getDeviceId
3420	 * @static
3421	 */
3422	this.getDeviceId = function() {
3423		return NNLUtils.initialize().then(function() {
3424			return nnl.FIDOClient.getDeviceId();
3425		});
3426	}
3427
3428	Object.freeze(this);
3429})();
3430
3431/**
3432 * Defines method for changing progress indicator visibility.
3433 * @interface IndicatorListener
3434 */
3435
3436/**
3437 * Called when progress indicator visibility needs to be changed.
3438 * @method IndicatorListener#changeIndicatorVisibility
3439 * @param {boolean} show - Flag indicating whether progress indicator should be visible or not.
3440 */
3441
3442
3443/**
3444 * @hideconstructor
3445 * @class
3446 * Singleton for controlling progress indicator.
3447 */
3448const ProgressIndicator = (function() {
3449	let instance = null;
3450
3451	// Default implementation of IndicatorListener
3452	const NNLIndicatorListener = function() {
3453		this.changeIndicatorVisibility = function(show) {
3454			const progress = document.querySelector('#nnl-progress-wrapper');
3455			if(show) {
3456				new ViewWrapper(document.querySelector('#nnl-progress')).onShow();
3457				progress.style.display = 'block';
3458			} else {
3459				progress.style.display = 'none';
3460			}
3461		}
3462	}
3463
3464	function ProgressIndicatorImpl() {
3465		let counter = 0;
3466		let suspended = false;
3467		let listener = new NNLIndicatorListener();
3468
3469		/**
3470		 * Increments the progress indicator count.
3471		 * Shows the progress indicator if the counter was 0 and indicator is not suspended.
3472		 * @method ProgressIndicator#incCounter
3473		 */
3474		this.incCounter = function() {
3475			if(++counter === 1 && !suspended) {
3476				listener.changeIndicatorVisibility(true);
3477			}
3478		}
3479
3480		/**
3481		 * Decrements the progress indicator count.
3482		 * Hides the progress indicator if the counter reaches 0 and indicator is not suspended.
3483		 * @method ProgressIndicator#decCounter
3484		 */
3485		this.decCounter = function() {
3486			if(counter === 0) {
3487				console.warn('Attempt to decrement ProgressIndicator counter when it is 0.');
3488				return;
3489			}
3490
3491			if(--counter === 0 && !suspended) {
3492				listener.changeIndicatorVisibility(false);
3493			}
3494		}
3495
3496		/**
3497		 * Suspends the progress indicator and hides it if it is visible.
3498		 * @method ProgressIndicator#suspend
3499		 */
3500		this.suspend = function() {
3501			if(suspended) {
3502				console.warn('Attempt to suspend already suspended ProgressIndicator.');
3503				return;
3504			}
3505
3506			suspended = true;
3507			if(counter > 0) {
3508				listener.changeIndicatorVisibility(false);
3509			}
3510		}
3511
3512		/**
3513		 * Restores the progress indicator from suspended state and shows it if it was visible before.
3514		 * @method ProgressIndicator#resume
3515		 */
3516		this.resume = function() {
3517			if(!suspended) {
3518				console.warn('Attempt to resume non suspended ProgressIndicator.');
3519				return;
3520			}
3521
3522			suspended = false;
3523			if(counter > 0) {
3524				listener.changeIndicatorVisibility(true);
3525			}
3526
3527		}
3528
3529		/**
3530		 * Sets a new listener for progress indicator.
3531		 * @returns {IndicatorListener} Previous listener.
3532		 * @method ProgressIndicator#setListener
3533		 */
3534		this.setListener = function(newListener) {
3535			var oldListener = listener;
3536			listener = newListener;
3537			return oldListener;
3538		}
3539
3540		Object.freeze(this);
3541	}
3542
3543	return {
3544		/**
3545		 * Returns ProgressIndicator instance.
3546		 * @returns {ProgressIndicator} The single ProgressIndicator instance.
3547		 * @memberof ProgressIndicator
3548		 */
3549		getInstance: function() {
3550			if(instance === null) {
3551				instance = new ProgressIndicatorImpl();
3552			}
3553
3554			return instance;
3555		}
3556	}
3557})();
3558Object.freeze(ProgressIndicator);
3559
3560//Singleton for displaying short toast message.
3561const NNLToastMessage = new (function() {
3562	// The offset from bottom of screen when virtual keyboard is opened.
3563	let mCurrentOffset = 0;
3564	if(window.visualViewport != null) {
3565		// Initialize the NNLUtils to ensure the DOM readiness.
3566		NNLUtils.initialize()
3567		.then(function () {
3568			// When virtual keyboard is opened or closed, the visualViewport is being resized.
3569			// In that case we need to update the virtual keyboard offset for new toast messages as well as
3570			// update position of existing ones.
3571			window.visualViewport.addEventListener('resize', function() {
3572				// The difference between actual window height and visual viewport height is the height of virtual keyboard.
3573				let newOffset = (window.innerHeight - window.visualViewport.height) * window.visualViewport.scale;
3574				let offsetDiff = newOffset - mCurrentOffset;
3575				// If there is an actual change in offset (keyboard opened or closed on mobile browser)
3576				if(offsetDiff !== 0) {
3577					document.querySelector('#nnl-toast').childNodes.forEach(function(childToast) {
3578						childToast.style.marginBottom = (parseInt(childToast.style.marginBottom) + offsetDiff) + 'px';
3579					});
3580					mCurrentOffset = newOffset;
3581				}
3582			});
3583		});
3584	}
3585
3586	let mNextToastMargin = 0;
3587
3588	this.show = function(message) {
3589		let toastContainer = document.querySelector('#nnl-toast');
3590		let toast = document.querySelector('#nnl-toast-template').cloneNode(true);
3591		toast.removeAttribute('id');
3592		toast.style.display = 'block';
3593		toast.querySelector('span').innerText = message;
3594		new ViewWrapper(toast).onShow();
3595
3596		if(toastContainer.childNodes.length === 0) {
3597			toastContainer.style.display = 'block';
3598		}
3599
3600		toastContainer.prepend(toast);
3601		toast.style.marginBottom = mNextToastMargin + mCurrentOffset + 'px';
3602		mNextToastMargin += (toast.offsetHeight + 10);
3603		setTimeout(function() {
3604			toast.style.opacity = 1;
3605		}, 10);
3606
3607		toast.addEventListener('transitionend', function(event) {
3608			if(event.propertyName !== 'opacity') {
3609				return;
3610			}
3611
3612			if(toast.style.opacity === '1') {
3613				toast.classList.add('nnl-toast-visible');
3614			} else {
3615				toast.remove();
3616				if(!toastContainer.hasChildNodes()) {
3617					toastContainer.style.display = 'none';
3618				}
3619			}
3620		});
3621
3622		setTimeout(function() {
3623			let margin = toast.offsetHeight + 10;
3624			mNextToastMargin -= margin;
3625			toastContainer.childNodes.forEach(function(childToast) {
3626				if(childToast !== toast) {
3627					childToast.style.marginBottom = (parseInt(childToast.style.marginBottom) - margin) + 'px';
3628				}
3629			});
3630			toast.style.opacity = 0;
3631		}, 3000);
3632	}
3633
3634	Object.freeze(this);
3635})();
3636
3637/**
3638 * @hideconstructor
3639 * @class
3640 * Static class that provides methods for storing and retrieving data.
3641 * Data is stored in browser's local storage and can be backed up in cookies.
3642 */
3643const NNLStorage = new (function() {
3644	var mEndpoint = null;
3645
3646	// Contains items loaded from cookies, key is item name, value is SET_SEPARATELY or SET_GLOBALLY.
3647	// The initialize method will add all globally stored items into this.
3648	// The fetchItem method will add each fetched separately stored item into this.
3649	// The setItem method will add/update the stored item according to the setSeparately flag.
3650	var mExistingItems = {};
3651
3652	const SET_SEPARATELY = 'separately';
3653	const SET_GLOBALLY = 'globally';
3654
3655	// Cleanup unneded flags.
3656	Object.keys(localStorage).forEach(function(key) {
3657		if(key.indexOf('nnl.cookieSet.') === 0) {
3658			localStorage.removeItem(key)
3659		}
3660	});
3661
3662	/**
3663	 * Initializes the storage with the REST API endpoint that will allow to store and retrieve data in cookies.
3664	 * When called, the method will retrieve existing data from cookies and update the local storage.
3665	 * @param {string} endpoint The cookie storage REST API endpoint.
3666	 * @returns {Promise} Promise that will be resolved when data is successfully transferred from cookies to local storage.
3667	 * @memberof NNLStorage
3668	 * @alias NNLStorage.initialize
3669	 */
3670    this.initialize = function(endpoint) {
3671		mEndpoint = endpoint;
3672		mExistingItems = {};
3673
3674		var message = {
3675			operation: 'GET'
3676		}
3677
3678		return NNLUtils.sendRestMessage(mEndpoint, message)
3679		.then(function(response) {
3680			if(response.outcome === Outcome.SUCCESS) {
3681				for(var name in response.data) {
3682					localStorage.setItem(name, response.data[name]);
3683					// All items set for root endpoint are considered global.
3684					mExistingItems[name] = SET_GLOBALLY;
3685				}
3686			}
3687		});
3688	};
3689
3690	/**
3691	 * Retrieves the item with the specified name from a cookie and stores it in the local storage.
3692	 * This method should be called only for items that were stored with storeSeparately=true.
3693	 * @param {string} name The name of the item to retrieve.
3694	 * @returns {Promise} A promise that will be resolved with the stored item.
3695	 */
3696	this.fetchItem = function(name) {
3697		// If cookie storage is not configured or the requested item is already fetched, just return the value without making request.
3698		if(mEndpoint == null || mExistingItems[name] != null) {
3699			return Promise.resolve(localStorage.getItem(name));
3700		}
3701
3702		var message = {
3703			operation: 'GET'
3704		}
3705
3706		// Append the name to the endpoint to send only cookies set separately for that name.
3707		return NNLUtils.sendRestMessage(mEndpoint + '/' + encodeURIComponent(name), message)
3708		.then(function(response) {
3709			if(response.outcome === Outcome.SUCCESS) {
3710				// Store the value
3711				localStorage.setItem(name, response.data[name]);
3712				// Remember that the value is already fetched.
3713				mExistingItems[name] = SET_SEPARATELY;
3714
3715				return response.data[name];
3716			}
3717
3718			return null;
3719		});
3720	}
3721
3722	/**
3723	 * Returns a stored item.
3724	 * This method should be called only for items that were stored with storeSeparately=false.
3725	 * @param {string} name Name of the item to get.
3726	 * @returns {string} The stored item.
3727	 * @memberof NNLStorage
3728	 * @alias NNLStorage.getItem
3729	 */
3730    this.getItem = function(name) {
3731		var value = localStorage.getItem(name);
3732		// If the requested item exists in the local storage, but is not stored in a cookie, store it.
3733		if(mEndpoint != null && value != null && mExistingItems[name] == null) {
3734			this.setItem(name, value);
3735		}
3736
3737		return value;
3738	};
3739
3740	/**
3741	 * Stores the specified item.
3742	 * @param {string} name Name of the item to store.
3743	 * @param {string} value Value to store.
3744	 * @param {boolean} [storeSeparately] Specifies whether the item should be stored
3745	 * in a separate cookie with its own path or not.
3746	 * @memberof NNLStorage
3747	 * @alias NNLStorage.setItem
3748	 */
3749    this.setItem = function(name, value, storeSeparately) {
3750		localStorage.setItem(name, value);
3751		if(mEndpoint != null) {
3752			var message = {
3753				operation: 'SET',
3754				name: name,
3755				value: value
3756			}
3757
3758			// If the cookie should be set separately, append its name to the path.
3759			var endpoint = mEndpoint;
3760			if(storeSeparately) {
3761				endpoint += ('/' + encodeURIComponent(name));
3762
3763				// If there already is a cookie stored with global path, delete it.
3764				if(mExistingItems[name] === SET_GLOBALLY) {
3765					var deleteMessage = {
3766						operation: 'DELETE',
3767						name: name
3768					}
3769					// Delete the cookie with the global path (without appending the name).
3770					NNLUtils.sendRestMessage(mEndpoint, deleteMessage);
3771				}
3772			}
3773
3774			// Remember how the cookie was set.
3775			mExistingItems[name] = storeSeparately ? SET_SEPARATELY : SET_GLOBALLY;
3776
3777			// Store the cookie with the calculated path.
3778			NNLUtils.sendRestMessage(endpoint, message)
3779			.then(function(response) {
3780				if(response.outcome !== Outcome.SUCCESS) {
3781					// Remove the flag if the cookie is not successfully set.
3782					delete mExistingItems[name];
3783				}
3784			});
3785		}
3786	};
3787
3788	/**
3789	 * Deletes a stored item.
3790	 * @param {string} name Name of the item to delete.
3791	 * @memberof NNLStorage
3792	 * @alias NNLStorage.removeItem
3793	 */
3794    this.removeItem = function(name) {
3795		localStorage.removeItem(name);
3796		if(mEndpoint != null) {
3797			var message = {
3798				operation: 'DELETE',
3799				name: name
3800			}
3801
3802			var endpoint = mEndpoint;
3803			// It the item being deleted was not stored in a global cookie, then append its name to the path.
3804			if(mExistingItems[name] !== SET_GLOBALLY) {
3805				endpoint += ('/' + encodeURIComponent(name));
3806			}
3807
3808			// Remove the flag regardless of the operation result.
3809			delete mExistingItems[name];
3810
3811			// Delete the cookie for the calculated path.
3812			NNLUtils.sendRestMessage(endpoint, message);
3813		}
3814	};
3815
3816	Object.freeze(this);
3817})();
3818
3819
3820/**
3821 * @hideconstructor
3822 * @class
3823 * @classdesc A facade singleton, whose APIs are used for detection of browser compatibility with FIDO APIs.
3824 */
3825const BrowserInfo = new (function () {
3826	const PUB_KEY_GET_FEATURE = 'publickey-credentials-get';
3827
3828	/**
3829	 * The regexp will test whether the browser is Chromium based or not.
3830	 * @type {RegExp}
3831	 */
3832	const CHROMIUM_REGEX = /Chrom(e|ium)/;
3833
3834	/**
3835	 * Regular expressions for parsing the browser information
3836	 * from the User-Agent String.
3837	 * @type {RegExp}
3838	 */
3839	const UA_STR_BROWSER_INFO_MATCHER = /(opr|edg|chrome|safari|firefox|msie)\/(\d+[.\d]+)?(\ mobile)?(?:\s(safari)\/[.\d]+)?$/i;
3840
3841	/**
3842	 * The regex will match with the general browsers on iOS (excluding
3843	 * Safari).
3844	 * Chrome, Firefox, Edge.
3845	 * <b>Note:</> Currently, for the Brave browser there is no suitable API
3846	 * for determining the browser name.
3847	 * @type {RegExp}
3848	 */
3849	const UA_STR_IOS_MOBILE_BROWSER_MATCHER = /((?:cr|fx|edg)ios)\/(\d+[.\d]+)/i;
3850
3851	/**
3852	 * This regexp is used for determining the version of the iOS, which
3853	 * matches with the Safari version.
3854	 * First capturing group is the version included in the device info (E.g. iPhone
3855	 * OS 16_2).
3856	 * The second capturing group is for getting the version from the
3857	 * "Version/" prefixed part of the UA (E.g "Version/16.2") .
3858	 * @type {RegExp}
3859	 */
3860	const UA_STR_APPLE_VERSION_MATCHER = /(?:(?:ipod|iphone|ipad|mac) os ((?!x)[^\s]+)|(?:version\/([^\s]+)))
3860/i;
3861
3862	/**
3863	 * A regex for determining whether the browser is WebKit based or not.
3864	 * @type {RegExp}
3865	 */
3866	const UA_WEBKIT_REGEX = /(i(pod|phone|pad)|mac os)/i;
3867
3868	const EXCLUDE_UA_BRAND_REGEXES = [
3869		// As per specification all user agents should include an arbitrary GREASE-like value in the brands array of client hints.
3870		// The regular expression captures the arbitrary value as defined in the specification and excludes it from the brands list.
3871		// This Client Hint might be revised and removed in the future as discussed in the https://github.com/WICG/ua-client-hints/pull/174 pull request.
3872		/([^a-z0-9])?not([^a-zA-Z0-9])((a([^a-zA-Z0-9])?brand([^a-z0-9])?)|([^a-z0-9]?browser([^a-z0-9])?))/i,
3873		/Chromium/i
3874	];
3875
3876	const BROWSER_NAME_MAP = {
3877		CriOS: 'Google Chrome',
3878		Chrome: 'Google Chrome',
3879		Safari: 'Safari',
3880		Firefox: 'Firefox',
3881		FxiOS: 'Firefox',
3882		Edg: 'Microsoft Edge',
3883		EdgiOS: 'Microsoft Edge',
3884		Opera: 'Opera',
3885		MSIE: 'Internet Explorer'
3886	}
3887
3888	let mName, mVersion, mIsChromiumBased, mIsWebKitBased;
3889	if (clientHintsAPIAvailable()) {
3890		getInfoFromCH();
3891	} else {
3892		getInfoFromUAString();
3893	}
3894
3895	/**
3896	 * Getter for the User-Agent name.
3897	 * @returns {string} the User-Agent name. (E.g. Google Chrome, Firefox, Safari, etc.)
3898	 */
3899	this.getName = function () {
3900		return mName;
3901	}
3902
3903	/**
3904	 * Getter for the User-Agent version.
3905	 * @returns {string} the User-Agent version.
3906	 */
3907	this.getVersion = function () {
3908		return mVersion;
3909	}
3910
3911	/**
3912	 * Checks whether the FIDO authentication is possible in cross-origin iframes.
3913	 * @returns {boolean} A boolean indicating whether the current user agent supports WebAuthn get in a cross-origin iframe.
3914	 */
3915	this.supportsFIDOInCrossOriginIFrame = function () {
3916		const policy = document.permissionsPolicy || document.featurePolicy;
3917
3918		// If the Permission-Policy (A.K.A Feature-Policy) API is available,
3919		// check if the 'publickey-credentials-get' is included in the list.
3920		if (policy != null && policy.features().indexOf(PUB_KEY_GET_FEATURE) !== -1) {
3921			return true;
3922		}
3923
3924		if (mVersion == null) {
3925			return false;
3926		}
3927
3928		let versionSplit = mVersion.split(/[_.]/);
3929		let major = parseInt(versionSplit[0] || '0');
3930		let minor = parseInt(versionSplit[1] || '0');
3931
3932		if (mIsChromiumBased) {
3933			return major > 84;
3934		}
3935
3936		if (mIsWebKitBased) {
3937			// When the user-agent runs on iOS or macOS, the FIDO in cross-origin iframes is supported in Safari 15.5+.
3938			// As per version table of iOS and macOS Safari the WebKit running in the platforms are of the same version.
3939			return (major === 15 && minor >= 5) || major > 15;
3940		}
3941
3942		return false;
3943	}
3944
3945	function clientHintsAPIAvailable() {
3946		// Check the required values of client hints API to be available.
3947		// In some cases the API might still be available, but with empty values. E.g.: In the desktop browser with mobile inspect mode.
3948		return navigator.userAgentData != null &&
3949			(navigator.userAgentData.platform != null && navigator.userAgentData.platform !== '') &&
3950			(navigator.userAgentData.brands != null && navigator.userAgentData.brands.length > 0);
3951	}
3952
3953	/**
3954	 * Parses browser information using the User Agent Client-Hints API.
3955	 */
3956	function getInfoFromCH() {
3957		// Get a copy of brands list
3958		let uaBrands = JSON.parse(JSON.stringify(navigator.userAgentData.brands));
3959		// Iterate through the brands array and exclude the redundant one.
3960		var brandIndex = 0;
3961		while (brandIndex < uaBrands.length) {
3962			var increment = true;
3963			var brand = uaBrands[brandIndex].brand;
3964			if (CHROMIUM_REGEX.test(brand)) {
3965				mIsChromiumBased = true;
3966			}
3967
3968			for (var excludedBrandKey in EXCLUDE_UA_BRAND_REGEXES) {
3969				// Excluded the redundant UA brands.
3970				if (EXCLUDE_UA_BRAND_REGEXES[excludedBrandKey].test(brand)) {
3971					uaBrands.splice(brandIndex, 1);
3972					increment = false;
3973					break;
3974				}
3975			}
3976
3977			// If a single item left in the brands array - break the iteration.
3978			if (uaBrands.length === 1) {
3979				break;
3980			}
3981
3982			if (increment) {
3983				brandIndex++
3984			}
3985		}
3986
3987		let platform = navigator.userAgentData.platform.toLowerCase();
3988		mIsWebKitBased = (platform === 'ios' || platform === 'macos');
3989		mName = uaBrands[0].brand;
3990		mVersion = uaBrands[0].version;
3991	}
3992
3993	/**
3994	 * Parses browser information using the User Agent String.
3995	 */
3996	function getInfoFromUAString() {
3997		let ua = navigator.userAgent;
3998		mIsChromiumBased = CHROMIUM_REGEX.test(ua);
3999
4000		// The matched groups will include the following.
4001		// 1st element - the UA name. E.g. Chrome, Edg, etc.
4002		// 2nd element - the version of UA. E.g. 110.
4003		let generalInfoMatch = ua.match(UA_STR_BROWSER_INFO_MATCHER);
4004		// In the IE the metadata will be null. So return.
4005		if (generalInfoMatch == null) {
4006			return;
4007		}
4008
4009		// This will match with the general browser names on iOS.
4010		let iosBrowserMatch = ua.match(UA_STR_IOS_MOBILE_BROWSER_MATCHER);
4011		// This will match with the Apple device version(iPhone, iPod, iPad,
4012		// macOS).
4013		let versionMatch = ua.match(UA_STR_APPLE_VERSION_MATCHER);
4014
4015		mIsWebKitBased = UA_WEBKIT_REGEX.test(ua);
4016		mVersion = versionMatch != null ? (versionMatch[1] || versionMatch[2]) : generalInfoMatch[2];
4017		mName = BROWSER_NAME_MAP[iosBrowserMatch != null ? iosBrowserMatch[1] : generalInfoMatch[1]] || "Chromium";
4018	}
4019
4020	/**
4021	 * Determines whether the current browser is a mobile browser.
4022	 *
4023	 * @returns {boolean} Returns true if the browser is a mobile browser, otherwise returns false.
4024	 */
4025	this.isMobileBrowser = function () {
4026		var userAgent = navigator.userAgent || navigator.vendor || window.opera;
4027		// Windows Phone must come first because its UA also contains "Android"
4028		return   ((/windows phone/i.test(userAgent)) ||
4029				 (/android/i.test(userAgent)) ||
4030				// iOS detection from: https://stackoverflow.com/questions/9038625
4031				((/iPad|iPhone|iPod/.test(userAgent) || (navigator.platform === 'MacIntel'
4032				&& navigator.maxTouchPoints > 1)) && !window.MSStream))
4033	}
4034
4035	/**
4036	 * Check if the current device is running Mobile Safari.
4037	 *
4038	 * @returns {boolean} Whether the device is running Mobile Safari.
4039	 */
4040	this.isMobileSafari = function() {
4041		return (/iPad|iPhone|iPod/.test(navigator.userAgent) &&
4042			navigator.userAgent.match('Safari') &&
4043			!navigator.userAgent.match('CriOS') &&
4044			!navigator.userAgent.match('FxiOS'));
4045	}
4046
4047	Object.freeze(this);
4048})();
4049
4050/**
4051 * The callback that is called when a FIDO operation is completed  and view needs to be refreshed.
4052 * @callback ViewUpdateListenerCallback
4053 * @param {FidoOperationType} operationType The operation type that has completed.
4054 * @param {Error} error The error information. If the operation was successful this will be null.
4055 */
4056
4057/**
4058 * @class
4059 * This class listens for FIDO operation completion events and calls the specified callback if
4060 * the specified view element is in the document. When the element is no longer in the document, 
4061 * it stops listening for events.
4062 * @param {Object} view An HTML element to observe.
4063 * @param {ViewUpdateListenerCallback} callback The callback to call when view refresh is needed.
4064 */
4065const ViewUpdateListener = function(view, callback) {
4066	const mListener = new (function() {
4067		this.onComplete = function(operationType, operationData, extras) {
4068			refreshIfVisible(operationType, null);
4069		}
4070
4071		this.onFailure = function(operationType, error) {
4072			refreshIfVisible(operationType, error);
4073		}
4074
4075		function refreshIfVisible(operationType, error) {
4076			// If the view is still in the document, call controller to refresh.
4077			if(document.body.contains(view)) {
4078				callback(operationType, error);
4079			} else {
4080				// Otherwise there is no need to listen for operation results.
4081				OperationResultListener.removeInstance(mListener);
4082			}
4083		}
4084	})();
4085
4086	/**
4087	 * Starts observing FIDO operation completions and view presence.
4088	 */
4089	this.start = function() {
4090		OperationResultListener.addInstance(mListener);
4091	}
4092
4093	/**
4094	 * Stops observing FIDO operation completions and view presence. 
4095	 */
4096	this.stop = function() {
4097		OperationResultListener.removeInstance(mListener);
4098	}
4099
4100	Object.freeze(this);
4101}
4102
4103/**
4104 * This interface defines methods for displaying the Sign Up form and confirmation dialogs when registering
4105 * authentication methods during sign up.
4106 * @interface ISignUpLiveData
4107 */
4108
4109/**
4110 * This method will be called to retrieve the sign up data from the user. The implementation should present a
4111 * form to the user to collect the data and call {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted}
4112 * when the user submits that form.
4113 * When this method is called first time, `error` will be `null`.
4114 * Then, if there is an error while processing the provided `signUpData`, this method will be called again with the actual error code. 
4115 * @method ISignUpLiveData#getSignUpData
4116 * @param {Object} signUpOpts An object containing the fields that should be included in the sign up form.
4117 * @param {SignUpError} error The error code indicating what error has happened after the previous call to this method.
4118 * @param {String} [message] Additional error message.  
4119 */
4120
4121/**
4122 * This method will be called when it is required to get user's consent to register a particular method.
4123 * The implementation should show a confirmation dialog and ask the user whether they want to register a
4124 * method specified in the `method` parameter. When the user takes an action, the implementation should call
4125 * {@link SignUpController#onConfirmationResponse|SignUpController.onConfirmationResponse} and pass in the same
4126 * `method` parameter as well as the user's choice.
4127 * @method ISignUpLiveData#confirmMethodRegistration
4128 * @param {SignUpMethod} method The type of the method that the user needs to confirm to register.
4129 */
4130
4131/**
4132 * This interface defines base methods for the view class that implements {@link ISignUpLiveData}.
4133 * @interface BaseSignUpView
4134 */
4135
4136/**
4137 * This method is called when the view should be displayed in a specified container.
4138 * The implementation should prepare the resources for the view (e.g. load HTML fragment) and call
4139 * {@link SignUpController#onRefresh|SignUpController.onRefresh}.
4140 * @method BaseSignUpView#show
4141 * @param {Object} container An HTML element where the view should be placed.
4142 */
4143
4144/**
4145 * This method is called when the view should be displayed in a modal dialog.
4146 * The implementation should prepare the resources for the view and modal dialog (e.g. load HTML fragment),
4147 * display the modal dialog and call {@link SignUpController#onRefresh|SignUpController.onRefresh}.
4148 * The implementation must hide the modal dialog before calling
4149 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted}.
4150 * When {@link ISignUpLiveData#getSignUpData|ISignUpLiveData.getSignUpData} is called,
4151 * the implementation must display the modal dialog if it is not already visible and place the view in it.
4152 * If the user closes the modal dialog, the implementation must call
4153 * {@link SignUpController#onCancel|SignUpController.onCancel}.
4154 * @method BaseSignUpView#showModal
4155 */
4156
4157/**
4158 * This method is called to close a modal dialog displayed by showMoodal.
4159 * The implementation should just close the modal dialog if it is visible.
4160 * @method BaseSignUpView#closeModal
4161 */
4162
4163/**
4164 * This method implementation should return the {@link SignUpController} instance passed in in the constructor.
4165 * @method BaseSignUpView#getController
4166 * @returns {SignUpController}
4167 */
4168
4169/**
4170 * Indicates the error when {@link ISignUpLiveData#getSignUpData|ISignUpLiveData.getSignUpData}
4171 * is called after an error happened. 
4172 * @enum {string}
4173 */
4174const SignUpError = {
4175	/**
4176	 * The "fullName" field was missing or was empty in the "signUpData" passed to
4177	 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call.
4178	 */
4179	FULL_NAME_REQUIRED: 'FULL_NAME_REQUIRED',
4180
4181	/**
4182	 * The "userName" field was missing or was empty in the "signUpData" passed to
4183	 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call.
4184	 */
4185	USERNAME_REQUIRED: 'USERNAME_REQUIRED',
4186
4187	/**
4188	 * The "emailAddress" field was missing or was empty in the "signUpData" passed to
4189	 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call.
4190	 */
4191	EMAIL_ADDRESS_REQUIRD: 'EMAIL_ADDRESS_REQUIRD',
4192
4193	/**
4194	 * The "phoneNumber" field was missing or was empty in the "signUpData" passed to
4195	 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call.
4196	 */
4197	PHONE_NUMBER_REQUIRED: 'PHONE_NUMBER_REQUIRED',
4198
4199	/**
4200	 * The "userName" field in "signUpData" passed to
4201	 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted}
4201 call was invalid.
4202	 */
4203	INVALID_USERNAME: 'INVALID_USERNAME',
4204
4205	/**
4206	 * The "emailAddress" field in "signUpData" passed to
4207	 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call was invalid.
4208	 */
4209	INVALID_EMAIL_ADDRESS: 'INVALID_EMAIL_ADDRESS',
4210
4211	/**
4212	 * The "phoneNumber" field in "signUpData" passed to
4213	 * {@link SignUpController#onSignUpFormSubmitted|SignUpController.onSignUpFormSubmitted} call was invalid.
4214	 */
4215	INVALID_PHONE_NUMBER: 'INVALID_PHONE_NUMBER',
4216
4217	/**
4218	 * An account corresponding to the provided sign up data already exists.
4219	 */
4220	ACCOUNT_ALREADY_EXISTS: 'ACCOUNT_ALREADY_EXISTS',
4221
4222	/**
4223	 * A network error has occurred.
4224	 */
4225	CONNECTION_ERROR: 'CONNECTION_ERROR',
4226
4227	/**
4228	 * Generic error happened.
4229	 */
4230	OTHER: 'OTHER'
4231}
4232Object.freeze(SignUpError);
4233
4234/**
4235 * Indicates the type of method that the user needs to confirm to register during sign up. 
4236 * @enum {string}
4237 */
4238const SignUpMethod = {
4239	/**
4240	 * A passkey (platform authenticator) is being registered.
4241	 */
4242	PASSKEY: 'PASSKEY',
4243
4244	/**
4245	 * A security key (cross platform authenticator) is being registered.
4246	 */
4247	SECURITY_KEY: 'SECURITY_KEY',
4248
4249	/**
4250	 * An ID document verification method is being registered.
4251	 */
4252	PHOTO_ID: 'PHOTO_ID'
4253}
4254Object.freeze(SignUpMethod);
4255
4256/**
4257 * A singleton factory class to create and provide {@link SignUpUIFactory} implementations.
4258 * @class
4259 * @hideconstructor
4260 */
4261const SignUpUIFactory = new (function() {
4262	let mInstance = null;
4263
4264	/**
4265	 * Sets the SignUpUIFactory instance.
4266	 * @param {SignUpUIFactory} instance The new instance.
4267	 * @alias SignUpUIFactory.setInstance
4268	 */
4269	this.setInstance = function(instance) {
4270		NNLUtils.validateArgument('instance', instance, 'object', null, false);
4271		mInstance = instance;
4272	}
4273
4274	/**
4275	 * Returns {@link SignUpUIFactory} instance.
4276	 * If no instance was set using the {@link SignUpUIFactory.setInstance|setInstance} method,
4277	 * creates and returns {@link DefaultSignUpUIFactory} instance.
4278	 * @returns {SignUpUIFactory} The factory instance to use.
4279	 * @alias SignUpUIFactory.getInstance
4280	 */
4281	this.getInstance = function() {
4282		if(mInstance == null) {
4283			mInstance = new DefaultSignUpUIFactory();
4284		}
4285
4286		return mInstance;
4287	}
4288
4289	/**
4290	 * This method implementation should create and return {@link BaseSignUpView} implementation.
4291	 * @method SignUpUIFactory#createView
4292	 * @param {SignUpController} signUpController The SignUpController instance to call back after user input.
4293	 * @returns {BaseSignUpView} The created instance.
4294	 * @abstract
4295	 */
4296
4297	Object.freeze(this);
4298})();
4299
4300/**
4301 * Default implementation of {@link SignUpUIFactory}.
4302 * @class
4303 * @implements {SignUpUIFactory}
4304 */
4305const DefaultSignUpUIFactory = function() {
4306	/**
4307	 * Creates and returns an instance of the default implementation of {@link BaseSignUpView}. 
4308	 * @param {SignUpController} signUpController The SignUpController instance to call back after user input.
4309	 * @returns {BaseSignUpView} A {@link DefaultSignUpView} instance.
4310	 */
4311	this.createView = function(signUpController) {
4312		return new DefaultSignUpView(signUpController);
4313	}
4314
4315	Object.freeze(this);
4316}
4317
4318/**
4319 * @classdesc Default implementation of {@link BaseSignUpView} and {@link ISignUpLiveData}.
4320 * Presents the sign up form and confirmation dialogs to the user.
4321 * @class
4322 * @implements BaseSignUpView
4323 * @implements ISignUpLiveData
4324 * @param {SignUpController} signUpController The SignUpController instance to call back after user input.
4325 */
4326const DefaultSignUpView = function(signUpController) {
4327	NNLUtils.validateArgument('signUpController', signUpController, 'object', SignUpController, false);
4328
4329	const _this = this;
4330
4331	let mSignUpDialog = null;
4332	let mSignUpFormTemplate;
4333	let mView;
4334	let mConfirmDialog;
4335
4336	// Load the HTML and store references to the UI elements.
4337	const mIsReady = NNLUtils.loadHTML('sign-up-ui.html', 'nnl-sign-up-section')
4338	.then(function() {
4339		mSignUpFormTemplate = document.querySelector('#nnl-sign-up-form-template');
4340		mConfirmDialog = document.querySelector('#nnl-sign-up-confirmation-dialog');
4341	});
4342
4343	/**
4344	 * Shows the view with sign up form in the specified container.
4345	 * @param {Object} container An HTML element where the view should be placed.
4346	 */
4347	this.show = function(container) {
4348		NNLUtils.validateArgument('container', container, 'object', null, false);
4349
4350		mIsReady.then(function() {
4351			container.innerHTML = '';
4352			mView = mSignUpFormTemplate.cloneNode(true);
4353			mView.removeAttribute('id');
4354			container.appendChild(mView);
4355			signUpController.onRefresh(_this);
4356		});
4357	}
4358
4359	/**
4360	 * Shows the view with sign up form in a modal dialog
4361	 */
4362	this.showModal = function() {
4363		mIsReady.then(function() {
4364			mSignUpDialog = document.querySelector('#nnl-sign-up-form-dialog');
4365			mView = mSignUpDialog.querySelector('.nnl-dialog-body');
4366			mView.innerHTML = '';
4367			mView.appendChild(mSignUpFormTemplate.querySelector('form').cloneNode(true));
4368			NNLUtils.showModal(mSignUpDialog, true);
4369			signUpController.onRefresh(_this);
4370		});
4371	}
4372
4373	/**
4374	 * Closes the modal dialog.
4375	 */
4376	this.closeModal = function() {
4377		if(mSignUpDialog != null) {
4378			NNLUtils.showModal(mSignUpDialog, false);
4379		}
4380	}
4381
4382	/**
4383	 * Returns the {SignUpController} associated with this view.
4384	 * @returns {SignUpController}
4385	 */
4386	this.getController = function() {
4387		return signUpController;
4388	}
4389
4390	/**
4391	 * Presents a modal dialog with the sign up form containing fields specified in the `signUpOpts`.
4392	 * @param {Object} signUpOpts An object containing the fields that should be included in the sign up form.
4393	 * @param {SignUpError} error The error code indicating what error has happened after the previous call to this method.
4394	 * @param {String} [message] Additional error message.  
4395	 */
4396	this.getSignUpData = function(signUpOpts, error, message) {
4397		NNLUtils.validateArgument('signUpOpts', signUpOpts, 'object', null, false);
4398		NNLUtils.validateArgument('error', error, 'string', null, true);
4399		NNLUtils.validateArgument('message', message, 'string', null, true);
4400
4401		mView.style.pointerEvents = '';
4402		const sections = {};
4403		const signUpForm = NNLUtils.removeEventListeners(mView.querySelector('.nnl-sign-up-form'));
4404		const genericErrorSection = signUpForm.querySelector('[data-generic-error]');
4405		hideErrors(genericErrorSection);
4406
4407		signUpForm.querySelectorAll('.nnl-sign-up-form-section').forEach(function(section) {
4408			// Store reference of the section to get the input value when the form is submitted.
4409			sections[section.dataset.sectionName] = section;
4410
4411			hideErrors(section);
4412			// Show or hide section according the provided option value.
4413			if(signUpOpts[section.dataset.sectionName] != null) {
4414				section.style.display = 'block';
4415				const hint = section.querySelector('.nnl-sign-up-optional');
4416				hint.style.display = signUpOpts[section.dataset.sectionName] === 'optional' ? 'inline' : 'none';
4417			} else {
4418				section.style.display = 'none';
4419			}
4420		});
4421
4422		// If there is an error, show corresponding error message.
4423		switch(error) {
4424		case SignUpError.FULL_NAME_REQUIRED:
4425			showError(sections['fullName'], 'required', message);
4426			break;
4427		case SignUpError.USERNAME_REQUIRED:
4428			showError(sections['userName'], 'required', message);
4429			break;
4430		case SignUpError.EMAIL_ADDRESS_REQUIRD:
4431			showError(sections['emailAddress'], 'required', message);
4432			break;
4433		case SignUpError.PHONE_NUMBER_REQUIRED:
4434			showError(sections['phoneNumber'], 'required', message);
4435			break;
4436
4437		case SignUpError.INVALID_USERNAME:
4438			showError(sections['userName'], 'invalid', message);
4439			break;
4440		case SignUpError.INVALID_EMAIL_ADDRESS:
4441			showError(sections['emailAddress'], 'invalid', message);
4442			break;
4443		case SignUpError.INVALID_PHONE_NUMBER:
4444			showError(sections['phoneNumber'], 'invalid', message);
4445			break;
4446
4447		case SignUpError.ACCOUNT_ALREADY_EXISTS:
4448			showError(genericErrorSection, 'account-exists', message);
4449			break;
4450		case SignUpError.CONNECTION_ERROR:
4451			showError(genericErrorSection, 'connection', message);
4452			break;
4453		case SignUpError.OTHER:
4454			showError(genericErrorSection, 'generic', message);
4455			break;
4456		}
4457
4458		signUpForm.addEventListener('submit', function(event) {
4459			event.preventDefault();
4460			const signUpData = {};
4461			// When the form is submitted, gather the values only for the fields requested in the signUpOpts.
4462			for(let sectionName in sections) {
4463				if(signUpOpts[sectionName] != null) {
4464					signUpData[sectionName] = sections[sectionName].querySelector('input').value.trim()
4465				}
4466			}
4467
4468			hideUI();
4469			signUpController.onSignUpFormSubmitted(_this, signUpData);
4470		});
4471
4472		if(mSignUpDialog != null) {
4473			new ViewWrapper(mSignUpDialog).onShow();
4474			const cancelButton = NNLUtils.removeEventListeners(mSignUpDialog.querySelector('.nnl-dialog-close'));
4475			cancelButton.addEventListener('click', function() {
4476				NNLUtils.showModal(mSignUpDialog, false);
4477				signUpController.onCancel();
4478			});
4479			NNLUtils.showModal(mSignUpDialog, true);
4480		} else {
4481			new ViewWrapper(mView).onShow();
4482			mView.style.display = 'block';
4483		}
4484	}
4485
4486	/**
4487	 * Displays a modal dialog asking the user's confirmation for registering the specified method.
4488	 * @param {SignUpMethod} method The type of the method that the user needs to confirm to register.
4489	 */
4490	this.confirmMethodRegistration = function(method) {
4491		NNLUtils.validateArgument('method', method, 'string', null, false);
4492
4493		mIsReady.then(function() {
4494			mConfirmDialog.classList.remove('nnl-sign-up-method-passkey', 'nnl-sign-up-method-security-key', 'nnl-sign-up-method-photo-id');
4495			switch(method) {
4496			case SignUpMethod.PASSKEY:
4497				mConfirmDialog.classList.add('nnl-sign-up-method-passkey');
4498				break;
4499			case SignUpMethod.SECURITY_KEY:
4500				mConfirmDialog.classList.add('nnl-sign-up-method-security-key');
4501				break;
4502			case SignUpMethod.PHOTO_ID:
4503				mConfirmDialog.classList.add('nnl-sign-up-method-photo-id');
4504				break;
4505			}
4506
4507			const confirmButton = NNLUtils.removeEventListeners(mConfirmDialog.querySelector('#nnl-sign-up-method-confirm'));
4508			confirmButton.addEventListener('click', function() {
4509				NNLUtils.showModal(mConfirmDialog, false);
4510				signUpController.onConfirmationResponse(_this, method, true);
4511			});
4512
4513			const cancelButton = NNLUtils.removeEventListeners(mConfirmDialog.querySelector('#nnl-sign-up-method-cancel'));
4514			cancelButton.addEventListener('click', function() {
4515				NNLUtils.showModal(mConfirmDialog, false);
4516				signUpController.onConfirmationResponse(_this, method, false);
4517			});
4518
4519			new ViewWrapper(mConfirmDialog.querySelector('.nnl-dialog')).onShow();
4520			NNLUtils.showModal(mConfirmDialog, true);
4521		});
4522	}
4523
4524	function hideUI() {
4525		// If modal dialog is displayed, hide it.
4526		if(mSignUpDialog != null) {
4527			NNLUtils.showModal(mSignUpDialog, false);
4528		} else {
4529			// Otherwise disable the container.
4530			mView.style.pointerEvents = 'none';
4531		}
4532	}
4533
4534	function hideErrors(section) {
4535		const classes = section.classList.values();
4536		let it = classes.next();
4537		while(!it.done) {
4538			if(it.value.indexOf('nnl-sign-up-error-') === 0) {
4539				section.classList.remove(it.value);
4540			}
4541			it = classes.next();
4542		}
4543	}
4544
4545	function showError(section, errorType, message) {
4546		if(message == null || message === '') {
4547			section.classList.add('nnl-sign-up-error-' + errorType);
4548		} else {
4549			section.classList.add('nnl-sign-up-error-custom');
4550			section.querySelector('.nnl-sign-up-error-custom').innerText = message;
4551		}
4552	}
4553}
4554
4555/**
4556 * This interface defines a method for creating {@link ISignUpProcessor} instance.
4557 * The RP application must implement this interface in order for the sign up functionality to work.
4558 * @interface ISignUpProcessorFactory
4559 */
4560
4561/**
4562 * The implementation should create and return {@link ISignUpProcessor} instance.
4563 * @method ISignUpProcessorFactory#createSignUpProcessor
4564 * @param {Extras} [signUpOpts] Extra options.
4565 * @returns {ISignUpProcessor} The {@link ISignUpProcessor} instance.
4566 */
4567
4568/**
4569 * This interface defines methods for starting and competing the sign up process by the RP application.
4570 * The RP application must implement this interface in order for the sign up functionality to work.
4571 * @interface ISignUpProcessor
4572 */
4573
4574/**
4575 * This method will be called when the user submits the sign up form. The `signUpData` parameter will
4576 * contain the sign up form fields' values.
4577 * The implementation should make a call to the RP backend to start account creation.
4578 * The backend should create an "unverified" account and a session key for that account that contains
4579 * a custom claim "sign_up" with the value "true" in the JWT payload. The implementation should return a Promise that should
4580 * be resolved with a {@link SignUpSession|SignUpSession} instance containing a userName and sessionData object containing that session key.
4581 * In case of errors the implementation should reject the Promise with one of the {@link SignUpError} values and optional error message.
4582 * @method ISignUpProcessor#startSignUp
4583 * @param {Object} signUpData The data collected from the sign up form.
4584 * @param {string} [signUpData.fullName] The value of the "Full Name" field from the sign up form if provided.
4585 * @param {string} [signUpData.userName] The value of the "Username" field from the sign up form if provided.
4586 * @param {string} [signUpData.emailAddress] The value of the "Email Address" field from the sign up form if provided.
4587 * @param {string} [signUpData.phoneNumber] The value of the "Phone Number" field from the sign up form if provided.
4588 * @returns {Promise} A Promise that should be resolved with a {@link SignUpSession|SignUpSession} instance or rejected 
4589 * with one of the {@link SignUpError} values.
4590 */
4591 
4592/**
4593 * This method will be called after the requested methods have been registered. The implementation
4594 * should make a call to the RP backend to complete the account creation. The RP backend should
4595 * verify that all required methods were registered by making `LIST_METHODS` and `LIST_REG` requests to API Server.
4596 * The implementation should return a Promise that should be resolved with a {@link SignUpSession|SignUpSession}
4597 * instance containing the `userName` and a new `sessionData` without the "sign_up" claim (as if the user has passed
4598 * an authentication). The `userName` and `sessionData` will be returned to the RP application to sign the user in.
4599 * In case of an error the implementation should reject the Promise with one of the {@link SignUpError} values.
4600 * @method ISignUpProcessor#finishSignUp
4601 * @param {SignUpSession} session Sign up session containing the `userName` and `sessionData` used during sign up,
4602 * which may be updated during the process.
4603 * @returns {Promise} A Promise that should be resolved with a {@link SignUpSession|SignUpSession} instance or rejected 
4604 * with one of the {@link SignUpError} values.
4605 */
4606
4607/**
4608 * @class
4609 * @classdesc Represents a session associated with a sign-up process.
4610 * @param {string} userName The user's username associated with the session.
4611 * @param {SessionData} sessionData The session data containing the session key.
4612 */
4613const SignUpSession = function(userName, sessionData) {
4614	this.userName = userName;
4615	this.sessionData = sessionData;
4616}
4617
4618/**
4619 * @class
4620 * @classdesc A singleton factory class to store and return {@link ISignUpProcessorFactory} implementation.
4621 * @hideconstructor
4622 */
4623const SignUpProcessorFactory = new (function() {
4624	let mInstance = null;
4625
4626	/**
4627	 * Stores the specified {@link ISignUpProcessorFactory} instance.
4628	 * @alias SignUpProcessorFactory.setInstance
4629	 */
4630	this.setInstance = function(instance) {
4631		NNLUtils.validateArgument('instance', instance, 'object', null, false);
4632		mInstance = instance;
4633	}
4634
4635	/**
4636	 * Returns the stored {@link ISignUpProcessorFactory} instance or null if setInstance
4637	 * method was not called. The RP application must call the setInstance method to specify
4638	 * the implementation, otherwise the sign up option will not be displayed.
4639	 * @alias SignUpProcessorFactory.getInstance
4640	 */
4641	this.getInstance = function() {
4642		return mInstance;
4643	}
4644
4645	Object.freeze(this);
4646})();
4647
4648/**
4649 * @class
4650 * @classdesc Executes the sign up process.
4651 * @param {object} appSdkConfig An object containing endpoints for registration operations.
4652 * @param {string} appSdkConfig.regEndpoint Endpoint for registration operations.
4653 * @param {object} signUpOpts Options for sign up operation.
4654 * @param {boolean} signUpOpts.enabled Flag indicating whether the sign up should be enabled or not.
4655 * @param {string} [signUpOpts.fullName] If specified, accepted values are "required" and "optional".
4656 * The sign up form will display the "Full Name" field and if the value is "required",
4657 * the input will not be allowed to be empty when the form is submitted.
4658 * @param {string} [signUpOpts.userName] If specified, accepted values are "required" and "optional".
4659 * The sign up form will display the "Username" field and if the value is "required",
4660 * the input will not be allowed to be empty when the form is submitted.
4661 * @param {string} [signUpOpts.emailAddress] If specified, accepted values are "required" and "optional".
4662 * The sign up form will display the "Email Address" field and if the value is "required",
4663 * the input will not be allowed to be empty when the form is submitted and the sign up process will fail
4664 * if the registration of the "Email OTP" method is canceled or has failed.
4665 * @param {string} [signUpOpts.phoneNumber] If specified, accepted values are "required" and "optional".
4666 * The sign up form will display the "Phone Number" field and if the value is "required",
4667 * the input will not be allowed to be empty when the form is submitted and the sign up process will fail
4668 * if the registration of the "SMS OTP" method is canceled or has failed.
4669 * @param {boolean} [signUpOpts.fido] When the value is `true`, the sign up process will ask the user to create a
4670 * passkey or register a security key if supported by the platform.
4671 * @param {boolean} [signUpOpts.photoID] When the value is `true`, the sign up process will ask the user to register
4672 * a ID Document for verification.
4673 */
4674const SignUpController = function(appSdkConfig, signUpOpts) {
4675	NNLUtils.validateArgument('appSdkConfig', appSdkConfig, 'object', null, false);
4676	NNLUtils.validateArgument('signUpOpts', signUpOpts, 'object', null, false);
4677
4678	const mSignUpOpts = NNLUtils.cloneObject(signUpOpts);
4679
4680	const mSignUpProcessor = SignUpProcessorFactory.getInstance().createSignUpProcessor(mSignUpOpts);
4681
4682	// Function that will either resolve or reject the Promise returned by the enable() method.
4683	let returnResult;
4684
4685	let mSignUpData = null;
4686	let mSignUpSession = null;
4687	let mNextMethodIndex = 0;
4688	let mOnConfirmReg;
4689
4690	const mRegistrationOrder = [
4691		AdaptiveType.FIDO,
4692		AdaptiveType.EMAIL,
4693		AdaptiveType.SMS,
4694		AdaptiveType.PHOTO_ID
4695	];
4696
4697	// Create and initialize SDK instances.
4698	const mAdaptiveCore = new AdaptiveCore();
4699	mAdaptiveCore.regEndpoint = appSdkConfig.regEndpoint;
4700
4701	const mAppSdk = new AppSdk();
4702	mAppSdk.regEndpoint = appSdkConfig.regEndpoint;
4703	mAppSdk.mode = AppSdk.MODE_NATIVE;
4704
4705	const mIsReady = Promise.all([
4706		NNLUtils.libphonenumber.load(),
4707		mAdaptiveCore.init(),
4708		mAppSdk.init(AppSdk.PROTOCOl_FIDO2)
4709	]);
4710
4711	/**
4712	 * Returns a Promise that will be resolved or rejected with AdaptiveResult.
4713	 * The "adaptiveOp" field of that AdaptiveResult will be set to {@link AdaptiveOperation|AdaptiveOperation.SIGN_UP}.
4714	 * When the Promise is resolved, the AdaptiveResult will contain the `sessionData` and `userName`. 
4715	 * The Promise will be rejected only when the user cancels the sign up form. The AdaptiveResult in this case will
4716	 * contain only the `outcome` field equal to {@link Outcome|CANCELED}.
4717	 */
4718	this.enable = function() {
4719		return new Promise(function(resolve, reject) {
4720			returnResult = function(result) {
4721				if(result.outcome == null) {
4722					resolve(result);
4723				} else {
4724					reject(result);
4725				}
4726			}
4727		});
4728	}
4729
4730	/**
4731	 * Starts the sign up process.
4732	 * @param {ISignUpLiveData} liveData The ISignUpLiveData instance to notify about UI updates.
4733	 */
4734	this.onRefresh = function(liveData) {
4735		mIsReady.then(function() {
4736			ProgressIndicator.getInstance().suspend();
4737			liveData.getSignUpData(mSignUpOpts, null, null);
4738		});
4739	}
4740
4741	/**
4742	 * Should be called by the implementation of {@link ISignUpLiveData#getSignUpData|ISignUpLiveData.getSignUpData}.
4743	 * Validates the `signUpData` and calls {@link ISignUpProcessor#startSignUp|startSignUp}.
4744	 * @param {ISignUpLiveData} liveData The ISignUpLiveData instance to notify about UI updates.
4745	 * @param {object} signUpData An object containing values from the sign up form.
4746	 * @param {string} [signUpData.fullName] The value of the "Full Name" field.
4747	 * @param {string} [signUpData.userName] The value of the "Username" field.
4748	 * @param {string} [signUpData.emailAddress] The value of the "Email Address" field.
4749	 * @param {string} [signUpData.phoneNumber] The value of the "Phone Number" field.
4750	 */	
4751	this.onSignUpFormSubmitted = function(liveData, signUpData) {
4752		NNLUtils.validateArgument('signUpData', signUpData, 'object', null, false);
4753
4754		if(mSignUpOpts.fullName === 'required' && signUpData.fullName === '') {
4755			liveData.getSignUpData(mSignUpOpts, SignUpError.FULL_NAME_REQUIRED, null);
4756			return;
4757		}
4758
4759		if(mSignUpOpts.userName === 'required' && signUpData.userName === '') {
4760			liveData.getSignUpData(mSignUpOpts, SignUpError.USERNAME_REQUIRED, null);
4761			return;
4762		}
4763
4764		if(mSignUpOpts.emailAddress === 'required' && signUpData.emailAddress === '') {
4765			liveData.getSignUpData(mSignUpOpts, SignUpError.EMAIL_ADDRESS_REQUIRD, null);
4766			return;
4767		}
4768
4769		if(mSignUpOpts.phoneNumber === 'required' && signUpData.phoneNumber === '') {
4770			liveData.getSignUpData(mSignUpOpts, SignUpError.PHONE_NUMBER_REQUIRED, null);
4771			return;
4772		}
4773
4774		if(signUpData.emailAddress !== '' && !NNLUtils.isEmail(signUpData.emailAddress)) {
4775			liveData.getSignUpData(mSignUpOpts, SignUpError.INVALID_EMAIL_ADDRESS, null);
4776			return;
4777		}
4778
4779		if(signUpData.phoneNumber !== '') {
4780			const phoneNumber = NNLUtils.libphonenumber.parse(signUpData.phoneNumber, mSignUpOpts);
4781			if(phoneNumber == null) {
4782				liveData.getSignUpData(mSignUpOpts, SignUpError.INVALID_PHONE_NUMBER, null);
4783				return;
4784			}
4785			signUpData.phoneNumber = phoneNumber;
4786		}
4787
4788		ProgressIndicator.getInstance().resume();
4789		mSignUpData = signUpData;
4790
4791		mSignUpProcessor.startSignUp(mSignUpData)
4792		.then(function(signUpSession) {
4793			mSignUpSession = NNLUtils.cloneObject(signUpSession);
4794			mNextMethodIndex = 0;
4795			return UserDataCache.getInstance().setActiveUser(mSignUpSession.userName);
4796		})
4797		.then(function() {
4798			registerNextMethod(liveData);
4799		})
4800		.catch(function(error) {
4801			onError(liveData, error, null);
4802		});
4803
4804	}
4805
4806	/**
4807	 * Should be called by the implementation of
4808	 * {@link ISignUpLiveData#confirmMethodRegistration|ISignUpLiveData.confirmMethodRegistration}.
4809	 * Starts or skips the registration of the specified `method` based on the value of the `confirmed` flag.
4810	 * @param {ISignUpLiveData} liveData The ISignUpLiveData instance to notify about UI updates.
4811	 * @param {SignUpMethod} method The method that the user was asked to register.
4812	 * @param {boolean} confirmed Flag indicating the user's choice.
4813	 */
4814	this.onConfirmationResponse = function(liveData, method, confirmed) {
4815		NNLUtils.validateArgument('method', method, 'string', null, false);
4816		NNLUtils.validateArgument('confirmed', confirmed, 'boolean', null, false);
4817
4818		mOnConfirmReg(method, confirmed);
4819	}
4820
4821	/**
4822	 * Cancels the sign up process.
4823	 */
4824	this.onCancel = function() {
4825		ProgressIndicator.getInstance().resume();
4826		returnResult({
4827			outcome: Outcome.CANCELED
4828		});
4829	}
4830
4831	// Completes the sign up process.
4832	function finishSignUp(liveData) {
4833		mSignUpProcessor.finishSignUp(mSignUpSession)
4834		.then(function(signUpSession) {
4835			const adaptiveResult = {
4836				userName: signUpSession.userName,
4837				sessionData: signUpSession.sessionData,
4838				adaptiveOp: AdaptiveOperation.SIGN_UP
4839			}
4840	
4841			returnResult(adaptiveResult);
4842		})
4843		.catch(function(error) {
4844			onError(liveData, error, null);
4845		});
4846	}
4847
4848	// Calls liveData to show the error on the sign up form.
4849	function onError(liveData, error, message) {
4850		UserDataCache.getInstance().setActiveUser(null);
4851		ProgressIndicator.getInstance().suspend();
4852		liveData.getSignUpData(mSignUpOpts, error, message);
4853	}
4854
4855	// Calls ISignUpLiveData.confirmMethodRegistration and returns a Promise that
4856	// will be resolved or rejected based on the user's choice.
4857	function confirmRegistration(liveData, signUpMethod) {
4858		return new Promise(function(resolve, reject) {
4859			mOnConfirmReg = function(method, confirmed) {
4860				ProgressIndicator.getInstance().resume();
4861				if(confirmed) {
4862					resolve(method);
4863				} else {
4864					reject({outcome: Outcome.CANCELED});
4865				}
4866			}
4867
4868			ProgressIndicator.getInstance().suspend();
4869			liveData.confirmMethodRegistration(signUpMethod);
4870		});
4871	}
4872
4873	// Starts the registration of the next method in the queue. Calls SignUpProcessor.finishSignUp()
4874	// when the end of the queue is reached.
4875	function registerNextMethod(liveData) {
4876		// No methods left to register, complete the process.
4877		if(mNextMethodIndex === mRegistrationOrder.length) {
4878			finishSignUp(liveData);
4879			return;
4880		}
4881
4882		// Get the next method type to register.
4883		const type = mRegistrationOrder[mNextMethodIndex];
4884		mNextMethodIndex++;
4885
4886		Promise.resolve()
4887		.then(function() {
4888			if(type === AdaptiveType.FIDO && mSignUpOpts.fido) {
4889				return checkAndRegisterMethod(liveData, AdaptiveType.FIDO);
4890			}
4891
4892			if(type === AdaptiveType.EMAIL && mSignUpData.emailAddress != null && mSignUpData.emailAddress !== '') {
4893				return checkAndRegisterMethod(liveData, AdaptiveType.EMAIL, mSignUpData.emailAddress);
4894			}
4895
4896			if(type === AdaptiveType.SMS && mSignUpData.phoneNumber != null && mSignUpData.phoneNumber !== '') {
4897				return checkAndRegisterMethod(liveData, AdaptiveType.SMS, mSignUpData.phoneNumber);
4898			}
4899
4900			if(type === AdaptiveType.PHOTO_ID && mSignUpOpts.photoID) {
4901				return checkAndRegisterMethod(liveData, AdaptiveType.PHOTO_ID);
4902			}
4903		})
4904		.then(function() {
4905			registerNextMethod(liveData);
4906		})
4907		.catch(function(result) {
4908			const isRequiredMethod = (type === AdaptiveType.EMAIL && mSignUpOpts.emailAddress === 'required' ||
4909										type === AdaptiveType.SMS && mSignUpOpts.phoneNumber === 'required');
4910			const message = Outcome.getSignUpErrorMessage(result.outcome);
4911
4912			// If sign up cannot continue (required method has failed, session has expired or connection is lost)
4913			// show the sign up form again with the corresponding error message.
4914			if(isRequiredMethod || result.outcome === Outcome.INVALID_SESSION || result.outcome === Outcome.CONNECTION_ERROR) {
4915				// If a required method is canceled, show the sign up form without error message.
4916				const error = result.outcome === Outcome.CANCELED ? null : SignUpError.OTHER;
4917				onError(liveData, error, message);
4918			} else {
4919				// Otherwise show an error message and register next method.
4920				if(message != null && message !== '') {
4921					NNLToastMessage.show(message);
4922				}
4923				registerNextMethod(liveData);
4924			}
4925		});
4926	}
4927
4928	function checkAndRegisterMethod(liveData, type, identifier) {
4929		// If method is not supported, resolve to skip it.
4930		if(!MethodUIFactory.getInstance().isSupported(type)) {
4931			return Promise.resolve();
4932		}
4933
4934		if(type === AdaptiveType.FIDO) {
4935			// For FIDO method, first do a check policy to find out if passkey or security key are supported. 
4936			return getFidoMethodType()
4937			.then(function(signUpMethod) {
4938				return confirmRegistration(liveData, signUpMethod);
4939			})
4940			.then(registerFidoMethod);
4941		}
4942
4943		// For adaptive methods, first check whether the method is already registered or not.
4944		return isMethodRegistered(type, identifier)
4945		.then(function(isRegistered) {
4946			if(!isRegistered) {
4947				return registerMethod(liveData, type, identifier);
4948			}
4949		});
4950	}
4951
4952	function registerMethod(liveData, type, identifier) {
4953		const method = {
4954			type: type,
4955			name: AdaptiveType.names[type]
4956		}
4957
4958		const extras = NNLUtils.cloneObject(mSignUpOpts);
4959
4960		return Promise.resolve()
4961		.then(function() {
4962			// If identifier is not provided, then we are registering a Photo ID method,
4963			// which should be confirmed first.
4964			if(identifier == null) {
4965				return confirmRegistration(liveData, SignUpMethod.PHOTO_ID);
4966			}
4967
4968			// For OTP methods put provided identifier into extras.
4969			extras[AppSdk.EXTRA_KEY_OTP_IDENTIFIER] = identifier;
4970		})
4971		.then(function() {
4972			const job = mAdaptiveCore.startRegister(mSignUpSession.sessionData, extras);
4973			return job.processMethod([method], OperationType.REGISTRATION, extras);
4974		})
4975		.then(function(result) {
4976			updateSessionData(result.sessionData);
4977			return result;
4978		});
4979	}
4980
4981	let mRegisteredMethods = null;
4982	function isMethodRegistered(type, identifier) {
4983		return Promise.resolve()
4984		.then(function() {
4985			// Use the cached list if it exists.
4986			if(mRegisteredMethods != null) {
4987				return mRegisteredMethods;
4988			}
4989
4990			// Otherwise get list from the server.
4991			return mAdaptiveCore.getMethods(mSignUpSession.sessionData);
4992		})
4993		.then(function(methods) {
4994			mRegisteredMethods = methods;
4995			// Check if the specified method is in the list of registered methods.
4996			return mRegisteredMethods.some(function(method) {
4997				// If the identifier is provided, is should match too. Otherwise only type is checked.
4998				return method.type === type && (identifier == null || method.data != null && method.data.identifier === identifier);
4999			});
5000		})
5001		.catch(function(result) {
5002			if(result.outcome === Outcome.SERVER_USER_NOT_FOUND) {
5003				// If the user does not have any registration, return false.
5004				mRegisteredMethods = [];
5005				return false;
5006			}
5007
5008			// Report other errors.
5009			return Promise.reject(result);
5010		});
5011	}
5012
5013	function registerFidoMethod(signUpMethod) {
5014		const extras = NNLUtils.cloneObject(mSignUpOpts);
5015		// Delete the flag (required/optional) from extras to not use it as user display name.
5016		delete extras.userName;
5017
5018		// Create a filter extension based on the method type.
5019		extras.extensions = [];
5020		var extData = {
5021			authenticatorAttachment: signUpMethod === SignUpMethod.PASSKEY ? 'platform' : 'cross-platform'
5022		}
5023
5024		var filterExtension = {
5025			id: 'noknok.webauthnAuthenticatorFilter',
5026			data: JSON.stringify(extData),
5027			fail_if_unknown: false
5028		}
5029
5030		extras.extensions.push(filterExtension);
5031
5032		// Register the FIDO method.
5033		return mAppSdk.register(mSignUpSession.sessionData, extras)
5034		.then(function(result) {
5035			updateSessionData(result.sessionData);
5036			return result;
5037		});
5038	}
5039
5040	function getFidoMethodType() {
5041		return mAppSdk.checkRegPossible(mSignUpSession.sessionData, mSignUpOpts)
5042		.then(function(result) {
5043			// If FIDO is supported (promise is resolved) but the SDK did not report which methods are possible, try to register a passkey.
5044			if(result.additionalInfo == null || result.additionalInfo.authenticators == null || result.additionalInfo.authenticators.length === 0) {
5045				return SignUpMethod.PASSKEY;
5046			}
5047
5048			let passkeyMethod = null;
5049			// Wrap platform authenticator in FidoMethod to get the "isRegPossible" flag value.
5050			result.additionalInfo.authenticators.forEach(function(availableAuthenticator) {
5051				if(availableAuthenticator.authenticatorAttachment === 'platform') {
5052					passkeyMethod = new FidoMethod(availableAuthenticator);
5053				}
5054			});
5055
5056			if(passkeyMethod != null) {
5057				if(passkeyMethod.isRegPossible) {
5058					// Passkey is supported and is not registered yet.
5059					return SignUpMethod.PASSKEY;
5060				}
5061
5062				// Passkey is already registered, return CANCELED to skip it.
5063				return Promise.reject({outcome: Outcome.CANCELED});
5064			}
5065
5066			// Passkey is not supported.
5067			return SignUpMethod.SECURITY_KEY;
5068		})
5069		.catch(function(result) {
5070			// If FIDO is not supported, return CANCELED to skip the registration.
5071			if(result.outcome === Outcome.NO_MATCH) {
5072				return Promise.reject({outcome: Outcome.CANCELED});
5073			}
5074
5075			// Report other errors.
5076			return Promise.reject(result);
5077		});
5078	}
5079
5080	function updateSessionData(newSessionData) {
5081		if(newSessionData != null) {
5082			for(let prop in newSessionData) {
5083				mSignUpSession.sessionData[prop] = newSessionData[prop];
5084			}
5085		}
5086	}
5087
5088	Object.freeze(this);
5089}
5090
5091/**
5092 * This interface defines a method for providing the current time in milliseconds.
5093 * @interface ITimeProvider
5094 */
5095
5096/**
5097 * This method implementation should return the current time in milliseconds.
5098 * @method ITimeProvider#getCurrentTime
5099 * @returns {Promise} A Promise that resolves with the value if the current time in milliseconds.
5100 */
5101
5102/**
5103 * The default implementation of {@link ITimeProvider}.
5104 * @class
5105 * @implements ITimeProvider
5106 */
5107const DefaultTimeProvider = function() {
5108	/**
5109	 * Returns the current time by calling JavaScript `new Date().getTime()` API.
5110	 * @returns {Promise} A Promise that resolves with the current time in milliseconds since midnight, January 1, 1970 UTC.
5111	 */
5112	this.getCurrentTime = function() {
5113		return Promise.resolve(new Date().getTime());
5114	}
5115
5116	Object.freeze(this);
5117}
5118
5119/**
5120 * @class
5121 * @classdesc A singleton class to store and return {@link ITimeProvider} implementation.
5122 * @hideconstructor
5123 */
5124const TimeProviderManager = new (function() {
5125	let mTimeProvider = null;
5126
5127	/**
5128	* Sets the {@link ITimeProvider} instance.
5129	* @param {ITimeProvider} timeProvider The new ITimeProvider instance.
5130	*/
5131	this.setInstance = function(timeProvider) {
5132		mTimeProvider = timeProvider;
5133	}
5134
5135	/**
5136	 * Returns {@link ITimeProvider} instance.
5137	 * If no instance was set using the {@link TimeProviderManager.setInstance|setInstance} method, creates and returns {@link DefaultTimeProvider} instance.
5138	 * @returns {ITimeProvider} The current ITimeProvider instance.
5139	 */
5140	this.getInstance = function() {
5141		if(mTimeProvider == null) {
5142			mTimeProvider = new DefaultTimeProvider();
5143		}
5144
5145		return mTimeProvider;
5146	}
5147
5148	Object.freeze(this);
5149})();
5150

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.