fune/toolkit/components/formautofill/FormAutofillChild.sys.mjs
Dimi a6fc3d1c7d Bug 1893623 - P1. Trigger autofill from the parent process r=credential-management-reviewers,sgalich
Currently, when users autocomplete a field for an address, credit card, or login, Firefox also "autofills"
the relevant fields. Here is a quick summary of how we currently manage this process:

1. Users click on an input field, the autocomplete popup is displayed, and Firefox searches for options
   so users can choose which value to autocomplete.
2. AutoCompleteChild searches for the value to autocomplete based on the type of the input field, along
   with the entire profile. For example, when we autocomplete a cc-number field, we also send cc-name, cc-exp, etc., to the child process.
3. AutoCompleteController autocompletes the focused input.
4. AutoCompleteController notifies the corresponding module, which then autofills the remaining fields.

Currently, step 4 is triggered directly in the child process. This patch moves the logic of step 4 from the
child process to the parent process. This change is a prerequisite for supporting autofill across frames and
will also enable us not to send the entire profile in step 2.

Differential Revision: https://phabricator.services.mozilla.com/D208752
2024-04-29 20:35:04 +00:00

864 lines
25 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
AddressResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
AutoCompleteChild: "resource://gre/actors/AutoCompleteChild.sys.mjs",
AutofillTelemetry: "resource://gre/modules/shared/AutofillTelemetry.sys.mjs",
CreditCardResult: "resource://autofill/ProfileAutoCompleteResult.sys.mjs",
GenericAutocompleteItem: "resource://gre/modules/FillHelpers.sys.mjs",
InsecurePasswordUtils: "resource://gre/modules/InsecurePasswordUtils.sys.mjs",
FormAutofill: "resource://autofill/FormAutofill.sys.mjs",
FormAutofillContent: "resource://autofill/FormAutofillContent.sys.mjs",
FormAutofillUtils: "resource://gre/modules/shared/FormAutofillUtils.sys.mjs",
FormScenarios: "resource://gre/modules/FormScenarios.sys.mjs",
FormStateManager: "resource://gre/modules/shared/FormStateManager.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ProfileAutocomplete:
"resource://autofill/AutofillProfileAutoComplete.sys.mjs",
setTimeout: "resource://gre/modules/Timer.sys.mjs",
FORM_SUBMISSION_REASON: "resource://gre/actors/FormHandlerChild.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"DELEGATE_AUTOCOMPLETE",
"toolkit.autocomplete.delegate",
false
);
const formFillController = Cc[
"@mozilla.org/satchel/form-fill-controller;1"
].getService(Ci.nsIFormFillController);
const observer = {
QueryInterface: ChromeUtils.generateQI([
"nsIWebProgressListener",
"nsISupportsWeakReference",
]),
onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
// Only handle pushState/replaceState here.
if (
!(aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) ||
!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_PUSHSTATE)
) {
return;
}
const window = aWebProgress.DOMWindow;
const formAutofillChild = window.windowGlobalChild.getActor("FormAutofill");
formAutofillChild.onPageNavigation();
},
onStateChange(aWebProgress, aRequest, aStateFlags, _aStatus) {
if (
// if restoring a previously-rendered presentation (bfcache)
aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING &&
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP
) {
return;
}
if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_START)) {
return;
}
// We only care about when a page triggered a load, not the user. For example:
// clicking refresh/back/forward, typing a URL and hitting enter, and loading a bookmark aren't
// likely to be when a user wants to save a formautofill data.
let channel = aRequest.QueryInterface(Ci.nsIChannel);
let triggeringPrincipal = channel.loadInfo.triggeringPrincipal;
if (
triggeringPrincipal.isNullPrincipal ||
triggeringPrincipal.equals(
Services.scriptSecurityManager.getSystemPrincipal()
)
) {
return;
}
// Don't handle history navigation, reload, or pushState not triggered via chrome UI.
// e.g. history.go(-1), location.reload(), history.replaceState()
if (!(aWebProgress.loadType & Ci.nsIDocShell.LOAD_CMD_NORMAL)) {
return;
}
const window = aWebProgress.DOMWindow;
const formAutofillChild = window.windowGlobalChild.getActor("FormAutofill");
formAutofillChild.onPageNavigation();
},
};
/**
* Handles content's interactions for the frame.
*/
export class FormAutofillChild extends JSWindowActorChild {
// Flag indicating whether the form is waiting to be filled by Autofill.
#autofillPending = false;
constructor() {
super();
this.log = lazy.FormAutofill.defineLogGetter(this, "FormAutofillChild");
this.debug("init");
this._nextHandleElement = null;
this._hasDOMContentLoadedHandler = false;
this._hasPendingTask = false;
/**
* @type {FormAutofillFieldDetailsManager} handling state management of current forms and handlers.
*/
this._fieldDetailsManager = new lazy.FormStateManager(
this.formSubmitted.bind(this),
this.formAutofilled.bind(this)
);
lazy.AutoCompleteChild.addPopupStateListener(this);
}
didDestroy() {
this._fieldDetailsManager.didDestroy();
lazy.AutoCompleteChild.removePopupStateListener(this);
}
popupStateChanged(messageName, _data, _target) {
if (!lazy.FormAutofill.isAutofillEnabled) {
return;
}
switch (messageName) {
case "AutoComplete:PopupClosed": {
this.onPopupClosed();
break;
}
case "AutoComplete:PopupOpened": {
this.onPopupOpened();
break;
}
}
}
/**
* Identifies and marks each autofill field
*/
identifyAutofillFields() {
if (this._hasPendingTask) {
return;
}
this._hasPendingTask = true;
lazy.setTimeout(() => {
const element = this._nextHandleElement;
this.debug(
`identifyAutofillFields: ${element.ownerDocument.location?.hostname}`
);
if (
lazy.DELEGATE_AUTOCOMPLETE ||
!lazy.FormAutofillContent.savedFieldNames
) {
this.debug("identifyAutofillFields: savedFieldNames are not known yet");
// Init can be asynchronous because we don't need anything from the parent
// at this point.
this.sendAsyncMessage("FormAutofill:InitStorage");
}
const validDetails =
this._fieldDetailsManager.identifyAutofillFields(element);
validDetails?.forEach(detail =>
this._markAsAutofillField(detail.element)
);
if (validDetails.length) {
if (lazy.FormAutofill.captureOnFormRemoval) {
this.registerDOMDocFetchSuccessEventListener();
}
if (lazy.FormAutofill.captureOnPageNavigation) {
this.registerProgressListener();
}
}
this._hasPendingTask = false;
this._nextHandleElement = null;
// This is for testing purpose only which sends a notification to indicate that the
// form has been identified, and ready to open popup.
this.sendAsyncMessage("FormAutofill:FieldsIdentified");
this.updateActiveInput();
});
}
/**
* Gets the highest accessible docShell
*
* @returns {DocShell} highest accessible docShell
*/
getHighestDocShell() {
const window = this.document.defaultView;
let docShell;
for (
let browsingContext = BrowsingContext.getFromWindow(window);
browsingContext?.docShell;
browsingContext = browsingContext.parent
) {
docShell = browsingContext.docShell;
}
return docShell
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebProgress);
}
/**
* After being notified of a page navigation, we check whether
* the navigated window is the active window or one of its parents
* (active window = activeHandler.window)
*
* @returns {boolean} whether the navigation affects the active window
*/
isActiveWindowNavigation() {
const activeWindow = lazy.FormAutofillContent.activeHandler?.window;
const navigatedWindow = this.document.defaultView;
if (!activeWindow || !navigatedWindow) {
return false;
}
const navigatedBrowsingContext =
BrowsingContext.getFromWindow(navigatedWindow);
for (
let browsingContext = BrowsingContext.getFromWindow(activeWindow);
browsingContext?.docShell;
browsingContext = browsingContext.parent
) {
if (navigatedBrowsingContext === browsingContext) {
return true;
}
}
return false;
}
/**
* Infer a form submission after document is navigated
*/
onPageNavigation() {
if (!this.isActiveWindowNavigation()) {
return;
}
// TODO: We should not use FormAutofillContent and let the
// parent decides which child to notify
const activeChild = lazy.FormAutofillContent.activeAutofillChild;
const activeElement = activeChild.activeFieldDetail?.elementWeakRef.deref();
if (!activeElement) {
return;
}
const formSubmissionReason = lazy.FORM_SUBMISSION_REASON.PAGE_NAVIGATION;
// We only capture the form of the active field right now,
// this means that we might miss some fields (see bug 1871356)
activeChild.formSubmitted(activeElement, formSubmissionReason);
}
/**
* After a form submission we unregister the
* nsIWebProgressListener from the top level doc shell
*/
unregisterProgressListener() {
const docShell = this.getHighestDocShell();
try {
docShell.removeProgressListener(observer);
} catch (ex) {
// Ignore NS_ERROR_FAILURE if the progress listener was not registered
}
}
/**
* After a focusin event and after we identified formautofill fields,
* we set up a nsIWebProgressListener that notifies of a request state
* change or window location change in the top level doc shell
*/
registerProgressListener() {
const docShell = this.getHighestDocShell();
const flags =
Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT |
Ci.nsIWebProgress.NOTIFY_LOCATION;
try {
docShell.addProgressListener(observer, flags);
} catch (ex) {
// Ignore NS_ERROR_FAILURE if the progress listener was already added
}
}
/**
* After a focusin event and after we identify formautofill fields,
* we set up an event listener for the DOMDocFetchSuccess event
*/
registerDOMDocFetchSuccessEventListener() {
this.document.setNotifyFetchSuccess(true);
// Is removed after a DOMDocFetchSuccess event (bug 1864855)
/* eslint-disable mozilla/balanced-listeners */
this.docShell.chromeEventHandler.addEventListener(
"DOMDocFetchSuccess",
this,
true
);
}
/**
* After a DOMDocFetchSuccess event, we register an event listener for the DOMFormRemoved event
*/
registerDOMFormRemovedEventListener() {
this.document.setNotifyFormOrPasswordRemoved(true);
// Is removed after a DOMFormRemoved event (bug 1864855)
/* eslint-disable mozilla/balanced-listeners */
this.docShell.chromeEventHandler.addEventListener(
"DOMFormRemoved",
this,
true
);
}
/**
* After a DOMDocFetchSuccess event we remove the DOMDocFetchSuccess event listener
*/
unregisterDOMDocFetchSuccessEventListener() {
this.document.setNotifyFetchSuccess(false);
this.docShell.chromeEventHandler.removeEventListener(
"DOMDocFetchSuccess",
this
);
}
/**
* After a DOMFormRemoved event we remove the DOMFormRemoved event listener
*/
unregisterDOMFormRemovedEventListener() {
this.document.setNotifyFormOrPasswordRemoved(false);
this.docShell.chromeEventHandler.removeEventListener(
"DOMFormRemoved",
this
);
}
shouldIgnoreFormAutofillEvent(event) {
let nodePrincipal = event.target.nodePrincipal;
return nodePrincipal.isSystemPrincipal || nodePrincipal.schemeIs("about");
}
handleEvent(evt) {
if (!evt.isTrusted) {
return;
}
if (this.shouldIgnoreFormAutofillEvent(evt)) {
return;
}
if (!this.windowContext) {
// !this.windowContext must not be null, because we need the
// windowContext and/or docShell to (un)register form submission listeners
return;
}
switch (evt.type) {
case "focusin": {
if (lazy.FormAutofill.isAutofillEnabled) {
this.onFocusIn(evt);
}
break;
}
case "DOMFormRemoved": {
this.onDOMFormRemoved(evt);
break;
}
case "DOMDocFetchSuccess": {
this.onDOMDocFetchSuccess();
break;
}
case "form-submission-detected": {
if (lazy.FormAutofill.isAutofillEnabled) {
this.onFormSubmission(evt);
}
break;
}
default: {
throw new Error("Unexpected event type");
}
}
}
onFocusIn(evt) {
this.updateActiveInput();
const element = evt.target;
if (!lazy.FormAutofillUtils.isCreditCardOrAddressFieldType(element)) {
return;
}
this._nextHandleElement = element;
const doc = element.ownerDocument;
if (doc.readyState === "loading") {
// For auto-focused input, we might receive focus event before document becomes ready.
// When this happens, run field identification after receiving `DOMContentLoaded` event
if (!this._hasDOMContentLoadedHandler) {
this._hasDOMContentLoadedHandler = true;
doc.addEventListener(
"DOMContentLoaded",
() => this.identifyAutofillFields(),
{ once: true }
);
}
return;
}
this.identifyAutofillFields();
}
/**
* Handle form-submission-detected event (dispatched by FormHandlerChild)
*
* @param {CustomEvent} evt form-submission-detected event
*/
onFormSubmission(evt) {
const formElement = evt.detail.form;
const formSubmissionReason = evt.detail.reason;
this.formSubmitted(formElement, formSubmissionReason);
}
/**
* Handle the DOMFormRemoved event.
*
* Infers a form submission when the form is removed
* after a successful fetch or XHR request.
*
* @param {Event} evt DOMFormRemoved
*/
onDOMFormRemoved(evt) {
const formSubmissionReason =
lazy.FORM_SUBMISSION_REASON.FORM_REMOVAL_AFTER_FETCH;
this.formSubmitted(evt.target, formSubmissionReason);
}
/**
* Handle the DOMDocFetchSuccess event.
*
* Sets up an event listener for the DOMFormRemoved event
* and unregisters the event listener for DOMDocFetchSuccess event.
*/
onDOMDocFetchSuccess() {
this.registerDOMFormRemovedEventListener();
this.unregisterDOMDocFetchSuccessEventListener();
}
/**
* Unregister all listeners that notify of a form submission,
* because we just detected and acted on a form submission
*/
unregisterFormSubmissionListeners() {
this.unregisterDOMDocFetchSuccessEventListener();
this.unregisterDOMFormRemovedEventListener();
this.unregisterProgressListener();
}
async receiveMessage(message) {
if (!lazy.FormAutofill.isAutofillEnabled) {
return;
}
switch (message.name) {
case "FormAutofill:PreviewProfile": {
this.previewProfile(message.data);
break;
}
case "FormAutofill:ClearForm": {
this.clearForm();
break;
}
case "FormAutofill:FillForm": {
await this.autofillFields(message.data);
break;
}
}
}
get activeFieldDetail() {
return this._fieldDetailsManager.activeFieldDetail;
}
get activeFormDetails() {
return this._fieldDetailsManager.activeFormDetails;
}
get activeInput() {
return this._fieldDetailsManager.activeInput;
}
get activeHandler() {
return this._fieldDetailsManager.activeHandler;
}
get activeSection() {
return this._fieldDetailsManager.activeSection;
}
/**
* Handle a form submission and early return when:
* 1. In private browsing mode.
* 2. Could not map any autofill handler by form element.
* 3. Number of filled fields is less than autofill threshold
*
* @param {HTMLElement} formElement Root element which receives submit event.
* @param {string} formSubmissionReason Reason for invoking the form submission
* (see options for FORM_SUBMISSION_REASON in FormAutofillUtils))
* @param {Window} domWin Content window; passed for unit tests and when
* invoked by the FormAutofillSection
* @param {object} handler FormAutofillHander, if known by caller
*/
formSubmitted(
formElement,
formSubmissionReason,
domWin = formElement.ownerGlobal,
handler = undefined
) {
this.debug(`Handling form submission - infered by ${formSubmissionReason}`);
lazy.AutofillTelemetry.recordFormSubmissionHeuristicCount(
formSubmissionReason
);
if (!lazy.FormAutofill.isAutofillEnabled) {
this.debug("Form Autofill is disabled");
return;
}
// The `domWin` truthiness test is used by unit tests to bypass this check.
if (domWin && lazy.PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
this.debug("Ignoring submission in a private window");
return;
}
handler = handler || this._fieldDetailsManager._getFormHandler(formElement);
const records = this._fieldDetailsManager.getRecords(formElement, handler);
if (!records || !handler) {
this.debug("Form element could not map to an existing handler");
return;
}
// Unregister the form submission listeners after handling a form submission
this.debug("Unregistering form submission listeners");
this.unregisterFormSubmissionListeners();
[records.address, records.creditCard].forEach((rs, idx) => {
lazy.AutofillTelemetry.recordSubmittedSectionCount(
idx == 0
? lazy.AutofillTelemetry.ADDRESS
: lazy.AutofillTelemetry.CREDIT_CARD,
rs?.length
);
rs?.forEach(r => {
lazy.AutofillTelemetry.recordFormInteractionEvent(
"submitted",
r.section,
{
record: r,
form: handler.form,
}
);
delete r.section;
});
});
this.sendAsyncMessage("FormAutofill:OnFormSubmit", records);
}
formAutofilled() {
lazy.FormAutofillContent.showPopup();
}
/**
* All active items should be updated according the active element of
* `formFillController.focusedInput`. All of them including element,
* handler, section, and field detail, can be retrieved by their own getters.
*
* @param {HTMLElement|null} element The active item should be updated based
* on this or `formFillController.focusedInput` will be taken.
*/
updateActiveInput(element) {
element = element || formFillController.focusedInput;
if (!element) {
this.debug("updateActiveElement: no element selected");
return;
}
lazy.FormAutofillContent.updateActiveAutofillChild(this);
this._fieldDetailsManager.updateActiveInput(element);
this.debug("updateActiveElement: checking for popup-on-focus");
// We know this element just received focus. If it's a credit card field,
// open its popup.
if (this.#autofillPending) {
this.debug("updateActiveElement: skipping check; autofill is imminent");
} else if (element.value?.length !== 0) {
this.debug(
`updateActiveElement: Not opening popup because field is not empty.`
);
} else {
this.debug(
"updateActiveElement: checking if empty field is cc-*: ",
this.activeFieldDetail?.fieldName
);
if (
this.activeFieldDetail?.fieldName?.startsWith("cc-") ||
AppConstants.platform === "android"
) {
lazy.FormAutofillContent.showPopup();
}
}
}
clearForm() {
let focusedInput =
this.activeInput ||
lazy.ProfileAutocomplete._lastAutoCompleteFocusedInput;
if (!focusedInput) {
return;
}
this.activeSection.clearPopulatedForm();
let fieldName = this.activeFieldDetail?.fieldName;
if (lazy.FormAutofillUtils.isCreditCardField(fieldName)) {
lazy.AutofillTelemetry.recordFormInteractionEvent(
"cleared",
this.activeSection,
{ fieldName }
);
}
}
get lastProfileAutoCompleteResult() {
return this.manager.getActor("AutoComplete")?.lastProfileAutoCompleteResult;
}
get lastProfileAutoCompleteFocusedInput() {
return this.manager.getActor("AutoComplete")
?.lastProfileAutoCompleteFocusedInput;
}
previewProfile(profile) {
if (profile && this.activeSection) {
const adaptedProfile = this.activeSection.getAdaptedProfiles([
profile,
])[0];
this.activeSection.previewFormFields(adaptedProfile);
} else {
this.activeSection.clearPreviewedFormFields();
}
}
async autofillFields(profile) {
this.#autofillPending = true;
Services.obs.notifyObservers(null, "autofill-fill-starting");
try {
Services.obs.notifyObservers(null, "autofill-fill-starting");
await this.activeHandler.autofillFormFields(profile);
Services.obs.notifyObservers(null, "autofill-fill-complete");
} finally {
this.#autofillPending = false;
}
}
onPopupClosed() {
this.debug("Popup has closed.");
this.activeSection?.clearPreviewedFormFields();
}
onPopupOpened() {
this.debug(
"Popup has opened, automatic =",
formFillController.passwordPopupAutomaticallyOpened
);
let fieldName = this.activeFieldDetail?.fieldName;
if (fieldName && this.activeSection) {
lazy.AutofillTelemetry.recordFormInteractionEvent(
"popup_shown",
this.activeSection,
{ fieldName }
);
}
}
_markAsAutofillField(field) {
// Since Form Autofill popup is only for input element, any non-Input
// element should be excluded here.
if (!HTMLInputElement.isInstance(field)) {
return;
}
this.manager
.getActor("AutoComplete")
?.markAsAutoCompletableField(field, this);
}
get actorName() {
return "FormAutofill";
}
/**
* Get the search options when searching for autocomplete entries in the parent
*
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
* @returns {object} the search options for the input
*/
getAutoCompleteSearchOption(input) {
const fieldDetail = this._fieldDetailsManager
._getFormHandler(input)
?.getFieldDetailByElement(input);
const scenarioName = lazy.FormScenarios.detect({ input }).signUpForm
? "SignUpFormScenario"
: "";
return { fieldName: fieldDetail?.fieldName, scenarioName };
}
/**
* Ask the provider whether it might have autocomplete entry to show
* for the given input.
*
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
* @returns {boolean} true if we shold search for autocomplete entries
*/
shouldSearchForAutoComplete(input) {
const fieldDetail = this._fieldDetailsManager
._getFormHandler(input)
?.getFieldDetailByElement(input);
if (!fieldDetail) {
return false;
}
const fieldName = fieldDetail.fieldName;
const isAddressField = lazy.FormAutofillUtils.isAddressField(fieldName);
const searchPermitted = isAddressField
? lazy.FormAutofill.isAutofillAddressesEnabled
: lazy.FormAutofill.isAutofillCreditCardsEnabled;
// If the specified autofill feature is pref off, do not search
if (!searchPermitted) {
return false;
}
// No profile can fill the currently-focused input.
if (!lazy.FormAutofillContent.savedFieldNames.has(fieldName)) {
return false;
}
// The current form has already been populated and the field is not
// an empty credit card field.
const isCreditCardField =
lazy.FormAutofillUtils.isCreditCardField(fieldName);
const isInputAutofilled =
this.activeHandler.getFilledStateByElement(input) ==
lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
const filledRecordGUID = this.activeSection.filledRecordGUID;
if (
!isInputAutofilled &&
filledRecordGUID &&
!(isCreditCardField && this.activeInput.value === "")
) {
return false;
}
// (address only) less than 3 inputs are covered by all saved fields in the storage.
if (
isAddressField &&
this.activeSection.allFieldNames.filter(field =>
lazy.FormAutofillContent.savedFieldNames.has(field)
).length < lazy.FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD
) {
return false;
}
return true;
}
/**
* Convert the search result to autocomplete results
*
* @param {string} searchString - The string to search for
* @param {HTMLInputElement} input - The input element to search for autocompelte entries
* @param {Array<object>} records - autocomplete records
* @returns {AutocompleteResult}
*/
searchResultToAutoCompleteResult(searchString, input, records) {
if (!records) {
return null;
}
const entries = records.records;
const externalEntries = records.externalEntries;
const fieldDetail = this._fieldDetailsManager
._getFormHandler(input)
?.getFieldDetailByElement(input);
if (!fieldDetail) {
return null;
}
const adaptedRecords = this.activeSection.getAdaptedProfiles(entries);
const isSecure = lazy.InsecurePasswordUtils.isFormSecure(
this.activeHandler.form
);
const isInputAutofilled =
this.activeHandler.getFilledStateByElement(input) ==
lazy.FormAutofillUtils.FIELD_STATES.AUTO_FILLED;
const allFieldNames = this.activeSection.allFieldNames;
const AutocompleteResult = lazy.FormAutofillUtils.isAddressField(
fieldDetail.fieldName
)
? lazy.AddressResult
: lazy.CreditCardResult;
const acResult = new AutocompleteResult(
searchString,
fieldDetail.fieldName,
allFieldNames,
adaptedRecords,
{ isSecure, isInputAutofilled }
);
acResult.externalEntries.push(
...externalEntries.map(
entry =>
new lazy.GenericAutocompleteItem(
entry.image,
entry.title,
entry.subtitle,
entry.fillMessageName,
entry.fillMessageData
)
)
);
return acResult;
}
}