diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js
index 7051c97d7..830416894 100644
--- a/dist/autofill-debug.js
+++ b/dist/autofill-debug.js
@@ -6578,7 +6578,9 @@ class AndroidInterface extends _InterfacePrototype.default {
createUIController() {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
/**
* @deprecated use `this.settings.availableInputTypes.email` in the future
@@ -6747,7 +6749,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
var _this$globalConfig$us, _this$globalConfig$us2;
if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
if (!this.globalConfig.supportsTopFrame) {
@@ -7090,32 +7094,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
poll();
});
}
- /**
- * on macOS we try to detect if a click occurred within a form
- * @param {PointerEvent} event
- */
-
-
- _onPointerDown(event) {
- if (this.settings.featureToggles.credentials_saving) {
- this._detectFormSubmission(event);
- }
- }
- /**
- * @param {PointerEvent} event
- */
-
-
- _detectFormSubmission(event) {
- const matchingForm = [...this.scanner.forms.values()].find(form => {
- const btns = [...form.submitButtons]; // @ts-ignore
-
- if (btns.includes(event.target)) return true; // @ts-ignore
-
- if (btns.find(btn => btn.contains(event.target))) return true;
- });
- matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
- }
}
@@ -7502,6 +7480,8 @@ var _deviceApi = require("../../packages/device-api");
var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls");
+var _selectorsCss = require("../Form/selectors-css");
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
@@ -8230,6 +8210,55 @@ class InterfacePrototype {
this.storeFormData(withAutoGeneratedFlag);
}
}
+ /**
+ * on macOS we try to detect if a click occurred within a form
+ * @param {PointerEvent} event
+ */
+
+
+ _onPointerDown(event) {
+ if (this.settings.featureToggles.credentials_saving) {
+ this._detectFormSubmission(event);
+ }
+ }
+ /**
+ * @param {PointerEvent} event
+ */
+
+
+ _detectFormSubmission(event) {
+ const matchingForm = [...this.scanner.forms.values()].find(form => {
+ const btns = [...form.submitButtons]; // @ts-ignore
+
+ if (btns.includes(event.target)) return true; // @ts-ignore
+
+ if (btns.find(btn => btn.contains(event.target))) return true;
+ });
+ matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
+
+ if (!matchingForm) {
+ var _event$target;
+
+ // check if the click happened on a button
+ const button =
+ /** @type HTMLElement */
+ (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR);
+ if (!button) return;
+ const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent);
+ const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text);
+
+ if (hasRelevantText && text.length < 25) {
+ // check if there's a form with values
+ const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues());
+
+ if (filledForm && (0, _autofillUtils.buttonMatchesFormType)(
+ /** @type HTMLElement */
+ button, filledForm)) {
+ filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
+ }
+ }
+ }
+ }
/**
* This serves as a single place to create a default instance
* of InterfacePrototype that can be useful in testing scenarios
@@ -8252,7 +8281,7 @@ class InterfacePrototype {
var _default = InterfacePrototype;
exports.default = _default;
-},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){
+},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../Form/selectors-css":35,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -8347,13 +8376,16 @@ class Form {
this.categorizeInputs();
}
/**
- * Checks if the form element contains the activeElement
+ * Checks if the form element contains the activeElement or the event target
* @return {boolean}
+ * @param {KeyboardEvent | null} [e]
*/
- hasFocus() {
- return this.form.contains(document.activeElement);
+ hasFocus(e) {
+ return this.form.contains(document.activeElement) || this.form.contains(
+ /** @type HTMLElement */
+ e === null || e === void 0 ? void 0 : e.target);
}
/**
* Checks that the form element doesn't contain an invalid field
@@ -8513,16 +8545,7 @@ class Form {
const allButtons =
/** @type {HTMLElement[]} */
[...this.form.querySelectorAll(selector)];
- return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form
- .filter(button => {
- if (this.isLogin) {
- return !/sign.?up/i.test(button.textContent || '');
- } else if (this.isSignup) {
- return !/(log|sign).?([io])n/i.test(button.textContent || '');
- } else {
- return true;
- }
- });
+ return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this));
}
/**
* Executes a function on input elements. Can be limited to certain element types
@@ -10260,15 +10283,15 @@ const listenForGlobalFormSubmission = forms => {
(_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler()
);
}, true);
- window.addEventListener('keypress', e => {
+ window.addEventListener('keydown', e => {
if (e.key === 'Enter') {
- const focusedForm = [...forms.values()].find(form => form.hasFocus());
+ const focusedForm = [...forms.values()].find(form => form.hasFocus(e));
focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler();
}
});
const observer = new PerformanceObserver(list => {
const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`?
- ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/));
+ ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name));
if (!entries.length) return;
const filledForm = [...forms.values()].find(form => form.hasValues());
filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
@@ -10545,14 +10568,14 @@ const matchingConfiguration = {
email: {
match: '.mail\\b',
skip: 'phone|name|reservation number',
- forceUnknown: 'search|filter|subject|title|tab'
+ forceUnknown: 'search|filter|subject|title|\btab\b'
},
password: {
match: 'password',
forceUnknown: 'captcha|mfa|2fa|two factor'
},
username: {
- match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$',
+ match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername',
forceUnknown: 'search'
},
// CC
@@ -11968,7 +11991,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd
const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]";
const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com`
"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login
-"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs
+"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs
// todo: ensure these can be set via configuration
module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR;
@@ -13713,15 +13736,13 @@ class OverlayUIController extends _UIController.UIController {
/** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */
- /**
- * @type {OverlayControllerOptions}
- */
-
/**
* @param {OverlayControllerOptions} options
*/
constructor(options) {
- super();
+ super(options); // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
_classPrivateFieldInitSpec(this, _state, {
writable: true,
@@ -13730,12 +13751,6 @@ class OverlayUIController extends _UIController.UIController {
_defineProperty(this, "_activeTooltip", null);
- _defineProperty(this, "_options", void 0);
-
- this._options = options; // We always register this 'pointerdown' event, regardless of
- // whether we have a tooltip currently open or not. This is to ensure
- // we can clear out any existing state before opening a new one.
-
window.addEventListener('pointerdown', this, true);
}
/**
@@ -13901,6 +13916,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.UIController = void 0;
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
/**
* @typedef AttachArgs The argument required to 'attach' a tooltip
* @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call
@@ -13915,6 +13932,34 @@ exports.UIController = void 0;
* This is the base interface that `UIControllers` should extend/implement
*/
class UIController {
+ /**
+ * @type {any}
+ */
+
+ /**
+ * @param {any} [options]
+ */
+ constructor(options) {
+ _defineProperty(this, "_options", void 0);
+
+ this._options = options; // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
+
+ window.addEventListener('pointerdown', this, true);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case 'pointerdown':
+ {
+ var _this$_options$onPoin, _this$_options;
+
+ (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event);
+ break;
+ }
+ }
+ }
/**
* Implement this method to control what happen when Autofill
* has enough information to 'attach' a tooltip.
@@ -13922,6 +13967,8 @@ class UIController {
* @param {AttachArgs} _args
* @returns {void}
*/
+
+
attach(_args) {
throw new Error('must implement attach');
}
@@ -14198,7 +14245,7 @@ exports.default = _default;
Object.defineProperty(exports, "__esModule", {
value: true
});
-exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
+exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
exports.escapeXML = escapeXML;
exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0;
@@ -14564,9 +14611,27 @@ const isLikelyASubmitButton = el => {
el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px
!SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel);
};
+/**
+ * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form
+ * @param {HTMLElement} el
+ * @param {import('./Form/Form').Form} formObj
+ */
+
exports.isLikelyASubmitButton = isLikelyASubmitButton;
+const buttonMatchesFormType = (el, formObj) => {
+ if (formObj.isLogin) {
+ return !/sign.?up/i.test(el.textContent || '');
+ } else if (formObj.isSignup) {
+ return !/(log|sign).?([io])n/i.test(el.textContent || '');
+ } else {
+ return true;
+ }
+};
+
+exports.buttonMatchesFormType = buttonMatchesFormType;
+
},{"./Form/matching":34}],55:[function(require,module,exports){
"use strict";
@@ -14579,8 +14644,18 @@ var _DeviceInterface = require("./DeviceInterface");
if (!window.isSecureContext) return false;
try {
- const deviceInterface = (0, _DeviceInterface.createDevice)();
- deviceInterface.init();
+ const startupAutofill = () => {
+ if (document.visibilityState === 'visible') {
+ const deviceInterface = (0, _DeviceInterface.createDevice)();
+ deviceInterface.init();
+ } else {
+ document.addEventListener('visibilitychange', startupAutofill, {
+ once: true
+ });
+ }
+ };
+
+ startupAutofill();
} catch (e) {
console.error(e); // Noop, we errored
}
@@ -14620,9 +14695,16 @@ function createGlobalConfig() {
// The native layer will inject a randomised secret here and use it to verify the origin
let secret = 'PLACEHOLDER_SECRET';
- let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame;
- const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent);
- const isMobileApp = isDDGApp && !isApp;
+ /**
+ * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name`
+ */
+ // @ts-ignore
+
+ const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore
+
+ const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore
+
+ const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid;
const isFirefox = navigator.userAgent.includes('Firefox');
const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX));
return {
@@ -14979,11 +15061,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport {
var _window$BrowserAutofi, _window$BrowserAutofi2;
if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') {
- throw new Error('window.BrowserAutofill.getAutofillData missing');
+ console.warn('window.BrowserAutofill.getAutofillData missing');
}
if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') {
- throw new Error('window.BrowserAutofill.storeFormData missing');
+ console.warn('window.BrowserAutofill.storeFormData missing');
}
}
}
diff --git a/dist/autofill.js b/dist/autofill.js
index f7e1d1780..436136508 100644
--- a/dist/autofill.js
+++ b/dist/autofill.js
@@ -2902,7 +2902,9 @@ class AndroidInterface extends _InterfacePrototype.default {
createUIController() {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
/**
* @deprecated use `this.settings.availableInputTypes.email` in the future
@@ -3071,7 +3073,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
var _this$globalConfig$us, _this$globalConfig$us2;
if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
if (!this.globalConfig.supportsTopFrame) {
@@ -3414,32 +3418,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
poll();
});
}
- /**
- * on macOS we try to detect if a click occurred within a form
- * @param {PointerEvent} event
- */
-
-
- _onPointerDown(event) {
- if (this.settings.featureToggles.credentials_saving) {
- this._detectFormSubmission(event);
- }
- }
- /**
- * @param {PointerEvent} event
- */
-
-
- _detectFormSubmission(event) {
- const matchingForm = [...this.scanner.forms.values()].find(form => {
- const btns = [...form.submitButtons]; // @ts-ignore
-
- if (btns.includes(event.target)) return true; // @ts-ignore
-
- if (btns.find(btn => btn.contains(event.target))) return true;
- });
- matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
- }
}
@@ -3826,6 +3804,8 @@ var _deviceApi = require("../../packages/device-api");
var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls");
+var _selectorsCss = require("../Form/selectors-css");
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
@@ -4554,6 +4534,55 @@ class InterfacePrototype {
this.storeFormData(withAutoGeneratedFlag);
}
}
+ /**
+ * on macOS we try to detect if a click occurred within a form
+ * @param {PointerEvent} event
+ */
+
+
+ _onPointerDown(event) {
+ if (this.settings.featureToggles.credentials_saving) {
+ this._detectFormSubmission(event);
+ }
+ }
+ /**
+ * @param {PointerEvent} event
+ */
+
+
+ _detectFormSubmission(event) {
+ const matchingForm = [...this.scanner.forms.values()].find(form => {
+ const btns = [...form.submitButtons]; // @ts-ignore
+
+ if (btns.includes(event.target)) return true; // @ts-ignore
+
+ if (btns.find(btn => btn.contains(event.target))) return true;
+ });
+ matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
+
+ if (!matchingForm) {
+ var _event$target;
+
+ // check if the click happened on a button
+ const button =
+ /** @type HTMLElement */
+ (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR);
+ if (!button) return;
+ const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent);
+ const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text);
+
+ if (hasRelevantText && text.length < 25) {
+ // check if there's a form with values
+ const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues());
+
+ if (filledForm && (0, _autofillUtils.buttonMatchesFormType)(
+ /** @type HTMLElement */
+ button, filledForm)) {
+ filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
+ }
+ }
+ }
+ }
/**
* This serves as a single place to create a default instance
* of InterfacePrototype that can be useful in testing scenarios
@@ -4576,7 +4605,7 @@ class InterfacePrototype {
var _default = InterfacePrototype;
exports.default = _default;
-},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){
+},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../Form/selectors-css":27,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -4671,13 +4700,16 @@ class Form {
this.categorizeInputs();
}
/**
- * Checks if the form element contains the activeElement
+ * Checks if the form element contains the activeElement or the event target
* @return {boolean}
+ * @param {KeyboardEvent | null} [e]
*/
- hasFocus() {
- return this.form.contains(document.activeElement);
+ hasFocus(e) {
+ return this.form.contains(document.activeElement) || this.form.contains(
+ /** @type HTMLElement */
+ e === null || e === void 0 ? void 0 : e.target);
}
/**
* Checks that the form element doesn't contain an invalid field
@@ -4837,16 +4869,7 @@ class Form {
const allButtons =
/** @type {HTMLElement[]} */
[...this.form.querySelectorAll(selector)];
- return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form
- .filter(button => {
- if (this.isLogin) {
- return !/sign.?up/i.test(button.textContent || '');
- } else if (this.isSignup) {
- return !/(log|sign).?([io])n/i.test(button.textContent || '');
- } else {
- return true;
- }
- });
+ return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this));
}
/**
* Executes a function on input elements. Can be limited to certain element types
@@ -6584,15 +6607,15 @@ const listenForGlobalFormSubmission = forms => {
(_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler()
);
}, true);
- window.addEventListener('keypress', e => {
+ window.addEventListener('keydown', e => {
if (e.key === 'Enter') {
- const focusedForm = [...forms.values()].find(form => form.hasFocus());
+ const focusedForm = [...forms.values()].find(form => form.hasFocus(e));
focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler();
}
});
const observer = new PerformanceObserver(list => {
const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`?
- ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/));
+ ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name));
if (!entries.length) return;
const filledForm = [...forms.values()].find(form => form.hasValues());
filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
@@ -6869,14 +6892,14 @@ const matchingConfiguration = {
email: {
match: '.mail\\b',
skip: 'phone|name|reservation number',
- forceUnknown: 'search|filter|subject|title|tab'
+ forceUnknown: 'search|filter|subject|title|\btab\b'
},
password: {
match: 'password',
forceUnknown: 'captcha|mfa|2fa|two factor'
},
username: {
- match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$',
+ match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername',
forceUnknown: 'search'
},
// CC
@@ -8292,7 +8315,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd
const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]";
const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com`
"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login
-"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs
+"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs
// todo: ensure these can be set via configuration
module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR;
@@ -10037,15 +10060,13 @@ class OverlayUIController extends _UIController.UIController {
/** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */
- /**
- * @type {OverlayControllerOptions}
- */
-
/**
* @param {OverlayControllerOptions} options
*/
constructor(options) {
- super();
+ super(options); // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
_classPrivateFieldInitSpec(this, _state, {
writable: true,
@@ -10054,12 +10075,6 @@ class OverlayUIController extends _UIController.UIController {
_defineProperty(this, "_activeTooltip", null);
- _defineProperty(this, "_options", void 0);
-
- this._options = options; // We always register this 'pointerdown' event, regardless of
- // whether we have a tooltip currently open or not. This is to ensure
- // we can clear out any existing state before opening a new one.
-
window.addEventListener('pointerdown', this, true);
}
/**
@@ -10225,6 +10240,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.UIController = void 0;
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
/**
* @typedef AttachArgs The argument required to 'attach' a tooltip
* @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call
@@ -10239,6 +10256,34 @@ exports.UIController = void 0;
* This is the base interface that `UIControllers` should extend/implement
*/
class UIController {
+ /**
+ * @type {any}
+ */
+
+ /**
+ * @param {any} [options]
+ */
+ constructor(options) {
+ _defineProperty(this, "_options", void 0);
+
+ this._options = options; // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
+
+ window.addEventListener('pointerdown', this, true);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case 'pointerdown':
+ {
+ var _this$_options$onPoin, _this$_options;
+
+ (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event);
+ break;
+ }
+ }
+ }
/**
* Implement this method to control what happen when Autofill
* has enough information to 'attach' a tooltip.
@@ -10246,6 +10291,8 @@ class UIController {
* @param {AttachArgs} _args
* @returns {void}
*/
+
+
attach(_args) {
throw new Error('must implement attach');
}
@@ -10522,7 +10569,7 @@ exports.default = _default;
Object.defineProperty(exports, "__esModule", {
value: true
});
-exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
+exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
exports.escapeXML = escapeXML;
exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0;
@@ -10888,9 +10935,27 @@ const isLikelyASubmitButton = el => {
el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px
!SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel);
};
+/**
+ * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form
+ * @param {HTMLElement} el
+ * @param {import('./Form/Form').Form} formObj
+ */
+
exports.isLikelyASubmitButton = isLikelyASubmitButton;
+const buttonMatchesFormType = (el, formObj) => {
+ if (formObj.isLogin) {
+ return !/sign.?up/i.test(el.textContent || '');
+ } else if (formObj.isSignup) {
+ return !/(log|sign).?([io])n/i.test(el.textContent || '');
+ } else {
+ return true;
+ }
+};
+
+exports.buttonMatchesFormType = buttonMatchesFormType;
+
},{"./Form/matching":26}],47:[function(require,module,exports){
"use strict";
@@ -10903,8 +10968,18 @@ var _DeviceInterface = require("./DeviceInterface");
if (!window.isSecureContext) return false;
try {
- const deviceInterface = (0, _DeviceInterface.createDevice)();
- deviceInterface.init();
+ const startupAutofill = () => {
+ if (document.visibilityState === 'visible') {
+ const deviceInterface = (0, _DeviceInterface.createDevice)();
+ deviceInterface.init();
+ } else {
+ document.addEventListener('visibilitychange', startupAutofill, {
+ once: true
+ });
+ }
+ };
+
+ startupAutofill();
} catch (e) {
console.error(e); // Noop, we errored
}
@@ -10944,9 +11019,16 @@ function createGlobalConfig() {
// The native layer will inject a randomised secret here and use it to verify the origin
let secret = 'PLACEHOLDER_SECRET';
- let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame;
- const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent);
- const isMobileApp = isDDGApp && !isApp;
+ /**
+ * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name`
+ */
+ // @ts-ignore
+
+ const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore
+
+ const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore
+
+ const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid;
const isFirefox = navigator.userAgent.includes('Firefox');
const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX));
return {
@@ -11188,11 +11270,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport {
var _window$BrowserAutofi, _window$BrowserAutofi2;
if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') {
- throw new Error('window.BrowserAutofill.getAutofillData missing');
+ console.warn('window.BrowserAutofill.getAutofillData missing');
}
if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') {
- throw new Error('window.BrowserAutofill.storeFormData missing');
+ console.warn('window.BrowserAutofill.storeFormData missing');
}
}
}
diff --git a/integration-test/helpers/mocks.js b/integration-test/helpers/mocks.js
index 24219f4cf..ab3249fd6 100644
--- a/integration-test/helpers/mocks.js
+++ b/integration-test/helpers/mocks.js
@@ -6,7 +6,8 @@ export const constants = {
'overlay': 'overlay.html',
'email-autofill': 'email-autofill.html',
'signup': 'signup.html',
- 'login': 'login.html'
+ 'login': 'login.html',
+ 'loginWithPoorForm': 'login-poor-form.html'
},
fields: {
email: {
diff --git a/integration-test/helpers/pages.js b/integration-test/helpers/pages.js
index aa6d0b6a8..e14f21e73 100644
--- a/integration-test/helpers/pages.js
+++ b/integration-test/helpers/pages.js
@@ -233,10 +233,16 @@ export function loginPage (page, server, opts = {}) {
await page.type('#email', data.username)
await page.click('#login button[type="submit"]')
},
- async shouldNotPromptToSave () {
- const calls = await page.evaluate('window.__playwright.mocks.calls')
- // todo(Shane): is it too apple specific?
- const mockCalls = calls.filter(([name]) => name === 'pmHandlerStoreData')
+ /** @param {Platform} platform */
+ async shouldNotPromptToSave (platform = 'ios') {
+ let mockCalls = []
+ if (['ios', 'macos'].includes(platform)) {
+ mockCalls = await mockedCalls(page, ['pmHandlerStoreData'])
+ }
+ if (platform === 'android') {
+ mockCalls = await mockedCalls(page, ['storeFormData'])
+ }
+
expect(mockCalls.length).toBe(0)
},
/** @param {string} mockCallName */
@@ -301,6 +307,23 @@ export function loginPage (page, server, opts = {}) {
}
}
+/**
+ * A wrapper around interactions for `integration-test/pages/login-poor-form.html`
+ *
+ * @param {import("playwright").Page} page
+ * @param {ServerWrapper} server
+ * @param {{overlay?: boolean, clickLabel?: boolean}} [opts]
+ */
+export function loginPageWithPoorForm (page, server, opts) {
+ const originalLoginPage = loginPage(page, server, opts)
+ return {
+ ...originalLoginPage,
+ async navigate () {
+ await page.goto(server.urlForPath(constants.pages['loginWithPoorForm']))
+ }
+ }
+}
+
/**
* A wrapper around interactions for `integration-test/pages/email-autofill.html`
*
diff --git a/integration-test/pages/login-poor-form.html b/integration-test/pages/login-poor-form.html
new file mode 100644
index 000000000..32b8fa609
--- /dev/null
+++ b/integration-test/pages/login-poor-form.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+ Login form
+
+
+
+
+[Home]
+
+
+
+
+
+
+
+
diff --git a/integration-test/tests/email-autofill.android.spec.js b/integration-test/tests/email-autofill.android.spec.js
index 17bccb6fa..972202028 100644
--- a/integration-test/tests/email-autofill.android.spec.js
+++ b/integration-test/tests/email-autofill.android.spec.js
@@ -38,6 +38,7 @@ test.describe('android', () => {
// create + inject the script
await createAutofillScript()
+ .replaceAll(androidStringReplacements())
.platform('android')
.applyTo(page)
diff --git a/integration-test/tests/save-prompts.android.spec.js b/integration-test/tests/save-prompts.android.spec.js
index 6e191a2b2..cb7589061 100644
--- a/integration-test/tests/save-prompts.android.spec.js
+++ b/integration-test/tests/save-prompts.android.spec.js
@@ -4,7 +4,7 @@ import {
setupServer, withAndroidContext
} from '../helpers/harness.js'
import {test as base} from '@playwright/test'
-import {loginPage, signupPage} from '../helpers/pages.js'
+import {loginPage, loginPageWithPoorForm, signupPage} from '../helpers/pages.js'
import {androidStringReplacements, createAndroidMocks} from '../helpers/mocks.android.js'
import {constants} from '../helpers/mocks.js'
@@ -124,4 +124,60 @@ test.describe('Android Save prompts', () => {
await login.promptWasNotShown()
})
})
+
+ test.describe('Prompting to save from a poor login form (using Enter and click on a button outside the form)', () => {
+ const credentials = {
+ username: 'dax@wearejh.com',
+ password: '123456'
+ }
+ /**
+ * @param {import("playwright").Page} page
+ */
+ async function setup (page) {
+ await forwardConsoleMessages(page)
+ const login = loginPageWithPoorForm(page, server)
+ await login.navigate()
+
+ await createAndroidMocks()
+ .applyTo(page)
+ await createAutofillScript()
+ .replaceAll(androidStringReplacements({
+ featureToggles: {
+ credentials_saving: true
+ }
+ }))
+ .platform('android')
+ .applyTo(page)
+
+ await page.type('#password', credentials.password)
+ await page.type('#email', credentials.username)
+
+ // Check that we haven't detected any submission at this point
+ await login.shouldNotPromptToSave()
+
+ return login
+ }
+
+ test('submit by clicking on the out-of-form button', async ({page}) => {
+ const login = await setup(page)
+
+ await page.click('"Log in"')
+ await login.assertWasPromptedToSave(credentials, 'android')
+ })
+ test('should not prompt if the out-of-form button does not match the form type', async ({page}) => {
+ const login = await setup(page)
+
+ await page.click('"Sign up"')
+ await login.shouldNotPromptToSave()
+ })
+ test('should prompt when hitting enter while an input is focused', async ({page}) => {
+ const login = await setup(page)
+
+ await page.press('#email', 'Tab')
+ await login.shouldNotPromptToSave()
+
+ await page.press('#password', 'Enter')
+ await login.assertWasPromptedToSave(credentials, 'android')
+ })
+ })
})
diff --git a/integration-test/tests/save-prompts.ios.spec.js b/integration-test/tests/save-prompts.ios.spec.js
index ff92e58f4..482398a10 100644
--- a/integration-test/tests/save-prompts.ios.spec.js
+++ b/integration-test/tests/save-prompts.ios.spec.js
@@ -4,7 +4,7 @@ import {
withIOSContext, withIOSFeatureToggles
} from '../helpers/harness.js'
import {test as base} from '@playwright/test'
-import {loginPage, signupPage} from '../helpers/pages.js'
+import {loginPage, loginPageWithPoorForm, signupPage} from '../helpers/pages.js'
import {createWebkitMocks} from '../helpers/mocks.webkit.js'
import {constants} from '../helpers/mocks.js'
@@ -123,5 +123,54 @@ test.describe('iOS Save prompts', () => {
await login.shouldNotPromptToSave()
})
})
+
+ test.describe('Prompting to save from a poor login form (using Enter and click on a button outside the form)', () => {
+ const credentials = {
+ username: 'dax@wearejh.com',
+ password: '123456'
+ }
+ /**
+ * @param {import("playwright").Page} page
+ */
+ async function setup (page) {
+ await forwardConsoleMessages(page)
+ await createWebkitMocks().applyTo(page)
+ await withIOSFeatureToggles(page, {
+ credentials_saving: true
+ })
+ const login = loginPageWithPoorForm(page, server)
+ await login.navigate()
+
+ await page.type('#password', credentials.password)
+ await page.type('#email', credentials.username)
+
+ // Check that we haven't detected any submission at this point
+ await login.shouldNotPromptToSave()
+
+ return login
+ }
+
+ test('submit by clicking on the out-of-form button', async ({page}) => {
+ const login = await setup(page)
+
+ await page.click('"Log in"')
+ await login.assertWasPromptedToSave(credentials)
+ })
+ test('should not prompt if the out-of-form button does not match the form type', async ({page}) => {
+ const login = await setup(page)
+
+ await page.click('"Sign up"')
+ await login.shouldNotPromptToSave()
+ })
+ test('should prompt when hitting enter while an input is focused', async ({page}) => {
+ const login = await setup(page)
+
+ await page.press('#email', 'Tab')
+ await login.shouldNotPromptToSave()
+
+ await page.press('#password', 'Enter')
+ await login.assertWasPromptedToSave(credentials)
+ })
+ })
})
})
diff --git a/src/DeviceInterface/AndroidInterface.js b/src/DeviceInterface/AndroidInterface.js
index 8713a387b..d6640dd65 100644
--- a/src/DeviceInterface/AndroidInterface.js
+++ b/src/DeviceInterface/AndroidInterface.js
@@ -19,7 +19,9 @@ class AndroidInterface extends InterfacePrototype {
* @override
*/
createUIController () {
- return new NativeUIController()
+ return new NativeUIController({
+ onPointerDown: (event) => this._onPointerDown(event)
+ })
}
/**
diff --git a/src/DeviceInterface/AppleDeviceInterface.js b/src/DeviceInterface/AppleDeviceInterface.js
index adaa3c365..e4cd825c3 100644
--- a/src/DeviceInterface/AppleDeviceInterface.js
+++ b/src/DeviceInterface/AppleDeviceInterface.js
@@ -29,7 +29,9 @@ class AppleDeviceInterface extends InterfacePrototype {
*/
createUIController () {
if (this.globalConfig.userPreferences?.platform?.name === 'ios') {
- return new NativeUIController()
+ return new NativeUIController({
+ onPointerDown: (event) => this._onPointerDown(event)
+ })
}
if (!this.globalConfig.supportsTopFrame) {
@@ -314,32 +316,6 @@ class AppleDeviceInterface extends InterfacePrototype {
poll()
})
}
- /**
- * on macOS we try to detect if a click occurred within a form
- * @param {PointerEvent} event
- */
- _onPointerDown (event) {
- if (this.settings.featureToggles.credentials_saving) {
- this._detectFormSubmission(event)
- }
- }
- /**
- * @param {PointerEvent} event
- */
- _detectFormSubmission (event) {
- const matchingForm = [...this.scanner.forms.values()].find(
- (form) => {
- const btns = [...form.submitButtons]
- // @ts-ignore
- if (btns.includes(event.target)) return true
-
- // @ts-ignore
- if (btns.find((btn) => btn.contains(event.target))) return true
- }
- )
-
- matchingForm?.submitHandler()
- }
}
export {AppleDeviceInterface}
diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js
index c75f8ddd9..6997f3fa8 100644
--- a/src/DeviceInterface/InterfacePrototype.js
+++ b/src/DeviceInterface/InterfacePrototype.js
@@ -4,10 +4,10 @@ import {
sendAndWaitForAnswer,
formatDuckAddress,
autofillEnabled,
- notifyWebApp, getDaxBoundingBox
+ notifyWebApp, getDaxBoundingBox, buttonMatchesFormType
} from '../autofill-utils'
-import { getInputType, getSubtypeFromType } from '../Form/matching'
+import {getInputType, getSubtypeFromType, removeExcessWhitespace} from '../Form/matching'
import { formatFullName } from '../Form/formatters'
import listenForGlobalFormSubmission from '../Form/listenForFormSubmission'
import { fromPassword, appendGeneratedId, AUTOGENERATED_KEY } from '../InputTypes/Credentials'
@@ -19,6 +19,7 @@ import {createTransport} from '../deviceApiCalls/transports/transports'
import {Settings} from '../Settings'
import {DeviceApi} from '../../packages/device-api'
import {StoreFormDataCall} from '../deviceApiCalls/__generated__/deviceApiCalls'
+import {SUBMIT_BUTTON_SELECTOR} from '../Form/selectors-css'
/**
* @typedef {import('../deviceApiCalls/__generated__/validators-ts').StoreFormData} StoreFormData
@@ -610,6 +611,48 @@ class InterfacePrototype {
this.storeFormData(withAutoGeneratedFlag)
}
}
+ /**
+ * on macOS we try to detect if a click occurred within a form
+ * @param {PointerEvent} event
+ */
+ _onPointerDown (event) {
+ if (this.settings.featureToggles.credentials_saving) {
+ this._detectFormSubmission(event)
+ }
+ }
+ /**
+ * @param {PointerEvent} event
+ */
+ _detectFormSubmission (event) {
+ const matchingForm = [...this.scanner.forms.values()].find(
+ (form) => {
+ const btns = [...form.submitButtons]
+ // @ts-ignore
+ if (btns.includes(event.target)) return true
+
+ // @ts-ignore
+ if (btns.find((btn) => btn.contains(event.target))) return true
+ }
+ )
+
+ matchingForm?.submitHandler()
+
+ if (!matchingForm) {
+ // check if the click happened on a button
+ const button = /** @type HTMLElement */(event.target)?.closest(SUBMIT_BUTTON_SELECTOR)
+ if (!button) return
+
+ const text = removeExcessWhitespace(button?.textContent)
+ const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text)
+ if (hasRelevantText && text.length < 25) {
+ // check if there's a form with values
+ const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues())
+ if (filledForm && buttonMatchesFormType(/** @type HTMLElement */(button), filledForm)) {
+ filledForm?.submitHandler()
+ }
+ }
+ }
+ }
/**
* This serves as a single place to create a default instance
diff --git a/src/Form/Form.js b/src/Form/Form.js
index 836137d57..cff9d4520 100644
--- a/src/Form/Form.js
+++ b/src/Form/Form.js
@@ -6,7 +6,7 @@ import {
setValue,
isEventWithinDax,
isLikelyASubmitButton,
- isVisible
+ isVisible, buttonMatchesFormType
} from '../autofill-utils'
import { getInputSubtype, getInputMainType, createMatching } from './matching'
@@ -85,11 +85,12 @@ class Form {
}
/**
- * Checks if the form element contains the activeElement
+ * Checks if the form element contains the activeElement or the event target
* @return {boolean}
+ * @param {KeyboardEvent | null} [e]
*/
- hasFocus () {
- return this.form.contains(document.activeElement)
+ hasFocus (e) {
+ return this.form.contains(document.activeElement) || this.form.contains(/** @type HTMLElement */(e?.target))
}
/**
@@ -225,17 +226,9 @@ class Form {
const allButtons = /** @type {HTMLElement[]} */([...this.form.querySelectorAll(selector)])
return allButtons
- .filter(isLikelyASubmitButton)
- // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form
- .filter((button) => {
- if (this.isLogin) {
- return !/sign.?up/i.test(button.textContent || '')
- } else if (this.isSignup) {
- return !/(log|sign).?([io])n/i.test(button.textContent || '')
- } else {
- return true
- }
- })
+ .filter((btn) =>
+ isLikelyASubmitButton(btn) && buttonMatchesFormType(btn, this)
+ )
}
/**
diff --git a/src/Form/listenForFormSubmission.js b/src/Form/listenForFormSubmission.js
index 14d5af181..aaaba51ac 100644
--- a/src/Form/listenForFormSubmission.js
+++ b/src/Form/listenForFormSubmission.js
@@ -8,9 +8,9 @@ const listenForGlobalFormSubmission = (forms) => {
forms.get(e.target)?.submitHandler(),
true)
- window.addEventListener('keypress', (e) => {
+ window.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
- const focusedForm = [...forms.values()].find((form) => form.hasFocus())
+ const focusedForm = [...forms.values()].find((form) => form.hasFocus(e))
focusedForm?.submitHandler()
}
})
@@ -19,7 +19,7 @@ const listenForGlobalFormSubmission = (forms) => {
const entries = list.getEntries().filter((entry) =>
// @ts-ignore why does TS not know about `entry.initiatorType`?
['fetch', 'xmlhttprequest'].includes(entry.initiatorType) &&
- entry.name.match(/login|sign-in|signin/)
+ /login|sign-in|signin/.test(entry.name)
)
if (!entries.length) return
diff --git a/src/Form/matching-configuration.js b/src/Form/matching-configuration.js
index 40e392cd2..7fc6b55fe 100644
--- a/src/Form/matching-configuration.js
+++ b/src/Form/matching-configuration.js
@@ -255,9 +255,9 @@ const matchingConfiguration = {
/** @type {DDGMatcherConfiguration} */
ddgMatcher: {
matchers: {
- email: {match: '.mail\\b', skip: 'phone|name|reservation number', forceUnknown: 'search|filter|subject|title|tab'},
+ email: {match: '.mail\\b', skip: 'phone|name|reservation number', forceUnknown: 'search|filter|subject|title|\btab\b'},
password: {match: 'password', forceUnknown: 'captcha|mfa|2fa|two factor'},
- username: {match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$', forceUnknown: 'search'},
+ username: {match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername', forceUnknown: 'search'},
// CC
cardName: {match: '(card.*name|name.*card)|(card.*holder|holder.*card)|(card.*owner|owner.*card)'},
diff --git a/src/Form/selectors-css.js b/src/Form/selectors-css.js
index 2916bcf9c..4d630c493 100644
--- a/src/Form/selectors-css.js
+++ b/src/Form/selectors-css.js
@@ -182,7 +182,8 @@ const username = [
// fix for https://online.mbank.pl/pl/Login
`input[name="userID" i]`,
`input[id="login-id" i]`,
- `input[name=accountname i]`
+ `input[name=accountname i]`,
+ `input[autocomplete=username]`
]
// todo: these are still used directly right now, mostly in scanForInputs
diff --git a/src/UI/controllers/OverlayUIController.js b/src/UI/controllers/OverlayUIController.js
index 120a0a949..d08ed6468 100644
--- a/src/UI/controllers/OverlayUIController.js
+++ b/src/UI/controllers/OverlayUIController.js
@@ -44,17 +44,11 @@ export class OverlayUIController extends UIController {
/** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */
_activeTooltip = null
- /**
- * @type {OverlayControllerOptions}
- */
- _options;
-
/**
* @param {OverlayControllerOptions} options
*/
constructor (options) {
- super()
- this._options = options
+ super(options)
// We always register this 'pointerdown' event, regardless of
// whether we have a tooltip currently open or not. This is to ensure
diff --git a/src/UI/controllers/UIController.js b/src/UI/controllers/UIController.js
index c16e97ac4..2333b249d 100644
--- a/src/UI/controllers/UIController.js
+++ b/src/UI/controllers/UIController.js
@@ -12,6 +12,32 @@
* This is the base interface that `UIControllers` should extend/implement
*/
export class UIController {
+ /**
+ * @type {any}
+ */
+ _options;
+
+ /**
+ * @param {any} [options]
+ */
+ constructor (options) {
+ this._options = options
+
+ // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
+ window.addEventListener('pointerdown', this, true)
+ }
+
+ handleEvent (event) {
+ switch (event.type) {
+ case 'pointerdown': {
+ this._options.onPointerDown?.(event)
+ break
+ }
+ }
+ }
+
/**
* Implement this method to control what happen when Autofill
* has enough information to 'attach' a tooltip.
diff --git a/src/autofill-utils.js b/src/autofill-utils.js
index 53d4478c2..d83276dc7 100644
--- a/src/autofill-utils.js
+++ b/src/autofill-utils.js
@@ -287,6 +287,21 @@ const isLikelyASubmitButton = (el) => {
!SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel)
}
+/**
+ * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form
+ * @param {HTMLElement} el
+ * @param {import('./Form/Form').Form} formObj
+ */
+const buttonMatchesFormType = (el, formObj) => {
+ if (formObj.isLogin) {
+ return !/sign.?up/i.test(el.textContent || '')
+ } else if (formObj.isSignup) {
+ return !/(log|sign).?([io])n/i.test(el.textContent || '')
+ } else {
+ return true
+ }
+}
+
export {
notifyWebApp,
sendAndWaitForAnswer,
@@ -303,5 +318,6 @@ export {
ADDRESS_DOMAIN,
formatDuckAddress,
escapeXML,
- isLikelyASubmitButton
+ isLikelyASubmitButton,
+ buttonMatchesFormType
}
diff --git a/src/autofill.js b/src/autofill.js
index 9ad819c46..76db638bf 100644
--- a/src/autofill.js
+++ b/src/autofill.js
@@ -5,8 +5,15 @@ import {createDevice} from './DeviceInterface'
(() => {
if (!window.isSecureContext) return false
try {
- const deviceInterface = createDevice()
- deviceInterface.init()
+ const startupAutofill = () => {
+ if (document.visibilityState === 'visible') {
+ const deviceInterface = createDevice()
+ deviceInterface.init()
+ } else {
+ document.addEventListener('visibilitychange', startupAutofill, {once: true})
+ }
+ }
+ startupAutofill()
} catch (e) {
console.error(e)
// Noop, we errored
diff --git a/src/config.js b/src/config.js
index b3c903a4e..277702e56 100644
--- a/src/config.js
+++ b/src/config.js
@@ -33,11 +33,15 @@ function createGlobalConfig () {
// The native layer will inject a randomised secret here and use it to verify the origin
let secret = 'PLACEHOLDER_SECRET'
- let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) ||
- isApp ||
- isTopFrame
- const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent)
- const isMobileApp = isDDGApp && !isApp
+ /**
+ * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name`
+ */
+ // @ts-ignore
+ const isAndroid = userPreferences?.platform.name === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent)
+ // @ts-ignore
+ const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences?.platform.name) || isAndroid
+ // @ts-ignore
+ const isMobileApp = ['ios', 'android'].includes(userPreferences?.platform.name) || isAndroid
const isFirefox = navigator.userAgent.includes('Firefox')
const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX))
diff --git a/src/deviceApiCalls/transports/android.transport.js b/src/deviceApiCalls/transports/android.transport.js
index 562a79dbe..031548827 100644
--- a/src/deviceApiCalls/transports/android.transport.js
+++ b/src/deviceApiCalls/transports/android.transport.js
@@ -17,10 +17,10 @@ export class AndroidTransport extends DeviceApiTransport {
if (this.config.isDDGTestMode) {
if (typeof window.BrowserAutofill?.getAutofillData !== 'function') {
- throw new Error('window.BrowserAutofill.getAutofillData missing')
+ console.warn('window.BrowserAutofill.getAutofillData missing')
}
if (typeof window.BrowserAutofill?.storeFormData !== 'function') {
- throw new Error('window.BrowserAutofill.storeFormData missing')
+ console.warn('window.BrowserAutofill.storeFormData missing')
}
}
}
diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js
index 7051c97d7..830416894 100644
--- a/swift-package/Resources/assets/autofill-debug.js
+++ b/swift-package/Resources/assets/autofill-debug.js
@@ -6578,7 +6578,9 @@ class AndroidInterface extends _InterfacePrototype.default {
createUIController() {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
/**
* @deprecated use `this.settings.availableInputTypes.email` in the future
@@ -6747,7 +6749,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
var _this$globalConfig$us, _this$globalConfig$us2;
if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
if (!this.globalConfig.supportsTopFrame) {
@@ -7090,32 +7094,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
poll();
});
}
- /**
- * on macOS we try to detect if a click occurred within a form
- * @param {PointerEvent} event
- */
-
-
- _onPointerDown(event) {
- if (this.settings.featureToggles.credentials_saving) {
- this._detectFormSubmission(event);
- }
- }
- /**
- * @param {PointerEvent} event
- */
-
-
- _detectFormSubmission(event) {
- const matchingForm = [...this.scanner.forms.values()].find(form => {
- const btns = [...form.submitButtons]; // @ts-ignore
-
- if (btns.includes(event.target)) return true; // @ts-ignore
-
- if (btns.find(btn => btn.contains(event.target))) return true;
- });
- matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
- }
}
@@ -7502,6 +7480,8 @@ var _deviceApi = require("../../packages/device-api");
var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls");
+var _selectorsCss = require("../Form/selectors-css");
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
@@ -8230,6 +8210,55 @@ class InterfacePrototype {
this.storeFormData(withAutoGeneratedFlag);
}
}
+ /**
+ * on macOS we try to detect if a click occurred within a form
+ * @param {PointerEvent} event
+ */
+
+
+ _onPointerDown(event) {
+ if (this.settings.featureToggles.credentials_saving) {
+ this._detectFormSubmission(event);
+ }
+ }
+ /**
+ * @param {PointerEvent} event
+ */
+
+
+ _detectFormSubmission(event) {
+ const matchingForm = [...this.scanner.forms.values()].find(form => {
+ const btns = [...form.submitButtons]; // @ts-ignore
+
+ if (btns.includes(event.target)) return true; // @ts-ignore
+
+ if (btns.find(btn => btn.contains(event.target))) return true;
+ });
+ matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
+
+ if (!matchingForm) {
+ var _event$target;
+
+ // check if the click happened on a button
+ const button =
+ /** @type HTMLElement */
+ (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR);
+ if (!button) return;
+ const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent);
+ const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text);
+
+ if (hasRelevantText && text.length < 25) {
+ // check if there's a form with values
+ const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues());
+
+ if (filledForm && (0, _autofillUtils.buttonMatchesFormType)(
+ /** @type HTMLElement */
+ button, filledForm)) {
+ filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
+ }
+ }
+ }
+ }
/**
* This serves as a single place to create a default instance
* of InterfacePrototype that can be useful in testing scenarios
@@ -8252,7 +8281,7 @@ class InterfacePrototype {
var _default = InterfacePrototype;
exports.default = _default;
-},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){
+},{"../../packages/device-api":10,"../Form/formatters":27,"../Form/listenForFormSubmission":31,"../Form/matching":34,"../Form/selectors-css":35,"../InputTypes/Credentials":37,"../PasswordGenerator":40,"../Scanner":41,"../Settings":42,"../UI/controllers/NativeUIController":47,"../autofill-utils":54,"../config":56,"../deviceApiCalls/__generated__/deviceApiCalls":58,"../deviceApiCalls/transports/transports":64}],24:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -8347,13 +8376,16 @@ class Form {
this.categorizeInputs();
}
/**
- * Checks if the form element contains the activeElement
+ * Checks if the form element contains the activeElement or the event target
* @return {boolean}
+ * @param {KeyboardEvent | null} [e]
*/
- hasFocus() {
- return this.form.contains(document.activeElement);
+ hasFocus(e) {
+ return this.form.contains(document.activeElement) || this.form.contains(
+ /** @type HTMLElement */
+ e === null || e === void 0 ? void 0 : e.target);
}
/**
* Checks that the form element doesn't contain an invalid field
@@ -8513,16 +8545,7 @@ class Form {
const allButtons =
/** @type {HTMLElement[]} */
[...this.form.querySelectorAll(selector)];
- return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form
- .filter(button => {
- if (this.isLogin) {
- return !/sign.?up/i.test(button.textContent || '');
- } else if (this.isSignup) {
- return !/(log|sign).?([io])n/i.test(button.textContent || '');
- } else {
- return true;
- }
- });
+ return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this));
}
/**
* Executes a function on input elements. Can be limited to certain element types
@@ -10260,15 +10283,15 @@ const listenForGlobalFormSubmission = forms => {
(_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler()
);
}, true);
- window.addEventListener('keypress', e => {
+ window.addEventListener('keydown', e => {
if (e.key === 'Enter') {
- const focusedForm = [...forms.values()].find(form => form.hasFocus());
+ const focusedForm = [...forms.values()].find(form => form.hasFocus(e));
focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler();
}
});
const observer = new PerformanceObserver(list => {
const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`?
- ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/));
+ ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name));
if (!entries.length) return;
const filledForm = [...forms.values()].find(form => form.hasValues());
filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
@@ -10545,14 +10568,14 @@ const matchingConfiguration = {
email: {
match: '.mail\\b',
skip: 'phone|name|reservation number',
- forceUnknown: 'search|filter|subject|title|tab'
+ forceUnknown: 'search|filter|subject|title|\btab\b'
},
password: {
match: 'password',
forceUnknown: 'captcha|mfa|2fa|two factor'
},
username: {
- match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$',
+ match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername',
forceUnknown: 'search'
},
// CC
@@ -11968,7 +11991,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd
const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]";
const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com`
"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login
-"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs
+"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs
// todo: ensure these can be set via configuration
module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR;
@@ -13713,15 +13736,13 @@ class OverlayUIController extends _UIController.UIController {
/** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */
- /**
- * @type {OverlayControllerOptions}
- */
-
/**
* @param {OverlayControllerOptions} options
*/
constructor(options) {
- super();
+ super(options); // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
_classPrivateFieldInitSpec(this, _state, {
writable: true,
@@ -13730,12 +13751,6 @@ class OverlayUIController extends _UIController.UIController {
_defineProperty(this, "_activeTooltip", null);
- _defineProperty(this, "_options", void 0);
-
- this._options = options; // We always register this 'pointerdown' event, regardless of
- // whether we have a tooltip currently open or not. This is to ensure
- // we can clear out any existing state before opening a new one.
-
window.addEventListener('pointerdown', this, true);
}
/**
@@ -13901,6 +13916,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.UIController = void 0;
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
/**
* @typedef AttachArgs The argument required to 'attach' a tooltip
* @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call
@@ -13915,6 +13932,34 @@ exports.UIController = void 0;
* This is the base interface that `UIControllers` should extend/implement
*/
class UIController {
+ /**
+ * @type {any}
+ */
+
+ /**
+ * @param {any} [options]
+ */
+ constructor(options) {
+ _defineProperty(this, "_options", void 0);
+
+ this._options = options; // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
+
+ window.addEventListener('pointerdown', this, true);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case 'pointerdown':
+ {
+ var _this$_options$onPoin, _this$_options;
+
+ (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event);
+ break;
+ }
+ }
+ }
/**
* Implement this method to control what happen when Autofill
* has enough information to 'attach' a tooltip.
@@ -13922,6 +13967,8 @@ class UIController {
* @param {AttachArgs} _args
* @returns {void}
*/
+
+
attach(_args) {
throw new Error('must implement attach');
}
@@ -14198,7 +14245,7 @@ exports.default = _default;
Object.defineProperty(exports, "__esModule", {
value: true
});
-exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
+exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
exports.escapeXML = escapeXML;
exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0;
@@ -14564,9 +14611,27 @@ const isLikelyASubmitButton = el => {
el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px
!SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel);
};
+/**
+ * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form
+ * @param {HTMLElement} el
+ * @param {import('./Form/Form').Form} formObj
+ */
+
exports.isLikelyASubmitButton = isLikelyASubmitButton;
+const buttonMatchesFormType = (el, formObj) => {
+ if (formObj.isLogin) {
+ return !/sign.?up/i.test(el.textContent || '');
+ } else if (formObj.isSignup) {
+ return !/(log|sign).?([io])n/i.test(el.textContent || '');
+ } else {
+ return true;
+ }
+};
+
+exports.buttonMatchesFormType = buttonMatchesFormType;
+
},{"./Form/matching":34}],55:[function(require,module,exports){
"use strict";
@@ -14579,8 +14644,18 @@ var _DeviceInterface = require("./DeviceInterface");
if (!window.isSecureContext) return false;
try {
- const deviceInterface = (0, _DeviceInterface.createDevice)();
- deviceInterface.init();
+ const startupAutofill = () => {
+ if (document.visibilityState === 'visible') {
+ const deviceInterface = (0, _DeviceInterface.createDevice)();
+ deviceInterface.init();
+ } else {
+ document.addEventListener('visibilitychange', startupAutofill, {
+ once: true
+ });
+ }
+ };
+
+ startupAutofill();
} catch (e) {
console.error(e); // Noop, we errored
}
@@ -14620,9 +14695,16 @@ function createGlobalConfig() {
// The native layer will inject a randomised secret here and use it to verify the origin
let secret = 'PLACEHOLDER_SECRET';
- let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame;
- const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent);
- const isMobileApp = isDDGApp && !isApp;
+ /**
+ * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name`
+ */
+ // @ts-ignore
+
+ const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore
+
+ const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore
+
+ const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid;
const isFirefox = navigator.userAgent.includes('Firefox');
const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX));
return {
@@ -14979,11 +15061,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport {
var _window$BrowserAutofi, _window$BrowserAutofi2;
if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') {
- throw new Error('window.BrowserAutofill.getAutofillData missing');
+ console.warn('window.BrowserAutofill.getAutofillData missing');
}
if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') {
- throw new Error('window.BrowserAutofill.storeFormData missing');
+ console.warn('window.BrowserAutofill.storeFormData missing');
}
}
}
diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js
index f7e1d1780..436136508 100644
--- a/swift-package/Resources/assets/autofill.js
+++ b/swift-package/Resources/assets/autofill.js
@@ -2902,7 +2902,9 @@ class AndroidInterface extends _InterfacePrototype.default {
createUIController() {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
/**
* @deprecated use `this.settings.availableInputTypes.email` in the future
@@ -3071,7 +3073,9 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
var _this$globalConfig$us, _this$globalConfig$us2;
if (((_this$globalConfig$us = this.globalConfig.userPreferences) === null || _this$globalConfig$us === void 0 ? void 0 : (_this$globalConfig$us2 = _this$globalConfig$us.platform) === null || _this$globalConfig$us2 === void 0 ? void 0 : _this$globalConfig$us2.name) === 'ios') {
- return new _NativeUIController.NativeUIController();
+ return new _NativeUIController.NativeUIController({
+ onPointerDown: event => this._onPointerDown(event)
+ });
}
if (!this.globalConfig.supportsTopFrame) {
@@ -3414,32 +3418,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default {
poll();
});
}
- /**
- * on macOS we try to detect if a click occurred within a form
- * @param {PointerEvent} event
- */
-
-
- _onPointerDown(event) {
- if (this.settings.featureToggles.credentials_saving) {
- this._detectFormSubmission(event);
- }
- }
- /**
- * @param {PointerEvent} event
- */
-
-
- _detectFormSubmission(event) {
- const matchingForm = [...this.scanner.forms.values()].find(form => {
- const btns = [...form.submitButtons]; // @ts-ignore
-
- if (btns.includes(event.target)) return true; // @ts-ignore
-
- if (btns.find(btn => btn.contains(event.target))) return true;
- });
- matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
- }
}
@@ -3826,6 +3804,8 @@ var _deviceApi = require("../../packages/device-api");
var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls");
+var _selectorsCss = require("../Form/selectors-css");
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classPrivateFieldInitSpec(obj, privateMap, value) { _checkPrivateRedeclaration(obj, privateMap); privateMap.set(obj, value); }
@@ -4554,6 +4534,55 @@ class InterfacePrototype {
this.storeFormData(withAutoGeneratedFlag);
}
}
+ /**
+ * on macOS we try to detect if a click occurred within a form
+ * @param {PointerEvent} event
+ */
+
+
+ _onPointerDown(event) {
+ if (this.settings.featureToggles.credentials_saving) {
+ this._detectFormSubmission(event);
+ }
+ }
+ /**
+ * @param {PointerEvent} event
+ */
+
+
+ _detectFormSubmission(event) {
+ const matchingForm = [...this.scanner.forms.values()].find(form => {
+ const btns = [...form.submitButtons]; // @ts-ignore
+
+ if (btns.includes(event.target)) return true; // @ts-ignore
+
+ if (btns.find(btn => btn.contains(event.target))) return true;
+ });
+ matchingForm === null || matchingForm === void 0 ? void 0 : matchingForm.submitHandler();
+
+ if (!matchingForm) {
+ var _event$target;
+
+ // check if the click happened on a button
+ const button =
+ /** @type HTMLElement */
+ (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(_selectorsCss.SUBMIT_BUTTON_SELECTOR);
+ if (!button) return;
+ const text = (0, _matching.removeExcessWhitespace)(button === null || button === void 0 ? void 0 : button.textContent);
+ const hasRelevantText = /(log|sign).?(in|up)|continue|next|submit/i.test(text);
+
+ if (hasRelevantText && text.length < 25) {
+ // check if there's a form with values
+ const filledForm = [...this.scanner.forms.values()].find(form => form.hasValues());
+
+ if (filledForm && (0, _autofillUtils.buttonMatchesFormType)(
+ /** @type HTMLElement */
+ button, filledForm)) {
+ filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
+ }
+ }
+ }
+ }
/**
* This serves as a single place to create a default instance
* of InterfacePrototype that can be useful in testing scenarios
@@ -4576,7 +4605,7 @@ class InterfacePrototype {
var _default = InterfacePrototype;
exports.default = _default;
-},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){
+},{"../../packages/device-api":2,"../Form/formatters":19,"../Form/listenForFormSubmission":23,"../Form/matching":26,"../Form/selectors-css":27,"../InputTypes/Credentials":29,"../PasswordGenerator":32,"../Scanner":33,"../Settings":34,"../UI/controllers/NativeUIController":39,"../autofill-utils":46,"../config":48,"../deviceApiCalls/__generated__/deviceApiCalls":50,"../deviceApiCalls/transports/transports":56}],16:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", {
@@ -4671,13 +4700,16 @@ class Form {
this.categorizeInputs();
}
/**
- * Checks if the form element contains the activeElement
+ * Checks if the form element contains the activeElement or the event target
* @return {boolean}
+ * @param {KeyboardEvent | null} [e]
*/
- hasFocus() {
- return this.form.contains(document.activeElement);
+ hasFocus(e) {
+ return this.form.contains(document.activeElement) || this.form.contains(
+ /** @type HTMLElement */
+ e === null || e === void 0 ? void 0 : e.target);
}
/**
* Checks that the form element doesn't contain an invalid field
@@ -4837,16 +4869,7 @@ class Form {
const allButtons =
/** @type {HTMLElement[]} */
[...this.form.querySelectorAll(selector)];
- return allButtons.filter(_autofillUtils.isLikelyASubmitButton) // filter out buttons of the wrong type - login buttons on a signup form, signup buttons on a login form
- .filter(button => {
- if (this.isLogin) {
- return !/sign.?up/i.test(button.textContent || '');
- } else if (this.isSignup) {
- return !/(log|sign).?([io])n/i.test(button.textContent || '');
- } else {
- return true;
- }
- });
+ return allButtons.filter(btn => (0, _autofillUtils.isLikelyASubmitButton)(btn) && (0, _autofillUtils.buttonMatchesFormType)(btn, this));
}
/**
* Executes a function on input elements. Can be limited to certain element types
@@ -6584,15 +6607,15 @@ const listenForGlobalFormSubmission = forms => {
(_forms$get = forms.get(e.target)) === null || _forms$get === void 0 ? void 0 : _forms$get.submitHandler()
);
}, true);
- window.addEventListener('keypress', e => {
+ window.addEventListener('keydown', e => {
if (e.key === 'Enter') {
- const focusedForm = [...forms.values()].find(form => form.hasFocus());
+ const focusedForm = [...forms.values()].find(form => form.hasFocus(e));
focusedForm === null || focusedForm === void 0 ? void 0 : focusedForm.submitHandler();
}
});
const observer = new PerformanceObserver(list => {
const entries = list.getEntries().filter(entry => // @ts-ignore why does TS not know about `entry.initiatorType`?
- ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && entry.name.match(/login|sign-in|signin/));
+ ['fetch', 'xmlhttprequest'].includes(entry.initiatorType) && /login|sign-in|signin/.test(entry.name));
if (!entries.length) return;
const filledForm = [...forms.values()].find(form => form.hasValues());
filledForm === null || filledForm === void 0 ? void 0 : filledForm.submitHandler();
@@ -6869,14 +6892,14 @@ const matchingConfiguration = {
email: {
match: '.mail\\b',
skip: 'phone|name|reservation number',
- forceUnknown: 'search|filter|subject|title|tab'
+ forceUnknown: 'search|filter|subject|title|\btab\b'
},
password: {
match: 'password',
forceUnknown: 'captcha|mfa|2fa|two factor'
},
username: {
- match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$',
+ match: '(user|account|apple|login)((.)?(name|id|login).?)?(.or.+)?$|benutzername',
forceUnknown: 'search'
},
// CC
@@ -8292,7 +8315,7 @@ const birthdayMonth = "\n[name=bday-month],\n[name=birthday_month], [name=birthd
const birthdayYear = "\n[name=bday-year],\n[name=birthday_year], [name=birthday-year],\n[name=date_of_birth_year], [name=date-of-birth-year],\n[name^=birthdate_y], [name^=birthdate-y],\n[aria-label=\"birthday\" i][placeholder=\"year\" i]";
const username = ["".concat(GENERIC_TEXT_FIELD, "[autocomplete^=user]"), "input[name=username i]", // fix for `aa.com`
"input[name=\"loginId\" i]", // fix for https://online.mbank.pl/pl/Login
-"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]"]; // todo: these are still used directly right now, mostly in scanForInputs
+"input[name=\"userID\" i]", "input[id=\"login-id\" i]", "input[name=accountname i]", "input[autocomplete=username]"]; // todo: these are still used directly right now, mostly in scanForInputs
// todo: ensure these can be set via configuration
module.exports.FORM_INPUTS_SELECTOR = FORM_INPUTS_SELECTOR;
@@ -10037,15 +10060,13 @@ class OverlayUIController extends _UIController.UIController {
/** @type {import('../HTMLTooltip.js').HTMLTooltip | null} */
- /**
- * @type {OverlayControllerOptions}
- */
-
/**
* @param {OverlayControllerOptions} options
*/
constructor(options) {
- super();
+ super(options); // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
_classPrivateFieldInitSpec(this, _state, {
writable: true,
@@ -10054,12 +10075,6 @@ class OverlayUIController extends _UIController.UIController {
_defineProperty(this, "_activeTooltip", null);
- _defineProperty(this, "_options", void 0);
-
- this._options = options; // We always register this 'pointerdown' event, regardless of
- // whether we have a tooltip currently open or not. This is to ensure
- // we can clear out any existing state before opening a new one.
-
window.addEventListener('pointerdown', this, true);
}
/**
@@ -10225,6 +10240,8 @@ Object.defineProperty(exports, "__esModule", {
});
exports.UIController = void 0;
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
/**
* @typedef AttachArgs The argument required to 'attach' a tooltip
* @property {import("../../Form/Form").Form} form the Form that triggered this 'attach' call
@@ -10239,6 +10256,34 @@ exports.UIController = void 0;
* This is the base interface that `UIControllers` should extend/implement
*/
class UIController {
+ /**
+ * @type {any}
+ */
+
+ /**
+ * @param {any} [options]
+ */
+ constructor(options) {
+ _defineProperty(this, "_options", void 0);
+
+ this._options = options; // We always register this 'pointerdown' event, regardless of
+ // whether we have a tooltip currently open or not. This is to ensure
+ // we can clear out any existing state before opening a new one.
+
+ window.addEventListener('pointerdown', this, true);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case 'pointerdown':
+ {
+ var _this$_options$onPoin, _this$_options;
+
+ (_this$_options$onPoin = (_this$_options = this._options).onPointerDown) === null || _this$_options$onPoin === void 0 ? void 0 : _this$_options$onPoin.call(_this$_options, event);
+ break;
+ }
+ }
+ }
/**
* Implement this method to control what happen when Autofill
* has enough information to 'attach' a tooltip.
@@ -10246,6 +10291,8 @@ class UIController {
* @param {AttachArgs} _args
* @returns {void}
*/
+
+
attach(_args) {
throw new Error('must implement attach');
}
@@ -10522,7 +10569,7 @@ exports.default = _default;
Object.defineProperty(exports, "__esModule", {
value: true
});
-exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
+exports.buttonMatchesFormType = exports.autofillEnabled = exports.addInlineStyles = exports.SIGN_IN_MSG = exports.ADDRESS_DOMAIN = void 0;
exports.escapeXML = escapeXML;
exports.setValue = exports.sendAndWaitForAnswer = exports.safeExecute = exports.removeInlineStyles = exports.notifyWebApp = exports.isVisible = exports.isLikelyASubmitButton = exports.isEventWithinDax = exports.isAutofillEnabledFromProcessedConfig = exports.getDaxBoundingBox = exports.formatDuckAddress = void 0;
@@ -10888,9 +10935,27 @@ const isLikelyASubmitButton = el => {
el.offsetHeight * el.offsetWidth >= 10000) && // it's a large element, at least 250x40px
!SUBMIT_BUTTON_UNLIKELY_REGEX.test(contentExcludingLabel + ' ' + ariaLabel);
};
+/**
+ * Check that a button matches the form type - login buttons on a login form, signup buttons on a signup form
+ * @param {HTMLElement} el
+ * @param {import('./Form/Form').Form} formObj
+ */
+
exports.isLikelyASubmitButton = isLikelyASubmitButton;
+const buttonMatchesFormType = (el, formObj) => {
+ if (formObj.isLogin) {
+ return !/sign.?up/i.test(el.textContent || '');
+ } else if (formObj.isSignup) {
+ return !/(log|sign).?([io])n/i.test(el.textContent || '');
+ } else {
+ return true;
+ }
+};
+
+exports.buttonMatchesFormType = buttonMatchesFormType;
+
},{"./Form/matching":26}],47:[function(require,module,exports){
"use strict";
@@ -10903,8 +10968,18 @@ var _DeviceInterface = require("./DeviceInterface");
if (!window.isSecureContext) return false;
try {
- const deviceInterface = (0, _DeviceInterface.createDevice)();
- deviceInterface.init();
+ const startupAutofill = () => {
+ if (document.visibilityState === 'visible') {
+ const deviceInterface = (0, _DeviceInterface.createDevice)();
+ deviceInterface.init();
+ } else {
+ document.addEventListener('visibilitychange', startupAutofill, {
+ once: true
+ });
+ }
+ };
+
+ startupAutofill();
} catch (e) {
console.error(e); // Noop, we errored
}
@@ -10944,9 +11019,16 @@ function createGlobalConfig() {
// The native layer will inject a randomised secret here and use it to verify the origin
let secret = 'PLACEHOLDER_SECRET';
- let isDDGApp = /(iPhone|iPad|Android|Mac).*DuckDuckGo\/[0-9]/i.test(window.navigator.userAgent) || isApp || isTopFrame;
- const isAndroid = isDDGApp && /Android/i.test(window.navigator.userAgent);
- const isMobileApp = isDDGApp && !isApp;
+ /**
+ * The user agent check will not be needed here once `android` supports `userPreferences?.platform.name`
+ */
+ // @ts-ignore
+
+ const isAndroid = (userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) === 'android' || /Android.*DuckDuckGo\/\d/i.test(window.navigator.userAgent); // @ts-ignore
+
+ const isDDGApp = ['ios', 'android', 'macos', 'windows'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid; // @ts-ignore
+
+ const isMobileApp = ['ios', 'android'].includes(userPreferences === null || userPreferences === void 0 ? void 0 : userPreferences.platform.name) || isAndroid;
const isFirefox = navigator.userAgent.includes('Firefox');
const isDDGDomain = Boolean(window.location.href.match(DDG_DOMAIN_REGEX));
return {
@@ -11188,11 +11270,11 @@ class AndroidTransport extends _deviceApi.DeviceApiTransport {
var _window$BrowserAutofi, _window$BrowserAutofi2;
if (typeof ((_window$BrowserAutofi = window.BrowserAutofill) === null || _window$BrowserAutofi === void 0 ? void 0 : _window$BrowserAutofi.getAutofillData) !== 'function') {
- throw new Error('window.BrowserAutofill.getAutofillData missing');
+ console.warn('window.BrowserAutofill.getAutofillData missing');
}
if (typeof ((_window$BrowserAutofi2 = window.BrowserAutofill) === null || _window$BrowserAutofi2 === void 0 ? void 0 : _window$BrowserAutofi2.storeFormData) !== 'function') {
- throw new Error('window.BrowserAutofill.storeFormData missing');
+ console.warn('window.BrowserAutofill.storeFormData missing');
}
}
}