Skip to content

Commit 7224654

Browse files
authored
[Google password import] autotap export button in modal (#1933)
* feat: autotap export button in modal * fix: use tappedElements to find confirm button * fix: use config * wip: keep hardcoded selector for ship review * Revert "wip: keep hardcoded selector for ship review" This reverts commit e36b0d7. * chore: remove debug * refactor: use weakset
1 parent c33e7b2 commit 7224654

File tree

2 files changed

+84
-20
lines changed

2 files changed

+84
-20
lines changed

injected/src/features/autofill-password-import.js

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const DELAY_BEFORE_ANIMATION = 300;
2020
/**
2121
* @typedef ElementConfig
2222
* @property {HTMLElement|Element|SVGElement} element
23-
* @property {ButtonAnimationStyle} animationStyle
23+
* @property {ButtonAnimationStyle|null} animationStyle
2424
* @property {boolean} shouldTap
2525
* @property {boolean} shouldWatchForRemoval
2626
* @property {boolean} tapOnce
@@ -40,6 +40,8 @@ export default class AutofillPasswordImport extends ContentFeature {
4040

4141
#signInButtonSettings;
4242

43+
#exportConfirmButtonSettings;
44+
4345
/** @type {HTMLElement|Element|SVGElement|null} */
4446
#elementToCenterOn;
4547

@@ -130,6 +132,50 @@ export default class AutofillPasswordImport extends ContentFeature {
130132
return this.#domLoaded;
131133
}
132134

135+
/**
136+
* @returns {Promise<Element|HTMLElement|null>}
137+
*/
138+
async runWithRetry(fn) {
139+
try {
140+
return await withExponentialBackoff(fn);
141+
} catch {
142+
return null;
143+
}
144+
}
145+
146+
/**
147+
* @returns {Promise<ElementConfig | null>}
148+
*/
149+
async getExportConfirmElementAndStyle() {
150+
const exportConfirmElement = await this.findExportConfirmElement();
151+
const shouldAutotap = this.#exportConfirmButtonSettings?.shouldAutotap && exportConfirmElement != null;
152+
return shouldAutotap
153+
? {
154+
animationStyle: null,
155+
element: exportConfirmElement,
156+
shouldTap: true,
157+
shouldWatchForRemoval: false,
158+
tapOnce: false,
159+
}
160+
: null;
161+
}
162+
163+
/**
164+
* @returns {Promise<ElementConfig | null>}
165+
*/
166+
async getExportElementAndStyle() {
167+
const element = await this.findExportElement();
168+
return element != null
169+
? {
170+
animationStyle: this.exportButtonAnimationStyle,
171+
element,
172+
shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false,
173+
shouldWatchForRemoval: true,
174+
tapOnce: true,
175+
}
176+
: null;
177+
}
178+
133179
/**
134180
* Takes a path and returns the element and style to animate.
135181
* @param {string} path
@@ -148,16 +194,14 @@ export default class AutofillPasswordImport extends ContentFeature {
148194
}
149195
: null;
150196
} else if (path === '/options') {
151-
const element = await this.findExportElement();
152-
return element != null
153-
? {
154-
animationStyle: this.exportButtonAnimationStyle,
155-
element,
156-
shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false,
157-
shouldWatchForRemoval: true,
158-
tapOnce: true,
159-
}
160-
: null;
197+
// If we have found the popup element, then we return that early.
198+
const isExportButtonTapped =
199+
this.currentElementConfig?.element != null && this.#tappedElements.has(this.currentElementConfig?.element);
200+
if (isExportButtonTapped) {
201+
return await this.getExportConfirmElementAndStyle();
202+
} else {
203+
return await this.getExportElementAndStyle();
204+
}
161205
} else if (path === '/intro') {
162206
const element = await this.findSignInButton();
163207
return element != null
@@ -338,6 +382,10 @@ export default class AutofillPasswordImport extends ContentFeature {
338382
element.click();
339383
}
340384

385+
async findExportConfirmElement() {
386+
return await this.runWithRetry(() => document.querySelector(this.exportConfirmButtonSelector));
387+
}
388+
341389
/**
342390
* On passwords.google.com the export button is in a container that is quite ambiguious.
343391
* To solve for that we first try to find the container and then the button inside it.
@@ -354,7 +402,7 @@ export default class AutofillPasswordImport extends ContentFeature {
354402
return document.querySelector(this.exportButtonLabelTextSelector);
355403
};
356404

357-
return await withExponentialBackoff(() => findInContainer() ?? findWithLabel());
405+
return await this.runWithRetry(() => findInContainer() ?? findWithLabel());
358406
}
359407

360408
/**
@@ -365,14 +413,14 @@ export default class AutofillPasswordImport extends ContentFeature {
365413
const settingsButton = document.querySelector(this.settingsButtonSelector);
366414
return settingsButton;
367415
};
368-
return await withExponentialBackoff(fn);
416+
return await this.runWithRetry(fn);
369417
}
370418

371419
/**
372420
* @returns {Promise<HTMLElement|Element|null>}
373421
*/
374422
async findSignInButton() {
375-
return await withExponentialBackoff(() => document.querySelector(this.signinButtonSelector));
423+
return await this.runWithRetry(() => document.querySelector(this.signinButtonSelector));
376424
}
377425

378426
/**
@@ -390,7 +438,8 @@ export default class AutofillPasswordImport extends ContentFeature {
390438
setCurrentElementConfig(config) {
391439
if (config != null) {
392440
this.#currentElementConfig = config;
393-
this.setElementToCenterOn(config.element, config.animationStyle);
441+
442+
if (config.animationStyle != null) this.setElementToCenterOn(config.element, config.animationStyle);
394443
}
395444
}
396445

@@ -400,7 +449,12 @@ export default class AutofillPasswordImport extends ContentFeature {
400449
* @returns {boolean}
401450
*/
402451
isSupportedPath(path) {
403-
return [this.#exportButtonSettings?.path, this.#settingsButtonSettings?.path, this.#signInButtonSettings?.path].includes(path);
452+
return [
453+
this.#exportButtonSettings?.path,
454+
this.#settingsButtonSettings?.path,
455+
this.#signInButtonSettings?.path,
456+
this.#exportConfirmButtonSettings?.path,
457+
].includes(path);
404458
}
405459

406460
async handlePath(path) {
@@ -426,12 +480,14 @@ export default class AutofillPasswordImport extends ContentFeature {
426480
*/
427481
async animateOrTapElement() {
428482
const { element, animationStyle, shouldTap, shouldWatchForRemoval } = this.currentElementConfig ?? {};
429-
if (element != null && animationStyle != null) {
483+
if (element != null) {
430484
if (shouldTap) {
431485
this.autotapElement(element);
432486
} else {
433-
await this.domLoaded;
434-
this.animateElement(element, animationStyle);
487+
if (animationStyle != null) {
488+
await this.domLoaded;
489+
this.animateElement(element, animationStyle);
490+
}
435491
}
436492
if (shouldWatchForRemoval) {
437493
// Sometimes navigation events are not triggered, then we need to watch for removal
@@ -449,6 +505,13 @@ export default class AutofillPasswordImport extends ContentFeature {
449505
return this.#exportButtonSettings?.selectors?.join(',');
450506
}
451507

508+
/**
509+
* @returns {string}
510+
*/
511+
get exportConfirmButtonSelector() {
512+
return this.#exportConfirmButtonSettings?.selectors?.join(',');
513+
}
514+
452515
/**
453516
* @returns {string}
454517
*/
@@ -488,6 +551,7 @@ export default class AutofillPasswordImport extends ContentFeature {
488551
this.#exportButtonSettings = this.getFeatureSetting('exportButton');
489552
this.#signInButtonSettings = this.getFeatureSetting('signInButton');
490553
this.#settingsButtonSettings = this.getFeatureSetting('settingsButton');
554+
this.#exportConfirmButtonSettings = this.getFeatureSetting('exportConfirmButton');
491555
}
492556

493557
urlChanged() {

injected/src/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -807,7 +807,7 @@ export function legacySendMessage(messageType, options) {
807807
* @param {number} delay
808808
* @param {number} [maxAttempts=4] - The maximum number of attempts to find the element.
809809
* @param {number} [delay=500] - The initial delay to be used to create the exponential backoff.
810-
* @returns {Promise<Element|HTMLElement|null>}
810+
* @returns {Promise<Element|HTMLElement>}
811811
*/
812812
export function withExponentialBackoff(fn, maxAttempts = 4, delay = 500) {
813813
return new Promise((resolve, reject) => {

0 commit comments

Comments
 (0)