From 89b52a8cf0c7b6320e630ee4bfe7103ac575aa6a Mon Sep 17 00:00:00 2001 From: dbajpeyi Date: Fri, 6 Dec 2024 11:45:43 +0100 Subject: [PATCH] refactor: simply value storage --- dist/autofill-debug.js | 50 +++++++++---------- dist/autofill.js | 50 +++++++++---------- src/DeviceInterface/InterfacePrototype.js | 12 ++--- src/Form/Form.js | 17 +------ src/Form/Form.test.js | 6 +-- src/Form/formatters.js | 22 ++++---- src/autofill-utils.js | 10 ++++ .../Resources/assets/autofill-debug.js | 50 +++++++++---------- swift-package/Resources/assets/autofill.js | 50 +++++++++---------- 9 files changed, 125 insertions(+), 142 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 0b3c6770a..c0428fecc 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -9476,7 +9476,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -9484,10 +9485,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -10881,18 +10879,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -11936,6 +11925,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -12112,7 +12102,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -12159,12 +12149,11 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -12220,7 +12209,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ +},{"../autofill-utils.js":64,"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17157,7 +17146,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -17825,6 +17816,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){ "use strict"; diff --git a/dist/autofill.js b/dist/autofill.js index 32824bd96..aafde5683 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -5113,7 +5113,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -5121,10 +5122,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -6518,18 +6516,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -7573,6 +7562,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -7749,7 +7739,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -7796,12 +7786,11 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -7857,7 +7846,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ +},{"../autofill-utils.js":54,"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12794,7 +12783,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -13462,6 +13453,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){ "use strict"; diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index 3fb39e621..b81022bb8 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -799,8 +799,9 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = appendGeneratedKey(values, { password: this.passwordGenerator.password, @@ -808,14 +809,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = - Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = - Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && - form.inputs.credentials.size === 0 && - form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } diff --git a/src/Form/Form.js b/src/Form/Form.js index 49ecc5b9e..6c76328e2 100644 --- a/src/Form/Form.js +++ b/src/Form/Form.js @@ -895,24 +895,11 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = - formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - - const hasOnlyOneCredentialOrEmail = - Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || - (hasOnlyEmail && hasNoCredentialsData); const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every( (subtype) => formValues[dataType][subtype] === data[subtype], ); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; diff --git a/src/Form/Form.test.js b/src/Form/Form.test.js index 4f6492bee..bf69ff3e9 100644 --- a/src/Form/Form.test.js +++ b/src/Form/Form.test.js @@ -276,11 +276,7 @@ describe('Test the form class reading values correctly', () => { `, expHasValues: true, expValues: { - identities: { - emailAddress: 'peppapig@email.com', - firstName: 'Peppa', - lastName: 'Pig', - }, + identities: undefined, creditCards: { cardName: 'Peppa Pig', cardSecurityCode: '123', diff --git a/src/Form/formatters.js b/src/Form/formatters.js index a34d9e383..15018c688 100644 --- a/src/Form/formatters.js +++ b/src/Form/formatters.js @@ -1,5 +1,6 @@ import { matchInPlaceholderAndLabels, checkPlaceholderAndLabels } from './matching.js'; import { COUNTRY_CODES_TO_NAMES, COUNTRY_NAMES_TO_CODES } from './countryNames.js'; +import { hasUsernameLikeIdentity } from '../autofill-utils.js'; // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; @@ -163,13 +164,8 @@ const getMMAndYYYYFromString = (expiration) => { * @param {InternalDataStorageObject} credentials * @return {boolean} */ -const shouldStoreIdentities = ({ identities }) => { - return ( - Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || - Boolean(identities.emailAddress) || - Boolean(identities.phone) - ); -}; +const shouldStoreIdentities = ({ identities }) => + Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); /** * @param {InternalDataStorageObject} credentials @@ -207,12 +203,12 @@ const prepareFormValuesForStorage = (formValues) => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && hasUsernameLikeIdentity(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 96eba5b48..0475d28f0 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -624,6 +624,15 @@ function queryElementsWithShadow(element, selector, forceScanShadowTree = false) return [...elements]; } +/** + * + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + export { notifyWebApp, sendAndWaitForAnswer, @@ -659,4 +668,5 @@ export { findElementsInShadowTree, queryElementsWithShadow, getFormControlElements, + hasUsernameLikeIdentity, }; diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 0b3c6770a..c0428fecc 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -9476,7 +9476,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -9484,10 +9485,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -10881,18 +10879,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -11936,6 +11925,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -12112,7 +12102,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -12159,12 +12149,11 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -12220,7 +12209,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ +},{"../autofill-utils.js":64,"./countryNames.js":36,"./matching.js":44}],38:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -17157,7 +17146,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -17825,6 +17816,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":44,"./constants.js":67,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],65:[function(require,module,exports){ "use strict"; diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index 32824bd96..aafde5683 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -5113,7 +5113,8 @@ class InterfacePrototype { postSubmit(values, form) { if (!form.form) return; if (!form.hasValues(values)) return; - const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated]; + const isUsernameOnly = Object.keys(values?.credentials ?? {}).length === 1 && values?.credentials?.username; + const checks = [form.shouldPromptToStoreData && !form.submitHandlerExecuted, this.passwordGenerator.generated, isUsernameOnly]; if (checks.some(Boolean)) { const formData = (0, _Credentials.appendGeneratedKey)(values, { password: this.passwordGenerator.password, @@ -5121,10 +5122,7 @@ class InterfacePrototype { }); // If credentials has only username field, and no password field, then trigger is a partialSave - const isUsernameOnly = Boolean(formData.credentials?.username) && !formData.credentials?.password && form.inputs.credentials.size === 1; - // Is an email or phone number present in the form, but no other credentials - const isEmailOrPhoneOnly = Boolean(formData.identities?.emailAddress) !== Boolean(formData.identities?.phone) && form.inputs.credentials.size === 0 && form.inputs.identities.size === 1; - const trigger = isUsernameOnly || isEmailOrPhoneOnly ? 'partialSave' : 'formSubmission'; + const trigger = isUsernameOnly ? 'partialSave' : 'formSubmission'; this.storeFormData(formData, trigger); } } @@ -6518,18 +6516,9 @@ class Form { // After autofill we check if form values match the data provided… const formValues = this.getValuesReadyForStorage(); - const hasNoCredentialsData = !formValues.credentials?.username && !formValues.credentials?.password; - const hasOnlyEmail = formValues.identities && Object.keys(formValues.identities ?? {}).length === 1 && formValues.identities?.emailAddress; - const hasOnlyOneCredentialOrEmail = Boolean(formValues.credentials?.username) !== Boolean(formValues.credentials?.password) || hasOnlyEmail && hasNoCredentialsData; const areAllFormValuesKnown = Object.keys(formValues[dataType] || {}).every(subtype => formValues[dataType][subtype] === data[subtype]); - - // If we only have a single credential field - then we want to prompt a partial save with username, - // So that in multi step forms (like reset-password), we can identify which username was picked, or complete a password save. - if (hasOnlyOneCredentialOrEmail) { - this.shouldPromptToStoreData = true; - this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; - } else if (areAllFormValuesKnown) { - // …if it's a normal form with more than one field and if we know all the values do not prompt to store data + if (areAllFormValuesKnown) { + // …if we know all the values do not prompt to store this.shouldPromptToStoreData = false; // reset this to its initial value this.shouldAutoSubmit = this.device.globalConfig.isMobileApp; @@ -7573,6 +7562,7 @@ Object.defineProperty(exports, "__esModule", { exports.prepareFormValuesForStorage = exports.inferCountryCodeFromElement = exports.getUnifiedExpiryDate = exports.getMMAndYYYYFromString = exports.getCountryName = exports.getCountryDisplayName = exports.formatPhoneNumber = exports.formatFullName = exports.formatCCYear = void 0; var _matching = require("./matching.js"); var _countryNames = require("./countryNames.js"); +var _autofillUtils = require("../autofill-utils.js"); // Matches strings like mm/yy, mm-yyyy, mm-aa, 12 / 2024 const DATE_SEPARATOR_REGEX = /\b((.)\2{1,3}|\d+)(?\s?[/\s.\-_—–]\s?)((.)\5{1,3}|\d+)\b/i; // Matches 4 non-digit repeated characters (YYYY or AAAA) or 4 digits (2022) @@ -7749,7 +7739,7 @@ const shouldStoreIdentities = _ref3 => { let { identities } = _ref3; - return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity) || Boolean(identities.emailAddress) || Boolean(identities.phone); + return Boolean((identities.firstName || identities.fullName) && identities.addressStreet && identities.addressCity); }; /** @@ -7796,12 +7786,11 @@ const prepareFormValuesForStorage = formValues => { } /** Fixes for credentials */ - if (credentials.username || credentials.password) { - // If we don't have a username to match a password, let's see if the email is available - if (credentials.password && !credentials.username && identities.emailAddress) { - credentials.username = identities.emailAddress; - } - } else { + if (!credentials.username && (0, _autofillUtils.hasUsernameLikeIdentity)(identities)) { + // @ts-ignore - We know that username is not a useful value here + credentials.username = identities.emailAddress ?? identities.phone; + } + if (Object.keys(credentials ?? {}).length === 0) { credentials = undefined; } @@ -7857,7 +7846,7 @@ const prepareFormValuesForStorage = formValues => { }; exports.prepareFormValuesForStorage = prepareFormValuesForStorage; -},{"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ +},{"../autofill-utils.js":54,"./countryNames.js":26,"./matching.js":34}],28:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -12794,7 +12783,9 @@ exports.formatDuckAddress = void 0; exports.getActiveElement = getActiveElement; exports.getDaxBoundingBox = void 0; exports.getFormControlElements = getFormControlElements; -exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getTextShallow = void 0; +exports.getTextShallow = void 0; +exports.hasUsernameLikeIdentity = hasUsernameLikeIdentity; +exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = void 0; exports.isFormLikelyToBeUsedAsPageWrapper = isFormLikelyToBeUsedAsPageWrapper; exports.isLikelyASubmitButton = exports.isIncontextSignupEnabledFromProcessedConfig = void 0; exports.isLocalNetwork = isLocalNetwork; @@ -13462,6 +13453,15 @@ function queryElementsWithShadow(element, selector) { return [...elements]; } +/** + * + * @param {InternalIdentityObject} identities + * @returns {boolean} + */ +function hasUsernameLikeIdentity(identities) { + return Object.keys(identities ?? {}).length === 1 && Boolean(identities?.emailAddress || identities.phone); +} + },{"./Form/matching.js":34,"./constants.js":57,"@duckduckgo/content-scope-scripts/src/apple-utils":1}],55:[function(require,module,exports){ "use strict";