Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Autofill password import] Misc fixes #1184

Merged
merged 25 commits into from
Nov 7, 2024
Merged
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2ab15e9
fix: supported path
dbajpeyi Oct 31, 2024
b079299
feat: add a more resilient animation
dbajpeyi Oct 31, 2024
b3ade5c
fixme: hardcode some offsets
dbajpeyi Nov 1, 2024
ccdb9ce
chore: lint-fix
dbajpeyi Nov 1, 2024
7a69964
fix: use calc
dbajpeyi Nov 1, 2024
38f843d
feat: use overlay to style elements
dbajpeyi Nov 4, 2024
6dde0cd
Merge branch 'main' into dbajpeyi/fix/supported-paths
dbajpeyi Nov 4, 2024
474a9aa
style: lint fix
dbajpeyi Nov 4, 2024
100aa6b
Merge branch 'dbajpeyi/fix/supported-paths' of github.com:duckduckgo/…
dbajpeyi Nov 4, 2024
e19c1a9
Merge branch 'main' into dbajpeyi/fix/supported-paths
dbajpeyi Nov 5, 2024
05f8244
chore: add some comments
dbajpeyi Nov 5, 2024
8ad828f
Merge branch 'dbajpeyi/fix/supported-paths' of github.com:duckduckgo/…
dbajpeyi Nov 5, 2024
39d938b
fix: remove offset
dbajpeyi Nov 5, 2024
f0c047a
chore: PR comments
dbajpeyi Nov 5, 2024
db41b3b
test: update for expecting overlay
dbajpeyi Nov 5, 2024
71aed62
fix: domcontentloaded event instead of timeout
dbajpeyi Nov 5, 2024
7d4a3af
Merge branch 'main' into dbajpeyi/fix/supported-paths
dbajpeyi Nov 5, 2024
da4ccee
feat: refactor for scroll
dbajpeyi Nov 5, 2024
019e259
Merge branch 'main' into dbajpeyi/fix/supported-paths
dbajpeyi Nov 5, 2024
b1fd8f0
chore: merge main, run eslint
dbajpeyi Nov 7, 2024
12b3e53
refactor: add state
dbajpeyi Nov 7, 2024
5236240
refactor: store domloaded promise
dbajpeyi Nov 7, 2024
f0bd874
fix: remove overlay early
dbajpeyi Nov 7, 2024
17a23e5
fix: account for slow inejction
dbajpeyi Nov 7, 2024
4e23fb3
Merge branch 'main' into dbajpeyi/fix/supported-paths
dbajpeyi Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 143 additions & 32 deletions injected/src/features/autofill-password-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { DDGProxy, DDGReflect, withExponentialBackoff } from '../utils'

const ANIMATION_DURATION_MS = 1000
const ANIMATION_ITERATIONS = Infinity
const BACKGROUND_COLOR_START = 'rgba(85, 127, 243, 0.10)'
const BACKGROUND_COLOR_END = 'rgba(85, 127, 243, 0.25)'
const OVERLAY_ID = 'ddg-password-import-overlay'
const ANIMATION_TIMEOUT = 300

/**
* This feature is responsible for animating some buttons passwords.google.com,
Expand All @@ -21,8 +25,14 @@ export default class AutofillPasswordImport extends ContentFeature {
*/
get settingsButtonStyle () {
return {
scale: 1,
backgroundColor: 'rgba(0, 39, 142, 0.5)'
transform: {
start: 'scale(0.90)',
mid: 'scale(0.96)'
},
zIndex: '984',
borderRadius: '100%',
offsetLeftEm: 0.02,
offsetTopEm: 0
}
}

Expand All @@ -31,8 +41,14 @@ export default class AutofillPasswordImport extends ContentFeature {
*/
get exportButtonStyle () {
return {
scale: 1.01,
backgroundColor: 'rgba(0, 39, 142, 0.5)'
transform: {
start: 'scale(1)',
mid: 'scale(1.01)'
},
zIndex: '984',
borderRadius: '100%',
offsetLeftEm: 0,
offsetTopEm: 0
}
}

Expand All @@ -41,15 +57,21 @@ export default class AutofillPasswordImport extends ContentFeature {
*/
get signInButtonStyle () {
return {
scale: 1.5,
backgroundColor: 'rgba(0, 39, 142, 0.5)'
transform: {
start: 'scale(1)',
mid: 'scale(1.3, 1.5)'
},
zIndex: '999',
borderRadius: '2px',
offsetLeftEm: 0,
offsetTopEm: -0.05
}
}

/**
* Takes a path and returns the element and style to animate.
* @param {string} path
* @returns {Promise<{element: HTMLElement|Element, style: any, shouldTap: boolean}|null>}
* @returns {Promise<{element: HTMLElement|Element, style: any, shouldTap: boolean, shouldWatchForRemoval: boolean}|null>}
*/
async getElementAndStyleFromPath (path) {
if (path === '/') {
Expand All @@ -58,7 +80,8 @@ export default class AutofillPasswordImport extends ContentFeature {
? {
style: this.settingsButtonStyle,
element,
shouldTap: this.#settingsButtonSettings?.shouldAutotap ?? false
shouldTap: this.#settingsButtonSettings?.shouldAutotap ?? false,
shouldWatchForRemoval: false
}
: null
} else if (path === '/options') {
Expand All @@ -67,7 +90,8 @@ export default class AutofillPasswordImport extends ContentFeature {
? {
style: this.exportButtonStyle,
element,
shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false
shouldTap: this.#exportButtonSettings?.shouldAutotap ?? false,
shouldWatchForRemoval: true
}
: null
} else if (path === '/intro') {
Expand All @@ -76,39 +100,116 @@ export default class AutofillPasswordImport extends ContentFeature {
? {
style: this.signInButtonStyle,
element,
shouldTap: this.#signInButtonSettings?.shouldAutotap ?? false
shouldTap: this.#signInButtonSettings?.shouldAutotap ?? false,
shouldWatchForRemoval: false
}
: null
} else {
return null
}
}

/**
* Removes the overlay if it exists.
*/
removeOverlayIfNeeded () {
const existingOverlay = document.getElementById(OVERLAY_ID)
if (existingOverlay != null) {
existingOverlay.style.display = 'none'
existingOverlay.remove()
}
}

/**
* Inserts an overlay element to animate, by adding a div to the body
* and styling it based on the found element.
* @param {HTMLElement|Element} mainElement
* @param {any} style
* @returns {HTMLElement|Element|null}
*/
insertOverlayElement (mainElement, style) {
this.removeOverlayIfNeeded()

const overlay = document.createElement('div')
overlay.setAttribute('id', OVERLAY_ID)
const svgElement = mainElement.parentNode?.querySelector('svg') ?? mainElement.querySelector('svg')

const isRound = style.borderRadius === '100%'
jonathanKingston marked this conversation as resolved.
Show resolved Hide resolved
const elementToCenterOn = isRound ? svgElement : mainElement
if (elementToCenterOn) {
const { top, left, width, height } = elementToCenterOn.getBoundingClientRect()
overlay.style.position = 'absolute'

overlay.style.top = `calc(${top}px + ${window.scrollY}px - ${isRound ? height / 2 : 0}px - 1px - ${style.offsetTopEm}em)`
dbajpeyi marked this conversation as resolved.
Show resolved Hide resolved
overlay.style.left = `calc(${left}px + ${window.scrollX}px - ${isRound ? width / 2 : 0}px - 1px - ${style.offsetLeftEm}em)`

const mainElementRect = mainElement.getBoundingClientRect()
overlay.style.width = `${mainElementRect.width}px`
overlay.style.height = `${mainElementRect.height}px`
overlay.style.zIndex = style.zIndex

// Ensure overlay is non-interactive
overlay.style.pointerEvents = 'none'

// insert in document.body
document.body.appendChild(overlay)
return overlay
} else {
return null
}

}

/**
* Observes the removal of an element from the DOM.
* @param {HTMLElement|Element} element
* @param {any} onRemoveCallback
*/
observeElementRemoval (element, onRemoveCallback) {
// Set up the mutation observer
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
// Check if the element has been removed from its parent
if (mutation.type === 'childList' && !document.contains(element)) {
// Element has been removed
onRemoveCallback()
observer.disconnect() // Stop observing
}
})
})

// Start observing the parent node for child list changes
observer.observe(document.body, { childList: true, subtree: true })
}

/**
* Moves the element into view and animates it.
* @param {HTMLElement|Element} element
* @param {any} style
*/
animateElement (element, style) {
element.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
}) // Scroll into view
const keyframes = [
{ backgroundColor: 'rgba(0, 0, 255, 0)', offset: 0, borderRadius: '2px' }, // Start: transparent
{ backgroundColor: style.backgroundColor, offset: 0.5, borderRadius: '2px', transform: `scale(${style.scale})` }, // Midpoint: blue with 50% opacity
{ backgroundColor: 'rgba(0, 0, 255, 0)', borderRadius: '2px', offset: 1 } // End: transparent
]

// Define the animation options
const options = {
duration: ANIMATION_DURATION_MS,
iterations: ANIMATION_ITERATIONS
const overlay = this.insertOverlayElement(element, style)
if (overlay != null) {
overlay.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
}) // Scroll into view
const keyframes = [
{ backgroundColor: BACKGROUND_COLOR_START, offset: 0, borderRadius: style.borderRadius, border: `1px solid ${BACKGROUND_COLOR_START}`, transform: style.transform.start }, // Start: 10% blue
{ backgroundColor: BACKGROUND_COLOR_END, offset: 0.5, borderRadius: style.borderRadius, border: `1px solid ${BACKGROUND_COLOR_END}`, transform: style.transform.mid, transformOrigin: 'center' }, // Middle: 25% blue
{ backgroundColor: BACKGROUND_COLOR_START, offset: 1, borderRadius: style.borderRadius, border: `1px solid ${BACKGROUND_COLOR_START}`, transform: style.transform.start } // End: 10% blue
]

// Define the animation options
const options = {
duration: ANIMATION_DURATION_MS,
iterations: ANIMATION_ITERATIONS
}

// Apply the animation to the element
overlay.animate(keyframes, options)
}

// Apply the animation to the element
element.animate(keyframes, options)
}

autotapElement (element) {
Expand Down Expand Up @@ -157,17 +258,27 @@ export default class AutofillPasswordImport extends ContentFeature {
* @param {string} path
*/
async handleElementForPath (path) {
this.removeOverlayIfNeeded()
const supportedPaths = [
this.#exportButtonSettings?.path,
this.#settingsButtonSettings?.path,
this.#signInButtonSettings?.path
]
if (supportedPaths.indexOf(path)) {
if (supportedPaths.includes(path)) {
try {
const { element, style, shouldTap } = await this.getElementAndStyleFromPath(path) ?? {}
const { element, style, shouldTap, shouldWatchForRemoval } = await this.getElementAndStyleFromPath(path) ?? {}
if (element != null) {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
shouldTap ? this.autotapElement(element) : this.animateElement(element, style)
if (shouldTap) {
this.autotapElement(element)
} else {
setTimeout(() => this.animateElement(element, style), ANIMATION_TIMEOUT)
dbajpeyi marked this conversation as resolved.
Show resolved Hide resolved
}
if (shouldWatchForRemoval) {
// Sometimes navigation events are not triggered, then we need to watch for removal
this.observeElementRemoval(element, () => {
this.removeOverlayIfNeeded()
})
}
}
} catch {
console.error('password-import: handleElementForPath failed for path:', path)
Expand Down
Loading