From 49c5e80e44306937cb3eaa7afb690b7889de3895 Mon Sep 17 00:00:00 2001 From: Mateusz Buskiewicz Date: Fri, 27 Jan 2023 15:19:24 +0100 Subject: [PATCH] Send autofill pixels on macOS (#249) --- dist/autofill-debug.js | 137 +++++++++++------- dist/autofill.js | 124 ++++++++++------ integration-test/helpers/mocks.js | 8 + integration-test/helpers/pages.js | 31 +++- integration-test/helpers/utils.js | 11 +- integration-test/pages/signup.html | 2 + .../tests/email-autofill.extension.spec.js | 4 +- .../tests/email-autofill.macos.spec.js | 116 ++++++++++++--- packages/device-api/lib/device-api-call.js | 2 +- src/DeviceInterface/AppleDeviceInterface.js | 6 +- .../AppleOverlayDeviceInterface.js | 6 +- src/DeviceInterface/ExtensionInterface.js | 5 - src/DeviceInterface/InterfacePrototype.js | 45 ++++-- src/Form/formatters.js | 1 + src/Form/inputTypeConfig.js | 2 +- src/UI/EmailHTMLTooltip.js | 4 +- src/UI/controllers/HTMLTooltipUIController.js | 18 ++- src/autofill-utils.js | 13 +- .../__generated__/validators-ts.ts | 16 +- .../__generated__/validators.zod.js | 13 +- .../schemas/sendJSPixel.params.json | 51 +++++-- .../Resources/assets/autofill-debug.js | 137 +++++++++++------- swift-package/Resources/assets/autofill.js | 124 ++++++++++------ 23 files changed, 594 insertions(+), 282 deletions(-) diff --git a/dist/autofill-debug.js b/dist/autofill-debug.js index 0b4a0cd6d..daa3b6e83 100644 --- a/dist/autofill-debug.js +++ b/dist/autofill-debug.js @@ -4619,7 +4619,7 @@ class DeviceApiCall { const result = validator === null || validator === void 0 ? void 0 : validator.safeParse(data); if (!result) { - throw new Error('unreachable'); + throw new Error('unreachable, data failure', data); } if (!result.success) { @@ -7895,12 +7895,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { }); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleDeviceInterface = AppleDeviceInterface; @@ -8020,12 +8014,6 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.updateItems(credentials); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; @@ -8046,8 +8034,6 @@ var _HTMLTooltipUIController = require("../UI/controllers/HTMLTooltipUIControlle var _HTMLTooltip = require("../UI/HTMLTooltip.js"); -var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls.js"); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const POPUP_TYPES = { @@ -8144,12 +8130,6 @@ class ExtensionInterface extends _InterfacePrototype.default { return resolve(data); })); } - - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } /** * Used by the email web app * Settings page displays data of the logged in user data @@ -8260,7 +8240,7 @@ class ExtensionInterface extends _InterfacePrototype.default { exports.ExtensionInterface = ExtensionInterface; -},{"../UI/HTMLTooltip.js":53,"../UI/controllers/HTMLTooltipUIController.js":54,"../autofill-utils.js":60,"../deviceApiCalls/__generated__/deviceApiCalls.js":64,"./InterfacePrototype.js":27}],27:[function(require,module,exports){ +},{"../UI/HTMLTooltip.js":53,"../UI/controllers/HTMLTooltipUIController.js":54,"../autofill-utils.js":60,"./InterfacePrototype.js":27}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8788,14 +8768,16 @@ class InterfacePrototype { * When an item was selected, we then call back to the device * to fetch the full suite of data needed to complete the autofill * - * @param {InputTypeConfigs} config + * @param {import('../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject|IdentityObject|CredentialsObject)[]} items * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - onSelect(config, items, id) { + onSelect(inputType, items, id) { id = String(id); + const mainType = (0, _matching.getMainTypeFromType)(inputType); + const subtype = (0, _matching.getSubtypeFromType)(inputType); if (id === _Credentials.PROVIDER_LOCKED) { return this.askToUnlockProvider(); @@ -8805,7 +8787,7 @@ class InterfacePrototype { if (!matchingData) throw new Error('unreachable (fatal)'); const dataPromise = (() => { - switch (config.type) { + switch (mainType) { case 'creditCards': return this.getAutofillCreditCard(id); @@ -8831,13 +8813,45 @@ class InterfacePrototype { dataPromise.then(response => { if (response) { - if (config.type === 'identities') { - this.firePixel('autofill_identity'); + const data = response.success || response; + + if (mainType === 'identities') { + this.firePixel({ + pixelName: 'autofill_identity', + params: { + fieldType: subtype + } + }); + + switch (id) { + case 'personalAddress': + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + break; + + case 'privateAddress': + this.firePixel({ + pixelName: 'autofill_private_address' + }); + break; + + default: + // Also fire pixel when filling an identity with the personal duck address from an email field + const checks = [subtype === 'emailAddress', this.hasLocalAddresses, (data === null || data === void 0 ? void 0 : data.emailAddress) === (0, _autofillUtils.formatDuckAddress)(_classPrivateFieldGet(this, _addresses).personalAddress)]; + + if (checks.every(Boolean)) { + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + } + + break; + } } // some platforms do not include a `success` object, why? - const data = response.success || response; - return this.selectedDetail(data, config.type); + return this.selectedDetail(data, mainType); } else { return Promise.reject(new Error('none-success response')); } @@ -9128,11 +9142,13 @@ class InterfacePrototype { } /** * Sends a pixel to be fired on the client side - * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams['pixelName']} _pixelName + * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams} pixelParams */ - firePixel(_pixelName) {} + firePixel(pixelParams) { + this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall(pixelParams)); + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -11256,6 +11272,7 @@ const inferCountryCodeFromElement = el => { exports.inferCountryCodeFromElement = inferCountryCodeFromElement; const getMMAndYYYYFromString = expiration => { + /** @type {string[]} */ const values = expiration.match(/(\d+)/g) || []; return values === null || values === void 0 ? void 0 : values.reduce((output, current) => { if (Number(current) > 12) { @@ -11669,7 +11686,7 @@ const inputTypeConfig = { shouldDecorate: async () => false, dataType: '', tooltipItem: _data => { - throw new Error('unreachable'); + throw new Error('unreachable - setting tooltip to unknown field type'); } } }; @@ -14710,11 +14727,15 @@ class EmailHTMLTooltip extends _HTMLTooltip.default { const firePixel = this.device.firePixel.bind(this.device); this.registerClickableButton(this.usePersonalButton, () => { this.fillForm('personalAddress'); - firePixel('autofill_personal_address'); + firePixel({ + pixelName: 'autofill_personal_address' + }); }); this.registerClickableButton(this.usePrivateButton, () => { this.fillForm('privateAddress'); - firePixel('autofill_private_address'); + firePixel({ + pixelName: 'autofill_private_address' + }); }); // Get the alias from the extension this.device.getAddresses().then(this.updateAddresses); @@ -15102,6 +15123,11 @@ class HTMLTooltipUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltipOptions} */ + /** + * Overwritten when calling createTooltip + * @type {import('../../Form/matching').SupportedTypes} + */ + /** * @param {HTMLTooltipControllerOptions} options * @param {Partial} htmlTooltipOptions @@ -15116,6 +15142,8 @@ class HTMLTooltipUIController extends _UIController.UIController { _defineProperty(this, "_htmlTooltipOptions", void 0); + _defineProperty(this, "_activeInputType", 'unknown'); + this._options = options; this._htmlTooltipOptions = Object.assign({}, _HTMLTooltip.defaultOptions, htmlTooltipOptions); window.addEventListener('pointerdown', this, true); @@ -15177,13 +15205,13 @@ class HTMLTooltipUIController extends _UIController.UIController { return new _DataHTMLTooltip.default(config, topContextData.inputType, getPosition, tooltipOptions).render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(topContextData.inputType, data, id); } }); } updateItems(data) { - if (!this._activeInputType) return; + if (this._activeInputType === 'unknown') return; const config = (0, _inputTypeConfig.getInputConfigFromType)(this._activeInputType); // convert the data into tool tip item renderers const asRenderers = data.map(d => config.tooltipItem(d)); @@ -15192,7 +15220,7 @@ class HTMLTooltipUIController extends _UIController.UIController { if (activeTooltip instanceof _DataHTMLTooltip.default) { activeTooltip === null || activeTooltip === void 0 ? void 0 : activeTooltip.render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(this._activeInputType, data, id); } }); } // TODO: can we remove this timeout once implemented with real APIs? @@ -15310,14 +15338,14 @@ class HTMLTooltipUIController extends _UIController.UIController { * * Note: ideally we'd pass this data instead, so that we didn't have a circular dependency * - * @param {InputTypeConfigs} config + * @param {import('../../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject | IdentityObject | CredentialsObject)[]} data * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - _onSelect(config, data, id) { - return this._options.device.onSelect(config, data, id); + _onSelect(inputType, data, id) { + return this._options.device.onSelect(inputType, data, id); } isActive() { @@ -16051,10 +16079,10 @@ const setValue = (el, val, config) => { exports.setValue = setValue; const safeExecute = function (el, fn) { - let opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - const { - checkVisibility = true - } = opts; + let _opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + // TODO: temporary fix to misterious bug in Chrome + // const {checkVisibility = true} = opts const intObs = new IntersectionObserver(changes => { for (const change of changes) { // Feature detection @@ -16068,9 +16096,11 @@ const safeExecute = function (el, fn) { * If 'checkVisibility' is 'false' (like on Windows), then we always execute the function * During testing it was found that windows does not `change.isVisible` properly. */ - if (!checkVisibility || change.isVisible) { - fn(); - } + // TODO: temporary fix to misterious bug in Chrome + // if (!checkVisibility || change.isVisible) { + // fn() + // } + fn(); } } @@ -16826,9 +16856,16 @@ const selectedDetailParamsSchema = _zod.z.object({ exports.selectedDetailParamsSchema = selectedDetailParamsSchema; -const sendJSPixelParamsSchema = _zod.z.object({ - pixelName: _zod.z.union([_zod.z.literal("autofill_identity"), _zod.z.literal("autofill_private_address"), _zod.z.literal("autofill_personal_address")]) -}); +const sendJSPixelParamsSchema = _zod.z.union([_zod.z.object({ + pixelName: _zod.z.literal("autofill_identity"), + params: _zod.z.object({ + fieldType: _zod.z.string().optional() + }).optional() +}), _zod.z.object({ + pixelName: _zod.z.literal("autofill_personal_address") +}), _zod.z.object({ + pixelName: _zod.z.literal("autofill_private_address") +})]); exports.sendJSPixelParamsSchema = sendJSPixelParamsSchema; diff --git a/dist/autofill.js b/dist/autofill.js index f95b817d0..f9902cbba 100644 --- a/dist/autofill.js +++ b/dist/autofill.js @@ -943,7 +943,7 @@ class DeviceApiCall { const result = validator === null || validator === void 0 ? void 0 : validator.safeParse(data); if (!result) { - throw new Error('unreachable'); + throw new Error('unreachable, data failure', data); } if (!result.success) { @@ -4219,12 +4219,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { }); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleDeviceInterface = AppleDeviceInterface; @@ -4344,12 +4338,6 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.updateItems(credentials); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; @@ -4370,8 +4358,6 @@ var _HTMLTooltipUIController = require("../UI/controllers/HTMLTooltipUIControlle var _HTMLTooltip = require("../UI/HTMLTooltip.js"); -var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls.js"); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const POPUP_TYPES = { @@ -4468,12 +4454,6 @@ class ExtensionInterface extends _InterfacePrototype.default { return resolve(data); })); } - - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } /** * Used by the email web app * Settings page displays data of the logged in user data @@ -4584,7 +4564,7 @@ class ExtensionInterface extends _InterfacePrototype.default { exports.ExtensionInterface = ExtensionInterface; -},{"../UI/HTMLTooltip.js":45,"../UI/controllers/HTMLTooltipUIController.js":46,"../autofill-utils.js":52,"../deviceApiCalls/__generated__/deviceApiCalls.js":56,"./InterfacePrototype.js":19}],19:[function(require,module,exports){ +},{"../UI/HTMLTooltip.js":45,"../UI/controllers/HTMLTooltipUIController.js":46,"../autofill-utils.js":52,"./InterfacePrototype.js":19}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5112,14 +5092,16 @@ class InterfacePrototype { * When an item was selected, we then call back to the device * to fetch the full suite of data needed to complete the autofill * - * @param {InputTypeConfigs} config + * @param {import('../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject|IdentityObject|CredentialsObject)[]} items * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - onSelect(config, items, id) { + onSelect(inputType, items, id) { id = String(id); + const mainType = (0, _matching.getMainTypeFromType)(inputType); + const subtype = (0, _matching.getSubtypeFromType)(inputType); if (id === _Credentials.PROVIDER_LOCKED) { return this.askToUnlockProvider(); @@ -5129,7 +5111,7 @@ class InterfacePrototype { if (!matchingData) throw new Error('unreachable (fatal)'); const dataPromise = (() => { - switch (config.type) { + switch (mainType) { case 'creditCards': return this.getAutofillCreditCard(id); @@ -5155,13 +5137,45 @@ class InterfacePrototype { dataPromise.then(response => { if (response) { - if (config.type === 'identities') { - this.firePixel('autofill_identity'); + const data = response.success || response; + + if (mainType === 'identities') { + this.firePixel({ + pixelName: 'autofill_identity', + params: { + fieldType: subtype + } + }); + + switch (id) { + case 'personalAddress': + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + break; + + case 'privateAddress': + this.firePixel({ + pixelName: 'autofill_private_address' + }); + break; + + default: + // Also fire pixel when filling an identity with the personal duck address from an email field + const checks = [subtype === 'emailAddress', this.hasLocalAddresses, (data === null || data === void 0 ? void 0 : data.emailAddress) === (0, _autofillUtils.formatDuckAddress)(_classPrivateFieldGet(this, _addresses).personalAddress)]; + + if (checks.every(Boolean)) { + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + } + + break; + } } // some platforms do not include a `success` object, why? - const data = response.success || response; - return this.selectedDetail(data, config.type); + return this.selectedDetail(data, mainType); } else { return Promise.reject(new Error('none-success response')); } @@ -5452,11 +5466,13 @@ class InterfacePrototype { } /** * Sends a pixel to be fired on the client side - * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams['pixelName']} _pixelName + * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams} pixelParams */ - firePixel(_pixelName) {} + firePixel(pixelParams) { + this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall(pixelParams)); + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -7580,6 +7596,7 @@ const inferCountryCodeFromElement = el => { exports.inferCountryCodeFromElement = inferCountryCodeFromElement; const getMMAndYYYYFromString = expiration => { + /** @type {string[]} */ const values = expiration.match(/(\d+)/g) || []; return values === null || values === void 0 ? void 0 : values.reduce((output, current) => { if (Number(current) > 12) { @@ -7993,7 +8010,7 @@ const inputTypeConfig = { shouldDecorate: async () => false, dataType: '', tooltipItem: _data => { - throw new Error('unreachable'); + throw new Error('unreachable - setting tooltip to unknown field type'); } } }; @@ -11034,11 +11051,15 @@ class EmailHTMLTooltip extends _HTMLTooltip.default { const firePixel = this.device.firePixel.bind(this.device); this.registerClickableButton(this.usePersonalButton, () => { this.fillForm('personalAddress'); - firePixel('autofill_personal_address'); + firePixel({ + pixelName: 'autofill_personal_address' + }); }); this.registerClickableButton(this.usePrivateButton, () => { this.fillForm('privateAddress'); - firePixel('autofill_private_address'); + firePixel({ + pixelName: 'autofill_private_address' + }); }); // Get the alias from the extension this.device.getAddresses().then(this.updateAddresses); @@ -11426,6 +11447,11 @@ class HTMLTooltipUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltipOptions} */ + /** + * Overwritten when calling createTooltip + * @type {import('../../Form/matching').SupportedTypes} + */ + /** * @param {HTMLTooltipControllerOptions} options * @param {Partial} htmlTooltipOptions @@ -11440,6 +11466,8 @@ class HTMLTooltipUIController extends _UIController.UIController { _defineProperty(this, "_htmlTooltipOptions", void 0); + _defineProperty(this, "_activeInputType", 'unknown'); + this._options = options; this._htmlTooltipOptions = Object.assign({}, _HTMLTooltip.defaultOptions, htmlTooltipOptions); window.addEventListener('pointerdown', this, true); @@ -11501,13 +11529,13 @@ class HTMLTooltipUIController extends _UIController.UIController { return new _DataHTMLTooltip.default(config, topContextData.inputType, getPosition, tooltipOptions).render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(topContextData.inputType, data, id); } }); } updateItems(data) { - if (!this._activeInputType) return; + if (this._activeInputType === 'unknown') return; const config = (0, _inputTypeConfig.getInputConfigFromType)(this._activeInputType); // convert the data into tool tip item renderers const asRenderers = data.map(d => config.tooltipItem(d)); @@ -11516,7 +11544,7 @@ class HTMLTooltipUIController extends _UIController.UIController { if (activeTooltip instanceof _DataHTMLTooltip.default) { activeTooltip === null || activeTooltip === void 0 ? void 0 : activeTooltip.render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(this._activeInputType, data, id); } }); } // TODO: can we remove this timeout once implemented with real APIs? @@ -11634,14 +11662,14 @@ class HTMLTooltipUIController extends _UIController.UIController { * * Note: ideally we'd pass this data instead, so that we didn't have a circular dependency * - * @param {InputTypeConfigs} config + * @param {import('../../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject | IdentityObject | CredentialsObject)[]} data * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - _onSelect(config, data, id) { - return this._options.device.onSelect(config, data, id); + _onSelect(inputType, data, id) { + return this._options.device.onSelect(inputType, data, id); } isActive() { @@ -12375,10 +12403,10 @@ const setValue = (el, val, config) => { exports.setValue = setValue; const safeExecute = function (el, fn) { - let opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - const { - checkVisibility = true - } = opts; + let _opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + // TODO: temporary fix to misterious bug in Chrome + // const {checkVisibility = true} = opts const intObs = new IntersectionObserver(changes => { for (const change of changes) { // Feature detection @@ -12392,9 +12420,11 @@ const safeExecute = function (el, fn) { * If 'checkVisibility' is 'false' (like on Windows), then we always execute the function * During testing it was found that windows does not `change.isVisible` properly. */ - if (!checkVisibility || change.isVisible) { - fn(); - } + // TODO: temporary fix to misterious bug in Chrome + // if (!checkVisibility || change.isVisible) { + // fn() + // } + fn(); } } diff --git a/integration-test/helpers/mocks.js b/integration-test/helpers/mocks.js index 6ec779be3..b720f77e2 100644 --- a/integration-test/helpers/mocks.js +++ b/integration-test/helpers/mocks.js @@ -30,6 +30,14 @@ export const constants = { selectors: { credential: '[data-ddg-inputtype="credentials.password"]' } + }, + identities: { + id: '01', + title: 'Main identity', + emailAddress: 'user@gmail.com', + firstName: 'First', + lastName: 'Last', + phone: '+1234567890' } }, /** @type {import('../../src/deviceApiCalls/__generated__/validators-ts').AutofillFeatureToggles} */ diff --git a/integration-test/helpers/pages.js b/integration-test/helpers/pages.js index c2ca9a08e..f921624cf 100644 --- a/integration-test/helpers/pages.js +++ b/integration-test/helpers/pages.js @@ -44,19 +44,31 @@ export function signupPage (page, server) { const button = await page.waitForSelector(`button:has-text("${name}")`) await button.click({ force: true }) }, + async selectLastName (name) { + const input = page.locator('#lastname') + await input.click() + const button = await page.waitForSelector(`button:has-text("${name}")`) + await button.click({ force: true }) + }, async assertEmailValue (emailAddress) { const {selectors} = constants.fields.email const email = page.locator(selectors.identity) await expect(email).toHaveValue(emailAddress) }, + async selectFirstEmailField (selector) { + const input = page.locator(decoratedFirstInputSelector) + await input.click() + const button = page.locator(`button:has-text("${selector}")`) + await button.click({ force: true }) + }, /** - * @param {import('../../src/deviceApiCalls/__generated__/validators-ts').SendJSPixelParams[pixelName]} pixelName + * @param {import('../../src/deviceApiCalls/__generated__/validators-ts').SendJSPixelParams[]} pixels */ - async assertPixelFired (pixelName) { + async assertPixelsFired (pixels) { const calls = await mockedCalls(page, ['sendJSPixel']) expect(calls.length).toBeGreaterThanOrEqual(1) - const [, sent] = calls[0] - expect(sent.pixelName).toEqual(pixelName) + const firedPixels = calls.map(([_, {pixelName, params}]) => params ? ({pixelName, params}) : ({pixelName})) + expect(firedPixels).toEqual(pixels) }, async addNewForm () { const btn = page.locator('text=Add new form') @@ -568,7 +580,16 @@ export function emailAutofillPage (page, server) { const email = page.locator(selectors.identity) await expect(email).toHaveValue(emailAddress) }, - async assertPixelsFired (expectedPixels) { + /** + * @param {import('../../src/deviceApiCalls/__generated__/validators-ts').SendJSPixelParams[]} pixels + */ + async assertPixelsFired (pixels) { + const calls = await mockedCalls(page, ['sendJSPixel']) + expect(calls.length).toBeGreaterThanOrEqual(1) + const firedPixels = calls.map(([_, {pixelName, params}]) => params ? ({pixelName, params}) : ({pixelName})) + expect(firedPixels).toEqual(pixels) + }, + async assertExtensionPixelsCaptured (expectedPixels) { let [backgroundPage] = await page.context().backgroundPages() const backgroundPagePixels = await backgroundPage.evaluateHandle(() => { // eslint-disable-next-line no-undef diff --git a/integration-test/helpers/utils.js b/integration-test/helpers/utils.js index a95701adc..8927352a4 100644 --- a/integration-test/helpers/utils.js +++ b/integration-test/helpers/utils.js @@ -15,4 +15,13 @@ const createAvailableInputTypes = (overrides) => { } } -export {createAvailableInputTypes} +/** + * Given a Duck address returns the username without the domain + * @param {string} emailAddress + * @returns {string} + */ +const stripDuckExtension = (emailAddress) => { + return emailAddress.replace('@duck.com', '') +} + +export {createAvailableInputTypes, stripDuckExtension} diff --git a/integration-test/pages/signup.html b/integration-test/pages/signup.html index 34e20f9f9..ee8d81b54 100644 --- a/integration-test/pages/signup.html +++ b/integration-test/pages/signup.html @@ -100,6 +100,8 @@

Sign up for our services

+ + diff --git a/integration-test/tests/email-autofill.extension.spec.js b/integration-test/tests/email-autofill.extension.spec.js index 60e0f852c..6ca5b7c42 100644 --- a/integration-test/tests/email-autofill.extension.spec.js +++ b/integration-test/tests/email-autofill.extension.spec.js @@ -39,7 +39,7 @@ test.describe('chrome extension', () => { await emailPage.assertEmailValue(personalAddress) // ensure background page received pixel - await emailPage.assertPixelsFired(['autofill_personal_address']) + await emailPage.assertExtensionPixelsCaptured(['autofill_personal_address']) // now ensure a second click into the input doesn't show the dropdown await emailPage.clickIntoInput() @@ -57,6 +57,6 @@ test.describe('chrome extension', () => { await emailPage.assertEmailValue(privateAddress0) // assert that the background page received pixel - await emailPage.assertPixelsFired(['autofill_personal_address', 'autofill_private_address']) + await emailPage.assertExtensionPixelsCaptured(['autofill_personal_address', 'autofill_private_address']) }) }) diff --git a/integration-test/tests/email-autofill.macos.spec.js b/integration-test/tests/email-autofill.macos.spec.js index 689bbe011..062e37267 100644 --- a/integration-test/tests/email-autofill.macos.spec.js +++ b/integration-test/tests/email-autofill.macos.spec.js @@ -7,7 +7,7 @@ import {test as base, expect} from '@playwright/test' import {constants} from '../helpers/mocks.js' import {emailAutofillPage, signupPage} from '../helpers/pages.js' import {createWebkitMocks, macosContentScopeReplacements} from '../helpers/mocks.webkit.js' -import {createAvailableInputTypes} from '../helpers/utils.js' +import {createAvailableInputTypes, stripDuckExtension} from '../helpers/utils.js' /** * Tests for various auto-fill scenarios on macos @@ -58,6 +58,12 @@ test.describe('macos', () => { // ensure autofill populates the field await emailPage.assertEmailValue(personalAddress) + // ensure pixel was fired + await emailPage.assertPixelsFired([ + {pixelName: 'autofill_identity', params: {fieldType: 'emailAddress'}}, + {pixelName: 'autofill_personal_address'} + ]) + // ensure the popup DOES show a second time, even though Dax was not clicked (this is mac specific) await emailPage.clickIntoInput() await expect(personalAddressBtn).toBeVisible() @@ -67,6 +73,14 @@ test.describe('macos', () => { // ...and ensure the second value is the private address await emailPage.assertEmailValue(privateAddress0) + + // ensure pixel was fired + await emailPage.assertPixelsFired([ + {pixelName: 'autofill_identity', params: {fieldType: 'emailAddress'}}, + {pixelName: 'autofill_personal_address'}, + {pixelName: 'autofill_identity', params: {fieldType: 'emailAddress'}}, + {pixelName: 'autofill_private_address'} + ]) }) test.describe('auto filling a signup form', () => { async function applyScript (page) { @@ -76,14 +90,13 @@ test.describe('macos', () => { .applyTo(page) } - const {personalAddress} = constants.fields.email - let identity = { - id: '01', - title: 'Main identity', - firstName: 'shane', + const {personalAddress, privateAddress0} = constants.fields.email + const identity = constants.fields.identities + const identityWithDuckAddress = { + ...identity, emailAddress: personalAddress } - test('with an identity only', async ({page}) => { + test('with an identity only - filling firstName', async ({page}) => { await forwardConsoleMessages(page) const signup = signupPage(page, server) @@ -97,9 +110,74 @@ test.describe('macos', () => { await signup.navigate() await signup.assertEmailHasNoDaxIcon() await signup.selectGeneratedPassword() - await signup.selectFirstName('shane Main identity') + await signup.selectFirstName(identity.firstName + ' Main identity') await signup.assertEmailValue(identity.emailAddress) - await signup.assertPixelFired('autofill_identity') + await signup.assertPixelsFired([ + {pixelName: 'autofill_identity', params: {fieldType: 'firstName'}} + ]) + }) + test('with an identity only - filling lastName', async ({page}) => { + await forwardConsoleMessages(page) + const signup = signupPage(page, server) + + await createWebkitMocks() + .withAvailableInputTypes(createAvailableInputTypes()) + .withIdentity(identity) + .applyTo(page) + + await applyScript(page) + + await signup.navigate() + await signup.assertEmailHasNoDaxIcon() + await signup.selectGeneratedPassword() + await signup.selectLastName(identity.lastName + ' Main identity') + await signup.assertEmailValue(identity.emailAddress) + await signup.assertPixelsFired([ + {pixelName: 'autofill_identity', params: {fieldType: 'lastName'}} + ]) + }) + test('with an identity + Email Protection, autofill using duck address in identity', async ({page}) => { + await forwardConsoleMessages(page) + const signup = signupPage(page, server) + + await createWebkitMocks() + .withAvailableInputTypes(createAvailableInputTypes()) + .withPersonalEmail(stripDuckExtension(personalAddress)) + .withPrivateEmail(stripDuckExtension(privateAddress0)) + .withIdentity(identityWithDuckAddress) + .applyTo(page) + + await applyScript(page) + + await signup.navigate() + await signup.selectGeneratedPassword() + await signup.selectFirstEmailField(identityWithDuckAddress.emailAddress) + await signup.assertEmailValue(identityWithDuckAddress.emailAddress) + await signup.assertPixelsFired([ + {pixelName: 'autofill_identity', params: {fieldType: 'emailAddress'}}, + {pixelName: 'autofill_personal_address'} + ]) + }) + test('with an identity + Email Protection, autofill using duck address in identity triggered from name field', async ({page}) => { + await forwardConsoleMessages(page) + const signup = signupPage(page, server) + + await createWebkitMocks() + .withAvailableInputTypes(createAvailableInputTypes()) + .withPersonalEmail(stripDuckExtension(personalAddress)) + .withPrivateEmail(stripDuckExtension(privateAddress0)) + .withIdentity(identityWithDuckAddress) + .applyTo(page) + + await applyScript(page) + + await signup.navigate() + await signup.selectGeneratedPassword() + await signup.selectFirstName(identityWithDuckAddress.firstName) + await signup.assertEmailValue(identityWithDuckAddress.emailAddress) + await signup.assertPixelsFired([ + {pixelName: 'autofill_identity', params: {fieldType: 'firstName'}} + ]) }) test('with no input types', async ({page}) => { await forwardConsoleMessages(page) @@ -116,18 +194,13 @@ test.describe('macos', () => { // enable in-terminal exceptions await forwardConsoleMessages(page) - const {personalAddress} = constants.fields.email + const {personalAddress, privateAddress0} = constants.fields.email await createWebkitMocks() .withAvailableInputTypes({email: true}) - .withPrivateEmail('0') - .withPersonalEmail('shane-123') - .withIdentity({ - id: '01', - title: 'Main identity', - firstName: 'shane', - emailAddress: personalAddress - }) + .withPersonalEmail(stripDuckExtension(personalAddress)) + .withPrivateEmail(stripDuckExtension(privateAddress0)) + .withIdentity(constants.fields.identities) .applyTo(page) // Load the autofill.js script with replacements @@ -139,10 +212,13 @@ test.describe('macos', () => { const signup = signupPage(page, server) await signup.navigate() await signup.addNewForm() - await signup.selectSecondEmailField(`${personalAddress} Main identity`) + await signup.selectSecondEmailField(personalAddress) await signup.assertSecondEmailValue(personalAddress) await signup.assertFirstEmailEmpty() - await signup.assertPixelFired('autofill_identity') + await signup.assertPixelsFired([ + {pixelName: 'autofill_identity', params: {fieldType: 'emailAddress'}}, + {pixelName: 'autofill_personal_address'} + ]) }) test.describe('matching performance', () => { test.skip('matching performance v1', async ({page}) => { diff --git a/packages/device-api/lib/device-api-call.js b/packages/device-api/lib/device-api-call.js index 6f850e0d1..bf03d3bc2 100644 --- a/packages/device-api/lib/device-api-call.js +++ b/packages/device-api/lib/device-api-call.js @@ -91,7 +91,7 @@ export class DeviceApiCall { if (validator) { const result = validator?.safeParse(data) if (!result) { - throw new Error('unreachable') + throw new Error('unreachable, data failure', data) } if (!result.success) { if ('error' in result) { diff --git a/src/DeviceInterface/AppleDeviceInterface.js b/src/DeviceInterface/AppleDeviceInterface.js index ce72e05a5..0a057582c 100644 --- a/src/DeviceInterface/AppleDeviceInterface.js +++ b/src/DeviceInterface/AppleDeviceInterface.js @@ -7,7 +7,7 @@ import { OverlayUIController } from '../UI/controllers/OverlayUIController.js' import { createNotification, createRequest } from '../../packages/device-api/index.js' import { GetAlias } from '../deviceApiCalls/additionalDeviceApiCalls.js' import { NativeUIController } from '../UI/controllers/NativeUIController.js' -import {CheckCredentialsProviderStatusCall, SendJSPixelCall} from '../deviceApiCalls/__generated__/deviceApiCalls.js' +import {CheckCredentialsProviderStatusCall} from '../deviceApiCalls/__generated__/deviceApiCalls.js' import {getInputType} from '../Form/matching.js' /** @@ -358,10 +358,6 @@ class AppleDeviceInterface extends InterfacePrototype { poll() }) } - - firePixel (pixelName) { - this.deviceApi.notify(new SendJSPixelCall({pixelName})) - } } export {AppleDeviceInterface} diff --git a/src/DeviceInterface/AppleOverlayDeviceInterface.js b/src/DeviceInterface/AppleOverlayDeviceInterface.js index 830042005..5f523df93 100644 --- a/src/DeviceInterface/AppleOverlayDeviceInterface.js +++ b/src/DeviceInterface/AppleOverlayDeviceInterface.js @@ -2,7 +2,7 @@ import {AppleDeviceInterface} from './AppleDeviceInterface.js' import {HTMLTooltipUIController} from '../UI/controllers/HTMLTooltipUIController.js' import {overlayApi} from './overlayApi.js' import {createNotification, validate} from '../../packages/device-api/index.js' -import {AskToUnlockProviderCall, SendJSPixelCall} from '../deviceApiCalls/__generated__/deviceApiCalls.js' +import {AskToUnlockProviderCall} from '../deviceApiCalls/__generated__/deviceApiCalls.js' import {providerStatusUpdatedSchema} from '../deviceApiCalls/__generated__/validators.zod.js' /** @@ -93,10 +93,6 @@ class AppleOverlayDeviceInterface extends AppleDeviceInterface { // rerender the tooltip this.uiController?.updateItems(credentials) } - - firePixel (pixelName) { - this.deviceApi.notify(new SendJSPixelCall({pixelName})) - } } export { AppleOverlayDeviceInterface } diff --git a/src/DeviceInterface/ExtensionInterface.js b/src/DeviceInterface/ExtensionInterface.js index 4220ae1e7..d3768f990 100644 --- a/src/DeviceInterface/ExtensionInterface.js +++ b/src/DeviceInterface/ExtensionInterface.js @@ -9,7 +9,6 @@ import { } from '../autofill-utils.js' import {HTMLTooltipUIController} from '../UI/controllers/HTMLTooltipUIController.js' import {defaultOptions} from '../UI/HTMLTooltip.js' -import { SendJSPixelCall } from '../deviceApiCalls/__generated__/deviceApiCalls.js' const POPUP_TYPES = { EmailProtection: 'EmailProtection', @@ -104,10 +103,6 @@ class ExtensionInterface extends InterfacePrototype { )) } - firePixel (pixelName) { - this.deviceApi.notify(new SendJSPixelCall({pixelName})) - } - /** * Used by the email web app * Settings page displays data of the logged in user data diff --git a/src/DeviceInterface/InterfacePrototype.js b/src/DeviceInterface/InterfacePrototype.js index 66939f499..4d3ace6f2 100644 --- a/src/DeviceInterface/InterfacePrototype.js +++ b/src/DeviceInterface/InterfacePrototype.js @@ -7,7 +7,7 @@ import { notifyWebApp, getDaxBoundingBox } from '../autofill-utils.js' -import {getInputType, getSubtypeFromType} from '../Form/matching.js' +import {getInputType, getMainTypeFromType, getSubtypeFromType} from '../Form/matching.js' import { formatFullName } from '../Form/formatters.js' import { fromPassword, appendGeneratedId, AUTOGENERATED_KEY, PROVIDER_LOCKED } from '../InputTypes/Credentials.js' import { PasswordGenerator } from '../PasswordGenerator.js' @@ -20,7 +20,7 @@ import {DeviceApi, validate} from '../../packages/device-api/index.js' import { GetAutofillCredentialsCall, StoreFormDataCall, - AskToUnlockProviderCall + AskToUnlockProviderCall, SendJSPixelCall } from '../deviceApiCalls/__generated__/deviceApiCalls.js' import {initFormSubmissionsApi} from './initFormSubmissionsApi.js' import {providerStatusUpdatedSchema} from '../deviceApiCalls/__generated__/validators.zod.js' @@ -436,12 +436,14 @@ class InterfacePrototype { * When an item was selected, we then call back to the device * to fetch the full suite of data needed to complete the autofill * - * @param {InputTypeConfigs} config + * @param {import('../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject|IdentityObject|CredentialsObject)[]} items * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - onSelect (config, items, id) { + onSelect (inputType, items, id) { id = String(id) + const mainType = getMainTypeFromType(inputType) + const subtype = getSubtypeFromType(inputType) if (id === PROVIDER_LOCKED) { return this.askToUnlockProvider() @@ -451,7 +453,7 @@ class InterfacePrototype { if (!matchingData) throw new Error('unreachable (fatal)') const dataPromise = (() => { - switch (config.type) { + switch (mainType) { case 'creditCards': return this.getAutofillCreditCard(id) case 'identities': return this.getAutofillIdentity(id) case 'credentials': { @@ -467,12 +469,31 @@ class InterfacePrototype { // wait for the data back from the device dataPromise.then(response => { if (response) { - if (config.type === 'identities') { - this.firePixel('autofill_identity') + const data = response.success || response + if (mainType === 'identities') { + this.firePixel({pixelName: 'autofill_identity', params: {fieldType: subtype}}) + switch (id) { + case 'personalAddress': + this.firePixel({pixelName: 'autofill_personal_address'}) + break + case 'privateAddress': + this.firePixel({pixelName: 'autofill_private_address'}) + break + default: + // Also fire pixel when filling an identity with the personal duck address from an email field + const checks = [ + subtype === 'emailAddress', + this.hasLocalAddresses, + data?.emailAddress === formatDuckAddress(this.#addresses.personalAddress) + ] + if (checks.every(Boolean)) { + this.firePixel({pixelName: 'autofill_personal_address'}) + } + break + } } // some platforms do not include a `success` object, why? - const data = response.success || response - return this.selectedDetail(data, config.type) + return this.selectedDetail(data, mainType) } else { return Promise.reject(new Error('none-success response')) } @@ -715,9 +736,11 @@ class InterfacePrototype { /** * Sends a pixel to be fired on the client side - * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams['pixelName']} _pixelName + * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams} pixelParams */ - firePixel (_pixelName) {} + firePixel (pixelParams) { + this.deviceApi.notify(new SendJSPixelCall(pixelParams)) + } /** * This serves as a single place to create a default instance diff --git a/src/Form/formatters.js b/src/Form/formatters.js index 70da00dc3..aa8a3bf09 100644 --- a/src/Form/formatters.js +++ b/src/Form/formatters.js @@ -155,6 +155,7 @@ const inferCountryCodeFromElement = (el) => { * @return {{expirationYear: string, expirationMonth: string}} */ const getMMAndYYYYFromString = (expiration) => { + /** @type {string[]} */ const values = expiration.match(/(\d+)/g) || [] return values?.reduce((output, current) => { if (Number(current) > 12) { diff --git a/src/Form/inputTypeConfig.js b/src/Form/inputTypeConfig.js index 0c01d16b8..b1ffc9475 100644 --- a/src/Form/inputTypeConfig.js +++ b/src/Form/inputTypeConfig.js @@ -123,7 +123,7 @@ const inputTypeConfig = { shouldDecorate: async () => false, dataType: '', tooltipItem: (_data) => { - throw new Error('unreachable') + throw new Error('unreachable - setting tooltip to unknown field type') } } } diff --git a/src/UI/EmailHTMLTooltip.js b/src/UI/EmailHTMLTooltip.js index b9cbeffd7..845d435e7 100644 --- a/src/UI/EmailHTMLTooltip.js +++ b/src/UI/EmailHTMLTooltip.js @@ -42,11 +42,11 @@ ${this.options.css} this.registerClickableButton(this.usePersonalButton, () => { this.fillForm('personalAddress') - firePixel('autofill_personal_address') + firePixel({pixelName: 'autofill_personal_address'}) }) this.registerClickableButton(this.usePrivateButton, () => { this.fillForm('privateAddress') - firePixel('autofill_private_address') + firePixel({pixelName: 'autofill_private_address'}) }) // Get the alias from the extension diff --git a/src/UI/controllers/HTMLTooltipUIController.js b/src/UI/controllers/HTMLTooltipUIController.js index c36288527..2c74cc98d 100644 --- a/src/UI/controllers/HTMLTooltipUIController.js +++ b/src/UI/controllers/HTMLTooltipUIController.js @@ -28,6 +28,12 @@ export class HTMLTooltipUIController extends UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltipOptions} */ _htmlTooltipOptions; + /** + * Overwritten when calling createTooltip + * @type {import('../../Form/matching').SupportedTypes} + */ + _activeInputType = 'unknown'; + /** * @param {HTMLTooltipControllerOptions} options * @param {Partial} htmlTooltipOptions @@ -91,13 +97,13 @@ export class HTMLTooltipUIController extends UIController { return new DataHTMLTooltip(config, topContextData.inputType, getPosition, tooltipOptions) .render(config, asRenderers, { onSelect: (id) => { - this._onSelect(config, data, id) + this._onSelect(topContextData.inputType, data, id) } }) } updateItems (data) { - if (!this._activeInputType) return + if (this._activeInputType === 'unknown') return const config = getInputConfigFromType(this._activeInputType) @@ -108,7 +114,7 @@ export class HTMLTooltipUIController extends UIController { if (activeTooltip instanceof DataHTMLTooltip) { activeTooltip?.render(config, asRenderers, { onSelect: (id) => { - this._onSelect(config, data, id) + this._onSelect(this._activeInputType, data, id) } }) } @@ -213,12 +219,12 @@ export class HTMLTooltipUIController extends UIController { * * Note: ideally we'd pass this data instead, so that we didn't have a circular dependency * - * @param {InputTypeConfigs} config + * @param {import('../../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject | IdentityObject | CredentialsObject)[]} data * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - _onSelect (config, data, id) { - return this._options.device.onSelect(config, data, id) + _onSelect (inputType, data, id) { + return this._options.device.onSelect(inputType, data, id) } isActive () { diff --git a/src/autofill-utils.js b/src/autofill-utils.js index 31a3a2a49..40806a977 100644 --- a/src/autofill-utils.js +++ b/src/autofill-utils.js @@ -176,8 +176,9 @@ const setValue = (el, val, config) => { * Use IntersectionObserver v2 to make sure the element is visible when clicked * https://developers.google.com/web/updates/2019/02/intersectionobserver-v2 */ -const safeExecute = (el, fn, opts = {}) => { - const {checkVisibility = true} = opts +const safeExecute = (el, fn, _opts = {}) => { + // TODO: temporary fix to misterious bug in Chrome + // const {checkVisibility = true} = opts const intObs = new IntersectionObserver((changes) => { for (const change of changes) { // Feature detection @@ -190,9 +191,11 @@ const safeExecute = (el, fn, opts = {}) => { * If 'checkVisibility' is 'false' (like on Windows), then we always execute the function * During testing it was found that windows does not `change.isVisible` properly. */ - if (!checkVisibility || change.isVisible) { - fn() - } + // TODO: temporary fix to misterious bug in Chrome + // if (!checkVisibility || change.isVisible) { + // fn() + // } + fn() } } intObs.disconnect() diff --git a/src/deviceApiCalls/__generated__/validators-ts.ts b/src/deviceApiCalls/__generated__/validators-ts.ts index c1ffe4665..1e4b6ddcf 100644 --- a/src/deviceApiCalls/__generated__/validators-ts.ts +++ b/src/deviceApiCalls/__generated__/validators-ts.ts @@ -649,9 +649,19 @@ export interface SelectedDetailParams { /** * Send pixels data to be fired from the native layer */ -export interface SendJSPixelParams { - pixelName: "autofill_identity" | "autofill_private_address" | "autofill_personal_address"; -} +export type SendJSPixelParams = + | { + pixelName: "autofill_identity"; + params?: { + fieldType?: string; + }; + } + | { + pixelName: "autofill_personal_address"; + } + | { + pixelName: "autofill_private_address"; + }; // setSize.params.json diff --git a/src/deviceApiCalls/__generated__/validators.zod.js b/src/deviceApiCalls/__generated__/validators.zod.js index a45c559bd..d32ab1818 100644 --- a/src/deviceApiCalls/__generated__/validators.zod.js +++ b/src/deviceApiCalls/__generated__/validators.zod.js @@ -156,9 +156,16 @@ export const selectedDetailParamsSchema = z.object({ configType: z.string() }); -export const sendJSPixelParamsSchema = z.object({ - pixelName: z.union([z.literal("autofill_identity"), z.literal("autofill_private_address"), z.literal("autofill_personal_address")]) -}); +export const sendJSPixelParamsSchema = z.union([z.object({ + pixelName: z.literal("autofill_identity"), + params: z.object({ + fieldType: z.string().optional() + }).optional() + }), z.object({ + pixelName: z.literal("autofill_personal_address") + }), z.object({ + pixelName: z.literal("autofill_private_address") + })]); export const setSizeParamsSchema = z.object({ height: z.number(), diff --git a/src/deviceApiCalls/schemas/sendJSPixel.params.json b/src/deviceApiCalls/schemas/sendJSPixel.params.json index 281b5c47d..c0055a116 100644 --- a/src/deviceApiCalls/schemas/sendJSPixel.params.json +++ b/src/deviceApiCalls/schemas/sendJSPixel.params.json @@ -4,25 +4,50 @@ "type": "object", "description": "Send pixels data to be fired from the native layer", "additionalProperties": false, - "properties": { - "pixelName": { - "oneOf": [ - { + "oneOf": [ + { + "properties": { + "pixelName": { "type": "string", "const": "autofill_identity" }, - { - "type": "string", - "const": "autofill_private_address" - }, - { + "params": { + "properties": { + "fieldType": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "required": [ + "pixelName" + ], + "additionalProperties": false + }, + { + "properties": { + "pixelName": { "type": "string", "const": "autofill_personal_address" } - ] + }, + "required": [ + "pixelName" + ], + "additionalProperties": false + }, + { + "properties": { + "pixelName": { + "type": "string", + "const": "autofill_private_address" + } + }, + "required": [ + "pixelName" + ], + "additionalProperties": false } - }, - "required": [ - "pixelName" ] } diff --git a/swift-package/Resources/assets/autofill-debug.js b/swift-package/Resources/assets/autofill-debug.js index 0b4a0cd6d..daa3b6e83 100644 --- a/swift-package/Resources/assets/autofill-debug.js +++ b/swift-package/Resources/assets/autofill-debug.js @@ -4619,7 +4619,7 @@ class DeviceApiCall { const result = validator === null || validator === void 0 ? void 0 : validator.safeParse(data); if (!result) { - throw new Error('unreachable'); + throw new Error('unreachable, data failure', data); } if (!result.success) { @@ -7895,12 +7895,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { }); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleDeviceInterface = AppleDeviceInterface; @@ -8020,12 +8014,6 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.updateItems(credentials); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; @@ -8046,8 +8034,6 @@ var _HTMLTooltipUIController = require("../UI/controllers/HTMLTooltipUIControlle var _HTMLTooltip = require("../UI/HTMLTooltip.js"); -var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls.js"); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const POPUP_TYPES = { @@ -8144,12 +8130,6 @@ class ExtensionInterface extends _InterfacePrototype.default { return resolve(data); })); } - - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } /** * Used by the email web app * Settings page displays data of the logged in user data @@ -8260,7 +8240,7 @@ class ExtensionInterface extends _InterfacePrototype.default { exports.ExtensionInterface = ExtensionInterface; -},{"../UI/HTMLTooltip.js":53,"../UI/controllers/HTMLTooltipUIController.js":54,"../autofill-utils.js":60,"../deviceApiCalls/__generated__/deviceApiCalls.js":64,"./InterfacePrototype.js":27}],27:[function(require,module,exports){ +},{"../UI/HTMLTooltip.js":53,"../UI/controllers/HTMLTooltipUIController.js":54,"../autofill-utils.js":60,"./InterfacePrototype.js":27}],27:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -8788,14 +8768,16 @@ class InterfacePrototype { * When an item was selected, we then call back to the device * to fetch the full suite of data needed to complete the autofill * - * @param {InputTypeConfigs} config + * @param {import('../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject|IdentityObject|CredentialsObject)[]} items * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - onSelect(config, items, id) { + onSelect(inputType, items, id) { id = String(id); + const mainType = (0, _matching.getMainTypeFromType)(inputType); + const subtype = (0, _matching.getSubtypeFromType)(inputType); if (id === _Credentials.PROVIDER_LOCKED) { return this.askToUnlockProvider(); @@ -8805,7 +8787,7 @@ class InterfacePrototype { if (!matchingData) throw new Error('unreachable (fatal)'); const dataPromise = (() => { - switch (config.type) { + switch (mainType) { case 'creditCards': return this.getAutofillCreditCard(id); @@ -8831,13 +8813,45 @@ class InterfacePrototype { dataPromise.then(response => { if (response) { - if (config.type === 'identities') { - this.firePixel('autofill_identity'); + const data = response.success || response; + + if (mainType === 'identities') { + this.firePixel({ + pixelName: 'autofill_identity', + params: { + fieldType: subtype + } + }); + + switch (id) { + case 'personalAddress': + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + break; + + case 'privateAddress': + this.firePixel({ + pixelName: 'autofill_private_address' + }); + break; + + default: + // Also fire pixel when filling an identity with the personal duck address from an email field + const checks = [subtype === 'emailAddress', this.hasLocalAddresses, (data === null || data === void 0 ? void 0 : data.emailAddress) === (0, _autofillUtils.formatDuckAddress)(_classPrivateFieldGet(this, _addresses).personalAddress)]; + + if (checks.every(Boolean)) { + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + } + + break; + } } // some platforms do not include a `success` object, why? - const data = response.success || response; - return this.selectedDetail(data, config.type); + return this.selectedDetail(data, mainType); } else { return Promise.reject(new Error('none-success response')); } @@ -9128,11 +9142,13 @@ class InterfacePrototype { } /** * Sends a pixel to be fired on the client side - * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams['pixelName']} _pixelName + * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams} pixelParams */ - firePixel(_pixelName) {} + firePixel(pixelParams) { + this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall(pixelParams)); + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -11256,6 +11272,7 @@ const inferCountryCodeFromElement = el => { exports.inferCountryCodeFromElement = inferCountryCodeFromElement; const getMMAndYYYYFromString = expiration => { + /** @type {string[]} */ const values = expiration.match(/(\d+)/g) || []; return values === null || values === void 0 ? void 0 : values.reduce((output, current) => { if (Number(current) > 12) { @@ -11669,7 +11686,7 @@ const inputTypeConfig = { shouldDecorate: async () => false, dataType: '', tooltipItem: _data => { - throw new Error('unreachable'); + throw new Error('unreachable - setting tooltip to unknown field type'); } } }; @@ -14710,11 +14727,15 @@ class EmailHTMLTooltip extends _HTMLTooltip.default { const firePixel = this.device.firePixel.bind(this.device); this.registerClickableButton(this.usePersonalButton, () => { this.fillForm('personalAddress'); - firePixel('autofill_personal_address'); + firePixel({ + pixelName: 'autofill_personal_address' + }); }); this.registerClickableButton(this.usePrivateButton, () => { this.fillForm('privateAddress'); - firePixel('autofill_private_address'); + firePixel({ + pixelName: 'autofill_private_address' + }); }); // Get the alias from the extension this.device.getAddresses().then(this.updateAddresses); @@ -15102,6 +15123,11 @@ class HTMLTooltipUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltipOptions} */ + /** + * Overwritten when calling createTooltip + * @type {import('../../Form/matching').SupportedTypes} + */ + /** * @param {HTMLTooltipControllerOptions} options * @param {Partial} htmlTooltipOptions @@ -15116,6 +15142,8 @@ class HTMLTooltipUIController extends _UIController.UIController { _defineProperty(this, "_htmlTooltipOptions", void 0); + _defineProperty(this, "_activeInputType", 'unknown'); + this._options = options; this._htmlTooltipOptions = Object.assign({}, _HTMLTooltip.defaultOptions, htmlTooltipOptions); window.addEventListener('pointerdown', this, true); @@ -15177,13 +15205,13 @@ class HTMLTooltipUIController extends _UIController.UIController { return new _DataHTMLTooltip.default(config, topContextData.inputType, getPosition, tooltipOptions).render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(topContextData.inputType, data, id); } }); } updateItems(data) { - if (!this._activeInputType) return; + if (this._activeInputType === 'unknown') return; const config = (0, _inputTypeConfig.getInputConfigFromType)(this._activeInputType); // convert the data into tool tip item renderers const asRenderers = data.map(d => config.tooltipItem(d)); @@ -15192,7 +15220,7 @@ class HTMLTooltipUIController extends _UIController.UIController { if (activeTooltip instanceof _DataHTMLTooltip.default) { activeTooltip === null || activeTooltip === void 0 ? void 0 : activeTooltip.render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(this._activeInputType, data, id); } }); } // TODO: can we remove this timeout once implemented with real APIs? @@ -15310,14 +15338,14 @@ class HTMLTooltipUIController extends _UIController.UIController { * * Note: ideally we'd pass this data instead, so that we didn't have a circular dependency * - * @param {InputTypeConfigs} config + * @param {import('../../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject | IdentityObject | CredentialsObject)[]} data * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - _onSelect(config, data, id) { - return this._options.device.onSelect(config, data, id); + _onSelect(inputType, data, id) { + return this._options.device.onSelect(inputType, data, id); } isActive() { @@ -16051,10 +16079,10 @@ const setValue = (el, val, config) => { exports.setValue = setValue; const safeExecute = function (el, fn) { - let opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - const { - checkVisibility = true - } = opts; + let _opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + // TODO: temporary fix to misterious bug in Chrome + // const {checkVisibility = true} = opts const intObs = new IntersectionObserver(changes => { for (const change of changes) { // Feature detection @@ -16068,9 +16096,11 @@ const safeExecute = function (el, fn) { * If 'checkVisibility' is 'false' (like on Windows), then we always execute the function * During testing it was found that windows does not `change.isVisible` properly. */ - if (!checkVisibility || change.isVisible) { - fn(); - } + // TODO: temporary fix to misterious bug in Chrome + // if (!checkVisibility || change.isVisible) { + // fn() + // } + fn(); } } @@ -16826,9 +16856,16 @@ const selectedDetailParamsSchema = _zod.z.object({ exports.selectedDetailParamsSchema = selectedDetailParamsSchema; -const sendJSPixelParamsSchema = _zod.z.object({ - pixelName: _zod.z.union([_zod.z.literal("autofill_identity"), _zod.z.literal("autofill_private_address"), _zod.z.literal("autofill_personal_address")]) -}); +const sendJSPixelParamsSchema = _zod.z.union([_zod.z.object({ + pixelName: _zod.z.literal("autofill_identity"), + params: _zod.z.object({ + fieldType: _zod.z.string().optional() + }).optional() +}), _zod.z.object({ + pixelName: _zod.z.literal("autofill_personal_address") +}), _zod.z.object({ + pixelName: _zod.z.literal("autofill_private_address") +})]); exports.sendJSPixelParamsSchema = sendJSPixelParamsSchema; diff --git a/swift-package/Resources/assets/autofill.js b/swift-package/Resources/assets/autofill.js index f95b817d0..f9902cbba 100644 --- a/swift-package/Resources/assets/autofill.js +++ b/swift-package/Resources/assets/autofill.js @@ -943,7 +943,7 @@ class DeviceApiCall { const result = validator === null || validator === void 0 ? void 0 : validator.safeParse(data); if (!result) { - throw new Error('unreachable'); + throw new Error('unreachable, data failure', data); } if (!result.success) { @@ -4219,12 +4219,6 @@ class AppleDeviceInterface extends _InterfacePrototype.default { }); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleDeviceInterface = AppleDeviceInterface; @@ -4344,12 +4338,6 @@ class AppleOverlayDeviceInterface extends _AppleDeviceInterface.AppleDeviceInter (_this$uiController = this.uiController) === null || _this$uiController === void 0 ? void 0 : _this$uiController.updateItems(credentials); } - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } - } exports.AppleOverlayDeviceInterface = AppleOverlayDeviceInterface; @@ -4370,8 +4358,6 @@ var _HTMLTooltipUIController = require("../UI/controllers/HTMLTooltipUIControlle var _HTMLTooltip = require("../UI/HTMLTooltip.js"); -var _deviceApiCalls = require("../deviceApiCalls/__generated__/deviceApiCalls.js"); - function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const POPUP_TYPES = { @@ -4468,12 +4454,6 @@ class ExtensionInterface extends _InterfacePrototype.default { return resolve(data); })); } - - firePixel(pixelName) { - this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall({ - pixelName - })); - } /** * Used by the email web app * Settings page displays data of the logged in user data @@ -4584,7 +4564,7 @@ class ExtensionInterface extends _InterfacePrototype.default { exports.ExtensionInterface = ExtensionInterface; -},{"../UI/HTMLTooltip.js":45,"../UI/controllers/HTMLTooltipUIController.js":46,"../autofill-utils.js":52,"../deviceApiCalls/__generated__/deviceApiCalls.js":56,"./InterfacePrototype.js":19}],19:[function(require,module,exports){ +},{"../UI/HTMLTooltip.js":45,"../UI/controllers/HTMLTooltipUIController.js":46,"../autofill-utils.js":52,"./InterfacePrototype.js":19}],19:[function(require,module,exports){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -5112,14 +5092,16 @@ class InterfacePrototype { * When an item was selected, we then call back to the device * to fetch the full suite of data needed to complete the autofill * - * @param {InputTypeConfigs} config + * @param {import('../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject|IdentityObject|CredentialsObject)[]} items * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - onSelect(config, items, id) { + onSelect(inputType, items, id) { id = String(id); + const mainType = (0, _matching.getMainTypeFromType)(inputType); + const subtype = (0, _matching.getSubtypeFromType)(inputType); if (id === _Credentials.PROVIDER_LOCKED) { return this.askToUnlockProvider(); @@ -5129,7 +5111,7 @@ class InterfacePrototype { if (!matchingData) throw new Error('unreachable (fatal)'); const dataPromise = (() => { - switch (config.type) { + switch (mainType) { case 'creditCards': return this.getAutofillCreditCard(id); @@ -5155,13 +5137,45 @@ class InterfacePrototype { dataPromise.then(response => { if (response) { - if (config.type === 'identities') { - this.firePixel('autofill_identity'); + const data = response.success || response; + + if (mainType === 'identities') { + this.firePixel({ + pixelName: 'autofill_identity', + params: { + fieldType: subtype + } + }); + + switch (id) { + case 'personalAddress': + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + break; + + case 'privateAddress': + this.firePixel({ + pixelName: 'autofill_private_address' + }); + break; + + default: + // Also fire pixel when filling an identity with the personal duck address from an email field + const checks = [subtype === 'emailAddress', this.hasLocalAddresses, (data === null || data === void 0 ? void 0 : data.emailAddress) === (0, _autofillUtils.formatDuckAddress)(_classPrivateFieldGet(this, _addresses).personalAddress)]; + + if (checks.every(Boolean)) { + this.firePixel({ + pixelName: 'autofill_personal_address' + }); + } + + break; + } } // some platforms do not include a `success` object, why? - const data = response.success || response; - return this.selectedDetail(data, config.type); + return this.selectedDetail(data, mainType); } else { return Promise.reject(new Error('none-success response')); } @@ -5452,11 +5466,13 @@ class InterfacePrototype { } /** * Sends a pixel to be fired on the client side - * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams['pixelName']} _pixelName + * @param {import('../deviceApiCalls/__generated__/validators-ts').SendJSPixelParams} pixelParams */ - firePixel(_pixelName) {} + firePixel(pixelParams) { + this.deviceApi.notify(new _deviceApiCalls.SendJSPixelCall(pixelParams)); + } /** * This serves as a single place to create a default instance * of InterfacePrototype that can be useful in testing scenarios @@ -7580,6 +7596,7 @@ const inferCountryCodeFromElement = el => { exports.inferCountryCodeFromElement = inferCountryCodeFromElement; const getMMAndYYYYFromString = expiration => { + /** @type {string[]} */ const values = expiration.match(/(\d+)/g) || []; return values === null || values === void 0 ? void 0 : values.reduce((output, current) => { if (Number(current) > 12) { @@ -7993,7 +8010,7 @@ const inputTypeConfig = { shouldDecorate: async () => false, dataType: '', tooltipItem: _data => { - throw new Error('unreachable'); + throw new Error('unreachable - setting tooltip to unknown field type'); } } }; @@ -11034,11 +11051,15 @@ class EmailHTMLTooltip extends _HTMLTooltip.default { const firePixel = this.device.firePixel.bind(this.device); this.registerClickableButton(this.usePersonalButton, () => { this.fillForm('personalAddress'); - firePixel('autofill_personal_address'); + firePixel({ + pixelName: 'autofill_personal_address' + }); }); this.registerClickableButton(this.usePrivateButton, () => { this.fillForm('privateAddress'); - firePixel('autofill_private_address'); + firePixel({ + pixelName: 'autofill_private_address' + }); }); // Get the alias from the extension this.device.getAddresses().then(this.updateAddresses); @@ -11426,6 +11447,11 @@ class HTMLTooltipUIController extends _UIController.UIController { /** @type {import('../HTMLTooltip.js').HTMLTooltipOptions} */ + /** + * Overwritten when calling createTooltip + * @type {import('../../Form/matching').SupportedTypes} + */ + /** * @param {HTMLTooltipControllerOptions} options * @param {Partial} htmlTooltipOptions @@ -11440,6 +11466,8 @@ class HTMLTooltipUIController extends _UIController.UIController { _defineProperty(this, "_htmlTooltipOptions", void 0); + _defineProperty(this, "_activeInputType", 'unknown'); + this._options = options; this._htmlTooltipOptions = Object.assign({}, _HTMLTooltip.defaultOptions, htmlTooltipOptions); window.addEventListener('pointerdown', this, true); @@ -11501,13 +11529,13 @@ class HTMLTooltipUIController extends _UIController.UIController { return new _DataHTMLTooltip.default(config, topContextData.inputType, getPosition, tooltipOptions).render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(topContextData.inputType, data, id); } }); } updateItems(data) { - if (!this._activeInputType) return; + if (this._activeInputType === 'unknown') return; const config = (0, _inputTypeConfig.getInputConfigFromType)(this._activeInputType); // convert the data into tool tip item renderers const asRenderers = data.map(d => config.tooltipItem(d)); @@ -11516,7 +11544,7 @@ class HTMLTooltipUIController extends _UIController.UIController { if (activeTooltip instanceof _DataHTMLTooltip.default) { activeTooltip === null || activeTooltip === void 0 ? void 0 : activeTooltip.render(config, asRenderers, { onSelect: id => { - this._onSelect(config, data, id); + this._onSelect(this._activeInputType, data, id); } }); } // TODO: can we remove this timeout once implemented with real APIs? @@ -11634,14 +11662,14 @@ class HTMLTooltipUIController extends _UIController.UIController { * * Note: ideally we'd pass this data instead, so that we didn't have a circular dependency * - * @param {InputTypeConfigs} config + * @param {import('../../Form/matching').SupportedTypes} inputType * @param {(CreditCardObject | IdentityObject | CredentialsObject)[]} data * @param {CreditCardObject['id']|IdentityObject['id']|CredentialsObject['id']} id */ - _onSelect(config, data, id) { - return this._options.device.onSelect(config, data, id); + _onSelect(inputType, data, id) { + return this._options.device.onSelect(inputType, data, id); } isActive() { @@ -12375,10 +12403,10 @@ const setValue = (el, val, config) => { exports.setValue = setValue; const safeExecute = function (el, fn) { - let opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - const { - checkVisibility = true - } = opts; + let _opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + // TODO: temporary fix to misterious bug in Chrome + // const {checkVisibility = true} = opts const intObs = new IntersectionObserver(changes => { for (const change of changes) { // Feature detection @@ -12392,9 +12420,11 @@ const safeExecute = function (el, fn) { * If 'checkVisibility' is 'false' (like on Windows), then we always execute the function * During testing it was found that windows does not `change.isVisible` properly. */ - if (!checkVisibility || change.isVisible) { - fn(); - } + // TODO: temporary fix to misterious bug in Chrome + // if (!checkVisibility || change.isVisible) { + // fn() + // } + fn(); } }